-
Notifications
You must be signed in to change notification settings - Fork 43
Add communications channels to loader outputs #525
Comments
I was leaning towards a global preload argument as well but I don't think it actually helps much. I think the more important one is to have data modules. For inter-module communication I'd prefer if we could use modules as the communication channel.
I'm not sure if ping-pong over the loader is actually the best way to achieve this. E.g. it seems pretty unfortunate if resetting a mock requires sending a message to another thread that then has to identify the originating thread, send a message back, to then finally reset the mock which was "right next" to the calling code already. I think it would be much easier to have that machinery run as a module in the affected thread.
I assume this rules out a message port when it comes to communication between preload and modules? Because a message port wouldn't be able to forward the instance?
That's an interesting idea! I think I would want to restrict that to the preload-to-module channel. E.g. the preload could gain "exports" in the CommonJS sense which the loader may choose to expose via a new property in |
I'm unclear on this idea.
Communications channels being established do not mean that you round trip through the loader, e.g. setting up a simple port that pings the global preload code using the same message channel. I agree that in general most communication should never leave the application thread.
Likely, but you also likely don't want to use an async comms channel for in-thread stuff if you need to do sync operations.
If someone wants to deal with the async hooks fallout of trying to make the bootstrap async we could try to do that. |
I'd be comfortable making it a module with lots and lots of limitations (just like it currently is a "script" with lots and lots of limitations). So I don't think it would have to be async necessarily. But likely not worth it as long as we can keep preload code as something that's relatively advanced and not necessary for most use cases. 🤞 |
For two modules that are both running in the same context and were generated by the same loader hooks, the channel could be an API exposed by a "normal" module. I'm not sure we need message ports there. // generated proxy code, exposed as my-mocking-loader:proxy?file:///some/original/url.mjs:
import {getCurrentImplementation} from 'my-mocking-loader:impl-channel';
import {f as originalF} from 'file:///some/original/url.mjs';
export function f(...args) {
return getCurrentImplementation(f, originalF)(...args);
}
// generated shared state code, exposed as my-mocking-loader:impl-channel:
export function getCurrentImplementation(fn, defaultImpl) {}
export function setCurrentImplementation(fn, impl) {} |
@jkrems I think thats a bit awkward but understand the idea. I still think they need a synchronous communications channel with the preload code and if such a thing existed you wouldn't need an intermediary module. // bikeshed
const {WebAssemblyInstantiateStreaming} = import.meta.preloadCodeMethod();
const out = await WebAssemblyInstantiateStreaming(import.meta.loaderData);
// ... set exports ... Necessitates the comms channel, and then you can always use |
It might just be a matter of preference but I don't see the "intermediate" module as an intermediate module. I see it as a first-class module that implements logic. Using a module gives a clear name and identity to that logic. Especially in a world where loader hooks want to do multiple independent things, using a single namespace (the preload code and/or some object on In my example above the channel module may be implemented as a normal But to clarify: I'm not arguing against having an optional |
On Transferring ObjectsThe first two use cases seem to be taken care of by adding the ability to transfer objects to the preload hook's interface. That way the loader can pass capabilities to the thread's initially running code, one of which could be a port to communicate back with the loader. I am assuming here that the preload hook and preload code are executed for each thread anew. That's correct, right? As to transferring objects to modules, e.g., so that a few modules can share some state, I don't think there needs to be any extra mechanism. I see two cases here:
On Interposition@jkrems, you mention that mocking code shouldn't require communication with an out-of-thread loader and that there should be some in-thread facility. I would ask a slightly different question, namely Can I interpose on an operation built into the language? If I can interpose/intercept/overload something, then I can control it, modify it, and also mock it. So far, Any realistic system needs some runtime support and that runtime support must be able to coordinate between in-thread components and loader components. I believe that adding ability to transfer objects to the preload code takes care of that. I would be reluctant to add anything more because what I just described (1) is sufficient for fully controlling code execution but (2) not yet validated by practical experience, i.e., we wouldn't even know where the pain points are. I might add that this rapidly approaches realms territory. I don't have a good sense for why realms are stuck in standards limbo, but I suspect it is for similar reasons. We just don't have enough experience yet. Deleting Modules AgainI propose another module loader hook to delete modules from the internal cache again.
Java originally didn't have the ability to garbage-collect classes and it became a limiting factor very quickly. I believe a similar situation is presenting itself here. My current loader implementation does everything I want and need, even without the ability to transfer objects. But it will eventually consume all memory. @giltayar was ready to adopt my approach for his mocking library as well and will be bitten by the same limitation. The thing is: In our use cases, we dynamically create module URLs to force reloading of modules. To that end, both of us rely on a global counter or epoch that is associated with some of the modules loaded into the application (but not necessarily all). Once the code from modules loaded during previous epochs has finished executing, it will never execute again. In other words, those modules have become pure garbage and should be collected as such. That was trivially implemented in CommonJS. It is lacking in our brave new world. That strongly suggests the addition of another hook. Now the question: Does V8 allow that? |
@apparebit please open a different issue if you want to discuss deleting modules. Currently V8 does not have any lifecycle hooks for module lifetimes and modules are increasingly aiming towards being officially unable to be GC'd due to things like how realms are looking at full specifiers which would be able to be recreated at any point during runtime. GC is only guaranteed to be done when the Realm/v8::Context a module lives is fully disposed. To my knowledge there is no communications channel that we could provide as providing the communications channel would keep the Realm alive. |
Right now loaders cannot directly share data from their implementation, the global preload code, or the modules that they generate. We need to add a communications channel such as a MessagePort between all of these locations (e.g. nodejs/node#31229 (comment) ).
Without these communications channels a few features are not feasible:
WebAssembly.instantiateStreaming()
in the global preload in order to use it inside of a module source text.I think adding a parameter to the global preload arguments is simple enough, but I do not have a clear idea on how we want to setup a channel between modules and the others. One idea is to allow putting it on
import.meta
if the module loader declares it somehow. Overall, it seems we need to implement this feature regardless of other designs.The text was updated successfully, but these errors were encountered: