@async/framework 0.7.0 → 0.9.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/CHANGELOG.md +22 -0
- package/README.md +122 -14
- package/{framework.d.ts → browser.d.ts} +118 -4
- package/{framework.js → browser.js} +898 -127
- package/browser.min.js +1 -0
- package/{framework.ts → browser.ts} +899 -128
- package/{framework.umd.js → browser.umd.js} +898 -127
- package/browser.umd.min.js +1 -0
- package/package.json +45 -30
- package/server.d.ts +690 -0
- package/src/app.js +110 -11
- package/src/async-signal.js +32 -4
- package/src/boundary-receiver.js +302 -0
- package/src/browser.js +16 -0
- package/src/component.js +42 -7
- package/src/index.js +6 -2
- package/src/loader.js +42 -10
- package/src/request-context.js +40 -0
- package/src/router.js +15 -2
- package/src/scheduler.js +300 -0
- package/src/server-entry.js +20 -0
- package/src/server-registry.js +97 -0
- package/src/server.js +5 -88
- package/src/signals.js +38 -6
- package/framework.min.js +0 -3820
- package/framework.umd.min.js +0 -3843
package/src/component.js
CHANGED
|
@@ -113,10 +113,15 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
113
113
|
html,
|
|
114
114
|
attach(target) {
|
|
115
115
|
for (const hook of attachHooks) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
runtime.scheduler?.enqueue("lifecycle", () => {
|
|
117
|
+
const cleanup = hook(target);
|
|
118
|
+
if (typeof cleanup === "function") {
|
|
119
|
+
cleanups.push(cleanup);
|
|
120
|
+
}
|
|
121
|
+
}, {
|
|
122
|
+
scope,
|
|
123
|
+
key: `attach:${attachHooks.indexOf(hook)}`
|
|
124
|
+
}) ?? runAttachHook(hook, target);
|
|
120
125
|
}
|
|
121
126
|
},
|
|
122
127
|
mount(target) {
|
|
@@ -124,7 +129,17 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
124
129
|
},
|
|
125
130
|
visible(target, observeVisible) {
|
|
126
131
|
for (const hook of visibleHooks) {
|
|
127
|
-
const cleanup = observeVisible(target,
|
|
132
|
+
const cleanup = observeVisible(target, () => {
|
|
133
|
+
runtime.scheduler?.enqueue("lifecycle", () => {
|
|
134
|
+
const hookCleanup = hook(target);
|
|
135
|
+
if (typeof hookCleanup === "function") {
|
|
136
|
+
cleanups.push(hookCleanup);
|
|
137
|
+
}
|
|
138
|
+
}, {
|
|
139
|
+
scope,
|
|
140
|
+
key: `visible:${visibleHooks.indexOf(hook)}`
|
|
141
|
+
}) ?? runVisibleHook(hook, target);
|
|
142
|
+
});
|
|
128
143
|
if (typeof cleanup === "function") {
|
|
129
144
|
cleanups.push(cleanup);
|
|
130
145
|
}
|
|
@@ -134,6 +149,7 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
134
149
|
while (destroyHooks.length > 0) {
|
|
135
150
|
destroyHooks.pop()?.();
|
|
136
151
|
}
|
|
152
|
+
runtime.scheduler?.markScopeDestroyed(scope);
|
|
137
153
|
while (cleanups.length > 0) {
|
|
138
154
|
cleanups.pop()?.();
|
|
139
155
|
}
|
|
@@ -142,10 +158,24 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
160
|
};
|
|
161
|
+
|
|
162
|
+
function runAttachHook(hook, target) {
|
|
163
|
+
const cleanup = hook(target);
|
|
164
|
+
if (typeof cleanup === "function") {
|
|
165
|
+
cleanups.push(cleanup);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function runVisibleHook(hook, target) {
|
|
170
|
+
const cleanup = hook(target);
|
|
171
|
+
if (typeof cleanup === "function") {
|
|
172
|
+
cleanups.push(cleanup);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
|
|
148
|
-
const { signals, handlers, loader, server, router, cache } = runtime;
|
|
178
|
+
const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
|
|
149
179
|
const generatedHandlers = new WeakMap();
|
|
150
180
|
let generatedHandlerCounter = 0;
|
|
151
181
|
let generatedSignalCounter = 0;
|
|
@@ -157,6 +187,7 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
|
|
|
157
187
|
server,
|
|
158
188
|
router,
|
|
159
189
|
cache,
|
|
190
|
+
scheduler,
|
|
160
191
|
|
|
161
192
|
signal(name, initial) {
|
|
162
193
|
if (arguments.length === 1) {
|
|
@@ -201,7 +232,11 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
|
|
|
201
232
|
},
|
|
202
233
|
|
|
203
234
|
effect(fn) {
|
|
204
|
-
const cleanup = signals.effect(() => fn.call(context)
|
|
235
|
+
const cleanup = signals.effect(() => fn.call(context), {
|
|
236
|
+
scheduler,
|
|
237
|
+
phase: "effect",
|
|
238
|
+
scope
|
|
239
|
+
});
|
|
205
240
|
cleanups.push(cleanup);
|
|
206
241
|
return cleanup;
|
|
207
242
|
},
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { asyncSignal } from "./async-signal.js";
|
|
2
|
-
export { Async, createApp, defineApp, readSnapshot } from "./
|
|
2
|
+
export { Async, createApp, defineApp, readSnapshot } from "./server-entry.js";
|
|
3
3
|
export { attributeName, defineAttributeConfig } from "./attributes.js";
|
|
4
|
+
export { createBoundaryReceiver } from "./boundary-receiver.js";
|
|
4
5
|
export { createCacheRegistry, defineCache } from "./cache.js";
|
|
5
6
|
export { component, createComponentRegistry, defineComponent } from "./component.js";
|
|
6
7
|
export { delay } from "./delay.js";
|
|
@@ -10,5 +11,8 @@ export { Loader, AsyncLoader } from "./loader.js";
|
|
|
10
11
|
export { createPartialRegistry } from "./partials.js";
|
|
11
12
|
export { createRegistryStore } from "./registry-store.js";
|
|
12
13
|
export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
|
|
13
|
-
export {
|
|
14
|
+
export { createScheduler } from "./scheduler.js";
|
|
15
|
+
export { createRequestContextStore } from "./request-context.js";
|
|
16
|
+
export { applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult } from "./server.js";
|
|
17
|
+
export { createServerRegistry } from "./server-registry.js";
|
|
14
18
|
export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";
|
package/src/loader.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { renderComponent } from "./component.js";
|
|
2
2
|
import { createHandlerRegistry } from "./handlers.js";
|
|
3
|
+
import { createScheduler } from "./scheduler.js";
|
|
3
4
|
import { createSignalRegistry, isSignalRef } from "./signals.js";
|
|
4
5
|
import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attributes.js";
|
|
5
6
|
|
|
6
7
|
const inlineBindingPrefix = "__async:inline:";
|
|
7
8
|
|
|
8
|
-
export function Loader({ root, signals, handlers, server, router, cache, attributes } = {}) {
|
|
9
|
+
export function Loader({ root, signals, handlers, server, router, cache, attributes, scheduler } = {}) {
|
|
9
10
|
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
10
11
|
const rootNode = root ?? documentRef;
|
|
11
12
|
const signalRegistry = signals ?? createSignalRegistry();
|
|
12
13
|
const handlerRegistry = handlers ?? createHandlerRegistry();
|
|
14
|
+
const schedulerInstance = scheduler ?? createScheduler();
|
|
15
|
+
const ownsScheduler = !scheduler;
|
|
13
16
|
const attributeConfig = normalizeAttributeConfig(attributes);
|
|
14
17
|
const cleanups = new Set();
|
|
15
18
|
const eventBindings = new WeakMap();
|
|
@@ -30,6 +33,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
30
33
|
server,
|
|
31
34
|
router,
|
|
32
35
|
cache,
|
|
36
|
+
scheduler: schedulerInstance,
|
|
33
37
|
attributes: attributeConfig,
|
|
34
38
|
|
|
35
39
|
start() {
|
|
@@ -69,6 +73,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
69
73
|
server: api.server,
|
|
70
74
|
router: api.router,
|
|
71
75
|
cache: api.cache,
|
|
76
|
+
scheduler: schedulerInstance,
|
|
72
77
|
attributes: attributeConfig
|
|
73
78
|
});
|
|
74
79
|
cleanupChildren(target);
|
|
@@ -89,6 +94,9 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
89
94
|
runCleanup(cleanup);
|
|
90
95
|
}
|
|
91
96
|
cleanups.clear();
|
|
97
|
+
if (ownsScheduler) {
|
|
98
|
+
schedulerInstance.destroy();
|
|
99
|
+
}
|
|
92
100
|
},
|
|
93
101
|
|
|
94
102
|
_observeVisible(target, fn) {
|
|
@@ -106,13 +114,14 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
106
114
|
}
|
|
107
115
|
};
|
|
108
116
|
|
|
109
|
-
signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
|
|
117
|
+
signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache, scheduler: schedulerInstance });
|
|
110
118
|
api.server?._setContext?.({
|
|
111
119
|
signals: signalRegistry,
|
|
112
120
|
handlers: handlerRegistry,
|
|
113
121
|
loader: api,
|
|
114
122
|
router: api.router,
|
|
115
|
-
cache: api.cache
|
|
123
|
+
cache: api.cache,
|
|
124
|
+
scheduler: schedulerInstance
|
|
116
125
|
});
|
|
117
126
|
|
|
118
127
|
function bindEventAttributes(scope) {
|
|
@@ -144,18 +153,19 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
144
153
|
|
|
145
154
|
const listener = async (event) => {
|
|
146
155
|
try {
|
|
147
|
-
await handlerRegistry.run(ref, {
|
|
156
|
+
await schedulerInstance.batch(() => handlerRegistry.run(ref, {
|
|
148
157
|
signals: signalRegistry,
|
|
149
158
|
handlers: handlerRegistry,
|
|
150
159
|
loader: api,
|
|
151
160
|
server: api.server,
|
|
152
161
|
router: api.router,
|
|
153
162
|
cache: api.cache,
|
|
163
|
+
scheduler: schedulerInstance,
|
|
154
164
|
event,
|
|
155
165
|
element,
|
|
156
166
|
el: element,
|
|
157
167
|
root: rootNode
|
|
158
|
-
});
|
|
168
|
+
}));
|
|
159
169
|
} catch (error) {
|
|
160
170
|
dispatchAsyncError(element, error);
|
|
161
171
|
}
|
|
@@ -271,7 +281,12 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
271
281
|
|
|
272
282
|
const read = () => readBinding(path, options);
|
|
273
283
|
apply(read());
|
|
274
|
-
addCleanup(subscribeBinding(path, () =>
|
|
284
|
+
addCleanup(subscribeBinding(path, () => {
|
|
285
|
+
schedulerInstance.enqueue("binding", () => apply(read()), {
|
|
286
|
+
scope: element,
|
|
287
|
+
key
|
|
288
|
+
});
|
|
289
|
+
}), element);
|
|
275
290
|
}
|
|
276
291
|
|
|
277
292
|
function bindValueWriter(element, path) {
|
|
@@ -332,7 +347,12 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
332
347
|
const state = {
|
|
333
348
|
id,
|
|
334
349
|
templates,
|
|
335
|
-
cleanup: signalRegistry.subscribe(`${id}.$status`, () =>
|
|
350
|
+
cleanup: signalRegistry.subscribe(`${id}.$status`, () => {
|
|
351
|
+
schedulerInstance.enqueue("binding", () => renderBoundary(boundary), {
|
|
352
|
+
scope: boundary,
|
|
353
|
+
key: `boundary:${id}`
|
|
354
|
+
});
|
|
355
|
+
})
|
|
336
356
|
};
|
|
337
357
|
boundaryState.set(boundary, state);
|
|
338
358
|
addCleanup(state.cleanup, boundary);
|
|
@@ -372,7 +392,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
372
392
|
}
|
|
373
393
|
mountedElements.add(element);
|
|
374
394
|
for (const ref of refs) {
|
|
375
|
-
runPseudo(element, ref);
|
|
395
|
+
scheduleLifecycle(element, () => runPseudo(element, ref), `attach:${ref}`);
|
|
376
396
|
}
|
|
377
397
|
}
|
|
378
398
|
|
|
@@ -385,7 +405,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
385
405
|
continue;
|
|
386
406
|
}
|
|
387
407
|
visibleElements.add(element);
|
|
388
|
-
addCleanup(observeVisible(element, () => runPseudo(element, ref)), element);
|
|
408
|
+
addCleanup(observeVisible(element, () => scheduleLifecycle(element, () => runPseudo(element, ref), `visible:${ref}`)), element);
|
|
389
409
|
}
|
|
390
410
|
}
|
|
391
411
|
|
|
@@ -409,6 +429,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
409
429
|
server: api.server,
|
|
410
430
|
router: api.router,
|
|
411
431
|
cache: api.cache,
|
|
432
|
+
scheduler: schedulerInstance,
|
|
412
433
|
element,
|
|
413
434
|
el: element,
|
|
414
435
|
root: rootNode
|
|
@@ -427,10 +448,13 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
427
448
|
const ownerWindow = target.ownerDocument?.defaultView ?? globalThis;
|
|
428
449
|
const Observer = ownerWindow.IntersectionObserver ?? globalThis.IntersectionObserver;
|
|
429
450
|
if (!Observer) {
|
|
430
|
-
|
|
451
|
+
schedulerInstance.enqueue("lifecycle", () => {
|
|
431
452
|
if (!destroyed) {
|
|
432
453
|
fn(target);
|
|
433
454
|
}
|
|
455
|
+
}, {
|
|
456
|
+
scope: target,
|
|
457
|
+
key: "visible:fallback"
|
|
434
458
|
});
|
|
435
459
|
return () => {};
|
|
436
460
|
}
|
|
@@ -485,6 +509,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
485
509
|
}
|
|
486
510
|
for (const element of elementsIn(node)) {
|
|
487
511
|
runScopedCleanups(element);
|
|
512
|
+
schedulerInstance.markScopeDestroyed(element);
|
|
488
513
|
}
|
|
489
514
|
}
|
|
490
515
|
|
|
@@ -508,6 +533,13 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
508
533
|
}
|
|
509
534
|
}
|
|
510
535
|
|
|
536
|
+
function scheduleLifecycle(element, fn, key) {
|
|
537
|
+
schedulerInstance.enqueue("lifecycle", fn, {
|
|
538
|
+
scope: element,
|
|
539
|
+
key
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
511
543
|
return api;
|
|
512
544
|
}
|
|
513
545
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
export function createRequestContextStore() {
|
|
4
|
+
const storage = new AsyncLocalStorage();
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
storage,
|
|
8
|
+
|
|
9
|
+
run(context, fn, ...args) {
|
|
10
|
+
if (typeof fn !== "function") {
|
|
11
|
+
throw new TypeError("requestContext.run(context, fn) requires a function.");
|
|
12
|
+
}
|
|
13
|
+
return storage.run(context ?? {}, fn, ...args);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
get() {
|
|
17
|
+
return storage.getStore();
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
snapshot() {
|
|
21
|
+
return { ...(storage.getStore() ?? {}) };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readRequestContext(store) {
|
|
27
|
+
if (!store) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
if (typeof store.get === "function") {
|
|
31
|
+
return store.get() ?? {};
|
|
32
|
+
}
|
|
33
|
+
if (typeof store.getStore === "function") {
|
|
34
|
+
return store.getStore() ?? {};
|
|
35
|
+
}
|
|
36
|
+
if (typeof store === "object") {
|
|
37
|
+
return store;
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
package/src/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Loader } from "./loader.js";
|
|
2
2
|
import { createHandlerRegistry } from "./handlers.js";
|
|
3
|
+
import { createScheduler } from "./scheduler.js";
|
|
3
4
|
import { createSignalRegistry } from "./signals.js";
|
|
4
5
|
import { applyServerResult } from "./server.js";
|
|
5
6
|
import { createRegistryStore } from "./registry-store.js";
|
|
@@ -121,12 +122,15 @@ export function createRouter({
|
|
|
121
122
|
partials,
|
|
122
123
|
fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
|
|
123
124
|
routeEndpoint = "/__async/route",
|
|
124
|
-
attributes
|
|
125
|
+
attributes,
|
|
126
|
+
scheduler
|
|
125
127
|
} = {}) {
|
|
126
128
|
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
127
129
|
const rootNode = root ?? documentRef;
|
|
128
130
|
const signalRegistry = signals ?? loader?.signals ?? createSignalRegistry();
|
|
129
131
|
const handlerRegistry = handlers ?? loader?.handlers ?? createHandlerRegistry();
|
|
132
|
+
const schedulerInstance = scheduler ?? loader?.scheduler ?? createScheduler();
|
|
133
|
+
const ownsScheduler = !scheduler && !loader?.scheduler;
|
|
130
134
|
const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
|
|
131
135
|
const loaderInstance =
|
|
132
136
|
loader ??
|
|
@@ -136,6 +140,7 @@ export function createRouter({
|
|
|
136
140
|
handlers: handlerRegistry,
|
|
137
141
|
server,
|
|
138
142
|
cache,
|
|
143
|
+
scheduler: schedulerInstance,
|
|
139
144
|
attributes: attributeConfig
|
|
140
145
|
});
|
|
141
146
|
const ownsLoader = !loader;
|
|
@@ -155,12 +160,13 @@ export function createRouter({
|
|
|
155
160
|
server,
|
|
156
161
|
cache,
|
|
157
162
|
partials,
|
|
163
|
+
scheduler: schedulerInstance,
|
|
158
164
|
attributes: attributeConfig,
|
|
159
165
|
|
|
160
166
|
start() {
|
|
161
167
|
assertActive();
|
|
162
168
|
loaderInstance.router = api;
|
|
163
|
-
signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache });
|
|
169
|
+
signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache, scheduler: schedulerInstance });
|
|
164
170
|
if (ownsLoader) {
|
|
165
171
|
loaderInstance.start();
|
|
166
172
|
}
|
|
@@ -224,6 +230,9 @@ export function createRouter({
|
|
|
224
230
|
cleanup();
|
|
225
231
|
}
|
|
226
232
|
cleanups.clear();
|
|
233
|
+
if (ownsScheduler) {
|
|
234
|
+
schedulerInstance.destroy();
|
|
235
|
+
}
|
|
227
236
|
}
|
|
228
237
|
};
|
|
229
238
|
|
|
@@ -329,13 +338,16 @@ export function createRouter({
|
|
|
329
338
|
loader: loaderInstance,
|
|
330
339
|
router: api,
|
|
331
340
|
cache,
|
|
341
|
+
scheduler: schedulerInstance,
|
|
332
342
|
abort: navigation?.abort
|
|
333
343
|
});
|
|
344
|
+
await schedulerInstance.flush();
|
|
334
345
|
if (!isActiveNavigation(navigation)) {
|
|
335
346
|
return;
|
|
336
347
|
}
|
|
337
348
|
if (result?.html != null && !result.boundary && !result.redirect) {
|
|
338
349
|
loaderInstance.swap(boundary, result.html);
|
|
350
|
+
await schedulerInstance.flush();
|
|
339
351
|
}
|
|
340
352
|
if (result?.redirect || options.history === false) {
|
|
341
353
|
return;
|
|
@@ -380,6 +392,7 @@ export function createRouter({
|
|
|
380
392
|
loader: loaderInstance,
|
|
381
393
|
server,
|
|
382
394
|
cache,
|
|
395
|
+
scheduler: schedulerInstance,
|
|
383
396
|
abort: navigation?.abort
|
|
384
397
|
};
|
|
385
398
|
}
|
package/src/scheduler.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const defaultPhases = ["binding", "lifecycle", "effect", "async", "post"];
|
|
2
|
+
|
|
3
|
+
export function createScheduler(options = {}) {
|
|
4
|
+
const phases = [...(options.phases ?? defaultPhases)];
|
|
5
|
+
const queues = new Map(phases.map((phase) => [phase, []]));
|
|
6
|
+
const keyedJobs = new Map();
|
|
7
|
+
const destroyedScopes = new Set();
|
|
8
|
+
const objectScopeIds = new WeakMap();
|
|
9
|
+
const onError = typeof options.onError === "function" ? options.onError : undefined;
|
|
10
|
+
const maxDepth = options.maxDepth ?? 100;
|
|
11
|
+
const strategy = options.strategy ?? "microtask";
|
|
12
|
+
let destroyed = false;
|
|
13
|
+
let flushing = false;
|
|
14
|
+
let scheduled = false;
|
|
15
|
+
let batchDepth = 0;
|
|
16
|
+
let jobCounter = 0;
|
|
17
|
+
let scopeCounter = 0;
|
|
18
|
+
|
|
19
|
+
const api = {
|
|
20
|
+
strategy,
|
|
21
|
+
phases,
|
|
22
|
+
|
|
23
|
+
batch(fn) {
|
|
24
|
+
if (typeof fn !== "function") {
|
|
25
|
+
throw new TypeError("scheduler.batch(fn) requires a function.");
|
|
26
|
+
}
|
|
27
|
+
assertActive();
|
|
28
|
+
batchDepth += 1;
|
|
29
|
+
let asyncBatch = false;
|
|
30
|
+
try {
|
|
31
|
+
const value = fn();
|
|
32
|
+
if (value && typeof value.then === "function") {
|
|
33
|
+
asyncBatch = true;
|
|
34
|
+
return value.finally(() => {
|
|
35
|
+
batchDepth -= 1;
|
|
36
|
+
requestFlush();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
} finally {
|
|
41
|
+
if (!asyncBatch && batchDepth > 0) {
|
|
42
|
+
batchDepth -= 1;
|
|
43
|
+
requestFlush();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
enqueue(phase, fn, options = {}) {
|
|
49
|
+
assertActive();
|
|
50
|
+
assertPhase(phase);
|
|
51
|
+
if (typeof fn !== "function") {
|
|
52
|
+
throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
|
|
53
|
+
}
|
|
54
|
+
const scope = options.scope;
|
|
55
|
+
if (scope !== undefined && destroyedScopes.has(scope)) {
|
|
56
|
+
return noop;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dedupeKey = options.key === undefined ? undefined : `${phase}:${scopeKey(scope)}:${String(options.key)}`;
|
|
60
|
+
if (dedupeKey && keyedJobs.has(dedupeKey)) {
|
|
61
|
+
return keyedJobs.get(dedupeKey).cancel;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const job = {
|
|
65
|
+
id: ++jobCounter,
|
|
66
|
+
phase,
|
|
67
|
+
fn,
|
|
68
|
+
scope,
|
|
69
|
+
boundary: options.boundary,
|
|
70
|
+
key: dedupeKey,
|
|
71
|
+
canceled: false,
|
|
72
|
+
cancel() {
|
|
73
|
+
job.canceled = true;
|
|
74
|
+
if (job.key) {
|
|
75
|
+
keyedJobs.delete(job.key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
queues.get(phase).push(job);
|
|
80
|
+
if (job.key) {
|
|
81
|
+
keyedJobs.set(job.key, job);
|
|
82
|
+
}
|
|
83
|
+
requestFlush();
|
|
84
|
+
return job.cancel;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
afterFlush(fn, options = {}) {
|
|
88
|
+
return api.enqueue("post", fn, options);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async flush() {
|
|
92
|
+
assertActive();
|
|
93
|
+
if (flushing) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
scheduled = false;
|
|
97
|
+
flushing = true;
|
|
98
|
+
let depth = 0;
|
|
99
|
+
try {
|
|
100
|
+
while (hasJobs()) {
|
|
101
|
+
depth += 1;
|
|
102
|
+
if (depth > maxDepth) {
|
|
103
|
+
throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
|
|
104
|
+
}
|
|
105
|
+
for (const phase of phases) {
|
|
106
|
+
await flushPhase(phase);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
flushing = false;
|
|
111
|
+
if (hasJobs()) {
|
|
112
|
+
requestFlush();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async flushScope(scope) {
|
|
118
|
+
assertActive();
|
|
119
|
+
if (flushing) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
scheduled = false;
|
|
123
|
+
flushing = true;
|
|
124
|
+
let depth = 0;
|
|
125
|
+
try {
|
|
126
|
+
while (hasJobsForScope(scope)) {
|
|
127
|
+
depth += 1;
|
|
128
|
+
if (depth > maxDepth) {
|
|
129
|
+
throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
|
|
130
|
+
}
|
|
131
|
+
for (const phase of phases) {
|
|
132
|
+
await flushPhase(phase, scope);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
flushing = false;
|
|
137
|
+
if (hasJobs()) {
|
|
138
|
+
requestFlush();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
cancelScope(scope) {
|
|
144
|
+
if (scope === undefined) {
|
|
145
|
+
return api;
|
|
146
|
+
}
|
|
147
|
+
for (const queue of queues.values()) {
|
|
148
|
+
for (const job of queue) {
|
|
149
|
+
if (job.scope === scope) {
|
|
150
|
+
job.cancel();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return api;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
markScopeDestroyed(scope) {
|
|
158
|
+
if (scope !== undefined) {
|
|
159
|
+
destroyedScopes.add(scope);
|
|
160
|
+
api.cancelScope(scope);
|
|
161
|
+
}
|
|
162
|
+
return api;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
isScopeDestroyed(scope) {
|
|
166
|
+
return scope !== undefined && destroyedScopes.has(scope);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
inspect() {
|
|
170
|
+
const counts = {};
|
|
171
|
+
for (const [phase, queue] of queues) {
|
|
172
|
+
counts[phase] = queue.filter((job) => !job.canceled).length;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
strategy,
|
|
176
|
+
phases: [...phases],
|
|
177
|
+
pending: counts,
|
|
178
|
+
scopesDestroyed: destroyedScopes.size,
|
|
179
|
+
flushing,
|
|
180
|
+
scheduled
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
destroy() {
|
|
185
|
+
destroyed = true;
|
|
186
|
+
for (const queue of queues.values()) {
|
|
187
|
+
for (const job of queue) {
|
|
188
|
+
job.cancel();
|
|
189
|
+
}
|
|
190
|
+
queue.length = 0;
|
|
191
|
+
}
|
|
192
|
+
keyedJobs.clear();
|
|
193
|
+
destroyedScopes.clear();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return api;
|
|
198
|
+
|
|
199
|
+
function requestFlush() {
|
|
200
|
+
if (strategy === "manual" || destroyed || flushing || batchDepth > 0 || scheduled) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
scheduled = true;
|
|
204
|
+
scheduleMicrotask(() => {
|
|
205
|
+
if (!destroyed) {
|
|
206
|
+
void api.flush();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function flushPhase(phase, scope) {
|
|
212
|
+
const queue = queues.get(phase);
|
|
213
|
+
const remaining = [];
|
|
214
|
+
const runnable = [];
|
|
215
|
+
|
|
216
|
+
for (const job of queue.splice(0)) {
|
|
217
|
+
if (job.canceled) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (scope !== undefined && job.scope !== scope) {
|
|
221
|
+
remaining.push(job);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
runnable.push(job);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
queue.push(...remaining);
|
|
228
|
+
|
|
229
|
+
for (const job of runnable) {
|
|
230
|
+
if (job.key) {
|
|
231
|
+
keyedJobs.delete(job.key);
|
|
232
|
+
}
|
|
233
|
+
if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await job.fn();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (onError) {
|
|
240
|
+
onError(error, job);
|
|
241
|
+
} else {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function hasJobs() {
|
|
249
|
+
for (const queue of queues.values()) {
|
|
250
|
+
if (queue.some((job) => !job.canceled)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hasJobsForScope(scope) {
|
|
258
|
+
for (const queue of queues.values()) {
|
|
259
|
+
if (queue.some((job) => !job.canceled && job.scope === scope)) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function assertActive() {
|
|
267
|
+
if (destroyed) {
|
|
268
|
+
throw new Error("Scheduler has been destroyed.");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function assertPhase(phase) {
|
|
273
|
+
if (!queues.has(phase)) {
|
|
274
|
+
throw new Error(`Unknown scheduler phase "${phase}".`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function scopeKey(scope) {
|
|
279
|
+
if (scope === undefined) {
|
|
280
|
+
return "global";
|
|
281
|
+
}
|
|
282
|
+
if ((typeof scope === "object" && scope !== null) || typeof scope === "function") {
|
|
283
|
+
if (!objectScopeIds.has(scope)) {
|
|
284
|
+
objectScopeIds.set(scope, `scope:${++scopeCounter}`);
|
|
285
|
+
}
|
|
286
|
+
return objectScopeIds.get(scope);
|
|
287
|
+
}
|
|
288
|
+
return String(scope);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function scheduleMicrotask(fn) {
|
|
293
|
+
if (typeof queueMicrotask === "function") {
|
|
294
|
+
queueMicrotask(fn);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
Promise.resolve().then(fn);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function noop() {}
|