@async/framework 0.6.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 +15 -0
- package/README.md +13 -2
- package/framework.d.ts +3 -0
- package/framework.js +205 -21
- package/framework.min.js +190 -18
- package/framework.ts +4320 -2
- package/framework.umd.js +205 -21
- package/framework.umd.min.js +190 -18
- package/package.json +1 -1
- package/src/app.js +33 -1
- package/src/cache.js +27 -3
- package/src/index.js +1 -1
- package/src/router.js +98 -14
- package/src/server.js +44 -1
package/framework.umd.min.js
CHANGED
|
@@ -481,6 +481,7 @@ function createCacheRegistry(initialMap = {}, { now = () => Date.now(), registry
|
|
|
481
481
|
const registryStore = registry ?? createRegistryStore();
|
|
482
482
|
const definitions = registryStore._map(type);
|
|
483
483
|
const entries = registryStore._map(`${type}.entries`);
|
|
484
|
+
const pending = new Map();
|
|
484
485
|
const registryApi = attachRegistryInspection({
|
|
485
486
|
register(id, definition = defineCache()) {
|
|
486
487
|
assertId(id);
|
|
@@ -535,17 +536,35 @@ const cached = registryApi.get(key);
|
|
|
535
536
|
if (cached !== undefined) {
|
|
536
537
|
return cached;
|
|
537
538
|
}
|
|
538
|
-
|
|
539
|
+
if (pending.has(key)) {
|
|
540
|
+
return pending.get(key);
|
|
541
|
+
}
|
|
542
|
+
let promise;
|
|
543
|
+
promise = Promise.resolve()
|
|
544
|
+
.then(fn)
|
|
545
|
+
.then((value) => {
|
|
546
|
+
if (pending.get(key) === promise) {
|
|
539
547
|
registryApi.set(key, value, options);
|
|
548
|
+
}
|
|
540
549
|
return value;
|
|
550
|
+
})
|
|
551
|
+
.finally(() => {
|
|
552
|
+
if (pending.get(key) === promise) {
|
|
553
|
+
pending.delete(key);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
pending.set(key, promise);
|
|
557
|
+
return promise;
|
|
541
558
|
},
|
|
542
559
|
delete(key) {
|
|
543
560
|
assertKey(key);
|
|
561
|
+
pending.delete(key);
|
|
544
562
|
return entries.delete(key);
|
|
545
563
|
},
|
|
546
564
|
clear(prefix) {
|
|
547
565
|
if (prefix === undefined) {
|
|
548
566
|
entries.clear();
|
|
567
|
+
pending.clear();
|
|
549
568
|
return registryApi;
|
|
550
569
|
}
|
|
551
570
|
for (const key of [...entries.keys()]) {
|
|
@@ -553,6 +572,11 @@ if (key.startsWith(prefix)) {
|
|
|
553
572
|
entries.delete(key);
|
|
554
573
|
}
|
|
555
574
|
}
|
|
575
|
+
for (const key of [...pending.keys()]) {
|
|
576
|
+
if (key.startsWith(prefix)) {
|
|
577
|
+
pending.delete(key);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
556
580
|
return registryApi;
|
|
557
581
|
},
|
|
558
582
|
snapshot() {
|
|
@@ -1544,6 +1568,8 @@ return { defineComponent, createComponentRegistry, isComponent, renderComponent,
|
|
|
1544
1568
|
const __serverModule = (() => {
|
|
1545
1569
|
const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
|
|
1546
1570
|
const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
|
|
1571
|
+
const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
|
|
1572
|
+
const appliedServerValues = new WeakSet();
|
|
1547
1573
|
function createServerRegistry(initialMap = {}, options = {}) {
|
|
1548
1574
|
const registryStore = options.registry ?? createRegistryStore();
|
|
1549
1575
|
const type = options.type ?? "server";
|
|
@@ -1634,6 +1660,7 @@ args,
|
|
|
1634
1660
|
input: context.input ?? defaultInput(runContext),
|
|
1635
1661
|
signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
|
|
1636
1662
|
};
|
|
1663
|
+
assertJsonTransportable(body);
|
|
1637
1664
|
const response = await fetchImpl(joinEndpoint(endpoint, id), {
|
|
1638
1665
|
method: "POST",
|
|
1639
1666
|
headers: {
|
|
@@ -1648,7 +1675,7 @@ throw new Error(`Server function "${id}" failed with ${response.status}.`);
|
|
|
1648
1675
|
}
|
|
1649
1676
|
const result = await readServerResponse(response);
|
|
1650
1677
|
await applyServerResult(result, runContext);
|
|
1651
|
-
return unwrapServerResult(result);
|
|
1678
|
+
return markAppliedServerValue(unwrapServerResult(result));
|
|
1652
1679
|
}
|
|
1653
1680
|
return createServerNamespace(run, {
|
|
1654
1681
|
run,
|
|
@@ -1677,6 +1704,9 @@ async function applyServerResult(result, context = {}) {
|
|
|
1677
1704
|
if (!isServerEnvelope(result)) {
|
|
1678
1705
|
return result;
|
|
1679
1706
|
}
|
|
1707
|
+
if (result[appliedServerResult] || appliedServerValues.has(result)) {
|
|
1708
|
+
return result;
|
|
1709
|
+
}
|
|
1680
1710
|
if (result.signals && context.signals) {
|
|
1681
1711
|
for (const [path, value] of Object.entries(result.signals)) {
|
|
1682
1712
|
context.signals.set?.(path, value);
|
|
@@ -1694,6 +1724,11 @@ await context.router?.navigate?.(result.redirect);
|
|
|
1694
1724
|
if (result.error) {
|
|
1695
1725
|
throw toError(result.error);
|
|
1696
1726
|
}
|
|
1727
|
+
Object.defineProperty(result, appliedServerResult, {
|
|
1728
|
+
configurable: true,
|
|
1729
|
+
enumerable: false,
|
|
1730
|
+
value: true
|
|
1731
|
+
});
|
|
1697
1732
|
return result;
|
|
1698
1733
|
}
|
|
1699
1734
|
function unwrapServerResult(result) {
|
|
@@ -1702,6 +1737,12 @@ return result.value;
|
|
|
1702
1737
|
}
|
|
1703
1738
|
return result;
|
|
1704
1739
|
}
|
|
1740
|
+
function markAppliedServerValue(value) {
|
|
1741
|
+
if (value && typeof value === "object") {
|
|
1742
|
+
appliedServerValues.add(value);
|
|
1743
|
+
}
|
|
1744
|
+
return value;
|
|
1745
|
+
}
|
|
1705
1746
|
function defaultInput(context = {}) {
|
|
1706
1747
|
const form = findForm(context);
|
|
1707
1748
|
if (form) {
|
|
@@ -1861,6 +1902,28 @@ output[key] = value;
|
|
|
1861
1902
|
}
|
|
1862
1903
|
return output;
|
|
1863
1904
|
}
|
|
1905
|
+
function assertJsonTransportable(value, seen = new Set()) {
|
|
1906
|
+
if (value == null || typeof value !== "object") {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (seen.has(value)) {
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
seen.add(value);
|
|
1913
|
+
const tag = Object.prototype.toString.call(value);
|
|
1914
|
+
if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
|
|
1915
|
+
throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
|
|
1916
|
+
}
|
|
1917
|
+
if (Array.isArray(value)) {
|
|
1918
|
+
for (const item of value) {
|
|
1919
|
+
assertJsonTransportable(item, seen);
|
|
1920
|
+
}
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
for (const item of Object.values(value)) {
|
|
1924
|
+
assertJsonTransportable(item, seen);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1864
1927
|
function joinEndpoint(endpoint, id) {
|
|
1865
1928
|
return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
|
|
1866
1929
|
}
|
|
@@ -2851,6 +2914,7 @@ throw new Error(`Route "${pattern}" is already registered.`);
|
|
|
2851
2914
|
const nextRoute = normalizeRoute(pattern, definition);
|
|
2852
2915
|
entries.set(pattern, nextRoute.definition);
|
|
2853
2916
|
routes.push(nextRoute);
|
|
2917
|
+
sortRoutes(routes);
|
|
2854
2918
|
return nextRoute;
|
|
2855
2919
|
},
|
|
2856
2920
|
registerMany(map) {
|
|
@@ -2914,6 +2978,7 @@ return;
|
|
|
2914
2978
|
const nextRoute = normalizeRoute(pattern, definition);
|
|
2915
2979
|
entries.set(pattern, nextRoute.definition);
|
|
2916
2980
|
routes.push(nextRoute);
|
|
2981
|
+
sortRoutes(routes);
|
|
2917
2982
|
}
|
|
2918
2983
|
}
|
|
2919
2984
|
function createRouter({
|
|
@@ -2949,6 +3014,8 @@ attributes: attributeConfig
|
|
|
2949
3014
|
const ownsLoader = !loader;
|
|
2950
3015
|
const cleanups = new Set();
|
|
2951
3016
|
let destroyed = false;
|
|
3017
|
+
let navigationVersion = 0;
|
|
3018
|
+
let activeNavigation;
|
|
2952
3019
|
const api = {
|
|
2953
3020
|
mode,
|
|
2954
3021
|
root: rootNode,
|
|
@@ -2985,7 +3052,7 @@ updateStateFromLocation();
|
|
|
2985
3052
|
return api;
|
|
2986
3053
|
},
|
|
2987
3054
|
match(url) {
|
|
2988
|
-
return routes.match(url);
|
|
3055
|
+
return routes.match(resolveUrl(url));
|
|
2989
3056
|
},
|
|
2990
3057
|
prefetch(url) {
|
|
2991
3058
|
assertActive();
|
|
@@ -3007,7 +3074,7 @@ if (mode === "mpa" || mode === "ssr") {
|
|
|
3007
3074
|
documentRef.defaultView?.location?.assign?.(url);
|
|
3008
3075
|
return null;
|
|
3009
3076
|
}
|
|
3010
|
-
const target =
|
|
3077
|
+
const target = resolveUrl(url);
|
|
3011
3078
|
if (mode === "ssr-spa") {
|
|
3012
3079
|
return fetchRoutePartial(target, options);
|
|
3013
3080
|
}
|
|
@@ -3018,6 +3085,7 @@ if (destroyed) {
|
|
|
3018
3085
|
return;
|
|
3019
3086
|
}
|
|
3020
3087
|
destroyed = true;
|
|
3088
|
+
activeNavigation?.controller.abort(new Error("Router has been destroyed."));
|
|
3021
3089
|
for (const cleanup of cleanups) {
|
|
3022
3090
|
cleanup();
|
|
3023
3091
|
}
|
|
@@ -3053,45 +3121,75 @@ cleanups.add(() => documentRef.defaultView?.removeEventListener?.("popstate", po
|
|
|
3053
3121
|
async function renderLocalRoutePartial(target, options = {}) {
|
|
3054
3122
|
const matched = api.match(target);
|
|
3055
3123
|
if (!matched) {
|
|
3124
|
+
beginNavigation(target, null);
|
|
3056
3125
|
setNoRouteError(target);
|
|
3057
3126
|
return null;
|
|
3058
3127
|
}
|
|
3128
|
+
const navigation = beginNavigation(target, matched);
|
|
3059
3129
|
setMatchedRouterState(target, matched, { pending: true, error: null });
|
|
3060
3130
|
try {
|
|
3061
3131
|
if (!matched.route?.partial || !partials?.resolve?.(matched.route.partial)) {
|
|
3062
3132
|
const error = new Error(`Route "${target.pathname}" does not have a registered partial.`);
|
|
3133
|
+
if (isActiveNavigation(navigation)) {
|
|
3063
3134
|
setRouterState({ pending: false, error });
|
|
3135
|
+
}
|
|
3136
|
+
return null;
|
|
3137
|
+
}
|
|
3138
|
+
const result = await partials.render(matched.route.partial, matched.params, contextFor(matched, navigation));
|
|
3139
|
+
if (!isActiveNavigation(navigation)) {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
await applyNavigationResult(result, target, options, navigation);
|
|
3143
|
+
if (!isActiveNavigation(navigation)) {
|
|
3064
3144
|
return null;
|
|
3065
3145
|
}
|
|
3066
|
-
const result = await partials.render(matched.route.partial, matched.params, contextFor(matched));
|
|
3067
|
-
await applyNavigationResult(result, target, options);
|
|
3068
3146
|
setRouterState({ pending: false, error: null });
|
|
3069
3147
|
return result;
|
|
3070
3148
|
} catch (error) {
|
|
3149
|
+
if (!isActiveNavigation(navigation)) {
|
|
3150
|
+
return null;
|
|
3151
|
+
}
|
|
3071
3152
|
setRouterState({ pending: false, error });
|
|
3072
3153
|
throw error;
|
|
3073
3154
|
}
|
|
3074
3155
|
}
|
|
3075
3156
|
async function fetchRoutePartial(target, options = {}) {
|
|
3076
3157
|
const matched = api.match(target);
|
|
3158
|
+
const navigation = beginNavigation(target, matched);
|
|
3077
3159
|
setMatchedRouterState(target, matched, { pending: true, error: null });
|
|
3078
3160
|
try {
|
|
3079
|
-
const result = await fetchRoute(target.href);
|
|
3080
|
-
|
|
3161
|
+
const result = await fetchRoute(target.href, { signal: navigation.abort });
|
|
3162
|
+
if (!isActiveNavigation(navigation)) {
|
|
3163
|
+
return null;
|
|
3164
|
+
}
|
|
3165
|
+
await applyNavigationResult(result, target, options, navigation);
|
|
3166
|
+
if (!isActiveNavigation(navigation)) {
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3081
3169
|
setRouterState({ pending: false, error: null });
|
|
3082
3170
|
return result;
|
|
3083
3171
|
} catch (error) {
|
|
3172
|
+
if (!isActiveNavigation(navigation)) {
|
|
3173
|
+
return null;
|
|
3174
|
+
}
|
|
3084
3175
|
setRouterState({ pending: false, error });
|
|
3085
3176
|
throw error;
|
|
3086
3177
|
}
|
|
3087
3178
|
}
|
|
3088
|
-
async function applyNavigationResult(result, target, options) {
|
|
3179
|
+
async function applyNavigationResult(result, target, options, navigation) {
|
|
3180
|
+
if (!isActiveNavigation(navigation)) {
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3089
3183
|
await applyServerResult(result, {
|
|
3090
3184
|
signals: signalRegistry,
|
|
3091
3185
|
loader: loaderInstance,
|
|
3092
3186
|
router: api,
|
|
3093
|
-
cache
|
|
3187
|
+
cache,
|
|
3188
|
+
abort: navigation?.abort
|
|
3094
3189
|
});
|
|
3190
|
+
if (!isActiveNavigation(navigation)) {
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3095
3193
|
if (result?.html != null && !result.boundary && !result.redirect) {
|
|
3096
3194
|
loaderInstance.swap(boundary, result.html);
|
|
3097
3195
|
}
|
|
@@ -3104,14 +3202,15 @@ return;
|
|
|
3104
3202
|
}
|
|
3105
3203
|
documentRef.defaultView?.history?.pushState?.({}, "", target.href);
|
|
3106
3204
|
}
|
|
3107
|
-
async function fetchRoute(url, { prefetch = false } = {}) {
|
|
3205
|
+
async function fetchRoute(url, { prefetch = false, signal } = {}) {
|
|
3108
3206
|
if (typeof fetchImpl !== "function") {
|
|
3109
3207
|
throw new Error("Router navigation requires a partial registry or fetch.");
|
|
3110
3208
|
}
|
|
3111
3209
|
const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
|
|
3112
3210
|
headers: {
|
|
3113
3211
|
accept: "application/json, text/html"
|
|
3114
|
-
}
|
|
3212
|
+
},
|
|
3213
|
+
signal
|
|
3115
3214
|
});
|
|
3116
3215
|
if (!response.ok) {
|
|
3117
3216
|
throw new Error(`Route "${url}" failed with ${response.status}.`);
|
|
@@ -3125,7 +3224,7 @@ return response.json();
|
|
|
3125
3224
|
}
|
|
3126
3225
|
return { boundary, html: await response.text() };
|
|
3127
3226
|
}
|
|
3128
|
-
function contextFor(matched) {
|
|
3227
|
+
function contextFor(matched, navigation) {
|
|
3129
3228
|
return {
|
|
3130
3229
|
params: matched.params,
|
|
3131
3230
|
route: matched.route,
|
|
@@ -3135,8 +3234,24 @@ handlers: handlerRegistry,
|
|
|
3135
3234
|
loader: loaderInstance,
|
|
3136
3235
|
server,
|
|
3137
3236
|
cache,
|
|
3138
|
-
abort:
|
|
3237
|
+
abort: navigation?.abort
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
function beginNavigation(target, matched) {
|
|
3241
|
+
activeNavigation?.controller.abort(new Error(`Router navigation superseded by ${target.pathname}${target.search}.`));
|
|
3242
|
+
const controller = new AbortController();
|
|
3243
|
+
const navigation = {
|
|
3244
|
+
id: ++navigationVersion,
|
|
3245
|
+
controller,
|
|
3246
|
+
abort: controller.signal,
|
|
3247
|
+
target,
|
|
3248
|
+
matched
|
|
3139
3249
|
};
|
|
3250
|
+
activeNavigation = navigation;
|
|
3251
|
+
return navigation;
|
|
3252
|
+
}
|
|
3253
|
+
function isActiveNavigation(navigation) {
|
|
3254
|
+
return !destroyed && navigation && activeNavigation?.id === navigation.id && !navigation.abort.aborted;
|
|
3140
3255
|
}
|
|
3141
3256
|
function updateStateFromLocation() {
|
|
3142
3257
|
const url = currentUrl();
|
|
@@ -3168,7 +3283,13 @@ signalRegistry.set(`router.${key}`, value);
|
|
|
3168
3283
|
}
|
|
3169
3284
|
}
|
|
3170
3285
|
function currentUrl() {
|
|
3171
|
-
return
|
|
3286
|
+
return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
|
|
3287
|
+
}
|
|
3288
|
+
function resolveUrl(url) {
|
|
3289
|
+
if (url instanceof URL) {
|
|
3290
|
+
return url;
|
|
3291
|
+
}
|
|
3292
|
+
return new URL(String(url), documentRef.defaultView?.location?.href ?? "http://localhost/");
|
|
3172
3293
|
}
|
|
3173
3294
|
function assertActive() {
|
|
3174
3295
|
if (destroyed) {
|
|
@@ -3183,6 +3304,7 @@ return {
|
|
|
3183
3304
|
pattern,
|
|
3184
3305
|
regex,
|
|
3185
3306
|
keys,
|
|
3307
|
+
score: routeScore(pattern),
|
|
3186
3308
|
definition: normalized
|
|
3187
3309
|
};
|
|
3188
3310
|
}
|
|
@@ -3240,6 +3362,26 @@ return Object.fromEntries(url.searchParams.entries());
|
|
|
3240
3362
|
function escapeRegExp(value) {
|
|
3241
3363
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3242
3364
|
}
|
|
3365
|
+
function sortRoutes(routes) {
|
|
3366
|
+
routes.sort((left, right) => right.score - left.score || right.pattern.length - left.pattern.length);
|
|
3367
|
+
}
|
|
3368
|
+
function routeScore(pattern) {
|
|
3369
|
+
if (pattern === "*") {
|
|
3370
|
+
return -1;
|
|
3371
|
+
}
|
|
3372
|
+
return pattern
|
|
3373
|
+
.split("/")
|
|
3374
|
+
.filter(Boolean)
|
|
3375
|
+
.reduce((score, segment) => {
|
|
3376
|
+
if (segment === "*") {
|
|
3377
|
+
return score;
|
|
3378
|
+
}
|
|
3379
|
+
if (segment.startsWith(":")) {
|
|
3380
|
+
return score + 2;
|
|
3381
|
+
}
|
|
3382
|
+
return score + 4;
|
|
3383
|
+
}, pattern === "/" ? 3 : 0);
|
|
3384
|
+
}
|
|
3243
3385
|
function assertPattern(pattern) {
|
|
3244
3386
|
if (typeof pattern !== "string" || (pattern !== "*" && !pattern.startsWith("/"))) {
|
|
3245
3387
|
throw new TypeError("Route pattern must be a path string or \"*\".");
|
|
@@ -3311,7 +3453,7 @@ let router = options.router;
|
|
|
3311
3453
|
let detach = () => {};
|
|
3312
3454
|
let started = false;
|
|
3313
3455
|
let destroyed = false;
|
|
3314
|
-
applySnapshot(signals, browserCache, options.snapshot);
|
|
3456
|
+
applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
|
|
3315
3457
|
attachServerCache(server, serverCache);
|
|
3316
3458
|
const runtime = {
|
|
3317
3459
|
app,
|
|
@@ -3459,6 +3601,35 @@ throw new Error("Async app runtime has been destroyed.");
|
|
|
3459
3601
|
}
|
|
3460
3602
|
}
|
|
3461
3603
|
const Async = defineApp();
|
|
3604
|
+
function readSnapshot(root = globalThis.document, { attributes } = {}) {
|
|
3605
|
+
const attributeConfig = normalizeAttributeConfig(attributes);
|
|
3606
|
+
const snapshotAttr = attributeName(attributeConfig, "async", "snapshot");
|
|
3607
|
+
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
3608
|
+
const rootNode = root ?? documentRef;
|
|
3609
|
+
if (!rootNode?.querySelectorAll && !documentRef?.querySelectorAll) {
|
|
3610
|
+
return {};
|
|
3611
|
+
}
|
|
3612
|
+
for (const searchRoot of new Set([rootNode, documentRef])) {
|
|
3613
|
+
if (!searchRoot?.querySelectorAll) {
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
for (const script of searchRoot.querySelectorAll("script[type='application/json'], script")) {
|
|
3617
|
+
if (!script.hasAttribute?.(snapshotAttr)) {
|
|
3618
|
+
continue;
|
|
3619
|
+
}
|
|
3620
|
+
const source = script.textContent?.trim() ?? "";
|
|
3621
|
+
if (!source) {
|
|
3622
|
+
return {};
|
|
3623
|
+
}
|
|
3624
|
+
try {
|
|
3625
|
+
return JSON.parse(source);
|
|
3626
|
+
} catch (cause) {
|
|
3627
|
+
throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
return {};
|
|
3632
|
+
}
|
|
3462
3633
|
function applyUseToRuntime(runtime, normalized) {
|
|
3463
3634
|
applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
|
|
3464
3635
|
applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
|
|
@@ -3598,7 +3769,7 @@ return String(value)
|
|
|
3598
3769
|
function escapeScriptJson(value) {
|
|
3599
3770
|
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
3600
3771
|
}
|
|
3601
|
-
return { defineApp, createApp, Async };
|
|
3772
|
+
return { defineApp, createApp, readSnapshot, Async };
|
|
3602
3773
|
})();
|
|
3603
3774
|
const __delayModule = (() => {
|
|
3604
3775
|
function delay(ms, signal) {
|
|
@@ -3632,6 +3803,7 @@ const { asyncSignal: asyncSignal } = __asyncSignalModule;
|
|
|
3632
3803
|
const { Async: Async } = __appModule;
|
|
3633
3804
|
const { createApp: createApp } = __appModule;
|
|
3634
3805
|
const { defineApp: defineApp } = __appModule;
|
|
3806
|
+
const { readSnapshot: readSnapshot } = __appModule;
|
|
3635
3807
|
const { attributeName: attributeName } = __attributesModule;
|
|
3636
3808
|
const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
|
|
3637
3809
|
const { createCacheRegistry: createCacheRegistry } = __cacheModule;
|
|
@@ -3657,7 +3829,7 @@ const { createSignal: createSignal } = __signalsModule;
|
|
|
3657
3829
|
const { createSignalRegistry: createSignalRegistry } = __signalsModule;
|
|
3658
3830
|
const { effect: effect } = __signalsModule;
|
|
3659
3831
|
const { signal: signal } = __signalsModule;
|
|
3660
|
-
const api = { asyncSignal, Async, createApp, defineApp, attributeName, defineAttributeConfig, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, createHandlerRegistry, html, Loader, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createServerProxy, createServerRegistry, computed, createSignal, createSignalRegistry, effect, signal };
|
|
3832
|
+
const api = { asyncSignal, Async, createApp, defineApp, readSnapshot, attributeName, defineAttributeConfig, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, createHandlerRegistry, html, Loader, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createServerProxy, createServerRegistry, computed, createSignal, createSignalRegistry, effect, signal };
|
|
3661
3833
|
assertNoUmdNamespaceConflicts(api, Async);
|
|
3662
3834
|
Object.assign(Async, api);
|
|
3663
3835
|
Async.Async = Async;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
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",
|
package/src/app.js
CHANGED
|
@@ -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 = {
|
|
@@ -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/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|