@async/framework 0.8.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 +6 -0
- package/README.md +41 -0
- package/browser.d.ts +50 -0
- package/browser.js +312 -1
- package/browser.min.js +1 -1
- package/browser.ts +312 -1
- package/browser.umd.js +312 -1
- package/browser.umd.min.js +1 -1
- package/package.json +1 -1
- package/server.d.ts +50 -0
- package/src/boundary-receiver.js +302 -0
- package/src/browser.js +1 -0
- package/src/index.js +1 -0
- package/src/scheduler.js +4 -0
package/server.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export interface Scheduler {
|
|
|
75
75
|
afterFlush(job: () => MaybePromise<unknown>, options?: { scope?: unknown; boundary?: string; key?: string }): Cleanup;
|
|
76
76
|
cancelScope(scope: unknown): this;
|
|
77
77
|
markScopeDestroyed(scope: unknown): this;
|
|
78
|
+
isScopeDestroyed(scope: unknown): boolean;
|
|
78
79
|
destroy(): void;
|
|
79
80
|
inspect(): SchedulerInspection;
|
|
80
81
|
}
|
|
@@ -458,6 +459,53 @@ export interface LoaderInstance {
|
|
|
458
459
|
export type AsyncLoaderOptions = LoaderOptions;
|
|
459
460
|
export type AsyncLoaderInstance = LoaderInstance;
|
|
460
461
|
|
|
462
|
+
export interface BoundaryPatch {
|
|
463
|
+
boundary: string;
|
|
464
|
+
seq: number;
|
|
465
|
+
html?: TemplateLike;
|
|
466
|
+
signals?: Record<string, unknown>;
|
|
467
|
+
cache?: { browser?: Record<string, unknown> };
|
|
468
|
+
redirect?: string;
|
|
469
|
+
error?: unknown;
|
|
470
|
+
parentScope?: string;
|
|
471
|
+
scope?: string;
|
|
472
|
+
meta?: Record<string, unknown>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export type BoundaryApplyResult =
|
|
476
|
+
| { status: "applied"; boundary: string; seq: number }
|
|
477
|
+
| { status: "ignored-stale"; boundary: string; seq: number; lastSeq: number }
|
|
478
|
+
| { status: "ignored-destroyed"; boundary: string; seq: number; parentScope?: string }
|
|
479
|
+
| { status: "redirected"; boundary: string; seq: number; redirect: string }
|
|
480
|
+
| { status: "errored"; boundary: string; seq: number; error: Error };
|
|
481
|
+
|
|
482
|
+
export interface BoundaryReceiverInspection {
|
|
483
|
+
destroyed: boolean;
|
|
484
|
+
boundaries: Record<string, { lastSeq: number; applied: number; ignored: number; errored?: number; lastStatus?: BoundaryApplyResult["status"] }>;
|
|
485
|
+
recent: Array<{ boundary: string; seq: number; status: BoundaryApplyResult["status"]; lastSeq?: number; parentScope?: string; redirect?: string }>;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface BoundaryReceiverOptions {
|
|
489
|
+
loader: LoaderInstance;
|
|
490
|
+
signals?: SignalRegistry;
|
|
491
|
+
cache?: CacheRegistry;
|
|
492
|
+
scheduler?: Scheduler;
|
|
493
|
+
router?: Router;
|
|
494
|
+
onApply?(result: BoundaryApplyResult, patch: BoundaryPatch): void;
|
|
495
|
+
onIgnore?(result: BoundaryApplyResult, patch: BoundaryPatch): void;
|
|
496
|
+
onError?(error: Error, result: BoundaryApplyResult, patch: BoundaryPatch): void;
|
|
497
|
+
throwOnError?: boolean;
|
|
498
|
+
recentLimit?: number;
|
|
499
|
+
isScopeDestroyed?(scope: string): boolean;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface BoundaryReceiver {
|
|
503
|
+
apply(patch: BoundaryPatch): Promise<BoundaryApplyResult>;
|
|
504
|
+
inspect(): BoundaryReceiverInspection;
|
|
505
|
+
reset(boundary?: string): this;
|
|
506
|
+
destroy(): void;
|
|
507
|
+
}
|
|
508
|
+
|
|
461
509
|
export interface RegistryStore {
|
|
462
510
|
target: RuntimeTarget;
|
|
463
511
|
register(type: RegistryType, id: string, value: unknown): string;
|
|
@@ -568,6 +616,7 @@ export interface AsyncNamespace extends AppHub {
|
|
|
568
616
|
readSnapshot: typeof readSnapshot;
|
|
569
617
|
attributeName: typeof attributeName;
|
|
570
618
|
defineAttributeConfig: typeof defineAttributeConfig;
|
|
619
|
+
createBoundaryReceiver: typeof createBoundaryReceiver;
|
|
571
620
|
createCacheRegistry: typeof createCacheRegistry;
|
|
572
621
|
defineCache: typeof defineCache;
|
|
573
622
|
component: typeof component;
|
|
@@ -605,6 +654,7 @@ export declare function defineApp(initial?: AppDefinition): AppHub;
|
|
|
605
654
|
export declare function readSnapshot(root?: Document | Element, options?: { attributes?: AttributeConfig }): { signals?: Record<string, unknown>; cache?: { browser?: Record<string, unknown> } };
|
|
606
655
|
export declare function attributeName(attributes: AttributeConfig | undefined, type: keyof NormalizedAttributeConfig, name: string): string;
|
|
607
656
|
export declare function defineAttributeConfig(config?: AttributeConfig): NormalizedAttributeConfig;
|
|
657
|
+
export declare function createBoundaryReceiver(options: BoundaryReceiverOptions): BoundaryReceiver;
|
|
608
658
|
export declare function createCacheRegistry(initialMap?: Record<string, CacheDefinition | CacheDefinitionOptions>, options?: { now?: () => number; registry?: RegistryStore; type?: "cache.browser" | "cache.server" }): CacheRegistry;
|
|
609
659
|
export declare function defineCache(options?: CacheDefinitionOptions): CacheDefinition;
|
|
610
660
|
export declare function component<TProps extends Record<string, unknown> = Record<string, unknown>>(fn: ComponentFunction<TProps>): ComponentFunction<TProps>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
const defaultRecentLimit = 50;
|
|
2
|
+
|
|
3
|
+
export function createBoundaryReceiver(options = {}) {
|
|
4
|
+
const loader = options.loader;
|
|
5
|
+
const signals = options.signals ?? loader?.signals;
|
|
6
|
+
const cache = options.cache ?? loader?.cache;
|
|
7
|
+
const scheduler = options.scheduler ?? loader?.scheduler;
|
|
8
|
+
const router = options.router ?? loader?.router;
|
|
9
|
+
const recentLimit = options.recentLimit ?? defaultRecentLimit;
|
|
10
|
+
const throwOnError = options.throwOnError === true;
|
|
11
|
+
const onApply = typeof options.onApply === "function" ? options.onApply : undefined;
|
|
12
|
+
const onIgnore = typeof options.onIgnore === "function" ? options.onIgnore : undefined;
|
|
13
|
+
const onError = typeof options.onError === "function" ? options.onError : undefined;
|
|
14
|
+
const isScopeDestroyed = typeof options.isScopeDestroyed === "function"
|
|
15
|
+
? options.isScopeDestroyed
|
|
16
|
+
: (scope) => scheduler?.isScopeDestroyed?.(scope) ?? scheduler?.inspectDestroyed?.(scope) ?? false;
|
|
17
|
+
|
|
18
|
+
if (!loader || typeof loader.swap !== "function") {
|
|
19
|
+
throw new TypeError("createBoundaryReceiver(...) requires a loader with swap(boundary, html).");
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isInteger(recentLimit) || recentLimit < 0) {
|
|
22
|
+
throw new TypeError("createBoundaryReceiver(...) recentLimit must be a non-negative integer.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const boundaries = new Map();
|
|
26
|
+
const recent = [];
|
|
27
|
+
let destroyed = false;
|
|
28
|
+
|
|
29
|
+
const receiver = {
|
|
30
|
+
async apply(patch) {
|
|
31
|
+
if (destroyed) {
|
|
32
|
+
throw new Error("Boundary receiver has been destroyed.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalized = validatePatch(patch);
|
|
36
|
+
const record = boundaryRecord(normalized.boundary);
|
|
37
|
+
if (normalized.seq <= record.lastSeq) {
|
|
38
|
+
const result = {
|
|
39
|
+
status: "ignored-stale",
|
|
40
|
+
boundary: normalized.boundary,
|
|
41
|
+
seq: normalized.seq,
|
|
42
|
+
lastSeq: record.lastSeq
|
|
43
|
+
};
|
|
44
|
+
record.ignored += 1;
|
|
45
|
+
record.lastStatus = result.status;
|
|
46
|
+
remember(result);
|
|
47
|
+
onIgnore?.(result, patch);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
|
|
52
|
+
const result = {
|
|
53
|
+
status: "ignored-destroyed",
|
|
54
|
+
boundary: normalized.boundary,
|
|
55
|
+
seq: normalized.seq,
|
|
56
|
+
parentScope: normalized.parentScope
|
|
57
|
+
};
|
|
58
|
+
record.ignored += 1;
|
|
59
|
+
record.lastStatus = result.status;
|
|
60
|
+
remember(result);
|
|
61
|
+
onIgnore?.(result, patch);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
record.lastSeq = normalized.seq;
|
|
66
|
+
|
|
67
|
+
if (Object.hasOwn(normalized, "error")) {
|
|
68
|
+
const error = toStableError(normalized.error);
|
|
69
|
+
const result = {
|
|
70
|
+
status: "errored",
|
|
71
|
+
boundary: normalized.boundary,
|
|
72
|
+
seq: normalized.seq,
|
|
73
|
+
error
|
|
74
|
+
};
|
|
75
|
+
record.errored += 1;
|
|
76
|
+
record.lastStatus = result.status;
|
|
77
|
+
remember(result);
|
|
78
|
+
onError?.(error, result, patch);
|
|
79
|
+
if (throwOnError) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (normalized.signals) {
|
|
86
|
+
if (!signals || typeof signals.set !== "function") {
|
|
87
|
+
throw new Error("Boundary patch includes signals, but no signal registry is available.");
|
|
88
|
+
}
|
|
89
|
+
for (const [path, value] of Object.entries(normalized.signals)) {
|
|
90
|
+
signals.set(path, value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (normalized.cache?.browser) {
|
|
95
|
+
if (!cache || typeof cache.restore !== "function") {
|
|
96
|
+
throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
|
|
97
|
+
}
|
|
98
|
+
cache.restore(normalized.cache.browser);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (normalized.html != null) {
|
|
102
|
+
loader.swap(normalized.boundary, normalized.html);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await flushScheduler(scheduler, normalized.scope);
|
|
106
|
+
|
|
107
|
+
if (normalized.redirect) {
|
|
108
|
+
await followRedirect(normalized.redirect, router, loader);
|
|
109
|
+
const result = {
|
|
110
|
+
status: "redirected",
|
|
111
|
+
boundary: normalized.boundary,
|
|
112
|
+
seq: normalized.seq,
|
|
113
|
+
redirect: normalized.redirect
|
|
114
|
+
};
|
|
115
|
+
record.applied += 1;
|
|
116
|
+
record.lastStatus = result.status;
|
|
117
|
+
remember(result);
|
|
118
|
+
onApply?.(result, patch);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = {
|
|
123
|
+
status: "applied",
|
|
124
|
+
boundary: normalized.boundary,
|
|
125
|
+
seq: normalized.seq
|
|
126
|
+
};
|
|
127
|
+
record.applied += 1;
|
|
128
|
+
record.lastStatus = result.status;
|
|
129
|
+
remember(result);
|
|
130
|
+
onApply?.(result, patch);
|
|
131
|
+
return result;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
inspect() {
|
|
135
|
+
const snapshot = {};
|
|
136
|
+
for (const [boundary, record] of boundaries) {
|
|
137
|
+
snapshot[boundary] = {
|
|
138
|
+
lastSeq: record.lastSeq,
|
|
139
|
+
applied: record.applied,
|
|
140
|
+
ignored: record.ignored,
|
|
141
|
+
lastStatus: record.lastStatus
|
|
142
|
+
};
|
|
143
|
+
if (record.errored > 0) {
|
|
144
|
+
snapshot[boundary].errored = record.errored;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
destroyed,
|
|
149
|
+
boundaries: snapshot,
|
|
150
|
+
recent: recent.map((entry) => ({ ...entry }))
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
reset(boundary) {
|
|
155
|
+
if (boundary === undefined) {
|
|
156
|
+
boundaries.clear();
|
|
157
|
+
recent.length = 0;
|
|
158
|
+
return receiver;
|
|
159
|
+
}
|
|
160
|
+
assertBoundary(boundary);
|
|
161
|
+
boundaries.delete(boundary);
|
|
162
|
+
for (let index = recent.length - 1; index >= 0; index -= 1) {
|
|
163
|
+
if (recent[index].boundary === boundary) {
|
|
164
|
+
recent.splice(index, 1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return receiver;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
destroy() {
|
|
171
|
+
destroyed = true;
|
|
172
|
+
boundaries.clear();
|
|
173
|
+
recent.length = 0;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return receiver;
|
|
178
|
+
|
|
179
|
+
function boundaryRecord(boundary) {
|
|
180
|
+
if (!boundaries.has(boundary)) {
|
|
181
|
+
boundaries.set(boundary, {
|
|
182
|
+
lastSeq: -Infinity,
|
|
183
|
+
applied: 0,
|
|
184
|
+
ignored: 0,
|
|
185
|
+
errored: 0,
|
|
186
|
+
lastStatus: undefined
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return boundaries.get(boundary);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function remember(result) {
|
|
193
|
+
if (recentLimit === 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
recent.push(toRecentEntry(result));
|
|
197
|
+
while (recent.length > recentLimit) {
|
|
198
|
+
recent.shift();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validatePatch(patch) {
|
|
204
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
205
|
+
throw new TypeError("receiver.apply(patch) requires a boundary patch object.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
assertBoundary(patch.boundary);
|
|
209
|
+
if (typeof patch.seq !== "number" || !Number.isFinite(patch.seq)) {
|
|
210
|
+
throw new TypeError("Boundary patch seq must be a finite number.");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (patch.signals !== undefined && !isPlainObject(patch.signals)) {
|
|
214
|
+
throw new TypeError("Boundary patch signals must be an object.");
|
|
215
|
+
}
|
|
216
|
+
if (patch.cache !== undefined && !isPlainObject(patch.cache)) {
|
|
217
|
+
throw new TypeError("Boundary patch cache must be an object.");
|
|
218
|
+
}
|
|
219
|
+
if (patch.cache?.browser !== undefined && !isPlainObject(patch.cache.browser)) {
|
|
220
|
+
throw new TypeError("Boundary patch cache.browser must be an object.");
|
|
221
|
+
}
|
|
222
|
+
if (patch.redirect !== undefined && (typeof patch.redirect !== "string" || patch.redirect.length === 0)) {
|
|
223
|
+
throw new TypeError("Boundary patch redirect must be a non-empty string.");
|
|
224
|
+
}
|
|
225
|
+
if (patch.parentScope !== undefined && typeof patch.parentScope !== "string") {
|
|
226
|
+
throw new TypeError("Boundary patch parentScope must be a string.");
|
|
227
|
+
}
|
|
228
|
+
if (patch.scope !== undefined && typeof patch.scope !== "string") {
|
|
229
|
+
throw new TypeError("Boundary patch scope must be a string.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const hasHtml = Object.hasOwn(patch, "html") && patch.html != null;
|
|
233
|
+
const hasSignals = patch.signals && Object.keys(patch.signals).length > 0;
|
|
234
|
+
const hasBrowserCache = patch.cache?.browser && Object.keys(patch.cache.browser).length > 0;
|
|
235
|
+
const hasRedirect = Boolean(patch.redirect);
|
|
236
|
+
const hasError = Object.hasOwn(patch, "error");
|
|
237
|
+
if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError) {
|
|
238
|
+
throw new TypeError("Boundary patch must include html, signals, cache.browser, redirect, or error.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return patch;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function assertBoundary(boundary) {
|
|
245
|
+
if (typeof boundary !== "string" || boundary.length === 0) {
|
|
246
|
+
throw new TypeError("Boundary patch boundary must be a non-empty string.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function flushScheduler(scheduler, scope) {
|
|
251
|
+
if (!scheduler) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (scope !== undefined && typeof scheduler.flushScope === "function") {
|
|
255
|
+
await scheduler.flushScope(scope);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof scheduler.flush === "function") {
|
|
259
|
+
await scheduler.flush();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function followRedirect(redirect, router, loader) {
|
|
264
|
+
if (router && typeof router.navigate === "function") {
|
|
265
|
+
await router.navigate(redirect);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const location = loader?.root?.ownerDocument?.defaultView?.location ?? globalThis.location;
|
|
269
|
+
location?.assign?.(redirect);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function toStableError(value) {
|
|
273
|
+
if (value instanceof Error) {
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
if (value && typeof value === "object" && typeof value.message === "string") {
|
|
277
|
+
return Object.assign(new Error(value.message), value);
|
|
278
|
+
}
|
|
279
|
+
return new Error(String(value));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toRecentEntry(result) {
|
|
283
|
+
const entry = {
|
|
284
|
+
boundary: result.boundary,
|
|
285
|
+
seq: result.seq,
|
|
286
|
+
status: result.status
|
|
287
|
+
};
|
|
288
|
+
if (result.status === "ignored-stale") {
|
|
289
|
+
entry.lastSeq = result.lastSeq;
|
|
290
|
+
}
|
|
291
|
+
if (result.status === "ignored-destroyed" && result.parentScope !== undefined) {
|
|
292
|
+
entry.parentScope = result.parentScope;
|
|
293
|
+
}
|
|
294
|
+
if (result.status === "redirected") {
|
|
295
|
+
entry.redirect = result.redirect;
|
|
296
|
+
}
|
|
297
|
+
return entry;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isPlainObject(value) {
|
|
301
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
302
|
+
}
|
package/src/browser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { asyncSignal } from "./async-signal.js";
|
|
2
2
|
export { Async, createApp, defineApp, readSnapshot } from "./app.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";
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { asyncSignal } from "./async-signal.js";
|
|
2
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";
|
package/src/scheduler.js
CHANGED
|
@@ -162,6 +162,10 @@ export function createScheduler(options = {}) {
|
|
|
162
162
|
return api;
|
|
163
163
|
},
|
|
164
164
|
|
|
165
|
+
isScopeDestroyed(scope) {
|
|
166
|
+
return scope !== undefined && destroyedScopes.has(scope);
|
|
167
|
+
},
|
|
168
|
+
|
|
165
169
|
inspect() {
|
|
166
170
|
const counts = {};
|
|
167
171
|
for (const [phase, queue] of queues) {
|