reflected
v0.2.7
Published
A primitive to allow workers to call synchronously any functionality exposed on the main thread.
Downloads
107
Maintainers
Readme
reflected
Social Media Photo by Marc-Olivier Jodoin on Unsplash
A primitive to allow workers to call synchronously any functionality exposed on the main thread, without blocking it.
Strategies
This module uses 3 synchronous strategies + 1 asynchronous fallback:
- message based on SharedArrayBuffer and MessageChannel, the fastest and most reliable "channel strategy" that requires headers to enable Cross Origin Isolation on the page.
- broadcast also based on SharedArrayBuffer but with BroadcastChannel instead to satisfy a long standing Firefox bug. This also requires headers to enable Cross Origin Isolation on the page.
- xhr based on synchronous XMLHttpRequest and a dedicated ServiceWorker able to intercept such POST requests, broadcast to all listening channels the request and resolve as response for the worker.
- async which will always return a Promise and will not need special headers or ServiceWorker. This is also a fallback for the xhr case if the
serviceWorkeroption field has not been provided.
All strategies are automatically detected through the default/main import but all dedicated strategies can be retrieved directly, for example:
import reflect from 'reflected'will decide automatically which strategy should be used.import reflect from 'reflected/message'will return the right message based module on the main thread and the worker mode within the worker.import reflect from 'reflected/main/message'will return the message strategy for the main thread only. This requires the least amount of bandwidth when you are sure that message strategy will work on main.import reflect from 'reflected/worker/message'will return the message strategy for the worker thread only. This requires the least amount of bandwidth when you are sure that message strategy will work within the worker.
Swap message with broadcast, xhr or async, and all exports will work equally well according to the chosen "channel strategy".
| Import | Use case |
|--------|----------|
| reflected | Auto-pick strategy (main + worker) |
| reflected/message | broadcast | xhr | async | Specific strategy, context-aware |
| reflected/main/<strategy> | Main thread only (smaller bundle) |
| reflected/worker/<strategy> | Worker only (smaller bundle) |
Requirements: Synchronous strategies except for xhr need Cross-Origin Isolation (e.g. Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp). The async strategy works without those headers and always returns a Promise.
Worker Thread API
// file: worker.js (always loaded as module)
import reflect from 'https://esm.run/reflected';
// ℹ️ must await the initialization
const reflected = await reflect({
// receives the returned data from the main thread.
// use this helper to transform such data into something
// that the worker can use/understand after invoke
// ⚠️ must be synchronous and it's invoked synchronously
onsync(response:unknown) {
return response;
},
// receives the data from the main thread when
// `worker.send(payload, ...rest)` is invoked.
// use this helper to transform the main thread request
// into something compatible with structuredClone algorithm
// ℹ️ works even if synchronous but it's resolved asynchronously
async onsend(payload) {
const data = await fetch('./data.json').then(r => r.json());
return process(payload, data);
},
});
// retrieve the result of `test_sum(1, 2, 3)`
// directly from the main thread.
// only the async channel variant would need to await
const value = reflected({
invoke: 'test_sum',
args: [1, 2, 3]
});
console.log(value); // 6Main Thread API
// file: index.js as module
import reflect from 'https://esm.run/reflected';
function test_sum(...args:number[]) {
let i = 0;
while (args.length)
i += args.pop();
return i;
}
// ℹ️ must await the initialization
const worker = await reflect(
// Worker scriptURL
'./worker.js',
// Worker options + required utilities / helpers
// ℹ️ type is enforced to be 'module' due to top-level await
{
// invoked when the worker asks to synchronize a call
// ℹ️ works even if synchronous but it's resolved asynchronously
// ⚠️ the worker is not responsive until this returns so
// be sure you handle errors gracefully to still provide
// a result the worker can consume out of the shared buffer!
async onsync(payload:unknown) {
const { invoke, args } = payload;
if (invoke === 'test_sum') {
// just demoing this can be async too
return await test_sum(...args);
}
// it is trivial to return no result or even errors
return new Error('unknown ' + invoke);
},
// *optional* helper to process data returned from the worker when
// the main thread `await worker.send(payload, ...rest)` operation
// is invoked. If not present, whatever payload the worker returned
// will be directly returned as invoke result, just like in here.
// ℹ️ works even if synchronous but it's resolved asynchronously
onsend(payload:unknown) {
return payload;
},
// optional: the initial SharedArrayBuffer length
initByteLength: 1024,
// optional: the max possible SharedArrayBuffer growth
maxByteLength: 8192,
// optional: the service worker as fallback
// * if it's a string, it's used to register it
// * if it's an object, it's used to initialize it
// but it must contain a `url` field to register it
// ℹ️ if already registered it will not try to register it
serviceWorker: undefined,
}
);
const value = await worker.send({ any: 'payload' });Test live or read the main thread and worker thread code.
Latest ffi should allow workers to drive the main thread without even needing SharedArrayBuffer but of course, if SharedArrayBuffer is available, these will be much faster.
reflected/ffi
This module also exports a bare-minimum way to directly drive, whenever the channel is not async, the main thread from a worker.
// main.js module
import reflect from 'https://esm.run/reflected/ffi/main';
// returns a worker with a special `ffi` field/namespace
// directly from reflected-ffi project
const worker = await reflect('./worker.js', { serviceWorker: './sw.js' });
// worker.js module
import reflect from 'https://esm.run/reflected/ffi/worker';
// retrieve the reflected-ffi namespace as it is
const ffi = await worker();
// will directly append a text node to the main thread body
ffi.global.document.body.append('it worked 🥳');To know more about reflected-ffi module and features, please visit the related project.
Extras
- Named export
channel: After initialization,import reflect, { channel } from 'reflected'gives the active strategy name ('message','broadcast','xhr', or'async'). - Errors: From main-thread
onsync, returnnew Int32Array(0)(or a convention of your choice) so the worker always gets a result; handle that in the worker’sonsyncto avoid hanging. - Types: you can import
MainOptionsandWorkerOptionsfrom the root of the porject because mainreflect(string, MainOptions)and workerreflect(WorkerOptions)are different in a subtle way you probably don't want to mess around with (in particular, theonsyncwhich must be sync on the worker side of affairs or it cannnot work)
