DuyTa's picture
Upload folder using huggingface_hub
bc20498 verified
raw
history blame
8.65 kB
'use strict';
/*! (c) Andrea Giammarchi - ISC */
const {FUNCTION} = require('proxy-target/types');
const {CHANNEL} = require('./channel.js');
const {GET, HAS, SET} = require('./shared/traps.js');
const {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync} = require('./bridge.js');
// just minifier friendly for Blob Workers' cases
const {Int32Array, Map, Uint16Array} = globalThis;
// common constants / utilities for repeated operations
const {BYTES_PER_ELEMENT: I32_BYTES} = Int32Array;
const {BYTES_PER_ELEMENT: UI16_BYTES} = Uint16Array;
const waitInterrupt = (sb, delay, handler) => {
while (wait(sb, 0, 0, delay) === 'timed-out')
handler();
};
// retain buffers to transfer
const buffers = new WeakSet;
// retain either main threads or workers global context
const context = new WeakMap;
const syncResult = {value: {then: fn => fn()}};
// used to generate a unique `id` per each worker `postMessage` "transaction"
let uid = 0;
/**
* @typedef {Object} Interrupt used to sanity-check interrupts while waiting synchronously.
* @prop {function} [handler] a callback invoked every `delay` milliseconds.
* @prop {number} [delay=42] define `handler` invokes in terms of milliseconds.
*/
/**
* Create once a `Proxy` able to orchestrate synchronous `postMessage` out of the box.
* @param {globalThis | Worker} self the context in which code should run
* @param {{parse: (serialized: string) => any, stringify: (serializable: any) => string, transform?: (value:any) => any, interrupt?: () => void | Interrupt}} [JSON] an optional `JSON` like interface to `parse` or `stringify` content with extra `transform` ability.
* @returns {ProxyHandler<globalThis> | ProxyHandler<Worker>}
*/
const coincident = (self, {parse = JSON.parse, stringify = JSON.stringify, transform, interrupt} = JSON) => {
// create a Proxy once for the given context (globalThis or Worker instance)
if (!context.has(self)) {
// ensure no SAB gets a chance to pass through this call
const sendMessage = postPatched || self.postMessage;
// ensure the CHANNEL and data are posted correctly
const post = (transfer, ...args) => sendMessage.call(self, {[CHANNEL]: args}, {transfer});
const handler = typeof interrupt === FUNCTION ? interrupt : interrupt?.handler;
const delay = interrupt?.delay || 42;
const decoder = new TextDecoder('utf-16');
// automatically uses sync wait (worker -> main)
// or fallback to async wait (main -> worker)
const waitFor = (isAsync, sb) => isAsync ?
waitAsync(sb, 0) :
((handler ? waitInterrupt(sb, delay, handler) : wait(sb, 0)), syncResult);
// prevent Harakiri https://github.com/WebReflection/coincident/issues/18
let seppuku = false;
context.set(self, new Proxy(new Map, {
// there is very little point in checking prop in proxy for this very specific case
// and I don't want to orchestrate a whole roundtrip neither, as stuff would fail
// regardless if from Worker we access non existent Main callback, and vice-versa.
// This is here mostly to guarantee that if such check is performed, at least the
// get trap goes through and then it's up to developers guarantee they are accessing
// stuff that actually exists elsewhere.
[HAS]: (_, action) => typeof action === 'string' && !action.startsWith('_'),
// worker related: get any utility that should be available on the main thread
[GET]: (_, action) => action === 'then' ? null : ((...args) => {
// transaction id
const id = uid++;
// first contact: just ask for how big the buffer should be
// the value would be stored at index [1] while [0] is just control
let sb = new Int32Array(new SharedArrayBuffer(I32_BYTES * 2));
// if a transfer list has been passed, drop it from args
let transfer = [];
if (buffers.has(args.at(-1) || transfer))
buffers.delete(transfer = args.pop());
// ask for invoke with arguments and wait for it
post(transfer, id, sb, action, transform ? args.map(transform) : args);
// helps deciding how to wait for results
const isAsync = self !== globalThis;
// warn users about possible deadlock still allowing them
// to explicitly `proxy.invoke().then(...)` without blocking
let deadlock = 0;
if (seppuku && isAsync)
deadlock = setTimeout(console.warn, 1000, `πŸ’€πŸ”’ - Possible deadlock if proxy.${action}(...args) is awaited`);
return waitFor(isAsync, sb).value.then(() => {
clearTimeout(deadlock);
// commit transaction using the returned / needed buffer length
const length = sb[1];
// filter undefined results
if (!length) return;
// calculate the needed ui16 bytes length to store the result string
const bytes = UI16_BYTES * length;
// round up to the next amount of bytes divided by 4 to allow i32 operations
sb = new Int32Array(new SharedArrayBuffer(bytes + (bytes % I32_BYTES)));
// ask for results and wait for it
post([], id, sb);
return waitFor(isAsync, sb).value.then(() => parse(
decoder.decode(new Uint16Array(sb.buffer).slice(0, length)))
);
});
}),
// main thread related: react to any utility a worker is asking for
[SET](actions, action, callback) {
const type = typeof callback;
if (type !== FUNCTION)
throw new Error(`Unable to assign ${action} as ${type}`);
// lazy event listener and logic handling, triggered once by setters actions
if (!actions.size) {
// maps results by `id` as they are asked for
const results = new Map;
// add the event listener once (first defined setter, all others work the same)
self.addEventListener('message', async (event) => {
// grub the very same library CHANNEL; ignore otherwise
const details = event.data?.[CHANNEL];
if (isArray(details)) {
// if early enough, avoid leaking data to other listeners
event.stopImmediatePropagation();
const [id, sb, ...rest] = details;
let error;
// action available: it must be defined/known on the main thread
if (rest.length) {
const [action, args] = rest;
if (actions.has(action)) {
seppuku = true;
try {
// await for result either sync or async and serialize it
const result = await actions.get(action)(...args);
if (result !== void 0) {
const serialized = stringify(transform ? transform(result) : result);
// store the result for "the very next" event listener call
results.set(id, serialized);
// communicate the required SharedArrayBuffer length out of the
// resulting serialized string
sb[1] = serialized.length;
}
}
catch (_) {
error = _;
}
finally {
seppuku = false;
}
}
// unknown action should be notified as missing on the main thread
else {
error = new Error(`Unsupported action: ${action}`);
}
// unlock the wait lock later on
sb[0] = 1;
}
// no action means: get results out of the well known `id`
// wait lock automatically unlocked here as no `0` value would
// possibly ever land at index `0`
else {
const result = results.get(id);
results.delete(id);
// populate the SharedArrayBuffer with utf-16 chars code
for (let ui16a = new Uint16Array(sb.buffer), i = 0; i < result.length; i++)
ui16a[i] = result.charCodeAt(i);
}
// release te worker waiting either the length or the result
notify(sb, 0);
if (error) throw error;
}
});
}
// store this action callback allowing the setter in the process
return !!actions.set(action, callback);
}
}));
}
return context.get(self);
};
coincident.transfer = (...args) => (buffers.add(args), args);
module.exports = coincident;