@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/framework.min.js CHANGED
@@ -469,6 +469,7 @@ function createCacheRegistry(initialMap = {}, { now = () => Date.now(), registry
469
469
  const registryStore = registry ?? createRegistryStore();
470
470
  const definitions = registryStore._map(type);
471
471
  const entries = registryStore._map(`${type}.entries`);
472
+ const pending = new Map();
472
473
  const registryApi = attachRegistryInspection({
473
474
  register(id, definition = defineCache()) {
474
475
  assertId(id);
@@ -523,17 +524,35 @@ const cached = registryApi.get(key);
523
524
  if (cached !== undefined) {
524
525
  return cached;
525
526
  }
526
- const value = await fn();
527
+ if (pending.has(key)) {
528
+ return pending.get(key);
529
+ }
530
+ let promise;
531
+ promise = Promise.resolve()
532
+ .then(fn)
533
+ .then((value) => {
534
+ if (pending.get(key) === promise) {
527
535
  registryApi.set(key, value, options);
536
+ }
528
537
  return value;
538
+ })
539
+ .finally(() => {
540
+ if (pending.get(key) === promise) {
541
+ pending.delete(key);
542
+ }
543
+ });
544
+ pending.set(key, promise);
545
+ return promise;
529
546
  },
530
547
  delete(key) {
531
548
  assertKey(key);
549
+ pending.delete(key);
532
550
  return entries.delete(key);
533
551
  },
534
552
  clear(prefix) {
535
553
  if (prefix === undefined) {
536
554
  entries.clear();
555
+ pending.clear();
537
556
  return registryApi;
538
557
  }
539
558
  for (const key of [...entries.keys()]) {
@@ -541,6 +560,11 @@ if (key.startsWith(prefix)) {
541
560
  entries.delete(key);
542
561
  }
543
562
  }
563
+ for (const key of [...pending.keys()]) {
564
+ if (key.startsWith(prefix)) {
565
+ pending.delete(key);
566
+ }
567
+ }
544
568
  return registryApi;
545
569
  },
546
570
  snapshot() {
@@ -1532,6 +1556,8 @@ return { defineComponent, createComponentRegistry, isComponent, renderComponent,
1532
1556
  const __serverModule = (() => {
1533
1557
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1534
1558
  const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
1559
+ const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
1560
+ const appliedServerValues = new WeakSet();
1535
1561
  function createServerRegistry(initialMap = {}, options = {}) {
1536
1562
  const registryStore = options.registry ?? createRegistryStore();
1537
1563
  const type = options.type ?? "server";
@@ -1622,6 +1648,7 @@ args,
1622
1648
  input: context.input ?? defaultInput(runContext),
1623
1649
  signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
1624
1650
  };
1651
+ assertJsonTransportable(body);
1625
1652
  const response = await fetchImpl(joinEndpoint(endpoint, id), {
1626
1653
  method: "POST",
1627
1654
  headers: {
@@ -1636,7 +1663,7 @@ throw new Error(`Server function "${id}" failed with ${response.status}.`);
1636
1663
  }
1637
1664
  const result = await readServerResponse(response);
1638
1665
  await applyServerResult(result, runContext);
1639
- return unwrapServerResult(result);
1666
+ return markAppliedServerValue(unwrapServerResult(result));
1640
1667
  }
1641
1668
  return createServerNamespace(run, {
1642
1669
  run,
@@ -1665,6 +1692,9 @@ async function applyServerResult(result, context = {}) {
1665
1692
  if (!isServerEnvelope(result)) {
1666
1693
  return result;
1667
1694
  }
1695
+ if (result[appliedServerResult] || appliedServerValues.has(result)) {
1696
+ return result;
1697
+ }
1668
1698
  if (result.signals && context.signals) {
1669
1699
  for (const [path, value] of Object.entries(result.signals)) {
1670
1700
  context.signals.set?.(path, value);
@@ -1682,6 +1712,11 @@ await context.router?.navigate?.(result.redirect);
1682
1712
  if (result.error) {
1683
1713
  throw toError(result.error);
1684
1714
  }
1715
+ Object.defineProperty(result, appliedServerResult, {
1716
+ configurable: true,
1717
+ enumerable: false,
1718
+ value: true
1719
+ });
1685
1720
  return result;
1686
1721
  }
1687
1722
  function unwrapServerResult(result) {
@@ -1690,6 +1725,12 @@ return result.value;
1690
1725
  }
1691
1726
  return result;
1692
1727
  }
1728
+ function markAppliedServerValue(value) {
1729
+ if (value && typeof value === "object") {
1730
+ appliedServerValues.add(value);
1731
+ }
1732
+ return value;
1733
+ }
1693
1734
  function defaultInput(context = {}) {
1694
1735
  const form = findForm(context);
1695
1736
  if (form) {
@@ -1849,6 +1890,28 @@ output[key] = value;
1849
1890
  }
1850
1891
  return output;
1851
1892
  }
1893
+ function assertJsonTransportable(value, seen = new Set()) {
1894
+ if (value == null || typeof value !== "object") {
1895
+ return;
1896
+ }
1897
+ if (seen.has(value)) {
1898
+ return;
1899
+ }
1900
+ seen.add(value);
1901
+ const tag = Object.prototype.toString.call(value);
1902
+ if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
1903
+ throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
1904
+ }
1905
+ if (Array.isArray(value)) {
1906
+ for (const item of value) {
1907
+ assertJsonTransportable(item, seen);
1908
+ }
1909
+ return;
1910
+ }
1911
+ for (const item of Object.values(value)) {
1912
+ assertJsonTransportable(item, seen);
1913
+ }
1914
+ }
1852
1915
  function joinEndpoint(endpoint, id) {
1853
1916
  return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
1854
1917
  }
@@ -2839,6 +2902,7 @@ throw new Error(`Route "${pattern}" is already registered.`);
2839
2902
  const nextRoute = normalizeRoute(pattern, definition);
2840
2903
  entries.set(pattern, nextRoute.definition);
2841
2904
  routes.push(nextRoute);
2905
+ sortRoutes(routes);
2842
2906
  return nextRoute;
2843
2907
  },
2844
2908
  registerMany(map) {
@@ -2902,6 +2966,7 @@ return;
2902
2966
  const nextRoute = normalizeRoute(pattern, definition);
2903
2967
  entries.set(pattern, nextRoute.definition);
2904
2968
  routes.push(nextRoute);
2969
+ sortRoutes(routes);
2905
2970
  }
2906
2971
  }
2907
2972
  function createRouter({
@@ -2937,6 +3002,8 @@ attributes: attributeConfig
2937
3002
  const ownsLoader = !loader;
2938
3003
  const cleanups = new Set();
2939
3004
  let destroyed = false;
3005
+ let navigationVersion = 0;
3006
+ let activeNavigation;
2940
3007
  const api = {
2941
3008
  mode,
2942
3009
  root: rootNode,
@@ -2973,7 +3040,7 @@ updateStateFromLocation();
2973
3040
  return api;
2974
3041
  },
2975
3042
  match(url) {
2976
- return routes.match(url);
3043
+ return routes.match(resolveUrl(url));
2977
3044
  },
2978
3045
  prefetch(url) {
2979
3046
  assertActive();
@@ -2995,7 +3062,7 @@ if (mode === "mpa" || mode === "ssr") {
2995
3062
  documentRef.defaultView?.location?.assign?.(url);
2996
3063
  return null;
2997
3064
  }
2998
- const target = toUrl(url);
3065
+ const target = resolveUrl(url);
2999
3066
  if (mode === "ssr-spa") {
3000
3067
  return fetchRoutePartial(target, options);
3001
3068
  }
@@ -3006,6 +3073,7 @@ if (destroyed) {
3006
3073
  return;
3007
3074
  }
3008
3075
  destroyed = true;
3076
+ activeNavigation?.controller.abort(new Error("Router has been destroyed."));
3009
3077
  for (const cleanup of cleanups) {
3010
3078
  cleanup();
3011
3079
  }
@@ -3041,45 +3109,75 @@ cleanups.add(() => documentRef.defaultView?.removeEventListener?.("popstate", po
3041
3109
  async function renderLocalRoutePartial(target, options = {}) {
3042
3110
  const matched = api.match(target);
3043
3111
  if (!matched) {
3112
+ beginNavigation(target, null);
3044
3113
  setNoRouteError(target);
3045
3114
  return null;
3046
3115
  }
3116
+ const navigation = beginNavigation(target, matched);
3047
3117
  setMatchedRouterState(target, matched, { pending: true, error: null });
3048
3118
  try {
3049
3119
  if (!matched.route?.partial || !partials?.resolve?.(matched.route.partial)) {
3050
3120
  const error = new Error(`Route "${target.pathname}" does not have a registered partial.`);
3121
+ if (isActiveNavigation(navigation)) {
3051
3122
  setRouterState({ pending: false, error });
3123
+ }
3124
+ return null;
3125
+ }
3126
+ const result = await partials.render(matched.route.partial, matched.params, contextFor(matched, navigation));
3127
+ if (!isActiveNavigation(navigation)) {
3128
+ return null;
3129
+ }
3130
+ await applyNavigationResult(result, target, options, navigation);
3131
+ if (!isActiveNavigation(navigation)) {
3052
3132
  return null;
3053
3133
  }
3054
- const result = await partials.render(matched.route.partial, matched.params, contextFor(matched));
3055
- await applyNavigationResult(result, target, options);
3056
3134
  setRouterState({ pending: false, error: null });
3057
3135
  return result;
3058
3136
  } catch (error) {
3137
+ if (!isActiveNavigation(navigation)) {
3138
+ return null;
3139
+ }
3059
3140
  setRouterState({ pending: false, error });
3060
3141
  throw error;
3061
3142
  }
3062
3143
  }
3063
3144
  async function fetchRoutePartial(target, options = {}) {
3064
3145
  const matched = api.match(target);
3146
+ const navigation = beginNavigation(target, matched);
3065
3147
  setMatchedRouterState(target, matched, { pending: true, error: null });
3066
3148
  try {
3067
- const result = await fetchRoute(target.href);
3068
- await applyNavigationResult(result, target, options);
3149
+ const result = await fetchRoute(target.href, { signal: navigation.abort });
3150
+ if (!isActiveNavigation(navigation)) {
3151
+ return null;
3152
+ }
3153
+ await applyNavigationResult(result, target, options, navigation);
3154
+ if (!isActiveNavigation(navigation)) {
3155
+ return null;
3156
+ }
3069
3157
  setRouterState({ pending: false, error: null });
3070
3158
  return result;
3071
3159
  } catch (error) {
3160
+ if (!isActiveNavigation(navigation)) {
3161
+ return null;
3162
+ }
3072
3163
  setRouterState({ pending: false, error });
3073
3164
  throw error;
3074
3165
  }
3075
3166
  }
3076
- async function applyNavigationResult(result, target, options) {
3167
+ async function applyNavigationResult(result, target, options, navigation) {
3168
+ if (!isActiveNavigation(navigation)) {
3169
+ return;
3170
+ }
3077
3171
  await applyServerResult(result, {
3078
3172
  signals: signalRegistry,
3079
3173
  loader: loaderInstance,
3080
3174
  router: api,
3081
- cache
3175
+ cache,
3176
+ abort: navigation?.abort
3082
3177
  });
3178
+ if (!isActiveNavigation(navigation)) {
3179
+ return;
3180
+ }
3083
3181
  if (result?.html != null && !result.boundary && !result.redirect) {
3084
3182
  loaderInstance.swap(boundary, result.html);
3085
3183
  }
@@ -3092,14 +3190,15 @@ return;
3092
3190
  }
3093
3191
  documentRef.defaultView?.history?.pushState?.({}, "", target.href);
3094
3192
  }
3095
- async function fetchRoute(url, { prefetch = false } = {}) {
3193
+ async function fetchRoute(url, { prefetch = false, signal } = {}) {
3096
3194
  if (typeof fetchImpl !== "function") {
3097
3195
  throw new Error("Router navigation requires a partial registry or fetch.");
3098
3196
  }
3099
3197
  const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
3100
3198
  headers: {
3101
3199
  accept: "application/json, text/html"
3102
- }
3200
+ },
3201
+ signal
3103
3202
  });
3104
3203
  if (!response.ok) {
3105
3204
  throw new Error(`Route "${url}" failed with ${response.status}.`);
@@ -3113,7 +3212,7 @@ return response.json();
3113
3212
  }
3114
3213
  return { boundary, html: await response.text() };
3115
3214
  }
3116
- function contextFor(matched) {
3215
+ function contextFor(matched, navigation) {
3117
3216
  return {
3118
3217
  params: matched.params,
3119
3218
  route: matched.route,
@@ -3123,8 +3222,24 @@ handlers: handlerRegistry,
3123
3222
  loader: loaderInstance,
3124
3223
  server,
3125
3224
  cache,
3126
- abort: undefined
3225
+ abort: navigation?.abort
3226
+ };
3227
+ }
3228
+ function beginNavigation(target, matched) {
3229
+ activeNavigation?.controller.abort(new Error(`Router navigation superseded by ${target.pathname}${target.search}.`));
3230
+ const controller = new AbortController();
3231
+ const navigation = {
3232
+ id: ++navigationVersion,
3233
+ controller,
3234
+ abort: controller.signal,
3235
+ target,
3236
+ matched
3127
3237
  };
3238
+ activeNavigation = navigation;
3239
+ return navigation;
3240
+ }
3241
+ function isActiveNavigation(navigation) {
3242
+ return !destroyed && navigation && activeNavigation?.id === navigation.id && !navigation.abort.aborted;
3128
3243
  }
3129
3244
  function updateStateFromLocation() {
3130
3245
  const url = currentUrl();
@@ -3156,7 +3271,13 @@ signalRegistry.set(`router.${key}`, value);
3156
3271
  }
3157
3272
  }
3158
3273
  function currentUrl() {
3159
- return toUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
3274
+ return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
3275
+ }
3276
+ function resolveUrl(url) {
3277
+ if (url instanceof URL) {
3278
+ return url;
3279
+ }
3280
+ return new URL(String(url), documentRef.defaultView?.location?.href ?? "http://localhost/");
3160
3281
  }
3161
3282
  function assertActive() {
3162
3283
  if (destroyed) {
@@ -3171,6 +3292,7 @@ return {
3171
3292
  pattern,
3172
3293
  regex,
3173
3294
  keys,
3295
+ score: routeScore(pattern),
3174
3296
  definition: normalized
3175
3297
  };
3176
3298
  }
@@ -3228,6 +3350,26 @@ return Object.fromEntries(url.searchParams.entries());
3228
3350
  function escapeRegExp(value) {
3229
3351
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3230
3352
  }
3353
+ function sortRoutes(routes) {
3354
+ routes.sort((left, right) => right.score - left.score || right.pattern.length - left.pattern.length);
3355
+ }
3356
+ function routeScore(pattern) {
3357
+ if (pattern === "*") {
3358
+ return -1;
3359
+ }
3360
+ return pattern
3361
+ .split("/")
3362
+ .filter(Boolean)
3363
+ .reduce((score, segment) => {
3364
+ if (segment === "*") {
3365
+ return score;
3366
+ }
3367
+ if (segment.startsWith(":")) {
3368
+ return score + 2;
3369
+ }
3370
+ return score + 4;
3371
+ }, pattern === "/" ? 3 : 0);
3372
+ }
3231
3373
  function assertPattern(pattern) {
3232
3374
  if (typeof pattern !== "string" || (pattern !== "*" && !pattern.startsWith("/"))) {
3233
3375
  throw new TypeError("Route pattern must be a path string or \"*\".");
@@ -3299,7 +3441,7 @@ let router = options.router;
3299
3441
  let detach = () => {};
3300
3442
  let started = false;
3301
3443
  let destroyed = false;
3302
- applySnapshot(signals, browserCache, options.snapshot);
3444
+ applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
3303
3445
  attachServerCache(server, serverCache);
3304
3446
  const runtime = {
3305
3447
  app,
@@ -3447,6 +3589,35 @@ throw new Error("Async app runtime has been destroyed.");
3447
3589
  }
3448
3590
  }
3449
3591
  const Async = defineApp();
3592
+ function readSnapshot(root = globalThis.document, { attributes } = {}) {
3593
+ const attributeConfig = normalizeAttributeConfig(attributes);
3594
+ const snapshotAttr = attributeName(attributeConfig, "async", "snapshot");
3595
+ const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
3596
+ const rootNode = root ?? documentRef;
3597
+ if (!rootNode?.querySelectorAll && !documentRef?.querySelectorAll) {
3598
+ return {};
3599
+ }
3600
+ for (const searchRoot of new Set([rootNode, documentRef])) {
3601
+ if (!searchRoot?.querySelectorAll) {
3602
+ continue;
3603
+ }
3604
+ for (const script of searchRoot.querySelectorAll("script[type='application/json'], script")) {
3605
+ if (!script.hasAttribute?.(snapshotAttr)) {
3606
+ continue;
3607
+ }
3608
+ const source = script.textContent?.trim() ?? "";
3609
+ if (!source) {
3610
+ return {};
3611
+ }
3612
+ try {
3613
+ return JSON.parse(source);
3614
+ } catch (cause) {
3615
+ throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
3616
+ }
3617
+ }
3618
+ }
3619
+ return {};
3620
+ }
3450
3621
  function applyUseToRuntime(runtime, normalized) {
3451
3622
  applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
3452
3623
  applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
@@ -3586,7 +3757,7 @@ return String(value)
3586
3757
  function escapeScriptJson(value) {
3587
3758
  return JSON.stringify(value).replaceAll("<", "\\u003c");
3588
3759
  }
3589
- return { defineApp, createApp, Async };
3760
+ return { defineApp, createApp, readSnapshot, Async };
3590
3761
  })();
3591
3762
  const __delayModule = (() => {
3592
3763
  function delay(ms, signal) {
@@ -3620,6 +3791,7 @@ const { asyncSignal: asyncSignal } = __asyncSignalModule;
3620
3791
  const { Async: Async } = __appModule;
3621
3792
  const { createApp: createApp } = __appModule;
3622
3793
  const { defineApp: defineApp } = __appModule;
3794
+ const { readSnapshot: readSnapshot } = __appModule;
3623
3795
  const { attributeName: attributeName } = __attributesModule;
3624
3796
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
3625
3797
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
@@ -3645,4 +3817,4 @@ const { createSignal: createSignal } = __signalsModule;
3645
3817
  const { createSignalRegistry: createSignalRegistry } = __signalsModule;
3646
3818
  const { effect: effect } = __signalsModule;
3647
3819
  const { signal: signal } = __signalsModule;
3648
- export { 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 };
3820
+ export { 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 };