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:
parent
b37352534b
commit
e32eabea1f
6 changed files with 278 additions and 122 deletions
110
src/mami.js/conman.js
Normal file
110
src/mami.js/conman.js
Normal 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,
|
||||
};
|
||||
};
|
|
@ -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 += ' <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 => {
|
||||
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -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(/ <br\/> /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)
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,15 +13,41 @@ const MamiPingIndicator = function(initialStrength) {
|
|||
for(let i = 1; i <= 3; ++i)
|
||||
bars.push(html.appendChild(<div class={`ping-bar ping-bar-${i}`}/>));
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue