@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/app.js
CHANGED
|
@@ -4,16 +4,18 @@ import { createHandlerRegistry } from "./handlers.js";
|
|
|
4
4
|
import { Loader } from "./loader.js";
|
|
5
5
|
import { createPartialRegistry } from "./partials.js";
|
|
6
6
|
import { createRouteRegistry, createRouter } from "./router.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createScheduler } from "./scheduler.js";
|
|
8
|
+
import { createServerNamespace } from "./server.js";
|
|
8
9
|
import { createSignal, createSignalRegistry } from "./signals.js";
|
|
9
10
|
import { createRegistryStore } from "./registry-store.js";
|
|
10
11
|
import { attributeName, normalizeAttributeConfig } from "./attributes.js";
|
|
11
12
|
|
|
12
13
|
const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
|
|
13
14
|
|
|
14
|
-
export function defineApp(initial) {
|
|
15
|
+
export function defineApp(initial, options = {}) {
|
|
15
16
|
const registry = createRegistryStore(undefined, { target: "browser" });
|
|
16
17
|
const runtimes = new Set();
|
|
18
|
+
const createRuntime = options.createRuntime ?? createApp;
|
|
17
19
|
|
|
18
20
|
const app = {
|
|
19
21
|
registry,
|
|
@@ -32,7 +34,7 @@ export function defineApp(initial) {
|
|
|
32
34
|
},
|
|
33
35
|
|
|
34
36
|
start(options = {}) {
|
|
35
|
-
const runtime =
|
|
37
|
+
const runtime = createRuntime(app, options).start();
|
|
36
38
|
app.runtime = runtime;
|
|
37
39
|
return runtime;
|
|
38
40
|
},
|
|
@@ -57,13 +59,18 @@ export function defineApp(initial) {
|
|
|
57
59
|
export function createApp(appOrDefinition = Async, options = {}) {
|
|
58
60
|
const app = isAppHub(appOrDefinition) ? appOrDefinition : defineApp(appOrDefinition ?? {});
|
|
59
61
|
const target = options.target ?? "browser";
|
|
62
|
+
const scheduler = options.scheduler ?? options.loader?.scheduler ?? createScheduler({
|
|
63
|
+
strategy: target === "server" ? "manual" : "microtask"
|
|
64
|
+
});
|
|
65
|
+
const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
|
|
60
66
|
const attributes = normalizeAttributeConfig(options.attributes);
|
|
61
67
|
const registry = options.registry ?? app.registry.view({ target });
|
|
62
68
|
const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
|
|
63
69
|
const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
|
|
64
70
|
const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
|
|
65
71
|
const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
|
|
66
|
-
const
|
|
72
|
+
const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
|
|
73
|
+
const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
|
|
67
74
|
const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
|
|
68
75
|
const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
|
|
69
76
|
const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
|
|
@@ -91,6 +98,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
91
98
|
},
|
|
92
99
|
loader,
|
|
93
100
|
router,
|
|
101
|
+
scheduler,
|
|
94
102
|
attributes,
|
|
95
103
|
|
|
96
104
|
start() {
|
|
@@ -107,12 +115,13 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
107
115
|
handlers,
|
|
108
116
|
server,
|
|
109
117
|
cache: browserCache,
|
|
118
|
+
scheduler,
|
|
110
119
|
attributes
|
|
111
120
|
});
|
|
112
121
|
runtime.loader = loader;
|
|
113
122
|
|
|
114
123
|
configureServerContext({ cache: browserCache });
|
|
115
|
-
signals._setContext?.({ server, loader, cache: browserCache });
|
|
124
|
+
signals._setContext?.({ server, loader, cache: browserCache, scheduler });
|
|
116
125
|
|
|
117
126
|
loader.start();
|
|
118
127
|
|
|
@@ -128,6 +137,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
128
137
|
server,
|
|
129
138
|
cache: browserCache,
|
|
130
139
|
partials,
|
|
140
|
+
scheduler,
|
|
131
141
|
fetch: options.fetch,
|
|
132
142
|
routeEndpoint: options.routeEndpoint,
|
|
133
143
|
attributes
|
|
@@ -139,7 +149,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
139
149
|
}
|
|
140
150
|
} else {
|
|
141
151
|
configureServerContext({ cache: serverCache });
|
|
142
|
-
signals._setContext?.({ server, cache: serverCache });
|
|
152
|
+
signals._setContext?.({ server, cache: serverCache, scheduler });
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
return runtime;
|
|
@@ -153,9 +163,10 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
153
163
|
async render(url) {
|
|
154
164
|
assertActive();
|
|
155
165
|
configureServerContext({ cache: serverCache });
|
|
156
|
-
signals._setContext?.({ server, cache: serverCache });
|
|
166
|
+
signals._setContext?.({ server, cache: serverCache, scheduler });
|
|
157
167
|
const matched = routes.match(url);
|
|
158
168
|
if (!matched) {
|
|
169
|
+
await scheduler.flush();
|
|
159
170
|
return {
|
|
160
171
|
html: renderDocument("", { status: 404, signals, browserCache, boundary: options.boundary ?? "route", attributes }),
|
|
161
172
|
status: 404,
|
|
@@ -175,8 +186,8 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
175
186
|
cache: serverCache,
|
|
176
187
|
browserCache,
|
|
177
188
|
partials,
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
scheduler,
|
|
190
|
+
...currentRequestContext()
|
|
180
191
|
})
|
|
181
192
|
: { html: "" };
|
|
182
193
|
|
|
@@ -189,6 +200,8 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
189
200
|
browserCache.restore(result.cache.browser);
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
await scheduler.flush();
|
|
204
|
+
|
|
192
205
|
const status = result.status ?? 200;
|
|
193
206
|
return {
|
|
194
207
|
html: renderDocument(result.html, { status, signals, browserCache, boundary: result.boundary ?? options.boundary ?? "route", attributes }),
|
|
@@ -207,6 +220,9 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
207
220
|
router?.destroy?.();
|
|
208
221
|
loader?.destroy?.();
|
|
209
222
|
signals.destroy?.();
|
|
223
|
+
if (ownsScheduler) {
|
|
224
|
+
scheduler.destroy();
|
|
225
|
+
}
|
|
210
226
|
},
|
|
211
227
|
|
|
212
228
|
_applyUse(normalized) {
|
|
@@ -227,11 +243,23 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
227
243
|
loader,
|
|
228
244
|
router,
|
|
229
245
|
cache,
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
scheduler,
|
|
247
|
+
requestContext: options.requestContext,
|
|
248
|
+
...currentRequestContext()
|
|
232
249
|
});
|
|
233
250
|
}
|
|
234
251
|
|
|
252
|
+
function currentRequestContext() {
|
|
253
|
+
const context = readRequestContextLike(options.requestContext);
|
|
254
|
+
return {
|
|
255
|
+
requestContext: context,
|
|
256
|
+
request: context.request ?? options.request,
|
|
257
|
+
headers: context.headers ?? options.headers,
|
|
258
|
+
cookies: context.cookies ?? options.cookies,
|
|
259
|
+
locals: context.locals ?? options.locals
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
235
263
|
function assertActive() {
|
|
236
264
|
if (destroyed) {
|
|
237
265
|
throw new Error("Async app runtime has been destroyed.");
|
|
@@ -385,6 +413,77 @@ function attachServerCache(server, cache) {
|
|
|
385
413
|
}
|
|
386
414
|
}
|
|
387
415
|
|
|
416
|
+
function createServerReferenceRegistry(initialMap = {}, options = {}) {
|
|
417
|
+
const registry = options.registry ?? createRegistryStore();
|
|
418
|
+
const type = options.type ?? "server";
|
|
419
|
+
const defaults = {};
|
|
420
|
+
|
|
421
|
+
const reference = {
|
|
422
|
+
registry,
|
|
423
|
+
|
|
424
|
+
register(id, value) {
|
|
425
|
+
registry.register(type, id, value);
|
|
426
|
+
return id;
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
registerMany(map) {
|
|
430
|
+
for (const [id, value] of Object.entries(map ?? {})) {
|
|
431
|
+
reference.register(id, value);
|
|
432
|
+
}
|
|
433
|
+
return reference;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
unregister(id) {
|
|
437
|
+
return registry.unregister(type, id);
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
resolve() {
|
|
441
|
+
return undefined;
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async run(id) {
|
|
445
|
+
throw new Error(`Server command "${id}" cannot run without a server proxy or server registry.`);
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
keys() {
|
|
449
|
+
return registry.keys(type);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
entries() {
|
|
453
|
+
return registry.entries(type);
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
inspect() {
|
|
457
|
+
return registry.entries(type);
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
_setContext(context = {}) {
|
|
461
|
+
Object.assign(defaults, context);
|
|
462
|
+
return reference;
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
_adoptMany() {
|
|
466
|
+
return reference;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
reference.registerMany(initialMap);
|
|
471
|
+
return createServerNamespace((id, args, context) => reference.run(id, args, context), reference, () => defaults);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function readRequestContextLike(store) {
|
|
475
|
+
if (!store) {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
if (typeof store.get === "function") {
|
|
479
|
+
return store.get() ?? {};
|
|
480
|
+
}
|
|
481
|
+
if (typeof store.getStore === "function") {
|
|
482
|
+
return store.getStore() ?? {};
|
|
483
|
+
}
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
|
|
388
487
|
function normalizeEntries(type, entries = {}) {
|
|
389
488
|
if (type !== "signal") {
|
|
390
489
|
return { ...(entries ?? {}) };
|
package/src/async-signal.js
CHANGED
|
@@ -90,7 +90,8 @@ export function asyncSignal(id, fn) {
|
|
|
90
90
|
router: context.router,
|
|
91
91
|
loader: context.loader,
|
|
92
92
|
cache: context.cache,
|
|
93
|
-
abort: activeAbort
|
|
93
|
+
abort: activeAbort,
|
|
94
|
+
scheduler: context.scheduler
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
return server;
|
|
@@ -104,6 +105,9 @@ export function asyncSignal(id, fn) {
|
|
|
104
105
|
get cache() {
|
|
105
106
|
return registry._context?.().cache;
|
|
106
107
|
},
|
|
108
|
+
get scheduler() {
|
|
109
|
+
return registry._context?.().scheduler;
|
|
110
|
+
},
|
|
107
111
|
get version() {
|
|
108
112
|
return runVersion;
|
|
109
113
|
},
|
|
@@ -180,11 +184,20 @@ export function asyncSignal(id, fn) {
|
|
|
180
184
|
_bindRegistry(nextRegistry, nextId) {
|
|
181
185
|
registry = nextRegistry;
|
|
182
186
|
registeredId = nextId;
|
|
183
|
-
|
|
187
|
+
const start = () => {
|
|
184
188
|
if (registry === nextRegistry && status === "idle") {
|
|
185
189
|
state.refresh();
|
|
186
190
|
}
|
|
187
|
-
}
|
|
191
|
+
};
|
|
192
|
+
const scheduler = registry._context?.().scheduler;
|
|
193
|
+
if (scheduler) {
|
|
194
|
+
scheduler.enqueue("async", start, {
|
|
195
|
+
scope: registeredId,
|
|
196
|
+
key: `asyncSignal:${registeredId}:initial`
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
queueMicrotask(start);
|
|
200
|
+
}
|
|
188
201
|
},
|
|
189
202
|
|
|
190
203
|
_dispose() {
|
|
@@ -220,11 +233,26 @@ export function asyncSignal(id, fn) {
|
|
|
220
233
|
for (const dependency of dependencies) {
|
|
221
234
|
const dependencyId = String(dependency).split(".")[0];
|
|
222
235
|
if (dependencyId && dependencyId !== registeredId) {
|
|
223
|
-
dependencyCleanups.add(registry.subscribe(dependency, () =>
|
|
236
|
+
dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
|
|
224
237
|
}
|
|
225
238
|
}
|
|
226
239
|
}
|
|
227
240
|
|
|
241
|
+
function scheduleRefresh() {
|
|
242
|
+
if (activeAbort && !activeAbort.aborted) {
|
|
243
|
+
activeAbort.cancel(new Error(`Async signal "${registeredId}" dependency changed.`));
|
|
244
|
+
}
|
|
245
|
+
const scheduler = registry?._context?.().scheduler;
|
|
246
|
+
if (!scheduler) {
|
|
247
|
+
state.refresh();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
scheduler.enqueue("async", () => state.refresh(), {
|
|
251
|
+
scope: registeredId,
|
|
252
|
+
key: `asyncSignal:${registeredId}:refresh`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
228
256
|
function notify() {
|
|
229
257
|
for (const subscriber of [...subscribers]) {
|
|
230
258
|
subscriber(state);
|
|
@@ -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
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { asyncSignal } from "./async-signal.js";
|
|
2
|
+
export { Async, createApp, defineApp, readSnapshot } from "./app.js";
|
|
3
|
+
export { attributeName, defineAttributeConfig } from "./attributes.js";
|
|
4
|
+
export { createBoundaryReceiver } from "./boundary-receiver.js";
|
|
5
|
+
export { createCacheRegistry, defineCache } from "./cache.js";
|
|
6
|
+
export { component, createComponentRegistry, defineComponent } from "./component.js";
|
|
7
|
+
export { delay } from "./delay.js";
|
|
8
|
+
export { createHandlerRegistry } from "./handlers.js";
|
|
9
|
+
export { html } from "./html.js";
|
|
10
|
+
export { Loader, AsyncLoader } from "./loader.js";
|
|
11
|
+
export { createPartialRegistry } from "./partials.js";
|
|
12
|
+
export { createRegistryStore } from "./registry-store.js";
|
|
13
|
+
export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
|
|
14
|
+
export { createScheduler } from "./scheduler.js";
|
|
15
|
+
export { applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult } from "./server.js";
|
|
16
|
+
export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";
|