Repository navigation
Replace globalPreload #147
Description
Activity
The most intuitive way for me is that the
registerreturns aportthat is used to communicate with this loader (how is this done today? It can't be the same port for all the loaders, and yet they all run in the same worker). In this option, we would have a function in the loader (initialize) which will accept the port to be used to communicate.Another variant of this option is for the
registeroption to accept aMessageChannel(instead of creating it and returning it), which the user ofregistercan use to communicate with the loader. The sameinitializefunction can be used (we can even callglobalPreloadwith theportfor backward compatibility! 😁). The nice thing about this option is that if the user doesn't pass theMessageChannelto theregister, then it signals that there is no need to callinitializeand no need forMessagePortsupport.In addition to passing a
MessageChannel, theregistershould allow passing any data whatsoever to theinitialize, e.g. the userland code can now passprocess.argvto the loader to satisfy the requirement in nodejs/help#4190. And actually, the user ofregistercan just create aMessageChannel, pass theport2via this data (it is a transferable object) and that's it: no need for any Node.js code for ports.To summarize the above and define what is for me the best option:
registercan pass any data it wants to the loader, which will be passed to the exportedinitializefunction of the loader. Additionally, if the user ofregisterwants to communicate with the loader, it can just create aMessageChanneland pass the port to the loader as data.Reacted by Matteo Collina and Michaël ZassoReacted by Jacob SmithReacted by Jacob SmithI like that idea! I made a quick PoC here: targos/node@c4f5e91
$ ./node --no-warnings --loader "data:text/javascript," poc.mjs initialize loader { entryPoint: '/Users/mzasso/git/nodejs/node/poc.mjs' } loader initializedReacted by Colin Ihrig, Matteo Collina, Jacob Smith and kirrg001Reacted by Jacob Smiththat'd be amazing! PR?
Reacted by Akinte ToluwalaseJakobJingleheimer commented
on Jun 21, 2023 MemberAuthorMore actionsSweeeet!
Lemme get the existing register PR stable (I think there are 1–2 small issues with it remaining), and then let's get that in 🎉
PS haven't had time to thoroughly review Gil's ideas, but I liked where they're going.
Reacted by Akinte ToluwalaseOne thing to consider is that the point of the communications port isn’t just to pass data at initialization time. There are plenty of use cases where loaders need to communicate back and forth to the main thread after initialization.
Now maybe all this means is that we should rename
initializeto something likeonMessage, if it runs on every message, orinitializecould return a function that runs on every message. But we should handle this use case in the design somehow.Edit: I just looked at the POC and I see the intent, that you’re passing in the port and can register callbacks to it independently from us needing to provide infrastructure for them. I think it can work.
Before converting this into a full-fledged PR, can someone update the docs in the POC so that I can see what the intended UX is supposed to be? I think it would be good to workshop that before someone goes and implements it. What I think is the proposed UX seems fine to me at first glance, but having it spelled out in the instructions we would write for loader authors would help me fully understand it.
@targos @GeoffreyBooth how is this going? I think this is blocking a few use cases to fully support Node.js v20.
Happy to help in shipping this.
JakobJingleheimer commented
on Jul 10, 2023 MemberAuthorMore actions@targos @GeoffreyBooth how is this going? I think this is blocking a few use cases to fully support Node.js v20.
Happy to help in shipping this.
I just got back from vacation.
Before I left, there appeared to be 1 issue in nodejs/node#48439 related to sequence that I didn't have time to resolve. Picking back up this week (you're welcome to take a gander at it too).
PS It looks like a duplicate PR attempting to do the same thing was started whilst I was away. I haven't had a chance yet to look at it.
Reacted by Geoffrey BoothPS It looks like a duplicate PR attempting to do the same thing was started whilst I was away. I haven't had a chance yet to look at it.
@GeoffreyBooth @JakobJingleheimer I updated my PR to address this issue 😄 I followed the proposal from @giltayar and the signature for
registeris updated to be able to pass arbitrary data to a loader'sinitializehook, including two additional tests demonstrating the capability. This change exists in its own commit in order to be able to be reviewed separately.Note, however, that I opted to add an additional specific
transferListparameter instead of attempting to automatically deeply inspect the sent data, so the signature forregisteris now:register(loader: string, parentUrl?: string, data?: any, transferList?: any[]): unknown;
The return value from the
initializehook is sent back, but there is currently no affordance for this to also provide atransferListand thus ports or other shared buffers cannot currently be returned frominitialize.I did not remove
globalPreloadas I am not yet comfortable enough knowing which bits and pieces are safe to pull out or if a deprecation notice is preferred over simply deleting the whole thing.This change exists in its own commit in order to be able to be reviewed separately.
Thank you so much! Do you mind opening this as its own PR? And it can include/depend on the earlier one. And the earlier PR can exclude the new commit so that it can be reviewed on its own.
Reacted by Izaak SchroederDone!
PR is here: izaakschroeder/node#1 I'm not sure how I can put the PR on node's repo without the base branch also living in node's repo 🤔 Would you prefer I just target
node:maininstead and include the previous commit? Or?I’m not sure how I can put the PR on node’s repo
So your first PR is based on your
support-nested-loader-chainsbranch. I suggest that you:- Branch off of that branch in your repo, something like
support-nested-loader-chains-and-register-args, so that the two branches both point to the same commit. - Rebase the
support-nested-loader-chainsbranch to drop the newest,register-related commit. - Force-push
support-nested-loader-chainsto your repo. This will reset the PR to exclude the newest commit. - Open a new PR with
support-nested-loader-chains-and-register-argsagainstnode:mainand mention in the PR description that it builds off esm: unflagModule.registerand allow nested loaderimport()node#48559.
That’s it. This is the general procedure for splitting a large PR into smaller ones.
Reacted by Akinte Toluwalase- Branch off of that branch in your repo, something like
Note, however, that I opted to add an additional specific
transferListparameter instead of attempting to automatically deeply inspect the sent data, so the signature forregisteris now:Why do we have both
dataandtransferListas separate arguments? What’s the difference between them?@izaakschroeder did you see/use @targos’ POC in #147 (comment)?
Reacted by Akinte Toluwalase- added a commit that references this issue
on Jul 19, 2023 21 remaining items
- added a commit that references this issue
on Aug 17, 2023 JakobJingleheimer commented
on Aug 21, 2023 MemberAuthorMore actionsThe question (or is it a thinly-veiled ask? 😁) is this: how can I know whether I already registered the loader or not?
As far as what node will tell you, there's no straightforward way to determine that (ex you can't do
registered.has(myLoader)). I can think of a couple heinous ways to figure it out. Where do you need to know–inimport's module, in your loader, or from the app/entry-point?True! But what happens when
testdouble.jsis loaded twice because it is used in two workers?Do you mean node's ESM worker? I think there may currently be unintended/undesired behaviour making that possible, but we'll squash that.
if I have two workers that want to communicate with the loader.
What are these workers / where did they come from? Are they spawned by the entry point?
@giltayar maybe? I can help… I built much of the new
registermachinery so perhaps I could try to build out an example that fits here?testdouble-super-register.jsimport {MessageChannel, Worker, isMainThread, workerData} from 'node:worker_threads'; import {register} from 'node:module'; const TestDoubleLock = Symbol(); // > True! But what happens when testdouble.js is loaded twice because it is used in two workers? // This will prevent testdouble.js from executing `register` multiple times from other workers // Node _should_ generally only execute a single module once but I people could get likely // around that if they tried. You could add an additional guard on `globalThis` which is shared // between every module on a single V8 context (~thread). if (isMainThread && !globalThis[TestDoubleLock]) { globalThis[TestDoubleLock] = true; const controlChannel = new MessageChannel(); const result = register('testdouble-super-loader.js', { parentURL: import.meta.url, data: {port: controlChannel.port2}, transferList: [controlChannel.port2], }); if (result.error) { throw new Error('Somehow registered testdouble twice!'); } const spawnWorker = () => { const workerChannel = new MessageChannel(); // > if I have _two_ workers that want to communicate with the loader. // You can pass ports around – one that goes to the loader and one to the worker. new Worker(import.meta.url, { workerData: {port: workerChannel.port1}, transferList: [workerChannel.port1], }); controlChannel.port1.postMessage( {type: 'NEW_WORKER', port: workerChannel.port2}, [workerChannel.port2] ); }; // Do what you need to do. spawnWorker(); spawnWorker(); spawnWorker(); } else { workerData.port.postMessage({ type: 'THIS_GOES_TO_LOADER' }); }
testdouble-super-loader.jsconst handleMessage = (msg) => { switch(msg.type) { case 'NEW_WORKER': msg.port.on('message', handleMessage); break; case 'THIS_GOES_TO_LOADER': // YOUR LOGIC HERE FROM YOUR WORKERS break; } } // > The question (or is it a thinly-veiled ask? 😁) is this: how can I // > know whether I already registered the loader or not? // You _CAN_ know this here; `initialize` is called each time the // loader is registered. // // *IMPORTANT*: There is currently no way for this value to be // mucked up, but if/when `deregister` lands to remove a loader // then we may need a `deinitialize` hook or similar to keep the // value here correct. let lock = 0; const initialize = (data) => { if (lock > 0) { // Loader has already been registered! return {error: true}; } lock++; data.port.on('message', handleMessage); return {error: false}; }
For what it's worth I'm also not against exposing a
getLoaders()type method onnode:module. I don't think it would be hard to implement and we could have it store the results ofinitializeacross the thread bridge.register(originalSpecifier, parentURL, data, transferList) { const id = ''; // TBD: Determine how to pull this out. this.loaders[id] = hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data); return this.loaders[id]; } deregister(id) { const result = hooksProxy.makeSyncRequest('deregister', undefined, id); delete this.loaders[id]; return result; } getLoaders() { return Object.values(this.loaders); }
JakobJingleheimer commented
on Aug 21, 2023 MemberAuthorMore actionsWould we want to expose the whole registry or would just name and position do the job? Would someone want to use the loader or just know about it?
Would we want to expose the whole registry or would just name and position do the job? Would someone want to use the loader or just know about it?
I'm… not sure… I guess we would probably want to define an interface for what it returns… I assume at least the
id… but maybe more.has()would be safer thangetLoaders(), since programs might be relying on the function they pass in not being reachable by userland code (same reason browsers don't have a "get all event listeners" function)Reacted by Jacob SmithI don't want to create APIs without a clear use case. If loader authors can solve this problem themselves with a trivial amount of code, I'd leave it at that until it becomes clear that the existing solution is insufficient somehow.
Reacted by Jordan Harband- added a commit that references this issue
on Nov 11, 2023 - added a commit that references this issue
on Nov 23, 2023 - added 2 commits that reference this issue
on Apr 25, 2024 I think this is super obsolete, so closing.
This hook is extremely confusing, even for maintainers.
The replacement needs to address:
process.argv) as identified by ESM loader v20.3.0 access parent path help#4190 (eg an ESM equivalent ofrequire.main)Assuming there is 1 loader worker.
Spit-balling ideas:
node:moduleregister()Will a message port need to be refreshed?
@giltayar,I would be very interested in your thoughts (or even a design proposal) for this, especially because you recently handled this in testdouble, so the pain is fresh 😅