Removed connection worker.

This commit is contained in:
flash 2024-08-28 21:36:25 +00:00
parent f1e787c807
commit 8ae77e1bf3
17 changed files with 22 additions and 618 deletions

View file

@ -31,8 +31,7 @@ const exec = require('util').promisify(require('child_process').exec);
const tasks = {
js: [
{ source: 'proto.js', target: '/assets', name: 'proto.{hash}.js', vars: { build: 'MAMI_PROTO_JS', html: ':source' } },
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', vars: { build: 'MAMI_MAIN_JS', html: ':source' } },
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', vars: { build: 'MAMI_JS', html: ':source' } },
{ source: 'init.js', target: '/assets', name: 'init.{hash}.js', es: 'es5', vars: { html: ':source' } },
],
css: [

View file

@ -42,7 +42,7 @@
if(isCompatible) {
(function(script) {
script.src = MAMI_MAIN_JS;
script.src = MAMI_JS;
script.type = 'text/javascript';
script.charset = 'utf-8';
document.body.appendChild(script);

View file

@ -21,7 +21,6 @@ const MamiContext = function(globalEventTarget, eventTarget) {
let textTriggers;
let eeprom;
let conMan;
let protoWorker;
return {
get globalEvents() { return globalEventTarget; },
@ -88,12 +87,5 @@ const MamiContext = function(globalEventTarget, eventTarget) {
throw 'conMan must be a non-null object';
conMan = value;
},
get protoWorker() { return protoWorker; },
set protoWorker(value) {
if(typeof value !== 'object' || value === null)
throw 'protoWorker must be a non-null object';
protoWorker = value;
},
};
};

View file

@ -18,7 +18,6 @@ window.Umi = { UI: {} };
#include users.js
#include utility.js
#include weeb.js
#include worker.js
#include audio/autoplay.js
#include chatform/form.jsx
#include colpick/picker.jsx
@ -905,10 +904,7 @@ const MamiInit = async args => {
}
};
const protoWorker = new MamiWorker(MAMI_PROTO_JS, ctx.events.scopeTo('worker'));
ctx.protoWorker = protoWorker;
const sockChat = new MamiSockChat(protoWorker);
const sockChat = new MamiSockChat(ctx.events.scopeTo('sockchat'));
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
ctx.conMan = conMan;
@ -982,40 +978,9 @@ const MamiInit = async args => {
conMan.watch('attempt', conManAttempt);
conMan.watch('fail', conManFail);
let workerStarting = false;
const initWorker = async () => {
if(workerStarting)
return;
workerStarting = true;
if(FUTAMI_DEBUG)
console.info('[proto] initialising worker...');
try {
await protoWorker.connect();
await sockChat.create();
conMan.client = sockChat;
await conMan.start();
} finally {
workerStarting = false;
}
};
protoWorker.watch(':timeout', ev => {
console.warn('worker timeout', ev.detail);
initWorker();
});
document.addEventListener('visibilitychange', () => {
if(document.visibilityState === 'visible') {
protoWorker.ping().catch(ex => {
console.warn('worker died', ex);
initWorker();
});
}
});
await initWorker();
await sockChat.create();
conMan.client = sockChat;
await conMan.start();
return ctx;
};

View file

@ -1,4 +1,4 @@
#include sockchat/utils.js
#include proto/sockchat/utils.js
const SockChatS2CPong = ctx => {
const lastPong = Date.now();

View file

@ -1,9 +1,9 @@
#include timedp.js
#include sockchat/authed.js
#include sockchat/ctx.js
#include sockchat/unauthed.js
#include proto/sockchat/authed.js
#include proto/sockchat/ctx.js
#include proto/sockchat/unauthed.js
const SockChatProtocol = function(dispatch, options) {
const SockChatClient = function(dispatch, options) {
if(typeof dispatch !== 'function')
throw 'dispatch must be a function';
if(typeof options !== 'object' || options === null)

View file

@ -1,4 +1,4 @@
#include sockchat/keepalive.js
#include proto/sockchat/keepalive.js
const SockChatContext = function(dispatch, sendPing, pingDelay) {
if(typeof dispatch !== 'function')

View file

@ -1,4 +1,4 @@
#include sockchat/utils.js
#include proto/sockchat/utils.js
const SockChatS2CAuthSuccess = (ctx, userId, userName, userColour, userPerms, chanName, maxLength) => {
ctx.userId = userId;

View file

@ -25,7 +25,7 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
});
const dispatchUpdate = (name, value, silent, local) => eventTarget.dispatch(createUpdateEvent(name, value, false, silent, local));
const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name}`);
const broadcast = new BroadcastChannel(`${MAMI_JS}:settings:${storage.name}`);
const broadcastUpdate = (name, value, silent) => {
setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value, silent: !!silent }), 0);
};

View file

@ -1,8 +1,8 @@
#include compat.js
#include mszauth.js
#include proto/sockchat/client.js
const MamiSockChat = function(protoWorker) {
const events = protoWorker.eventTarget('sockchat');
const MamiSockChat = function(eventTarget) {
let restarting = false;
let client;
let dumpPackets = false;
@ -10,8 +10,8 @@ const MamiSockChat = function(protoWorker) {
return {
get client() { return client; },
watch: events.watch,
unwatch: events.unwatch,
watch: eventTarget.watch,
unwatch: eventTarget.unwatch,
create: async () => {
if(client !== undefined && typeof client.close === 'function')
@ -19,8 +19,8 @@ const MamiSockChat = function(protoWorker) {
client.close();
restarting = false;
client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') });
await client.setDumpPackets(dumpPackets);
client = new SockChatClient(eventTarget.dispatch, { ping: futami.get('ping') });
client.setDumpPackets(dumpPackets);
MamiCompat('Umi.Server', { get: () => client, configurable: true });
MamiCompat('Umi.Server.SendMessage', { value: text => client.sendMessage(text), configurable: true });
@ -38,11 +38,11 @@ const MamiSockChat = function(protoWorker) {
const authInfo = MamiMisuzuAuth.getInfo();
await client.sendAuth(authInfo.method, authInfo.token);
},
setDumpPackets: async state => {
setDumpPackets: state => {
dumpPackets = !!state;
if(client !== undefined && typeof client.setDumpPackets === 'function')
await client.setDumpPackets(dumpPackets);
client.setDumpPackets(dumpPackets);
},
};
};

View file

@ -1,281 +0,0 @@
#include uniqstr.js
const MamiWorker = function(url, eventTarget) {
const timeOutMs = 30000;
let worker, workerId;
let connectTimeout;
let pingId;
let hasTimedout;
const root = {};
const objects = new Map;
const pending = new Map;
const clearObjects = () => {
for(const [name, object] of objects)
for(const method in object)
delete object[method];
objects.clear();
objects.set('', root);
};
clearObjects();
const broadcastTimeoutZone = body => {
const localWorkerId = workerId;
body(detail => {
if(localWorkerId !== workerId || hasTimedout)
return;
hasTimedout = true;
eventTarget.dispatch(':timeout', detail);
});
};
const handlers = {};
const handleMessage = ev => {
if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
if(ev.data.type in handlers)
handlers[ev.data.type](ev.data.detail);
return;
}
};
const callObjectMethod = (objName, metName, ...args) => {
return new Promise((resolve, reject) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof metName !== 'string')
throw 'metName must be a string';
const id = MamiUniqueStr(8);
const info = { id: id, resolve: resolve, reject: reject };
pending.set(id, info);
worker.postMessage({ type: 'metcall', detail: { id: id, object: objName, method: metName, args: args } });
broadcastTimeoutZone(timeout => {
info.timeOut = setTimeout(() => {
const reject = info.reject;
info.resolve = info.reject = undefined;
info.timeOut = undefined;
pending.delete(id);
timeout({ at: 'call', obj: objName, met: metName });
if(typeof reject === 'function')
reject('timeout');
}, timeOutMs);
});
});
};
const defineObjectMethod = (object, objName, method) => object[method] = (...args) => callObjectMethod(objName, method, ...args);
handlers['objdef'] = info => {
let object = objects.get(info.object);
if(object === undefined)
objects.set(info.object, object = {});
if(typeof info.eventPrefix === 'string') {
const scopedTarget = eventTarget.scopeTo(info.eventPrefix);
object.watch = scopedTarget.watch;
object.unwatch = scopedTarget.unwatch;
}
for(const method of info.methods)
defineObjectMethod(object, info.object, method);
};
handlers['objdel'] = info => {
// this should never happen
if(info.object === '') {
console.error('Worker attempted to delete root object!!!!!');
return;
}
const object = objects.get(info.object);
if(object === undefined)
return;
objects.delete(info.object);
const methods = Object.keys(object);
for(const method of methods)
delete object[method];
};
handlers['metdef'] = info => {
const object = objects.get(info.object);
if(object === undefined) {
console.error('Worker attempted to define method on undefined object.');
return;
}
defineObjectMethod(object, info.object, info.method);
};
handlers['metdel'] = info => {
const object = objects.get(info.object);
if(object === undefined) {
console.error('Worker attempted to delete method on undefined object.');
return;
}
delete object[info.method];
};
handlers['funcret'] = resp => {
const info = pending.get(resp.id);
if(info === undefined)
return;
pending.delete(info.id);
if(info.timeOut !== undefined)
clearTimeout(info.timeOut);
const handler = resp.success ? info.resolve : info.reject;
info.resolve = info.reject = undefined;
if(handler !== undefined) {
let result = resp.result;
if(resp.object)
result = objects.get(result);
handler(result);
}
};
handlers['evtdisp'] = resp => {
eventTarget.dispatch(resp.name, resp.detail);
};
return {
get root() { return root; },
watch: eventTarget.watch,
unwatch: eventTarget.unwatch,
eventTarget: prefix => eventTarget.scopeTo(prefix),
ping: () => {
return new Promise((resolve, reject) => {
if(worker === undefined)
throw 'no worker active';
let pingTimeout;
let localPingId = pingId;
const pingHandleMessage = ev => {
if(typeof ev.data === 'string' && ev.data.startsWith('pong:') && ev.data.substring(5) === localPingId)
try {
reject = undefined;
pingId = undefined;
if(pingTimeout !== undefined)
clearTimeout(pingTimeout);
worker?.removeEventListener('message', pingHandleMessage);
if(typeof resolve === 'function')
resolve();
} finally {
resolve = undefined;
}
};
worker.addEventListener('message', pingHandleMessage);
if(localPingId === undefined) {
pingId = localPingId = MamiUniqueStr(8);
broadcastTimeoutZone(timeout => {
pingTimeout = setTimeout(() => {
try {
resolve = undefined;
worker?.removeEventListener('message', pingHandleMessage);
timeout({ at: 'ping' });
if(typeof reject === 'function')
reject('ping timeout');
} finally {
reject = undefined;
}
}, 200);
});
worker.postMessage(`ping:${localPingId}`);
}
});
},
sabotage: () => {
worker?.terminate();
},
connect: () => {
return new Promise((resolve, reject) => {
const connectFinally = () => {
if(connectTimeout !== undefined) {
clearTimeout(connectTimeout);
connectTimeout = undefined;
}
};
const connectHandleMessage = ev => {
worker?.removeEventListener('message', connectHandleMessage);
if(typeof ev.data !== 'object' || ev.data === null || ev.data.type !== 'objdef'
|| typeof ev.data.detail !== 'object' || ev.data.detail.object !== '') {
callReject('data');
} else
callResolve();
};
const callResolve = () => {
reject = undefined;
connectFinally();
try {
if(typeof resolve === 'function')
resolve(root);
} finally {
resolve = undefined;
}
};
const callReject = (...args) => {
resolve = undefined;
connectFinally();
try {
broadcastTimeoutZone(timeout => {
timeout({ at: 'connect' });
});
if(typeof reject === 'function')
reject(...args);
} finally {
reject = undefined;
}
};
if(worker !== undefined) {
worker.terminate();
workerId = worker = undefined;
}
hasTimedout = false;
workerId = MamiUniqueStr(5);
worker = new Worker(url);
worker.addEventListener('message', handleMessage);
worker.addEventListener('message', connectHandleMessage);
connectTimeout = setTimeout(() => callReject('timeout'), timeOutMs);
worker.postMessage({ type: 'init' });
});
},
};
};

View file

@ -1,19 +0,0 @@
#include skel.js
#include sockchat/proto.js
const skel = new WorkerSkeleton;
skel.defineMethod('create', (name, options) => {
if(typeof name !== 'string')
throw 'name must be a string';
let proto, prefix;
if(name === 'sockchat')
proto = new SockChatProtocol(
skel.createDispatcher(prefix = 'sockchat'),
options
);
return skel.defineObject(proto, prefix);
}, true);

View file

@ -1,214 +0,0 @@
#include uniqstr.js
const WorkerSkeletonObject = function(name, defineObjectMethod, deleteObjectMethod, deleteObject) {
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof defineObjectMethod !== 'function')
throw 'defineObjectMethod must be a function';
if(typeof deleteObjectMethod !== 'function')
throw 'deleteObjectMethod must be a function';
if(typeof deleteObject !== 'function')
throw 'deleteObject must be a function';
return {
getObjectName: () => name,
defineMethod: (...args) => defineObjectMethod(name, ...args),
deleteMethod: (...args) => deleteObjectMethod(name, ...args),
destroy: () => deleteObject(name),
};
};
const WorkerSkeleton = function(globalScope) {
if(globalScope === undefined)
globalScope = self;
const objects = new Map;
const handlers = {};
let initialised = false;
const sendPayload = (type, detail) => {
globalScope.postMessage({ type: type, detail: detail });
};
const sendObjectDefinePayload = (objName, objBody, eventPrefix) => {
sendPayload('objdef', { object: objName, methods: Object.keys(objBody), eventPrefix: eventPrefix });
};
const defineObject = (objName, objBody, eventPrefix) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof eventPrefix !== 'string' && eventPrefix !== undefined)
throw 'eventPrefix must be string or undefined';
if(objects.has(objName))
throw 'objName is already defined';
const object = {};
for(const name in objBody) {
const item = objBody[name];
if(typeof item === 'function')
object[name] = { body: item };
}
objects.set(objName, object);
if(initialised)
sendObjectDefinePayload(objName, object, eventPrefix);
};
const deleteObject = objName => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(!objects.has(objName))
throw 'objName is not defined';
objects.delete(objName);
if(initialised)
sendPayload('objdel', { object: objName });
};
const defineObjectMethod = (objName, metName, metBody, returnsObject) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof metName !== 'string')
throw 'metName must be a string';
if(typeof metBody !== 'function')
throw 'metBody must be a function';
const objBody = objects.get(objName);
if(objBody === undefined)
throw 'objName has not been defined';
objBody[metName] = { body: metBody, returnsObject: returnsObject === true };
if(initialised)
sendPayload('metdef', { object: objName, method: metName });
};
const deleteObjectMethod = (objName, metName) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof metName !== 'string')
throw 'metName must be a string';
const objBody = objects.get(objName);
if(objBody === undefined)
throw 'objName has not been defined';
delete objBody[objName];
if(initialised)
sendPayload('metdel', { object: objName, method: metName });
};
const createDispatcher = prefix => {
if(prefix === undefined)
prefix = '';
else if(typeof prefix !== 'string')
throw 'prefix must be a string or undefined';
if(prefix !== '' && !prefix.endsWith(':'))
prefix += ':';
return (name, detail) => sendPayload('evtdisp', { name: prefix + name, detail: detail });
};
defineObject('', {});
const defineHandler = (name, handler) => {
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof handler !== 'function')
throw 'handler must be a function';
if(name in handlers)
throw 'name is already defined';
handlers[name] = handler;
};
defineHandler('init', () => {
if(initialised)
return;
initialised = true;
sendObjectDefinePayload('', objects.get(''));
});
defineHandler('metcall', req => {
if(typeof req.id !== 'string')
throw 'call id is not a string';
const respond = (id, success, result, mightBeObject) => {
let isObject = false;
if(mightBeObject) {
const resultType = typeof result;
if(resultType === 'string' && objects.has(result))
isObject = true;
else if(resultType === 'object' && result !== null && typeof result.getObjectName === 'function') {
const objectName = result.getObjectName();
if(objects.has(objectName)) {
isObject = true;
result = objectName;
}
}
}
sendPayload('funcret', {
id: id,
success: success,
result: result,
object: isObject,
});
};
try {
if(typeof req.object !== 'string')
throw 'object name is not a string';
if(typeof req.method !== 'string')
throw 'method name is not a string';
const object = objects.get(req.object);
if(object === undefined)
throw 'object is not defined';
if(!(req.method in object))
throw 'method is not defined in object';
const args = Array.isArray(req.args) ? req.args : [];
const info = object[req.method];
let result = info.body(...args);
if(result instanceof Promise) {
result.then(result => respond(req.id, true, result, info.returnsObject)).catch(ex => respond(req.id, false, ex));
} else
respond(req.id, true, result, info.returnsObject);
} catch(ex) {
respond(req.id, false, ex);
}
});
globalScope.addEventListener('message', ev => {
if(typeof ev.data === 'string') {
if(ev.data.startsWith('ping:')) {
globalScope.postMessage(`pong:${ev.data.substring(5)}`);
return;
}
}
if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
if(ev.data.type in handlers)
handlers[ev.data.type](ev.data.detail);
return;
}
});
return {
sendPayload: sendPayload,
createDispatcher: createDispatcher,
defineObject: (object, eventPrefix) => {
if(typeof object !== 'object' || object === null)
return undefined;
const name = MamiUniqueStr(8);
defineObject(name, object, eventPrefix);
return new WorkerSkeletonObject(name, defineObjectMethod, deleteObjectMethod, deleteObject);
},
defineMethod: (...args) => defineObjectMethod('', ...args),
deleteMethod: (...args) => deleteObjectMethod('', ...args),
};
};

View file

@ -1,38 +0,0 @@
const MamiRandomInt = (min, max) => {
let ret = 0;
const range = max - min;
const bitsNeeded = Math.ceil(Math.log2(range));
if(bitsNeeded > 53)
return -1;
const bytesNeeded = Math.ceil(bitsNeeded / 8),
mask = Math.pow(2, bitsNeeded) - 1;
const bytes = new Uint8Array(bytesNeeded);
crypto.getRandomValues(bytes);
let p = (bytesNeeded - 1) * 8;
for(let i = 0; i < bytesNeeded; ++i) {
ret += bytes[i] * Math.pow(2, p);
p -= 8;
}
ret &= mask;
if(ret >= range)
return MamiRandomInt(min, max);
return min + ret;
};
const MamiUniqueStr = (() => {
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
return length => {
let str = '';
for(let i = 0; i < length; ++i)
str += chars[MamiRandomInt(0, chars.length)];
return str;
};
})();