modules icon indicating copy to clipboard operation
modules copied to clipboard

Invalidate cache when using import

Open jonathantneal opened this issue 6 years ago • 100 comments

How do I invalidate the cache of import?

I have a function that installs missing modules when an import fails, but the import statement seems to preserve the failure while the script is still running.

import('some-module').catch(
  // this catch will only be reached the first time the script is run because resolveMissingModule will successfully install the module
  () => resolveMissingModule('some-module').then(
    // again, this will only be reached once, but it will fail, because the import seems to have cached the previous failure
    () => import('some-module')
  )
)

The only information I found in regards to import caching was this documentation, which does not tell me where the “separate cache” used by import can be found.

No require.cache

require.cache is not used by import. It has a separate cache. — https://nodejs.org/api/esm.html#esm_no_require_cache

jonathantneal avatar Apr 05 '19 16:04 jonathantneal

the import cache is purposely unexposed. adding a query has been the generally accepted ecosystem practice to re-import something.

however, a failure to import something will not fill the cache.

this trivial program works fine for me (assuming nope.mjs does not exist):

import fs from 'fs';

import('./nope.mjs')
  .catch(() => fs.writeFileSync('./nope.mjs'))
  .then(() => import('./nope.mjs'))
  .then(console.log);

devsnek avatar Apr 05 '19 17:04 devsnek

@devsnek, hmm, might this be limited to imports that use node_modules? This similarly trivial program fails for me the first time, but not the second.

import child_process from 'child_process';

import('color-names')
  .catch(() => child_process.execSync('npm install --no-save color-names'))
  .then(() => import('color-names'))
  .then(console.log);

jonathantneal avatar Apr 05 '19 17:04 jonathantneal

Note that the JS spec requires imports to be deterministic/idempotent on a source text. Exposure of a cache would not allow you to fix the code above.

On Fri, Apr 5, 2019, 12:01 PM Gus Caplan [email protected] wrote:

the import cache is purposely unexposed. adding a query has been the generally accepted ecosystem practice to re-import something.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/nodejs/modules/issues/307#issuecomment-480349244, or mute the thread https://github.com/notifications/unsubscribe-auth/AAOUo5N5tioTq2s_t431OrihH_wH8Qagks5vd4FSgaJpZM4cfV4g .

bmeck avatar Apr 05 '19 17:04 bmeck

if its just happening with node_modules it could be https://github.com/nodejs/node/issues/26926

devsnek avatar Apr 05 '19 17:04 devsnek

can this be closd?

MylesBorins avatar Aug 29 '19 20:08 MylesBorins

I think a use case like this would hopefully be implemented as a loader. Do we already track this as a use case in that context?

hybrist avatar Aug 29 '19 20:08 hybrist

@jkrems we have old documents with that as a feature, but no success criteria examples.

bmeck avatar Aug 30 '19 13:08 bmeck

FYI, I'm implementing ESM support in Mocha (https://github.com/mochajs/mocha/pull/4038), and cannot currently implement "watch mode", whereby Mocha watches the test files, and reruns them when they change. So "watch mode" in Mocha, in the first iteration, will probably not support ESM, which is a bummer.

While we could use cache busting query parameters, that would mean that we are always increasing memory usage, and old and never-to-be-used versions of the file will continue staying in memory due to the cache holding on to them.

And I'm not sure a loader would help here, as the loader also has no access to the cache.

giltayar avatar Nov 26 '19 05:11 giltayar

An API for unloading modules certainly makes sense.

Usually with a direct registry API there is the tracing issue. An API that handles dependency removal can be useful.

A simple API might be something like -

import { unload } from ‘module’;

unload(import.meta.url); // returns true

Where the unload function would remove that module including all its dependencies from the registry. If in a cycle the whole cycle would be removed.

A subsequent module load would refresh all the loads anew.

Other problems to ensure work out is what if modules in the tree are still in-progress. I’d be tempted to say it should fail for that case and only work when all modules have either errored or completed.

We still have memory leak concerns as v8 doesn’t lend itself easily to module GC still. But Node.js can lead the way here as it should. It will be an ongoing process to get there, but the API can come first.

The main questions then seem to be:

  • are we ok exposing this as a module or should it be tied to loaders? If tied to loaders how would userland code request this? Or don’t we want it to - as in Mocha should run a new context with a loader?
  • should it be a direct registry API (get/set) with tracing, or should it be a deep API like the example above
  • finally, ironing out the partially loaded tree edge cases

On Tue, Nov 26, 2019 at 00:09 Gil Tayar [email protected] wrote:

FYI, I'm implementing ESM support in Mocha (mochajs/mocha#4038 https://github.com/mochajs/mocha/pull/4038), and cannot currently implement "watch mode", whereby Mocha watches the test files, and reruns them when they change. So "watch mode" in Mocha, in the first iteration, will probably not support ESM, which is a bummer.

