Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ assert(moduleExports.y === 1);
assert(await import(moduleBlock) === moduleExports); // cached in the module map
```

Importing a module block needs to be async, as module blocks may import other modules from the network. Module blocks may get imported multiple times, but will get cached in the module map and will return a reference to the same module.
Importing a module block needs to be async, as module blocks may import other modules from the network. Module blocks may get imported multiple times, but will get cached in the module map and will return a reference to the same module. You can read more about the caching behavior in [_Modules evaluation and caching_](./docs/module-caching.md).

Module blocks are only imported through dynamic `import()`, and not through `import` statements, as there is no way to address them as a specifier string.

Expand Down
178 changes: 178 additions & 0 deletions docs/module-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Modules evaluation and caching

This proposal aims to enforce some new guarantees around how module evaluations are cached and in which contexts. While ECMAScript already guarantees that importing the same specifier from the same script or module results in a single evaluation, Module Blocks increases the complexity by separating evaluation module records from compilation module records and permitting compiled module records to be passed between realms. We thus need to extend the idempotency of resolution to handle these new interactions.

## Invariants

1. Importing a module with the same string specifier twice from the same script or module results in a single evaluation (this is already guaranteed by ECMA-262):
```js
// main.js
import { check as c1 } from "./file.js";
import { check as c2 } from "./file.js";
assert(c1 === c2);

// file.js
export const check = {};
```
```js
// main.js
import { check as c1 } from "./file.js";
const { check: c2 } = await import("./file.js");
assert(c1 === c2);

// file.js
export const check = {};
```
```js
// main.js
const { check: c1 } = await import("./file.js");
const { check: c2 } = await import("./file.js");
assert(c1 === c2);

// file.js
export const check = {};
```
2. Importing a module with the same string specifier from the top-level or from any module blocks defined in the same source text module and evaluated the same Realm results in a single evaluation:
```js
// main.js
import { check as c1 } from "./file.js";
const mod = module { export { check } from "./file.js"; };
const { check: c2 } = await import(mod);
assert(c1 === c2);

// file.js
export const check = {};
```
```js
// main.js
const mod1 = module { export { check } from "./file.js"; };
const mod2 = module { export { check } from "./file.js"; };
const { check: c2 } = await import(mod1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { check: c2 } = await import(mod1);
const { check: c1 } = await import(mod1);

const { check: c2 } = await import(mod2);
assert(c2 === c2);

// file.js
export const check = {};
```
```js
import { check as c1 } from "./file.js";
const mod = module {
const nested = module { export { check } from "./file.js"; };
export const { check } = await import(nested);
};
const { check: c2 } = await import(mod);
assert(c1 === c2);

// file.js
export const check = {};
```
3. Importing the same module block twice _from the same Realm_, even if from different scripts or modules, results in a single evaluation:
```js
globalThis.count = 0;
const mod = module { globalThis.count++; };
await import(mod);
await import(mod);
assert(globalThis.count === 1);
```
```js
// main.js
import { mod, check as c1 } from "./dep.js";
const { check: c2 } = await import(mod);
assert(c1 === c2);

// dep.js
export const mod = module { export const check = {}; };
export const { check } = await import(mod);
```
4. A module block is evaluated in the same realm as where it is imported:
```js
const mod = module { globalThis.modEvaluated = true; };
const realm = createLegacyRealm(); // [1]
await realm.eval(`s => import(s)`)(mod);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is very interesting. since mod instanceof Module, what happen if I do import(mod) before or after realm.eval()? To be more clear, this is the question:

  • import(mod) !== realm.eval('s => import(s)')(mod)

Copy link

@caridy caridy Nov 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, the inconsistency here with my mental model might be coming from the fact that you might do new Module(moduleSource) twice, and it produces two different modules, while here, it is really about the module instance. And to be a lot more specific, does it affect the way you have to cache things inside a module hook? if instead of a module block, you use a programmatic module instance using new ModuleSource, and you provide a hook for resolution, which resolves to other modules instances, what happen when you import the module instance from another legacy realm? And what happen when a module instance exists but wasn't created from source?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this document a while ago and I haven't checked if it's still correct. The proposal initially cached modules based on the realm of the import call instead of the one of the module expression.

This example is probably just outdated, I have to go through the document and review it. When in doubt, please check the proposal spec text!

assert(globalThis.modEvaluated === undefined);
assert(realm.modEvaluated === true);
```
This is consistent with the behavior of imports with string specifiers.
5. Importing the same module block twice _from different Realms_ results in multiple evaluations:
```js
const realm = createLegacyRealm(); // [1]
globalThis.count = 0;
realm.count = 0;
const mod = module { globalThis.count++; export const check = {}; };
const { check: c1 } = await import(mod);
const { check: c2 } = await realm.eval("s => import(s)")(mod);
assert(globalThis.count + realm.count === 2);
assert(c1 !== c2);
```
This is consistent with the behavior of imports with string specifiers.
6. Importing two different module blocks, even with identical content, results in two evaluations and two different namespace objects. Two module blocks `mod1` and `mod2` are considered different if `mod1 !== mod2`:
```js
globalThis.count = 0;
const mod1 = module { globalThis.count++; export const check = {}; };
const mod2 = module { globalThis.count++; export const check = {}; };
const { check: c1 } = await import(mod1);
const { check: c1 } = await import(mod2);
assert(c1 !== c2);
assert(globalThis.count === 2);
```
```js
globalThis.count = 0;
const mod = module { globalThis.count++; export const check = {} };
const modClone = structuredClone(mod);
assert(mod !== modClone);
const { check: c1 } = await import(mod);
const { check: c2 } = await import(modClone);
assert(c1 !== c2);
assert(globalThis.count === 2);
```

## Invariants enforced by the HTML specification

These invariants cannot be enforced by ECMA-262, since it doesn't define how cloning works and how string specifiers are resolved. They will be respected by the HTML integration, and the champion group suggests that hosts that have a similar modules model could follow these patterns.

7. When serializing and deserializing a module block, the "base" used to resolve string specifiers (for example, the URL of the importer module) should be kept the same:
```js
// /main.js
const worker = new Worker("./worker/worker.js");
worker.postMessage(module {
import dir from "./foo.js";
assert(dir === "/");
});

// /foo.js
export default "/";

// /worker/worker.js
addEventListener("message", msg => import(msg.data));

// /worker/foo.js
export default "/worker";
```
It follows that when importing two copies of the same module block from the same realm, transitive dependencies should only be evaluated once:
```js
globalThis.count = 0;
const mod = module { export { check } from "./foo.js"; };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const mod = module { export { check } from "./foo.js"; };
const mod = module { export { check } from "./file.js"; };

const modClone = structuredClone(mod);
const { check: c1 } = await import(mod);
const { check: c2 } = await import(modClone);
assert(c1 === c2);
assert(globalThis.count === 1);

// file.js
globalThis.count++;
export const check = {};
```
We will be able to enforce the second example in ecma262 if we move the serialization&deserialization algorithms to ecma262: [tc39/ecma262#2555](https://github.com/tc39/ecma262/issues/2555).

---

[1] The `createLegacyRealm` function used in some code snippets returns the `globalThis` object of a new Realm. In browsers it can be implemented like this:

```js
function createLegacyRealm() {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
return iframe.contentWindow;
}
```