Moved reconnect handling out of protocol handler.

UI is extremely shoddy right now, redo will follow. Enjoy LoadingOverlay while you still can.
This commit is contained in:
flash 2024-03-02 01:31:26 +00:00
parent b37352534b
commit e32eabea1f
6 changed files with 278 additions and 122 deletions

110
src/mami.js/conman.js Normal file
View file

@ -0,0 +1,110 @@
#include eventtarget.js
#include utility.js
const MamiConnectionManager = function(urls) {
if(!Array.isArray(urls))
throw 'urls must be an array';
const eventTarget = new MamiEventTarget('mami:conn');
const delays = [0, 2000, 2000, 2000, 5000, 5000, 5000, 5000, 5000, 10000, 10000, 10000, 10000, 10000, 15000, 30000, 45000, 60000, 120000, 300000];
let timeout;
let attempts, started, delay, url;
let attemptConnect;
let startResolve;
const resetTimeout = () => {
if(timeout !== undefined) {
clearTimeout(timeout);
timeout = undefined;
}
};
const clear = () => {
resetTimeout();
attempts = started = delay = 0;
url = undefined;
};
$as(urls);
const onFailure = ex => {
++attempts;
delay = attempts < delays.length ? delays[attempts] : delays[delays.length - 1];
eventTarget.dispatch('fail', {
url: url,
started: started,
attempt: attempts,
delay: delay,
error: ex,
});
attempt();
};
const attempt = () => {
started = Date.now();
url = urls[attempts % urls.length];
const attempt = attempts + 1;
timeout = setTimeout(() => {
resetTimeout();
eventTarget.dispatch('attempt', {
url: url,
started: started,
attempt: attempt,
});
attemptConnect(url).then(result => {
if(typeof result === 'boolean' && !result) {
onFailure();
return;
}
eventTarget.dispatch('success', {
url: url,
started: started,
attempt: attempt,
});
startResolve();
startResolve = undefined;
attemptConnect = undefined;
}).catch(ex => onFailure(ex));
}, delay);
};
const isActive = () => timeout !== undefined || startResolve !== undefined;
return {
isActive: isActive,
start: body => {
return new Promise(resolve => {
if(typeof body !== 'function')
throw 'body must be a function';
if(isActive())
throw 'already attempting to connect';
attemptConnect = body;
startResolve = resolve;
clear();
attempt();
});
},
force: () => {
if(!isActive())
return;
resetTimeout();
delay = 0;
attempt();
},
clear: clear,
watch: eventTarget.watch,
unwatch: eventTarget.unwatch,
};
};

View file

