flash
cf71bab92d
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.
282 lines
8.8 KiB
JavaScript
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' });
|
|
});
|
|
},
|
|
};
|
|
};
|