While we could use cache busting query parameters, that would mean that we are always increasing memory usage, and old and never-to-be-used versions of the file will continue staying in memory due to the cache holding on to them.

And I'm not sure a loader would help here, as the loader also has no access to the cache.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/nodejs/modules/issues/307?email_source=notifications&email_token=AAESFSTBSWVLXPTWDYZOHSDQVSVQRA5CNFSM4HD5LYQKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEFEXNDI#issuecomment-558462605, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAESFSWTJXGB4J6WWNMOLRDQVSVQRANCNFSM4HD5LYQA .

guybedford avatar Nov 26 '19 05:11 guybedford

I'm really not a fan of the idea of our module cache being anything except insert-only. CJS cache modification is bad already, and CJS modules don't even form graphs.

Additionally, other runtimes (like browsers) will never expose this functionality, so some alternative system will have to be used for them regardless of what node does, in which case it seems like that system could just be used for node.

devsnek avatar Nov 26 '19 06:11 devsnek

@giltayar have you looked into using Workers or other solutions to have a module cache that you can destroy (such as by killing the Worker)?

bmeck avatar Nov 26 '19 14:11 bmeck

@bmeck - interesting. That would mean that the tests themselves run in Workers. While I am theoretically familiar with workers, I haven't yet had any experience with them: is any code that runs in the main process compatible with worker inside a worker? In other words, compatibility-wise, would all test code that works today in the "main process" work inside workers?

I wouldn't want Mocha to have a version (even a semver-major breaking one) where developers will need to tweak their code because now it's running inside a worker. I'm guessing that there's a vast amount of that code running inside Mocha, and any incompatibility would be a deal breaker.

giltayar avatar Nov 28 '19 04:11 giltayar

there are differences between workers and the main thread, mostly surrounding the functions on process, like process.exit() in a worker doesn't end the process, just the thread. There's a good list here: https://nodejs.org/api/worker_threads.html#worker_threads_class_worker

devsnek avatar Nov 28 '19 04:11 devsnek

Looking at the list, I can see process.chdir() is not available, which is probably a deal breaker in many tests (unit tests probably don't use process.chdir(), but Mocha is used for all sorts of tests), as is breaking some native add-ons (although I'm not sure how big of a problem this is in the real world).

I would hesitate to say this, as my only contribution to Mocha currently is this pull request, but I would guess that the owners would veto this. Or maybe allow this only if we add a --run-in-workers option. In any case, without looking too much at the code, this is probably a significant investment to implement for supporting ES Modules, as this is not a simple refactor, but rather an architectural change in how Mocha works.

giltayar avatar Nov 28 '19 05:11 giltayar

If it wasn't apparent from the above, I believe I would still prefer a "module unloading" API, unless the working group is adamant and official about not having one, of course. Which would probably mean going the "subprocess"/"worker" route.

giltayar avatar Nov 28 '19 05:11 giltayar

i admittedly don't know much about mocha... is using a separate process not doable either?

devsnek avatar Nov 28 '19 05:11 devsnek

I'll go back to the Mocha contributors team with this.

giltayar avatar Nov 28 '19 07:11 giltayar

Hi, I work on Mocha!! I am trying to see how we can move @giltayar's PR forward.

