Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node:module register hook chaining order behavior is incorrect #51876

Closed
yklcs opened this issue Feb 26, 2024 · 6 comments · Fixed by #51884
Closed

node:module register hook chaining order behavior is incorrect #51876

yklcs opened this issue Feb 26, 2024 · 6 comments · Fixed by #51884

Comments

@yklcs
Copy link

yklcs commented Feb 26, 2024

Version

v21.6.1

Platform

Darwin mbpll.local 23.1.0 Darwin Kernel Version 23.1.0: Mon Oct 9 21:28:12 PDT 2023; root:xnu-10002.41.9~6/RELEASE_ARM64_T8103 arm64

Subsystem

module

What steps will reproduce the bug?

Chaining hooks via multiple register calls: https://gist.github.com/yklcs/9b4182e365aead6bad32e637621fd6fb

How often does it reproduce? Is there a required condition?

No response

What is the expected behavior? Why is that the expected behavior?

Loader hooks should get executed in registration order (FIFO). This is the behavior consistent with documentation:

The chaining section for node:module states:

In this example, the registered hooks will form chains. If both first.mjs and second.mjs define a resolve hook, both will be called, in the order they were registered. The same applies to all the other hooks.

The hooks section for node:module states:

Hooks are part of a chain, even if that chain consists of only one custom (user-provided) hook and the default hook, which is always present. Hook functions nest: each one must always return a plain object, and chaining happens as a result of each function calling next<hookName>(), which is a reference to the subsequent loader's hook.

loader 1 initialize
loader 2 initialize
loader 3 initialize

loader 1 resolve
loader 2 resolve
loader 3 resolve

loader 1 load
loader 2 load
loader 3 load

What do you see instead?

Recursive calls to next<hookName> are executed in LIFO order, with the first calls happening in FIFO order:

loader 1 initialize
loader 1 resolve
loader 1 load

loader 2 initialize
loader 2 resolve
loader 1 resolve
loader 2 load
loader 1 load

loader 3 initialize
loader 3 resolve
loader 2 resolve
loader 1 resolve
loader 3 load
loader 2 load
loader 1 load

This appears to be incorrect because:

1. The same hook is executed multiple times (n^2 behavior)

2. The dependency chain between hooks is broken. For example, if we tried to import by chaining https-loader and typescript-loader (as is the intended use case for chaining hooks), https-loader must run before typescript-loader. However, the current behavior gives us the following loading order:

https-loader load

typescript-loader load
https-loader load

Or, when chained in reverse:

typescript-loader load

https-loader load
typescript-loader load

Where neither version can function as intended.

Additional information

No response

yklcs added a commit to yklcs/node that referenced this issue Feb 26, 2024
Chains module register hooks in order of registration.
Fixes: nodejs#51876
@yklcs
Copy link
Author

yklcs commented Feb 26, 2024

I'm not sure anymore what the intended behavior of register hook chaining is.
Public facing documentation describes a FIFO approach where hooks registered first are ran first, and next<hookName>() function calls the next registered function.

In this example, the registered hooks will form chains. If both first.mjs and second.mjs define a resolve hook, both will be called, in the order they were registered.

Hook functions nest: each one must always return a plain object, and chaining happens as a result of each function calling next<hookName>(), which is a reference to the subsequent loader's hook.

Meanwhile, #42623 shows that LIFO is intended, and tests are written in a way that the n^2 calling behavior above is intended.

I drafted a PR that shows how FIFO would work at #51878.

@JakobJingleheimer
Copy link
Contributor

The intended behaviour is LIFO:

A is registered first and is called last (assuming nothing special).

B is registered last and is called first.

Subsequently, C is registered dynamically, making it "last" so next chain run, it is called first.

(Node's default hook) ← A ← B ← C

@yklcs
Copy link
Author

yklcs commented Feb 26, 2024

I see. Appears to be a documentation issue if LIFO is intended, since https://nodejs.org/api/module.html is pretty clear that hooks are ran FIFO.

@JakobJingleheimer
Copy link
Contributor

Unless something changed that I'm forgetting (and I use this at my job, on node 20, so I'm pretty sure it hasn't changed), the chain is a stack.

From

[the example in the docs](https://nodejs.org/api/module.html#chaining)
// entrypoint.mjs
import { register } from 'node:module';

register('./first.mjs', import.meta.url);
register('./second.mjs', import.meta.url);
await import('./my-app.mjs');

first.mjs is registered first, and second.mjs is registered second. These names are misleading (when I wrote the original docs, I specifically chose non-ordinal names to avoid this confusion).

The subsequent dynamic import of my-app.mjs is loaded via: (node's default) ← firstsecond.

@JakobJingleheimer
Copy link
Contributor

I'll update the docs today to restore the less confusing names and specifically mention LIFO (I'm pretty sure they previously did).

@himself65
Copy link
Member

Looks same as #50886 which also confusing me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants