I haven't had a chance to use this library yet. Is there something about it that precludes calling wasm routines from the JavaScript closure passed to the web worker?
(I suspect, to paraphrase Greenspun's rule, any sufficiently complicated app using Web Workers contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of this library...)
I like this, but unfortunately it doesn't solve one annoying problem: lexical scope doesn't work and it will fail in an unexpected way.
If you reference something lexically, your code fails at runtime. Want to use an import? You have to use import() inside the closure you pass to spawn(). Typescript doesn't know this. Your language server doesn't know this. Access a variable that shadows a built in global? Now you're accessing the built in global.
The only way this could even be addressed is by having a full on parser. Even then you can't guarantee things will work.
I think the only "fix" is for JS to introduce a new syntax to have a function that can't access lexical scope, returning a value that either extends a subclass of Function or has a cheeky symbol set on it. At least then, it'll fail at compile time.
I've been using a functionally identical implementation of this since I wrote it in my startup's codebase a decade ago. It's really handy, but definitely not without edge case issues. I've occasionally had to put in workarounds for false positive TypeScript/lint errors or a tool in the bundling pipeline trying to be too clever and breaking the output.
Overall it's great, and I'm glad to see a generic implementation of it which will hopefully become a thriving open source project, but ultimately it's a kludge. What's really needed is for JS to introduce a native standardized version of this construct which TypeScript and the rest of the ecosystem have to play nice with.
There is a simple solution to this problem, but it's not very popular: do the same thing Workers do, require using a separate file. All the tooling works out of the box, you have no issues with lexical scoping, etc. The only downside is it's (currently) clunky to work with, but that can be fixed with better interfaces.
This looks great. If it works as well as the readme suggests, this’ll let me reach for Bun in some of the scenarios where I currently reach for Go. Typescript has become my favorite language, but the lack of efficient multithreading is sometimes a deal breaker.
Documentation here is exceptionally well written for a JS project, although move() doing different things depending on the type of data you pass to it feels like a foot-gun, and also how is it blocking access to arrays you pass to it?
From an overall system point of view, this is the current pinnacle of footgun design.
The OS does thread management and scheduling, facilitates IPC, locking, etc. All of this is one big largely-solved problem (at least for the kind of things most people are doing in JavaScript today). But because of history, we now have a very popular language and runtimes that are trying to replicate all these features, reinventing wheels, and adding layers on inefficiency to the overall execution.
I don’t disagree with you about the additional inefficiency that is very likely to accumulate as JS adds more and more ‘features’ (via the language, frameworks, or libraries). But as a genuine question, isn’t this reimplementation (or any comparable library for multithreading) required by JavaScript’s position on sandboxing. I would be suspicious of intent if browsers were allowed to spawn any number of threads to execute non-trusted scripts at the level typically seen from more native application code.
I'd be interested to see a comparison with https://piscinajs.dev/ - does this achieve more efficient data passing for example?
Lack of easy shared memory has always felt like a problem to me in this space, as often the computation I want to off-load requires (or returns) a lot of data.
> Serialization Protocol: The library uses a custom "Envelope" protocol (PayloadType.RAW vs PayloadType.LIB). This allows complex objects like Mutex handles to be serialized, sent to a worker, and rehydrated into a functional object connected to the same SharedArrayBuffer on the other side.
It's kinda "well, yes, you can't share objects, but you can share memory. So make objects that are just thin wrappers around shared memory"
Related tangent: Platformatic's NodeJS "Watt" server^1 supports parallelization with kernel-level load balancing across CPU cores. It looks like a game-changer for Node performance and efficiency in production. Apparently the new AWS "Lambda Managed Instances" do something similar (tho I'm short on details).
Hi @w4g1, this is a very beautiful API! I am curious if it supports my hypothetical use case.
I want the pre-existing DOM thread to enter into a long term relationship with a particular web worker thread. I'd like the web worker to hold onto a transferrable that has ties back to the DOM thread, the WebGLRenderingContext, and have the DOM thread send draw commands to the worker and its gl context over time.
I imagine this could be achieved by having a dedicated web worker pool of size 1 and allowing the DOM thread to initiate an async send to the worker pool.
Can your API do something like this? Thank you for this early Christmas gift!
22 comments
[ 5.7 ms ] story [ 41.3 ms ] thread(I suspect, to paraphrase Greenspun's rule, any sufficiently complicated app using Web Workers contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of this library...)
If you reference something lexically, your code fails at runtime. Want to use an import? You have to use import() inside the closure you pass to spawn(). Typescript doesn't know this. Your language server doesn't know this. Access a variable that shadows a built in global? Now you're accessing the built in global.
The only way this could even be addressed is by having a full on parser. Even then you can't guarantee things will work.
I think the only "fix" is for JS to introduce a new syntax to have a function that can't access lexical scope, returning a value that either extends a subclass of Function or has a cheeky symbol set on it. At least then, it'll fail at compile time.
Overall it's great, and I'm glad to see a generic implementation of it which will hopefully become a thriving open source project, but ultimately it's a kludge. What's really needed is for JS to introduce a native standardized version of this construct which TypeScript and the rest of the ecosystem have to play nice with.
Writing module bundlers in Javascript had diminishing returns from multi threading because of the overhead of serializing and deserializing ASTs.
I wonder how far something like this would push the ceiling. Would love to see some benchmarks of this thing hauling ASTs around.
The OS does thread management and scheduling, facilitates IPC, locking, etc. All of this is one big largely-solved problem (at least for the kind of things most people are doing in JavaScript today). But because of history, we now have a very popular language and runtimes that are trying to replicate all these features, reinventing wheels, and adding layers on inefficiency to the overall execution.
Sigh.
Lack of easy shared memory has always felt like a problem to me in this space, as often the computation I want to off-load requires (or returns) a lot of data.
> Serialization Protocol: The library uses a custom "Envelope" protocol (PayloadType.RAW vs PayloadType.LIB). This allows complex objects like Mutex handles to be serialized, sent to a worker, and rehydrated into a functional object connected to the same SharedArrayBuffer on the other side.
It's kinda "well, yes, you can't share objects, but you can share memory. So make objects that are just thin wrappers around shared memory"
One such example: https://github.com/developit/workerize-loader
1. https://www.platformatichq.com/watt
I want the pre-existing DOM thread to enter into a long term relationship with a particular web worker thread. I'd like the web worker to hold onto a transferrable that has ties back to the DOM thread, the WebGLRenderingContext, and have the DOM thread send draw commands to the worker and its gl context over time.
I imagine this could be achieved by having a dedicated web worker pool of size 1 and allowing the DOM thread to initiate an async send to the worker pool.
Can your API do something like this? Thank you for this early Christmas gift!