-
Notifications
You must be signed in to change notification settings - Fork 18
Add "Modules evaluation and caching" docs #68
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
base: main
Are you sure you want to change the base?
Changes from all commits
f06cd98
4f894b7
2e37651
bfd8003
ff13c9f
8f6aa5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||||||
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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this one is very interesting. since
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; }; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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; | ||||||
} | ||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.