diff --git a/src/mami.js/conman.js b/src/mami.js/conman.js new file mode 100644 index 0000000..c9d2249 --- /dev/null +++ b/src/mami.js/conman.js @@ -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, + }; +}; diff --git a/src/mami.js/main.js b/src/mami.js/main.js index bb4499b..cfc99fa 100644 --- a/src/mami.js/main.js +++ b/src/mami.js/main.js @@ -5,6 +5,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; #include channels.js #include common.js #include compat.js +#include conman.js #include context.js #include emotes.js #include message.js @@ -151,7 +152,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; } settings.watch('soundEnable', ev => { - console.log(ev); if(ev.detail.value) { if(!soundCtx.ready) soundCtx.reset(); @@ -221,14 +221,16 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; loadingOverlay.setMessage('Preparing UI...'); 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({ settings: settings, views: views, sound: soundCtx, textTriggers: textTriggers, eeprom: eeprom, + conMan: conMan, }); 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('emotes', 'Emoticons'); + let isUnloading = false; + window.addEventListener('beforeunload', function(ev) { if(settings.get('closeTabConfirm')) { ev.preventDefault(); 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.', '_1009': 'Your client sent too much data to the server at once.', '_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.', '_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.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 => { if(dumpEvents) console.log('conn:lost', ev); - getLoadingOverlay( - 'unlink', 'Disconnected!', - wsCloseReasons[`_${ev.detail.code}`] ?? `Something caused an unexpected connection loss. (${ev.detail.code})` - ); + if(conMan.isActive() || isUnloading) + return; + + 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 += ' Retry now'; + + 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 => { @@ -643,7 +673,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; pingIndicator.setStrength(strength); }); - sockChat.watch('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.Parsing.Init(); - views.pop(ctx => MamiAnimate({ - async: true, - duration: 120, - easing: 'inOutSine', - start: () => { - ctx.toElem.style.zIndex = '100'; - ctx.fromElem.style.pointerEvents = 'none'; - ctx.fromElem.style.zIndex = '200'; - }, - update: t => { - ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; - ctx.fromElem.style.opacity = 1 - (1 * t).toString(); - }, - end: () => { - ctx.toElem.style.zIndex = null; - }, - })); + if(views.count() > 1) + views.pop(ctx => MamiAnimate({ + async: true, + duration: 120, + easing: 'inOutSine', + start: () => { + ctx.toElem.style.zIndex = '100'; + ctx.fromElem.style.pointerEvents = 'none'; + ctx.fromElem.style.zIndex = '200'; + }, + update: t => { + ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; + ctx.fromElem.style.opacity = 1 - (1 * t).toString(); + }, + end: () => { + ctx.toElem.style.zIndex = null; + }, + })); }); sockChat.watch('session:fail', ev => { if(dumpEvents) console.log('session:fail', ev); @@ -880,7 +910,28 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; 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) window.dispatchEvent(new Event('umi:connect')); diff --git a/src/mami.js/servers.js b/src/mami.js/servers.js deleted file mode 100644 index 315359e..0000000 --- a/src/mami.js/servers.js +++ /dev/null @@ -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); - }, - }; -})(); diff --git a/src/mami.js/sockchat_old.js b/src/mami.js/sockchat_old.js index 509cb1d..6886db0 100644 --- a/src/mami.js/sockchat_old.js +++ b/src/mami.js/sockchat_old.js @@ -1,5 +1,4 @@ #include eventtarget.js -#include servers.js #include websock.js Umi.Protocol.SockChat.Protocol = function(pingDuration) { @@ -31,8 +30,6 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) { }; let wasConnected = false; - let noReconnect = false; - let connectAttempts = 0; let wasKicked = false; let isRestarting = false; let dumpPackets = false; @@ -40,6 +37,7 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) { let sock; let selfUserId, selfChannelName, selfPseudoChannelName; let lastPing, lastPong, pingTimer, pingWatcher; + let openResolve, openReject; const handlers = {}; @@ -104,7 +102,10 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) { isRestarting = false; - eventTarget.dispatch('conn:ready', { wasConnected: wasConnected }); + if(typeof openResolve === 'function') { + openResolve(); + openResolve = undefined; + } }; const onClose = ev => { @@ -126,15 +127,20 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) { } else if(code === 1012) isRestarting = true; + if(typeof openReject === 'function') { + openReject({ + code: code, + wasConnected: wasConnected, + isRestarting: isRestarting, + }); + openReject = undefined; + } + eventTarget.dispatch('conn:lost', { wasConnected: wasConnected, isRestarting: isRestarting, code: code, }); - - connectAttempts = 0; - - setTimeout(() => beginConnecting(), 5000); }; const unfuckText = text => text.replace(/ /g, "\n"); @@ -501,7 +507,6 @@ Umi.Protocol.SockChat.Protocol = function(pingDuration) { // baka (ban/kick) handlers['9'] = (type, expiry) => { - noReconnect = true; wasKicked = true; 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 { sendAuth: sendAuth, sendMessage: sendMessage, - open: () => { - noReconnect = false; - beginConnecting(); + open: url => { + return new Promise((resolve, reject) => { + 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: () => { - noReconnect = true; sock?.close(); + sock = undefined; }, - watch: (name, handler) => eventTarget.watch(name, handler), - unwatch: (name, handler) => eventTarget.unwatch(name, handler), + watch: eventTarget.watch, + unwatch: eventTarget.unwatch, setDumpPackets: state => dumpPackets = !!state, switchChannel: channelInfo => { if(selfUserId === undefined) diff --git a/src/mami.js/ui/loading-overlay.jsx b/src/mami.js/ui/loading-overlay.jsx index 2b8eb9d..bc8eb79 100644 --- a/src/mami.js/ui/loading-overlay.jsx +++ b/src/mami.js/ui/loading-overlay.jsx @@ -31,7 +31,7 @@ Umi.UI.LoadingOverlay = function(icon, header, message) { }; const setHeader = text => headerElem.textContent = (text || '').toString(); - const setMessage = text => messageElem.textContent = (text || '').toString(); + const setMessage = text => messageElem.innerHTML = (text || '').toString(); setIcon(icon); setHeader(header); @@ -44,8 +44,5 @@ Umi.UI.LoadingOverlay = function(icon, header, message) { getElement: function() { return html; }, - implode: function() { - html.parentNode.removeChild(html); - }, }; }; diff --git a/src/mami.js/ui/ping.jsx b/src/mami.js/ui/ping.jsx index b7c6fca..6270b2e 100644 --- a/src/mami.js/ui/ping.jsx +++ b/src/mami.js/ui/ping.jsx @@ -13,15 +13,41 @@ const MamiPingIndicator = function(initialStrength) { for(let i = 1; i <= 3; ++i) bars.push(html.appendChild(
)); + let interval; + const setStrength = strength => { if(typeof strength !== 'number') throw 'strength must be a number'; - for(const i in bars) - bars[i].classList.toggle('ping-bar-on', i < strength); + if(strength < 0) { + 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-warn', strength === 1); + html.classList.toggle('ping-state-warn', strength == 1); html.classList.toggle('ping-state-poor', strength < 1); };