coincident
Version:
An Atomics based Proxy to simplify, and synchronize, Worker related tasks
139 lines (123 loc) • 3.42 kB
JavaScript
// (c) Andrea Giammarchi - MIT
import {
ACTION_INIT,
ACTION_NOTIFY,
ACTION_WAIT,
Atomics,
isChannel,
withResolvers,
} from 'sabayon/shared';
const { BYTES_PER_ELEMENT: I32_BYTES } = Int32Array;
const { BYTES_PER_ELEMENT: UI16_BYTES } = Uint16Array;
const { notify } = Atomics;
const decoder = new TextDecoder('utf-16');
const buffers = new WeakSet;
const transfer = (...args) => (buffers.add(args), args);
let seppuku = '';
const actionLength = (stringify, transform) => async (callback, results, [name, id, sb, args, isSync]) => {
if (isSync) seppuku = name;
try {
const result = await callback(...args);
if (result !== void 0) {
const serialized = stringify(transform ? transform(result) : result);
results.set(id, serialized);
sb[1] = serialized.length;
}
}
finally {
if (isSync) seppuku = '';
sb[0] = 1;
notify(sb, 0);
}
};
const actionFill = (results, [id, sb]) => {
const result = results.get(id);
results.delete(id);
for (let ui16a = new Uint16Array(sb.buffer), i = 0, { length } = result; i < length; i++)
ui16a[i] = result.charCodeAt(i);
notify(sb, 0);
};
const actionWait = (waitLength, results, map, rest) => {
const [name] = rest;
const callback = map.get(name);
if (!callback) throw new Error(`Unknown proxy.${name}()`);
waitLength(callback, results, rest);
};
const warn = (name, seppuku) => setTimeout(
console.warn,
3e3,
`💀🔒 - proxy.${name}() in proxy.${seppuku}()`
);
let uid = 0;
const invoke = (
[
CHANNEL,
i32View,
ignore,
isSync,
parse,
polyfill,
postMessage,
transform,
waitAsync,
],
name,
) => (...args) => {
let deadlock = seppuku !== '', timer = 0;
// slow operations between main and worker should likely not
// be tracked as the dance there would never deadlock, it just
// eventually slow down and that's it
if (deadlock && seppuku[0] !== '=' && seppuku[0] !== '-')
timer = warn(name, seppuku);
const id = uid++;
let transfer = [];
if (buffers.has(args.at(-1) || transfer))
buffers.delete(transfer = args.pop());
const data = ignore(transform ? args.map(transform) : args);
let sb = i32View(I32_BYTES * 2);
postMessage([CHANNEL, ACTION_WAIT, name, id, sb, data, isSync], { transfer });
return waitAsync(sb, 0).value.then(() => {
if (deadlock) clearTimeout(timer);
const length = sb[1];
if (!length) return;
const bytes = UI16_BYTES * length;
sb = i32View(bytes + (bytes % I32_BYTES));
postMessage([CHANNEL, ACTION_NOTIFY, id, sb]);
return waitAsync(sb, 0).value.then(() =>{
const ui16a = new Uint16Array(sb.buffer);
const sub = polyfill ? ui16a.subarray(0, length) : ui16a.slice(0, length);
return parse(decoder.decode(sub));
});
});
};
const createProxy = (details, map) => new Proxy(map, {
get: (map, name) => {
let cb;
// the curse of potentially awaiting proxies in the wild
// requires this ugly guard around `then`
if (name !== 'then') {
cb = map.get(name);
if (!cb) {
cb = invoke(details, name);
map.set(name, cb);
}
}
return cb;
},
set: (map, name, callback) => (
name !== 'then' &&
!!map.set(name, callback)
),
});
export {
ACTION_INIT,
ACTION_WAIT,
ACTION_NOTIFY,
actionLength,
actionFill,
actionWait,
createProxy,
isChannel,
transfer,
withResolvers,
};