@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.
@@ -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
- const value = await fn();
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 = toUrl(url);
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
- await applyNavigationResult(result, target, options);
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: undefined
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 toUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
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.6.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
- const value = await fn();
80
- registryApi.set(key, value, options);
81
- return value;
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";