@async/framework 0.5.0 → 0.7.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 +32 -0
- package/README.md +64 -9
- package/examples/cache/index.html +1 -1
- package/examples/components/index.html +1 -1
- package/examples/components/main.js +2 -2
- package/examples/counter/index.html +1 -1
- package/examples/partials/index.html +1 -1
- package/examples/product/index.html +1 -1
- package/examples/product/main.js +2 -2
- package/examples/router/index.html +1 -1
- package/examples/server-call/index.html +1 -1
- package/examples/ssr/index.html +1 -1
- package/examples/streaming/index.html +1 -1
- package/framework.d.ts +572 -0
- package/framework.js +216 -29
- package/framework.min.js +3820 -0
- package/framework.ts +4321 -0
- package/framework.umd.js +4342 -0
- package/framework.umd.min.js +3843 -0
- package/package.json +34 -5
- package/src/app.js +35 -3
- package/src/cache.js +27 -3
- package/src/component.js +1 -1
- package/src/index.js +2 -2
- package/src/loader.js +4 -2
- package/src/router.js +100 -16
- package/src/server.js +44 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "No-build
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "No-build Loader app runtime with signals, command events, server calls, route partials, cache split, SSR activation, and streaming boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.1.0",
|
|
7
7
|
"engines": {
|
|
@@ -28,21 +28,49 @@
|
|
|
28
28
|
"web-framework"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
|
-
"
|
|
31
|
+
"types": "./framework.d.ts",
|
|
32
|
+
"unpkg": "./framework.umd.min.js",
|
|
33
|
+
"jsdelivr": "./framework.umd.min.js",
|
|
32
34
|
"exports": {
|
|
33
35
|
".": {
|
|
34
|
-
"
|
|
36
|
+
"types": "./framework.d.ts",
|
|
37
|
+
"unpkg": "./framework.umd.min.js",
|
|
35
38
|
"browser": "./framework.js",
|
|
36
39
|
"import": "./src/index.js",
|
|
37
40
|
"default": "./src/index.js"
|
|
38
41
|
},
|
|
39
|
-
"./framework.js":
|
|
42
|
+
"./framework.js": {
|
|
43
|
+
"types": "./framework.d.ts",
|
|
44
|
+
"default": "./framework.js"
|
|
45
|
+
},
|
|
46
|
+
"./framework.min.js": {
|
|
47
|
+
"types": "./framework.d.ts",
|
|
48
|
+
"default": "./framework.min.js"
|
|
49
|
+
},
|
|
50
|
+
"./framework.umd.js": {
|
|
51
|
+
"types": "./framework.d.ts",
|
|
52
|
+
"default": "./framework.umd.js"
|
|
53
|
+
},
|
|
54
|
+
"./framework.umd.min.js": {
|
|
55
|
+
"types": "./framework.d.ts",
|
|
56
|
+
"default": "./framework.umd.min.js"
|
|
57
|
+
},
|
|
58
|
+
"./framework.ts": {
|
|
59
|
+
"types": "./framework.d.ts",
|
|
60
|
+
"default": "./framework.ts"
|
|
61
|
+
},
|
|
62
|
+
"./framework.d.ts": "./framework.d.ts",
|
|
40
63
|
"./package.json": "./package.json"
|
|
41
64
|
},
|
|
42
65
|
"files": [
|
|
43
66
|
"CHANGELOG.md",
|
|
44
67
|
"README.md",
|
|
68
|
+
"framework.d.ts",
|
|
45
69
|
"framework.js",
|
|
70
|
+
"framework.min.js",
|
|
71
|
+
"framework.umd.js",
|
|
72
|
+
"framework.umd.min.js",
|
|
73
|
+
"framework.ts",
|
|
46
74
|
"src",
|
|
47
75
|
"examples",
|
|
48
76
|
"LICENSE",
|
|
@@ -68,6 +96,7 @@
|
|
|
68
96
|
"pipeline:sync:generate": "async-pipeline sync generate",
|
|
69
97
|
"pipeline:task:docs.site": "async-pipeline run-task docs.site",
|
|
70
98
|
"pipeline:verify": "async-pipeline run verify",
|
|
99
|
+
"registry:lint": "node scripts/registry-lint.js",
|
|
71
100
|
"release:check": "pnpm run pipeline:verify -- --force && pnpm run pipeline:pages -- --force && pnpm run pipeline:sync:check && pnpm run pipeline:github:check",
|
|
72
101
|
"test": "node --test tests/*.test.js"
|
|
73
102
|
},
|
package/src/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCacheRegistry } from "./cache.js";
|
|
2
2
|
import { createComponentRegistry } from "./component.js";
|
|
3
3
|
import { createHandlerRegistry } from "./handlers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { Loader } from "./loader.js";
|
|
5
5
|
import { createPartialRegistry } from "./partials.js";
|
|
6
6
|
import { createRouteRegistry, createRouter } from "./router.js";
|
|
7
7
|
import { createServerRegistry } from "./server.js";
|
|
@@ -73,7 +73,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
73
73
|
let started = false;
|
|
74
74
|
let destroyed = false;
|
|
75
75
|
|
|
76
|
-
applySnapshot(signals, browserCache, options.snapshot);
|
|
76
|
+
applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
|
|
77
77
|
attachServerCache(server, serverCache);
|
|
78
78
|
|
|
79
79
|
const runtime = {
|
|
@@ -101,7 +101,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
101
101
|
started = true;
|
|
102
102
|
|
|
103
103
|
if (target !== "server") {
|
|
104
|
-
loader = loader ??
|
|
104
|
+
loader = loader ?? Loader({
|
|
105
105
|
root: options.root,
|
|
106
106
|
signals,
|
|
107
107
|
handlers,
|
|
@@ -241,6 +241,38 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
241
241
|
|
|
242
242
|
export const Async = defineApp();
|
|
243
243
|
|
|
244
|
+
export function readSnapshot(root = globalThis.document, { attributes } = {}) {
|
|
245
|
+
const attributeConfig = normalizeAttributeConfig(attributes);
|
|
246
|
+
const snapshotAttr = attributeName(attributeConfig, "async", "snapshot");
|
|
247
|
+
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
248
|
+
const rootNode = root ?? documentRef;
|
|
249
|
+
if (!rootNode?.querySelectorAll && !documentRef?.querySelectorAll) {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const searchRoot of new Set([rootNode, documentRef])) {
|
|
254
|
+
if (!searchRoot?.querySelectorAll) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const script of searchRoot.querySelectorAll("script[type='application/json'], script")) {
|
|
258
|
+
if (!script.hasAttribute?.(snapshotAttr)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const source = script.textContent?.trim() ?? "";
|
|
262
|
+
if (!source) {
|
|
263
|
+
return {};
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(source);
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
|
|
244
276
|
function applyUseToRuntime(runtime, normalized) {
|
|
245
277
|
applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
|
|
246
278
|
applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
|
package/src/cache.js
CHANGED
|
@@ -15,6 +15,7 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
|
|
|
15
15
|
const registryStore = registry ?? createRegistryStore();
|
|
16
16
|
const definitions = registryStore._map(type);
|
|
17
17
|
const entries = registryStore._map(`${type}.entries`);
|
|
18
|
+
const pending = new Map();
|
|
18
19
|
|
|
19
20
|
const registryApi = attachRegistryInspection({
|
|
20
21
|
register(id, definition = defineCache()) {
|
|
@@ -76,19 +77,37 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
|
|
|
76
77
|
if (cached !== undefined) {
|
|
77
78
|
return cached;
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if (pending.has(key)) {
|
|
81
|
+
return pending.get(key);
|
|
82
|
+
}
|
|
83
|
+
let promise;
|
|
84
|
+
promise = Promise.resolve()
|
|
85
|
+
.then(fn)
|
|
86
|
+
.then((value) => {
|
|
87
|
+
if (pending.get(key) === promise) {
|
|
88
|
+
registryApi.set(key, value, options);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
})
|
|
92
|
+
.finally(() => {
|
|
93
|
+
if (pending.get(key) === promise) {
|
|
94
|
+
pending.delete(key);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
pending.set(key, promise);
|
|
98
|
+
return promise;
|
|
82
99
|
},
|
|
83
100
|
|
|
84
101
|
delete(key) {
|
|
85
102
|
assertKey(key);
|
|
103
|
+
pending.delete(key);
|
|
86
104
|
return entries.delete(key);
|
|
87
105
|
},
|
|
88
106
|
|
|
89
107
|
clear(prefix) {
|
|
90
108
|
if (prefix === undefined) {
|
|
91
109
|
entries.clear();
|
|
110
|
+
pending.clear();
|
|
92
111
|
return registryApi;
|
|
93
112
|
}
|
|
94
113
|
for (const key of [...entries.keys()]) {
|
|
@@ -96,6 +115,11 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
|
|
|
96
115
|
entries.delete(key);
|
|
97
116
|
}
|
|
98
117
|
}
|
|
118
|
+
for (const key of [...pending.keys()]) {
|
|
119
|
+
if (key.startsWith(prefix)) {
|
|
120
|
+
pending.delete(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
99
123
|
return registryApi;
|
|
100
124
|
},
|
|
101
125
|
|
package/src/component.js
CHANGED
|
@@ -89,7 +89,7 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
89
89
|
bind(value) {
|
|
90
90
|
const id = runtime.loader?._registerBinding?.(value);
|
|
91
91
|
if (!id) {
|
|
92
|
-
throw new Error("Inline template bindings require
|
|
92
|
+
throw new Error("Inline template bindings require a Loader.");
|
|
93
93
|
}
|
|
94
94
|
bindingIds.push(id);
|
|
95
95
|
return id;
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { asyncSignal } from "./async-signal.js";
|
|
2
|
-
export { Async, createApp, defineApp } from "./app.js";
|
|
2
|
+
export { Async, createApp, defineApp, readSnapshot } from "./app.js";
|
|
3
3
|
export { attributeName, defineAttributeConfig } from "./attributes.js";
|
|
4
4
|
export { createCacheRegistry, defineCache } from "./cache.js";
|
|
5
5
|
export { component, createComponentRegistry, defineComponent } from "./component.js";
|
|
6
6
|
export { delay } from "./delay.js";
|
|
7
7
|
export { createHandlerRegistry } from "./handlers.js";
|
|
8
8
|
export { html } from "./html.js";
|
|
9
|
-
export { AsyncLoader } from "./loader.js";
|
|
9
|
+
export { Loader, AsyncLoader } from "./loader.js";
|
|
10
10
|
export { createPartialRegistry } from "./partials.js";
|
|
11
11
|
export { createRegistryStore } from "./registry-store.js";
|
|
12
12
|
export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
|
package/src/loader.js
CHANGED
|
@@ -5,7 +5,7 @@ import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attri
|
|
|
5
5
|
|
|
6
6
|
const inlineBindingPrefix = "__async:inline:";
|
|
7
7
|
|
|
8
|
-
export function
|
|
8
|
+
export function Loader({ root, signals, handlers, server, router, cache, attributes } = {}) {
|
|
9
9
|
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
10
10
|
const rootNode = root ?? documentRef;
|
|
11
11
|
const signalRegistry = signals ?? createSignalRegistry();
|
|
@@ -447,7 +447,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
447
447
|
|
|
448
448
|
function assertActive() {
|
|
449
449
|
if (destroyed) {
|
|
450
|
-
throw new Error("
|
|
450
|
+
throw new Error("Loader has been destroyed.");
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
@@ -511,6 +511,8 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
511
511
|
return api;
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
export const AsyncLoader = Loader;
|
|
515
|
+
|
|
514
516
|
function normalizeClassTokens(value, tokens = new Set()) {
|
|
515
517
|
if (value == null || value === false) {
|
|
516
518
|
return tokens;
|
package/src/router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Loader } from "./loader.js";
|
|
2
2
|
import { createHandlerRegistry } from "./handlers.js";
|
|
3
3
|
import { createSignalRegistry } from "./signals.js";
|
|
4
4
|
import { applyServerResult } from "./server.js";
|
|
@@ -31,6 +31,7 @@ export function createRouteRegistry(initialMap = {}, options = {}) {
|
|
|
31
31
|
const nextRoute = normalizeRoute(pattern, definition);
|
|
32
32
|
entries.set(pattern, nextRoute.definition);
|
|
33
33
|
routes.push(nextRoute);
|
|
34
|
+
sortRoutes(routes);
|
|
34
35
|
return nextRoute;
|
|
35
36
|
},
|
|
36
37
|
|
|
@@ -103,6 +104,7 @@ export function createRouteRegistry(initialMap = {}, options = {}) {
|
|
|
103
104
|
const nextRoute = normalizeRoute(pattern, definition);
|
|
104
105
|
entries.set(pattern, nextRoute.definition);
|
|
105
106
|
routes.push(nextRoute);
|
|
107
|
+
sortRoutes(routes);
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -128,7 +130,7 @@ export function createRouter({
|
|
|
128
130
|
const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
|
|
129
131
|
const loaderInstance =
|
|
130
132
|
loader ??
|
|
131
|
-
|
|
133
|
+
Loader({
|
|
132
134
|
root: rootNode,
|
|
133
135
|
signals: signalRegistry,
|
|
134
136
|
handlers: handlerRegistry,
|
|
@@ -139,6 +141,8 @@ export function createRouter({
|
|
|
139
141
|
const ownsLoader = !loader;
|
|
140
142
|
const cleanups = new Set();
|
|
141
143
|
let destroyed = false;
|
|
144
|
+
let navigationVersion = 0;
|
|
145
|
+
let activeNavigation;
|
|
142
146
|
|
|
143
147
|
const api = {
|
|
144
148
|
mode,
|
|
@@ -178,7 +182,7 @@ export function createRouter({
|
|
|
178
182
|
},
|
|
179
183
|
|
|
180
184
|
match(url) {
|
|
181
|
-
return routes.match(url);
|
|
185
|
+
return routes.match(resolveUrl(url));
|
|
182
186
|
},
|
|
183
187
|
|
|
184
188
|
prefetch(url) {
|
|
@@ -203,7 +207,7 @@ export function createRouter({
|
|
|
203
207
|
return null;
|
|
204
208
|
}
|
|
205
209
|
|
|
206
|
-
const target =
|
|
210
|
+
const target = resolveUrl(url);
|
|
207
211
|
if (mode === "ssr-spa") {
|
|
208
212
|
return fetchRoutePartial(target, options);
|
|
209
213
|
}
|
|
@@ -215,6 +219,7 @@ export function createRouter({
|
|
|
215
219
|
return;
|
|
216
220
|
}
|
|
217
221
|
destroyed = true;
|
|
222
|
+
activeNavigation?.controller.abort(new Error("Router has been destroyed."));
|
|
218
223
|
for (const cleanup of cleanups) {
|
|
219
224
|
cleanup();
|
|
220
225
|
}
|
|
@@ -254,24 +259,37 @@ export function createRouter({
|
|
|
254
259
|
async function renderLocalRoutePartial(target, options = {}) {
|
|
255
260
|
const matched = api.match(target);
|
|
256
261
|
if (!matched) {
|
|
262
|
+
beginNavigation(target, null);
|
|
257
263
|
setNoRouteError(target);
|
|
258
264
|
return null;
|
|
259
265
|
}
|
|
260
266
|
|
|
267
|
+
const navigation = beginNavigation(target, matched);
|
|
261
268
|
setMatchedRouterState(target, matched, { pending: true, error: null });
|
|
262
269
|
|
|
263
270
|
try {
|
|
264
271
|
if (!matched.route?.partial || !partials?.resolve?.(matched.route.partial)) {
|
|
265
272
|
const error = new Error(`Route "${target.pathname}" does not have a registered partial.`);
|
|
266
|
-
|
|
273
|
+
if (isActiveNavigation(navigation)) {
|
|
274
|
+
setRouterState({ pending: false, error });
|
|
275
|
+
}
|
|
267
276
|
return null;
|
|
268
277
|
}
|
|
269
278
|
|
|
270
|
-
const result = await partials.render(matched.route.partial, matched.params, contextFor(matched));
|
|
271
|
-
|
|
279
|
+
const result = await partials.render(matched.route.partial, matched.params, contextFor(matched, navigation));
|
|
280
|
+
if (!isActiveNavigation(navigation)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
await applyNavigationResult(result, target, options, navigation);
|
|
284
|
+
if (!isActiveNavigation(navigation)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
272
287
|
setRouterState({ pending: false, error: null });
|
|
273
288
|
return result;
|
|
274
289
|
} catch (error) {
|
|
290
|
+
if (!isActiveNavigation(navigation)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
275
293
|
setRouterState({ pending: false, error });
|
|
276
294
|
throw error;
|
|
277
295
|
}
|
|
@@ -279,26 +297,43 @@ export function createRouter({
|
|
|
279
297
|
|
|
280
298
|
async function fetchRoutePartial(target, options = {}) {
|
|
281
299
|
const matched = api.match(target);
|
|
300
|
+
const navigation = beginNavigation(target, matched);
|
|
282
301
|
setMatchedRouterState(target, matched, { pending: true, error: null });
|
|
283
302
|
|
|
284
303
|
try {
|
|
285
|
-
const result = await fetchRoute(target.href);
|
|
286
|
-
|
|
304
|
+
const result = await fetchRoute(target.href, { signal: navigation.abort });
|
|
305
|
+
if (!isActiveNavigation(navigation)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
await applyNavigationResult(result, target, options, navigation);
|
|
309
|
+
if (!isActiveNavigation(navigation)) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
287
312
|
setRouterState({ pending: false, error: null });
|
|
288
313
|
return result;
|
|
289
314
|
} catch (error) {
|
|
315
|
+
if (!isActiveNavigation(navigation)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
290
318
|
setRouterState({ pending: false, error });
|
|
291
319
|
throw error;
|
|
292
320
|
}
|
|
293
321
|
}
|
|
294
322
|
|
|
295
|
-
async function applyNavigationResult(result, target, options) {
|
|
323
|
+
async function applyNavigationResult(result, target, options, navigation) {
|
|
324
|
+
if (!isActiveNavigation(navigation)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
296
327
|
await applyServerResult(result, {
|
|
297
328
|
signals: signalRegistry,
|
|
298
329
|
loader: loaderInstance,
|
|
299
330
|
router: api,
|
|
300
|
-
cache
|
|
331
|
+
cache,
|
|
332
|
+
abort: navigation?.abort
|
|
301
333
|
});
|
|
334
|
+
if (!isActiveNavigation(navigation)) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
302
337
|
if (result?.html != null && !result.boundary && !result.redirect) {
|
|
303
338
|
loaderInstance.swap(boundary, result.html);
|
|
304
339
|
}
|
|
@@ -312,14 +347,15 @@ export function createRouter({
|
|
|
312
347
|
documentRef.defaultView?.history?.pushState?.({}, "", target.href);
|
|
313
348
|
}
|
|
314
349
|
|
|
315
|
-
async function fetchRoute(url, { prefetch = false } = {}) {
|
|
350
|
+
async function fetchRoute(url, { prefetch = false, signal } = {}) {
|
|
316
351
|
if (typeof fetchImpl !== "function") {
|
|
317
352
|
throw new Error("Router navigation requires a partial registry or fetch.");
|
|
318
353
|
}
|
|
319
354
|
const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
|
|
320
355
|
headers: {
|
|
321
356
|
accept: "application/json, text/html"
|
|
322
|
-
}
|
|
357
|
+
},
|
|
358
|
+
signal
|
|
323
359
|
});
|
|
324
360
|
if (!response.ok) {
|
|
325
361
|
throw new Error(`Route "${url}" failed with ${response.status}.`);
|
|
@@ -334,7 +370,7 @@ export function createRouter({
|
|
|
334
370
|
return { boundary, html: await response.text() };
|
|
335
371
|
}
|
|
336
372
|
|
|
337
|
-
function contextFor(matched) {
|
|
373
|
+
function contextFor(matched, navigation) {
|
|
338
374
|
return {
|
|
339
375
|
params: matched.params,
|
|
340
376
|
route: matched.route,
|
|
@@ -344,10 +380,28 @@ export function createRouter({
|
|
|
344
380
|
loader: loaderInstance,
|
|
345
381
|
server,
|
|
346
382
|
cache,
|
|
347
|
-
abort:
|
|
383
|
+
abort: navigation?.abort
|
|
348
384
|
};
|
|
349
385
|
}
|
|
350
386
|
|
|
387
|
+
function beginNavigation(target, matched) {
|
|
388
|
+
activeNavigation?.controller.abort(new Error(`Router navigation superseded by ${target.pathname}${target.search}.`));
|
|
389
|
+
const controller = new AbortController();
|
|
390
|
+
const navigation = {
|
|
391
|
+
id: ++navigationVersion,
|
|
392
|
+
controller,
|
|
393
|
+
abort: controller.signal,
|
|
394
|
+
target,
|
|
395
|
+
matched
|
|
396
|
+
};
|
|
397
|
+
activeNavigation = navigation;
|
|
398
|
+
return navigation;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isActiveNavigation(navigation) {
|
|
402
|
+
return !destroyed && navigation && activeNavigation?.id === navigation.id && !navigation.abort.aborted;
|
|
403
|
+
}
|
|
404
|
+
|
|
351
405
|
function updateStateFromLocation() {
|
|
352
406
|
const url = currentUrl();
|
|
353
407
|
const matched = api.match(url);
|
|
@@ -382,7 +436,14 @@ export function createRouter({
|
|
|
382
436
|
}
|
|
383
437
|
|
|
384
438
|
function currentUrl() {
|
|
385
|
-
return
|
|
439
|
+
return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function resolveUrl(url) {
|
|
443
|
+
if (url instanceof URL) {
|
|
444
|
+
return url;
|
|
445
|
+
}
|
|
446
|
+
return new URL(String(url), documentRef.defaultView?.location?.href ?? "http://localhost/");
|
|
386
447
|
}
|
|
387
448
|
|
|
388
449
|
function assertActive() {
|
|
@@ -399,6 +460,7 @@ function normalizeRoute(pattern, definition) {
|
|
|
399
460
|
pattern,
|
|
400
461
|
regex,
|
|
401
462
|
keys,
|
|
463
|
+
score: routeScore(pattern),
|
|
402
464
|
definition: normalized
|
|
403
465
|
};
|
|
404
466
|
}
|
|
@@ -467,6 +529,28 @@ function escapeRegExp(value) {
|
|
|
467
529
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
468
530
|
}
|
|
469
531
|
|
|
532
|
+
function sortRoutes(routes) {
|
|
533
|
+
routes.sort((left, right) => right.score - left.score || right.pattern.length - left.pattern.length);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function routeScore(pattern) {
|
|
537
|
+
if (pattern === "*") {
|
|
538
|
+
return -1;
|
|
539
|
+
}
|
|
540
|
+
return pattern
|
|
541
|
+
.split("/")
|
|
542
|
+
.filter(Boolean)
|
|
543
|
+
.reduce((score, segment) => {
|
|
544
|
+
if (segment === "*") {
|
|
545
|
+
return score;
|
|
546
|
+
}
|
|
547
|
+
if (segment.startsWith(":")) {
|
|
548
|
+
return score + 2;
|
|
549
|
+
}
|
|
550
|
+
return score + 4;
|
|
551
|
+
}, pattern === "/" ? 3 : 0);
|
|
552
|
+
}
|
|
553
|
+
|
|
470
554
|
function assertPattern(pattern) {
|
|
471
555
|
if (typeof pattern !== "string" || (pattern !== "*" && !pattern.startsWith("/"))) {
|
|
472
556
|
throw new TypeError("Route pattern must be a path string or \"*\".");
|
package/src/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
2
2
|
|
|
3
3
|
const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
|
|
4
|
+
const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
|
|
5
|
+
const appliedServerValues = new WeakSet();
|
|
4
6
|
|
|
5
7
|
export function createServerRegistry(initialMap = {}, options = {}) {
|
|
6
8
|
const registryStore = options.registry ?? createRegistryStore();
|
|
@@ -107,6 +109,7 @@ export function createServerProxy({
|
|
|
107
109
|
input: context.input ?? defaultInput(runContext),
|
|
108
110
|
signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
|
|
109
111
|
};
|
|
112
|
+
assertJsonTransportable(body);
|
|
110
113
|
|
|
111
114
|
const response = await fetchImpl(joinEndpoint(endpoint, id), {
|
|
112
115
|
method: "POST",
|
|
@@ -124,7 +127,7 @@ export function createServerProxy({
|
|
|
124
127
|
|
|
125
128
|
const result = await readServerResponse(response);
|
|
126
129
|
await applyServerResult(result, runContext);
|
|
127
|
-
return unwrapServerResult(result);
|
|
130
|
+
return markAppliedServerValue(unwrapServerResult(result));
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
return createServerNamespace(run, {
|
|
@@ -159,6 +162,9 @@ export async function applyServerResult(result, context = {}) {
|
|
|
159
162
|
if (!isServerEnvelope(result)) {
|
|
160
163
|
return result;
|
|
161
164
|
}
|
|
165
|
+
if (result[appliedServerResult] || appliedServerValues.has(result)) {
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
162
168
|
|
|
163
169
|
if (result.signals && context.signals) {
|
|
164
170
|
for (const [path, value] of Object.entries(result.signals)) {
|
|
@@ -182,6 +188,12 @@ export async function applyServerResult(result, context = {}) {
|
|
|
182
188
|
throw toError(result.error);
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
Object.defineProperty(result, appliedServerResult, {
|
|
192
|
+
configurable: true,
|
|
193
|
+
enumerable: false,
|
|
194
|
+
value: true
|
|
195
|
+
});
|
|
196
|
+
|
|
185
197
|
return result;
|
|
186
198
|
}
|
|
187
199
|
|
|
@@ -192,6 +204,13 @@ export function unwrapServerResult(result) {
|
|
|
192
204
|
return result;
|
|
193
205
|
}
|
|
194
206
|
|
|
207
|
+
function markAppliedServerValue(value) {
|
|
208
|
+
if (value && typeof value === "object") {
|
|
209
|
+
appliedServerValues.add(value);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
195
214
|
export function defaultInput(context = {}) {
|
|
196
215
|
const form = findForm(context);
|
|
197
216
|
if (form) {
|
|
@@ -369,6 +388,30 @@ function formDataToObject(formData) {
|
|
|
369
388
|
return output;
|
|
370
389
|
}
|
|
371
390
|
|
|
391
|
+
function assertJsonTransportable(value, seen = new Set()) {
|
|
392
|
+
if (value == null || typeof value !== "object") {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (seen.has(value)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
seen.add(value);
|
|
399
|
+
|
|
400
|
+
const tag = Object.prototype.toString.call(value);
|
|
401
|
+
if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
|
|
402
|
+
throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(value)) {
|
|
405
|
+
for (const item of value) {
|
|
406
|
+
assertJsonTransportable(item, seen);
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
for (const item of Object.values(value)) {
|
|
411
|
+
assertJsonTransportable(item, seen);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
372
415
|
function joinEndpoint(endpoint, id) {
|
|
373
416
|
return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
|
|
374
417
|
}
|