rollup-plugin-concurrent-top-level-await 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +105 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ This Vite-compatible plugin enables concurrent execution of TLA modules.
|
|
|
8
8
|
Note that this plugin requires TLA support at runtime; it does _not_ provide a TLA polyfill.
|
|
9
9
|
For that, check out [vite-plugin-top-level-await](https://www.npmjs.com/package/vite-plugin-top-level-await).
|
|
10
10
|
|
|
11
|
+
### Evaluation Order
|
|
12
|
+
|
|
13
|
+
The evaluation order closely matches V8's behavior according to [tla-fuzzer](https://github.com/evanw/tla-fuzzer).
|
|
14
|
+
Minor deviations can still occur though.
|
|
15
|
+
|
|
16
|
+
| Variant | Rollup | Rollup with Plugin |
|
|
17
|
+
| ------------------------ | ------ | ------------------ |
|
|
18
|
+
| Simple | 80% | 99% |
|
|
19
|
+
| Trailing Promise | 10% | 99% |
|
|
20
|
+
| Cyclic | 69% | 99% |
|
|
21
|
+
| Cyclic, Trailing Promise | 15% | 99% |
|
|
22
|
+
|
|
11
23
|
## Installation
|
|
12
24
|
|
|
13
25
|
Using npm:
|
|
@@ -41,7 +53,9 @@ export default {
|
|
|
41
53
|
|
|
42
54
|
### Which modules to include?
|
|
43
55
|
|
|
44
|
-
The plugin needs to handle not only modules that directly contain a top-level `await`, but also their ancestor modules up to the lowest common ancestor. Ancestor modules must be transformed to handle the asynchronous completion of their children concurrently.
|
|
56
|
+
The plugin needs to handle not only modules that directly contain a top-level `await`, but also their ancestor modules up to the lowest common ancestor. Ancestor modules must be transformed to handle the asynchronous completion of their children concurrently. If an ancestor module is not transformed, its direct dependencies will become blocking and therefore alter the evaluation order.
|
|
57
|
+
|
|
58
|
+
Consider the following module structure as an example:
|
|
45
59
|
|
|
46
60
|
```mermaid
|
|
47
61
|
flowchart LR
|
|
@@ -75,22 +89,11 @@ If the red modules contain top level awaits, these and their yellow ancestors sh
|
|
|
75
89
|
|
|
76
90
|
## Known Limitations
|
|
77
91
|
|
|
78
|
-
###
|
|
79
|
-
|
|
80
|
-
We currently prioritize minimizing the required code transformations over complete compliance with the standard.
|
|
81
|
-
As a result, the execution order of TLA modules may differ from the standard behavior in certain cases, as can be seen
|
|
82
|
-
by the results for [tla-fuzzer](https://github.com/evanw/tla-fuzzer):
|
|
83
|
-
|
|
84
|
-
| Variant | Rollup | Rollup with Plugin |
|
|
85
|
-
| ------------------------ | ------ | ------------------ |
|
|
86
|
-
| Simple | 80% | 100% |
|
|
87
|
-
| Trailing Promise | 10% | 94% |
|
|
88
|
-
| Cyclic | 69% | 77% |
|
|
89
|
-
| Cyclic, Trailing Promise | 15% | 64% |
|
|
92
|
+
### Evaluation Order
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
As can be seen in the table above, the plugin does not guarantee 100% matching of V8's evaluation order, although the deviations should be pretty minor in practice. If you encounter significant deviations, please open an issue.
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
Additionally, some scenarios are known to cause deviations, e.g. when the [`include` option is not correctly configured](#which-modules-to-include) or when there is a dependency cycle that is split across multiple chunks.
|
|
94
97
|
|
|
95
98
|
### Build Performance
|
|
96
99
|
|
package/dist/index.d.mts
CHANGED
|
@@ -17,6 +17,8 @@ declare function concurrentTopLevelAwait(options?: {
|
|
|
17
17
|
}): {
|
|
18
18
|
name: string;
|
|
19
19
|
apply: "build";
|
|
20
|
+
resolveId(this: rollup0.PluginContext, source: string): string | undefined;
|
|
21
|
+
load(this: rollup0.PluginContext, id: string): "export default function register(fn, evaluate_accesses) {\n let state = \"ready\";\n let whenDones = [];\n let whenErrors = [];\n let remaining = 1;\n let error = undefined;\n // evaluate eagerly\n evaluate();\n\n return evaluate;\n\n function evaluate(whenDone, onError) {\n if (state === \"done\") {\n if (whenDone)\n whenDone();\n return;\n }\n if (state === \"failed\") {\n if (onError)\n onError(error);\n return;\n }\n if (whenDone)\n whenDones.push(whenDone);\n if (onError)\n whenErrors.push(onError);\n if (state === \"busy\") {\n return;\n }\n state = \"busy\";\n let moduleDone = () => {\n state = \"done\";\n for (let x of whenDones)\n x();\n };\n let moduleError = (err) => {\n state = \"failed\";\n error = err;\n for (let x of whenErrors)\n x(err);\n };\n let importDone = () => {\n if (state !== \"busy\")\n return;\n if (--remaining !== 0)\n return;\n try {\n let result = fn();\n if (result) {\n result\n .then(moduleDone)\n .catch(moduleError);\n }\n else {\n moduleDone();\n }\n }\n catch (err) {\n moduleError(err);\n }\n };\n for (const access of evaluate_accesses) {\n let evaluate;\n try {\n // Environment-dependent behavior:\n // - throws on cyclic dependencies in V8\n // - returns undefined in some environments (e.g., vitest)\n evaluate = access();\n if (evaluate == null)\n continue;\n }\n catch (_a) {\n continue;\n }\n remaining++;\n evaluate(importDone, moduleError);\n }\n importDone();\n }\n}\n" | undefined;
|
|
20
22
|
transform: {
|
|
21
23
|
handler(this: TransformPluginContext, code: string, id: string, transformOptions: {
|
|
22
24
|
ssr?: boolean | undefined;
|
package/dist/index.mjs
CHANGED
|
@@ -128,26 +128,14 @@ var AsyncModuleTracker = class {
|
|
|
128
128
|
|
|
129
129
|
//#endregion
|
|
130
130
|
//#region src/transform.ts
|
|
131
|
-
function transform(s, ast, asyncImports, hasAwait, variablePrefix) {
|
|
131
|
+
function transform(s, ast, registerModuleSource, asyncImports, hasAwait, variablePrefix) {
|
|
132
132
|
const declarationsEnd = transformAndMoveDeclarationsToModuleScope(s, ast, asyncImports, variablePrefix);
|
|
133
|
-
s.appendRight(declarationsEnd,
|
|
133
|
+
s.appendRight(declarationsEnd, `${hasAwait ? "async " : ""}function ${variablePrefix}_initModuleExports() {\n`);
|
|
134
134
|
s.append("\n}\n");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return result
|
|
140
|
-
}
|
|
141
|
-
return [a];
|
|
142
|
-
} catch {
|
|
143
|
-
return []; // happens for cyclic dependencies
|
|
144
|
-
}
|
|
145
|
-
})`;
|
|
146
|
-
const initModuleExportsAfterDeps = asyncImports.length === 0 ? `${variablePrefix}_initModuleExports()` : `Promise.all(${asyncDeps}.map(e => e())).then(() => ${variablePrefix}_initModuleExports())`;
|
|
147
|
-
if (hasAwait) s.append(`const ${variablePrefix} = ${initModuleExportsAfterDeps};\nconst ${variablePrefix}_initPromise = ${variablePrefix};\n`);
|
|
148
|
-
else s.append(`const ${variablePrefix} = ${asyncDeps};\nconst ${variablePrefix}_initPromise = ${initModuleExportsAfterDeps};\n`);
|
|
149
|
-
s.append(`if (import.meta.useTla) await ${variablePrefix}_initPromise;\n`);
|
|
150
|
-
s.append(`export function ${variablePrefix}_access() { return ${variablePrefix}; };\n`);
|
|
135
|
+
s.prepend(`import ${variablePrefix}_register from ${JSON.stringify(registerModuleSource)};\n`);
|
|
136
|
+
const asyncDeps = `[${asyncImports.map((_, i) => `() => ${variablePrefix}${i}`).join(", ")}]`;
|
|
137
|
+
s.append(`export const ${variablePrefix}_access = ${variablePrefix}_register(${variablePrefix}_initModuleExports, ${asyncDeps});\n`);
|
|
138
|
+
s.append(`if (import.meta.useTla) await new Promise(${variablePrefix}_access);\n`);
|
|
151
139
|
}
|
|
152
140
|
function transformAndMoveDeclarationsToModuleScope(s, ast, asyncImports, variablePrefix) {
|
|
153
141
|
let moduleScopeEnd = 0;
|
|
@@ -250,12 +238,100 @@ function resolveDeclarationSource(context, id, importerAttributes = {}, declarat
|
|
|
250
238
|
custom: {}
|
|
251
239
|
});
|
|
252
240
|
}
|
|
241
|
+
const tlaModule = `export default function register(fn, evaluate_accesses) {
|
|
242
|
+
let state = "ready";
|
|
243
|
+
let whenDones = [];
|
|
244
|
+
let whenErrors = [];
|
|
245
|
+
let remaining = 1;
|
|
246
|
+
let error = undefined;
|
|
247
|
+
// evaluate eagerly
|
|
248
|
+
evaluate();
|
|
249
|
+
|
|
250
|
+
return evaluate;
|
|
251
|
+
|
|
252
|
+
function evaluate(whenDone, onError) {
|
|
253
|
+
if (state === "done") {
|
|
254
|
+
if (whenDone)
|
|
255
|
+
whenDone();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (state === "failed") {
|
|
259
|
+
if (onError)
|
|
260
|
+
onError(error);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (whenDone)
|
|
264
|
+
whenDones.push(whenDone);
|
|
265
|
+
if (onError)
|
|
266
|
+
whenErrors.push(onError);
|
|
267
|
+
if (state === "busy") {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
state = "busy";
|
|
271
|
+
let moduleDone = () => {
|
|
272
|
+
state = "done";
|
|
273
|
+
for (let x of whenDones)
|
|
274
|
+
x();
|
|
275
|
+
};
|
|
276
|
+
let moduleError = (err) => {
|
|
277
|
+
state = "failed";
|
|
278
|
+
error = err;
|
|
279
|
+
for (let x of whenErrors)
|
|
280
|
+
x(err);
|
|
281
|
+
};
|
|
282
|
+
let importDone = () => {
|
|
283
|
+
if (state !== "busy")
|
|
284
|
+
return;
|
|
285
|
+
if (--remaining !== 0)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
let result = fn();
|
|
289
|
+
if (result) {
|
|
290
|
+
result
|
|
291
|
+
.then(moduleDone)
|
|
292
|
+
.catch(moduleError);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
moduleDone();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
moduleError(err);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
for (const access of evaluate_accesses) {
|
|
303
|
+
let evaluate;
|
|
304
|
+
try {
|
|
305
|
+
// Environment-dependent behavior:
|
|
306
|
+
// - throws on cyclic dependencies in V8
|
|
307
|
+
// - returns undefined in some environments (e.g., vitest)
|
|
308
|
+
evaluate = access();
|
|
309
|
+
if (evaluate == null)
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
catch (_a) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
remaining++;
|
|
316
|
+
evaluate(importDone, moduleError);
|
|
317
|
+
}
|
|
318
|
+
importDone();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
253
322
|
function concurrentTopLevelAwait(options = {}) {
|
|
254
323
|
const filter = createFilter(options.include, options.exclude);
|
|
324
|
+
const registerModuleSource = `\0${options.generatedVariablePrefix ?? "__tla"}Register`;
|
|
255
325
|
const asyncTracker = new AsyncModuleTracker();
|
|
256
326
|
return {
|
|
257
327
|
name: "rollup-plugin-concurrent-tla-plugin",
|
|
258
328
|
apply: "build",
|
|
329
|
+
resolveId(source) {
|
|
330
|
+
if (source === registerModuleSource) return registerModuleSource;
|
|
331
|
+
},
|
|
332
|
+
load(id) {
|
|
333
|
+
if (id === registerModuleSource) return tlaModule;
|
|
334
|
+
},
|
|
259
335
|
transform: { async handler(code, id, transformOptions) {
|
|
260
336
|
if (!filter(id)) return;
|
|
261
337
|
const ast = this.parse(code);
|
|
@@ -263,24 +339,23 @@ function concurrentTopLevelAwait(options = {}) {
|
|
|
263
339
|
const hasAwait = hasTopLevelAwait(ast);
|
|
264
340
|
asyncTracker.setEntryAsync(id, hasAwait);
|
|
265
341
|
if (hasAwait) asyncTracker.setDependencies(id, []);
|
|
266
|
-
|
|
267
|
-
const childrenIds = (await Promise.all(importDeclarations.map(async (declaration) => {
|
|
268
|
-
const importId = await resolveDeclarationSource(this, id, transformOptions?.attributes, declaration);
|
|
269
|
-
if (!importId || !filter(importId.id)) return null;
|
|
270
|
-
return importId.id;
|
|
271
|
-
}))).filter((a) => a != null);
|
|
272
|
-
asyncTracker.setDependencies(id, childrenIds);
|
|
273
|
-
}
|
|
274
|
-
const asyncImports = (await Promise.all(importDeclarations.map(async (declaration) => {
|
|
342
|
+
let imports = (await Promise.all(importDeclarations.map(async (declaration) => {
|
|
275
343
|
const importId = await resolveDeclarationSource(this, id, transformOptions?.attributes, declaration);
|
|
276
344
|
if (!importId || !filter(importId.id)) return null;
|
|
277
|
-
|
|
278
|
-
|
|
345
|
+
return {
|
|
346
|
+
declaration,
|
|
347
|
+
id: importId.id
|
|
348
|
+
};
|
|
349
|
+
}))).filter(Boolean);
|
|
350
|
+
if (!hasAwait) asyncTracker.setDependencies(id, imports.map((x) => x.id));
|
|
351
|
+
const asyncImports = (await Promise.all(imports.map(async ({ declaration, id: id$1 }) => {
|
|
352
|
+
this.load({ id: id$1 });
|
|
353
|
+
if (!await asyncTracker.isAsync(id$1)) return null;
|
|
279
354
|
return declaration;
|
|
280
355
|
}))).filter(Boolean);
|
|
281
356
|
if (!(asyncImports.length > 0 || hasAwait)) return;
|
|
282
357
|
const s = new MagicString(code);
|
|
283
|
-
transform(s, ast, asyncImports, hasAwait, options.generatedVariablePrefix ?? "__tla");
|
|
358
|
+
transform(s, ast, registerModuleSource, asyncImports, hasAwait, options.generatedVariablePrefix ?? "__tla");
|
|
284
359
|
return {
|
|
285
360
|
code: s.toString(),
|
|
286
361
|
map: options.sourceMap !== false ? s.generateMap({ hires: true }) : null
|
package/package.json
CHANGED