@ -5,6 +5,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
#include channels.js #include channels.js
#include common.js #include common.js
#include compat.js #include compat.js
#include conman.js
#include context.js #include context.js
#include emotes.js #include emotes.js
#include message.js #include message.js
@ -151,7 +152,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
} }
settings.watch('soundEnable', ev => { settings.watch('soundEnable', ev => {
console.log(ev);
if(ev.detail.value) { if(ev.detail.value) {
if(!soundCtx.ready) if(!soundCtx.ready)
soundCtx.reset(); soundCtx.reset();
@ -221,14 +221,16 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
loadingOverlay.setMessage('Preparing UI...'); loadingOverlay.setMessage('Preparing UI...');
const textTriggers = new MamiTextTriggers; const textTriggers = new MamiTextTriggers;
const conMan = new MamiConnectionManager(futami.get('servers'));
// define this as late as possible' // define this as late as possible
const ctx = new MamiContext({ const ctx = new MamiContext({
settings: settings, settings: settings,
views: views, views: views,
sound: soundCtx, sound: soundCtx,
textTriggers: textTriggers, textTriggers: textTriggers,
eeprom: eeprom, eeprom: eeprom,
conMan: conMan,
}); });
Object.defineProperty(window, 'mami', { enumerable: true, value: ctx }); Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });
@ -535,11 +537,15 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
Umi.UI.InputMenus.Add('markup', 'BB Code'); Umi.UI.InputMenus.Add('markup', 'BB Code');
Umi.UI.InputMenus.Add('emotes', 'Emoticons'); Umi.UI.InputMenus.Add('emotes', 'Emoticons');
let isUnloading = false;
window.addEventListener('beforeunload', function(ev) { window.addEventListener('beforeunload', function(ev) {
if(settings.get('closeTabConfirm')) { if(settings.get('closeTabConfirm')) {
ev.preventDefault(); ev.preventDefault();
return ev.returnValue = 'Are you sure you want to close the tab?'; return ev.returnValue = 'Are you sure you want to close the tab?';
} }
isUnloading = true;
}); });
@ -573,7 +579,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
'_1008': 'Your client did something the server did not agree with.', '_1008': 'Your client did something the server did not agree with.',
'_1009': 'Your client sent too much data to the server at once.', '_1009': 'Your client sent too much data to the server at once.',
'_1011': 'Something went wrong on the server side.', '_1011': 'Something went wrong on the server side.',
'_1012': 'The server is restarting, reconnecting soon...', '_1012': 'The chat server is restarting.',
'_1013': 'You cannot connect to the server right now, try again later.', '_1013': 'You cannot connect to the server right now, try again later.',
'_1015': 'Your client and the server could not establish a secure connection.', '_1015': 'Your client and the server could not establish a secure connection.',
}; };
@ -597,30 +603,54 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true }); MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
MamiCompat('Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true }); MamiCompat('Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
sockChat.watch('conn:init', ev => {
if(dumpEvents) console.log('conn:init', ev);
let message = 'Connecting to server...';
if(ev.detail.attempt > 2)
message += ` (Attempt ${connectAttempts})`;
getLoadingOverlay('spinner', 'Loading...', message);
});
sockChat.watch('conn:ready', ev => {
if(dumpEvents) console.log('conn:ready', ev);
getLoadingOverlay('spinner', 'Loading...', 'Authenticating...');
const authInfo = MamiMisuzuAuth.getInfo();
sockChat.sendAuth(authInfo.method, authInfo.token);
});
sockChat.watch('conn:lost', ev => { sockChat.watch('conn:lost', ev => {
if(dumpEvents) console.log('conn:lost', ev); if(dumpEvents) console.log('conn:lost', ev);
getLoadingOverlay( if(conMan.isActive() || isUnloading)
'unlink', 'Disconnected!', return;
wsCloseReasons[`_${ev.detail.code}`] ?? `Something caused an unexpected connection loss. (${ev.detail.code})`
); const errorCode = ev.detail.code;
const isRestarting = ev.detail.isRestarting;
pingToggle.title = '∞ms';
pingIndicator.setStrength(-1);
const conManAttempt = ev => {
if(isRestarting || ev.detail.attempt > 1) {
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
getLoadingOverlay('spinner', 'Connecting...', message);
}
};
const conManFail = ev => {
if(isRestarting || ev.detail.attempt > 1) {
let header = isRestarting ? 'Restarting...' : 'Disconnected';
let message = wsCloseReasons[`_${errorCode}`] ?? `Something caused an unexpected connection loss. (${errorCode})`;
if(ev.detail.delay > 0)
message += ` Retrying in ${ev.detail.delay / 1000} seconds...`;
// this is absolutely disgusting but i really don't care right now sorry
message += ' <a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>';
getLoadingOverlay('unlink', header, message);
}
};
conMan.watch('attempt', conManAttempt);
conMan.watch('fail', conManFail);
conMan.start(async url => {
await sockChat.open(url);
}).then(() => {
conMan.unwatch('attempt', conManAttempt);
conMan.unwatch('fail', conManFail);
pingToggle.title = 'Ready~';
pingIndicator.setStrength(3);
const authInfo = MamiMisuzuAuth.getInfo();
sockChat.sendAuth(authInfo.method, authInfo.token);
});
}); });
sockChat.watch('ping:send', ev => { sockChat.watch('ping:send', ev => {
@ -643,7 +673,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
pingIndicator.setStrength(strength); pingIndicator.setStrength(strength);
}); });
sockChat.watch('session:start', ev => { sockChat.watch('session:start', ev => {
if(dumpEvents) console.log('session:start', ev); if(dumpEvents) console.log('session:start', ev);
@ -655,23 +684,24 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
Umi.UI.Emoticons.Init(); Umi.UI.Emoticons.Init();
Umi.Parsing.Init(); Umi.Parsing.Init();
views.pop(ctx => MamiAnimate({ if(views.count() > 1)
async: true, views.pop(ctx => MamiAnimate({
duration: 120, async: true,
easing: 'inOutSine', duration: 120,
start: () => { easing: 'inOutSine',
ctx.toElem.style.zIndex = '100'; start: () => {
ctx.fromElem.style.pointerEvents = 'none'; ctx.toElem.style.zIndex = '100';
ctx.fromElem.style.zIndex = '200'; ctx.fromElem.style.pointerEvents = 'none';
}, ctx.fromElem.style.zIndex = '200';
update: t => { },
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; update: t => {
ctx.fromElem.style.opacity = 1 - (1 * t).toString(); ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
}, ctx.fromElem.style.opacity = 1 - (1 * t).toString();
end: () => { },
ctx.toElem.style.zIndex = null; end: () => {
}, ctx.toElem.style.zIndex = null;
})); },
}));
}); });
sockChat.watch('session:fail', ev => { sockChat.watch('session:fail', ev => {
if(dumpEvents) console.log('session:fail', ev); if(dumpEvents) console.log('session:fail', ev);
@ -880,7 +910,28 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
Umi.UI.Messages.RemoveAll(); Umi.UI.Messages.RemoveAll();
}); });
sockChat.open(); const conManAttempt = ev => {
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
getLoadingOverlay('spinner', 'Connecting...', message);
};
const conManFail = ev => {
getLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
};
conMan.watch('attempt', conManAttempt);
conMan.watch('fail', conManFail);
await conMan.start(async url => {
await sockChat.open(url);
});
conMan.unwatch('attempt', conManAttempt);
conMan.unwatch('fail', conManFail);
getLoadingOverlay('spinner', 'Connecting...', 'Authenticating...');
const authInfo = MamiMisuzuAuth.getInfo();
sockChat.sendAuth(authInfo.method, authInfo.token);
if(window.dispatchEvent) if(window.dispatchEvent)
window.dispatchEvent(new Event('umi:connect')); window.dispatchEvent(new Event('umi:connect'));

View file

@ -1,27 +0,0 @@
#include common.js
#include utility.js
const UmiServers = (function() {
let servers = undefined,
index = Number.MAX_SAFE_INTEGER - 1;
return {
getServer: function(callback) {
// FutamiCommon is delayed load
if(servers === undefined) {
const futamiServers = futami.get('servers');
$as(futamiServers);
servers = futamiServers;
}
if(++index >= servers.length)
index = 0;
let server = servers[index];
if(server.includes('//'))
server = location.protocol.replace('http', 'ws') + server;
callback(server);
},
};
})();

View file

@ -1,5 +1,4 @@
#include eventtarget.js #include eventtarget.js
#include servers.js
#include websock.js #include websock.js
Umi.Protocol.SockChat.Protocol = function(pingDuration) { Umi.Protocol.SockChat.Protocol = function(pingDuration) {
@ -31,8 +30,6 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) {
}; };
let wasConnected = false; let wasConnected = false;
let noReconnect = false;
let connectAttempts = 0;
let wasKicked = false; let wasKicked = false;
let isRestarting = false; let isRestarting = false;
let dumpPackets = false; let dumpPackets = false;
@ -40,6 +37,7 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) {
let sock; let sock;
let selfUserId, selfChannelName, selfPseudoChannelName; let selfUserId, selfChannelName, selfPseudoChannelName;
let lastPing, lastPong, pingTimer, pingWatcher; let lastPing, lastPong, pingTimer, pingWatcher;
let openResolve, openReject;
const handlers = {}; const handlers = {};
@ -104,7 +102,10 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) {
isRestarting = false; isRestarting = false;
eventTarget.dispatch('conn:ready', { wasConnected: wasConnected }); if(typeof openResolve === 'function') {
openResolve();
openResolve = undefined;
}
}; };
const onClose = ev => { const onClose = ev => {
@ -126,15 +127,20 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) {
} else if(code === 1012) } else if(code === 1012)
isRestarting = true; isRestarting = true;
if(typeof openReject === 'function') {
openReject({
code: code,
wasConnected: wasConnected,
isRestarting: isRestarting,
});
openReject = undefined;
}
eventTarget.dispatch('conn:lost', { eventTarget.dispatch('conn:lost', {
wasConnected: wasConnected, wasConnected: wasConnected,
isRestarting: isRestarting, isRestarting: isRestarting,
code: code, code: code,
}); });
connectAttempts = 0;
setTimeout(() => beginConnecting(), 5000);
}; };
const unfuckText = text => text.replace(/ <br\/> /g, "\n"); const unfuckText = text => text.replace(/ <br\/> /g, "\n");
@ -501,7 +507,6 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) {
// baka (ban/kick) // baka (ban/kick)
handlers['9'] = (type, expiry) => { handlers['9'] = (type, expiry) => {
noReconnect = true;
wasKicked = true; wasKicked = true;
const bakaInfo = { const bakaInfo = {
@ -534,49 +539,43 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) {
}; };
const beginConnecting = () => {
sock?.close();
if(noReconnect)
return;
UmiServers.getServer(server => {
eventTarget.dispatch('conn:init', {
server: server,
wasConnected: wasConnected,
attempt: ++connectAttempts,
});
sock = new UmiWebSocket(server);
sock.watch('open', onOpen);
sock.watch('close', onClose);
sock.watch('message', onMessage);
sock.watch('create_interval', ev => {
pingTimer = ev.detail.id;
});
sock.watch('call_interval', ev => {
if(ev.detail.id === pingTimer)
onSendPing();
});
sock.watch('create_intervals', ev => {
pingTimer = undefined;
});
});
};
return { return {
sendAuth: sendAuth, sendAuth: sendAuth,
sendMessage: sendMessage, sendMessage: sendMessage,
open: () => { open: url => {
noReconnect = false; return new Promise((resolve, reject) => {
beginConnecting(); if(typeof url !== 'string')
throw 'url must be a string';
if(url.startsWith('//'))
url = location.protocol.replace('http', 'ws') + url;
openResolve = resolve;
openReject = reject;
sock?.close();
sock = new UmiWebSocket(url);
sock.watch('open', onOpen);
sock.watch('close', onClose);
sock.watch('message', onMessage);
sock.watch('create_interval', ev => {
pingTimer = ev.detail.id;
});
sock.watch('call_interval', ev => {
if(ev.detail.id === pingTimer)
onSendPing();
});
sock.watch('create_intervals', ev => {
pingTimer = undefined;
});
});
}, },
close: () => { close: () => {
noReconnect = true;
sock?.close(); sock?.close();
sock = undefined;
}, },
watch: (name, handler) => eventTarget.watch(name, handler), watch: eventTarget.watch,
unwatch: (name, handler) => eventTarget.unwatch(name, handler), unwatch: eventTarget.unwatch,
setDumpPackets: state => dumpPackets = !!state, setDumpPackets: state => dumpPackets = !!state,
switchChannel: channelInfo => { switchChannel: channelInfo => {
if(selfUserId === undefined) if(selfUserId === undefined)

View file

@ -31,7 +31,7 @@ Umi.UI.LoadingOverlay = function(icon, header, message) {
}; };
const setHeader = text => headerElem.textContent = (text || '').toString(); const setHeader = text => headerElem.textContent = (text || '').toString();
const setMessage = text => messageElem.textContent = (text || '').toString(); const setMessage = text => messageElem.innerHTML = (text || '').toString();
setIcon(icon); setIcon(icon);
setHeader(header); setHeader(header);
@ -44,8 +44,5 @@ Umi.UI.LoadingOverlay = function(icon, header, message) {
getElement: function() { getElement: function() {
return html; return html;
}, },
implode: function() {
html.parentNode.removeChild(html);
},
}; };
}; };

View file

@ -13,15 +13,41 @@ const MamiPingIndicator = function(initialStrength) {
for(let i = 1; i <= 3; ++i) for(let i = 1; i <= 3; ++i)
bars.push(html.appendChild(<div class={`ping-bar ping-bar-${i}`}/>)); bars.push(html.appendChild(<div class={`ping-bar ping-bar-${i}`}/>));
let interval;
const setStrength = strength => { const setStrength = strength => {
if(typeof strength !== 'number') if(typeof strength !== 'number')
throw 'strength must be a number'; throw 'strength must be a number';
for(const i in bars) if(strength < 0) {
bars[i].classList.toggle('ping-bar-on', i < strength); if(interval === undefined) {
const cyclesMax = bars.length * 2;
let cycles = -1;
const updateCycle = () => {
let curCycle = ++cycles % cyclesMax;
if(curCycle > bars.length)
curCycle = cyclesMax - curCycle;
for(const i in bars)
bars[i].classList.toggle('ping-bar-on', curCycle === (parseInt(i) + 1));
};
interval = setInterval(updateCycle, 200);
updateCycle();
}
} else {
if(interval !== undefined) {
clearInterval(interval);
interval = undefined;
}
for(const i in bars)
bars[i].classList.toggle('ping-bar-on', i < strength);
}
html.classList.toggle('ping-state-good', strength > 1); html.classList.toggle('ping-state-good', strength > 1);
html.classList.toggle('ping-state-warn', strength === 1); html.classList.toggle('ping-state-warn', strength == 1);
html.classList.toggle('ping-state-poor', strength < 1); html.classList.toggle('ping-state-poor', strength < 1);
}; };