There are actually two situations in which "module unloading" is needed in Mocha:

  1. In "watch" mode with CJS scripts, when Mocha detects a file must be reloaded, it is deleted from require.cache and re-required, then tests are re-run. Mocha is not the only tool that does this sort of cache-busting.
  2. When developers are writing tests with Mocha (and many other test frameworks), they may want to use module-level mocking--they essentially replace one module with another phony one (I'm going to tag @theKashey here because he knows more about this ). Or even pretend like a module does not exist at all. It is then very important to Mocha that users can consume these sort of mocking frameworks to write their test code.

In the first case, it's possible, though probably at a performance cost, Mocha could leverage workers to handle ESM. I don't know enough about workers to say whether this will provide a sufficient environment for the test cases, but it feels like a misuse of the workers feature. At minimum it seems like a lot of added complexity.

In the second case, I can't see how using workers would be feasible. Test authors need to be able to mock modules on-the-fly and reference them directly from test cases, using mocking frameworks.


I don't know why this sort of behavior was omitted from the official specification. If the reasons involve "browser security", well, it further reinforces that browsers are a hostile environment for testing. I do know that this behavior is a very real need for many, from library and tooling authors down to developers working on production code.

We do need an "unload module" API; until such a thing lands, tools will be limited, implementations will be difficult (if possible), and end users will be frustrated when their tests written in ESM don't work. I will also be frustrated, because those frustrated users will complain in Mocha's issue tracker!

I'm happy to talk in further detail about use-cases, but I'm eager to put an eventual API description in the more-capable hands of people like @guybedford.

@devsnek Given that enabling it also enables tooling, I'm curious why you feel locking this sort of thing down is a better direction?

cc @nodejs/tooling

P.S. I will be at the collab summit, and the tooling group will be hosting a collaboration session; maybe this can be a topic of discussion, or vice-versa if there's a modules group meeting...?

boneskull avatar Dec 02 '19 23:12 boneskull

Do you need unloading API for the watch mode? Yes, you need it to update the changed module code.

However, it is enough to handle watch mode? No, as long as the idea is to use changed module, as you have to find parents between you(a test) and changed module, and wipe them to perform a proper reinitialization.

So - an ability to invalidate a cache line is not enough, for the mocking task we also have to know the cache graph, we could traverse and understand which work should be done.

An API for unloading modules certainly makes sense.

In my opinion - this feature is something missing for a proper code splitting. There are already 100Mb bundles, separated into hundreds of pieces, you will never load simultaneously. But you if will - there is no way to unload them. Eventually, the Page or an Application would just crash.

theKashey avatar Dec 03 '19 03:12 theKashey

@boneskull - the second case you mentioned, I believe can and should be handled by module "loaders", which are a formal way to do "require hooks" for ESM. These will enable testing frameworks (like sinon and others) to manipulate how ES modules are loaded, and, for example, exchange other modules for theirs.

The spec and implementation for that are actively being discussed and worked on by the modules working group (see https://github.com/nodejs/modules/issues/351).

giltayar avatar Dec 04 '19 05:12 giltayar

I also need this. I'm making a template rendering engine. When generating the compiled template, I read from a custom format and output to a .js file (a standard ES Module). In order to use the file, I just import it. Upon file changes, I would like to re-write the file, clear the import cache and then re-import it.

jonerer avatar Feb 19 '20 13:02 jonerer

These all sound like use cases for V8's LiveEdit debug api (https://chromedevtools.github.io/devtools-protocol/v8/Debugger#method-setScriptSource). You can call into it using https://nodejs.org/api/inspector.html. cc @giltayar @boneskull

devsnek avatar Feb 19 '20 16:02 devsnek

+1 for unloading ES Modules. It's hard to make Hot Module Reload otherwise. Not for production but for development tools. And using a ?query=x doesn't seem to work on file node 13.11.0 at least. Thanks

georges-gomes avatar Mar 20 '20 07:03 georges-gomes

@devsnek Can you provide a little example or pseudo-code on usage of setScriptSource. I have been researching for an 1hour without progress. Thanks

georges-gomes avatar Mar 20 '20 08:03 georges-gomes

@devsnek ok I progressed, I will post my findings back

georges-gomes avatar Mar 20 '20 08:03 georges-gomes

@georges-gomes you can subscribe to the Debugger.scriptParsed event to track the script id, and then when you need to modify the script you can call Debugger.setScriptSource.

devsnek avatar Mar 20 '20 09:03 devsnek

@georges-gomes If you are successful, I would be very grateful if you could post a short description on how you could use setScriptSource to solve this problem. On a blog post or something.

jonerer avatar Mar 20 '20 09:03 jonerer

@lulzmachine here is a working prototype https://gist.github.com/georges-gomes/6dc743addb90d2e7c5739bba00cf95ea

Unfinished but working. I have seen a few unexpected issues but let see how far we can get with this. Thanks @devsnek 👍

georges-gomes avatar Mar 20 '20 10:03 georges-gomes

@devsnek I get segmentation fault if I start using import in the new loaded script. I'm not sure setScriptSource supports ES Modules

georges-gomes avatar Mar 20 '20 13:03 georges-gomes

The current issues I have:

  • calling setScriptSource with the exact same source => segmentation fault

  • Loading a class from a new import and then extend the existing class =>

#
# Fatal error in , line 0
# Check failed: args[1].IsJSObject().
#
#
#
#FailureMessage Object: 0x7ffeefbf6860
 1: 0x1001000d2 node::NodePlatform::GetStackTracePrinter()::$_3::__invoke() [node]
 2: 0x100ef507f V8_Fatal(char const*, ...) [node]
 3: 0x1006576ee v8::internal::Runtime_LoadFromSuper(int, unsigned long*, v8::internal::Isolate*) [node]
 4: 0x1009b0af4 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvInRegister_NoBuiltinExit [node]
 5: 0x100a1b0ce Builtins_CallRuntimeHandler [node]
 6: 0x10093cabb Builtins_InterpreterEntryTrampoline [node]
 7: 0x10093cabb Builtins_InterpreterEntryTrampoline [node]
zsh: illegal hardware instruction

If the new import was previously imported then it works. I can't see any import happening so I can only guess that setScriptSource doesn't trigger module loading if missing.

georges-gomes avatar Mar 20 '20 13:03 georges-gomes