mami/src/mami.js/worker.js
flash cf71bab92d Rewrote connection handling.
This has been in the works for over a month and might break things because it's a very radical change.
If it causes you to be unable to join chat, report it on the forum or try joining using the legacy chat on https://sockchat.flashii.net.
2024-04-17 15:42:50 +00:00

282 lines
8.8 KiB
JavaScript

#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' });
});
},
};
};