@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.umd.js CHANGED
@@ -547,6 +547,7 @@
547
547
  const registryStore = registry ?? createRegistryStore();
548
548
  const definitions = registryStore._map(type);
549
549
  const entries = registryStore._map(`${type}.entries`);
550
+ const pending = new Map();
550
551
 
551
552
  const registryApi = attachRegistryInspection({
552
553
  register(id, definition = defineCache()) {
@@ -608,19 +609,37 @@
608
609
  if (cached !== undefined) {
609
610
  return cached;
610
611
  }
611
- const value = await fn();
612
- registryApi.set(key, value, options);
613
- return value;
612
+ if (pending.has(key)) {
613
+ return pending.get(key);
614
+ }
615
+ let promise;
616
+ promise = Promise.resolve()
617
+ .then(fn)
618
+ .then((value) => {
619
+ if (pending.get(key) === promise) {
620
+ registryApi.set(key, value, options);
621
+ }
622
+ return value;
623
+ })
624
+ .finally(() => {
625
+ if (pending.get(key) === promise) {
626
+ pending.delete(key);
627
+ }
628
+ });
629
+ pending.set(key, promise);
630
+ return promise;
614
631
  },
615
632
 
616
633
  delete(key) {
617
634
  assertKey(key);
635
+ pending.delete(key);
618
636
  return entries.delete(key);
619
637
  },
620
638
 
621
639
  clear(prefix) {
622
640
  if (prefix === undefined) {
623
641
  entries.clear();
642
+ pending.clear();
624
643
  return registryApi;
625
644
  }
626
645
  for (const key of [...entries.keys()]) {
@@ -628,6 +647,11 @@
628
647
  entries.delete(key);
629
648
  }
630
649
  }
650
+ for (const key of [...pending.keys()]) {
651
+ if (key.startsWith(prefix)) {
652
+ pending.delete(key);
653
+ }
654
+ }
631
655
  return registryApi;
632
656
  },
633
657
 
@@ -1763,6 +1787,8 @@
1763
1787
  const __serverModule = (() => {
1764
1788
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1765
1789
  const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
1790
+ const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
1791
+ const appliedServerValues = new WeakSet();
1766
1792
 
1767
1793
  function createServerRegistry(initialMap = {}, options = {}) {
1768
1794
  const registryStore = options.registry ?? createRegistryStore();
@@ -1869,6 +1895,7 @@
1869
1895
  input: context.input ?? defaultInput(runContext),
1870
1896
  signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
1871
1897
  };
1898
+ assertJsonTransportable(body);
1872
1899
 
1873
1900
  const response = await fetchImpl(joinEndpoint(endpoint, id), {
1874
1901
  method: "POST",
@@ -1886,7 +1913,7 @@
1886
1913
 
1887
1914
  const result = await readServerResponse(response);
1888
1915
  await applyServerResult(result, runContext);
1889
- return unwrapServerResult(result);
1916
+ return markAppliedServerValue(unwrapServerResult(result));
1890
1917
  }
1891
1918
 
1892
1919
  return createServerNamespace(run, {
@@ -1921,6 +1948,9 @@
1921
1948
  if (!isServerEnvelope(result)) {
1922
1949
  return result;
1923
1950
  }
1951
+ if (result[appliedServerResult] || appliedServerValues.has(result)) {
1952
+ return result;
1953
+ }
1924
1954
 
1925
1955
  if (result.signals && context.signals) {
1926
1956
  for (const [path, value] of Object.entries(result.signals)) {
@@ -1944,6 +1974,12 @@
1944
1974
  throw toError(result.error);
1945
1975
  }
1946
1976
 
1977
+ Object.defineProperty(result, appliedServerResult, {
1978
+ configurable: true,
1979
+ enumerable: false,
1980
+ value: true
1981
+ });
1982
+
1947
1983
  return result;
1948
1984
  }
1949
1985
 
@@ -1954,6 +1990,13 @@
1954
1990
  return result;
1955
1991
  }
1956
1992
 
1993
+ function markAppliedServerValue(value) {
1994
+ if (value && typeof value === "object") {
1995
+ appliedServerValues.add(value);
1996
+ }
1997
+ return value;
1998
+ }
1999
+
1957
2000
  function defaultInput(context = {}) {
1958
2001
  const form = findForm(context);
1959
2002
  if (form) {
@@ -2131,6 +2174,30 @@
2131
2174
  return output;
2132
2175
  }
2133
2176
 
2177
+ function assertJsonTransportable(value, seen = new Set()) {
2178
+ if (value == null || typeof value !== "object") {
2179
+ return;
2180
+ }
2181
+ if (seen.has(value)) {
2182
+ return;
2183
+ }
2184
+ seen.add(value);
2185
+
2186
+ const tag = Object.prototype.toString.call(value);
2187
+ if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
2188
+ throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
2189
+ }
2190
+ if (Array.isArray(value)) {
2191
+ for (const item of value) {
2192
+ assertJsonTransportable(item, seen);
2193
+ }
2194
+ return;
2195
+ }
2196
+ for (const item of Object.values(value)) {
2197
+ assertJsonTransportable(item, seen);
2198
+ }
2199
+ }
2200
+
2134
2201
  function joinEndpoint(endpoint, id) {
2135
2202
  return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
2136
2203
  }
@@ -3230,6 +3297,7 @@
3230
3297
  const nextRoute = normalizeRoute(pattern, definition);
3231
3298
  entries.set(pattern, nextRoute.definition);
3232
3299
  routes.push(nextRoute);
3300
+ sortRoutes(routes);
3233
3301
  return nextRoute;
3234
3302
  },
3235
3303
 
@@ -3302,6 +3370,7 @@
3302
3370
  const nextRoute = normalizeRoute(pattern, definition);
3303
3371
  entries.set(pattern, nextRoute.definition);
3304
3372
  routes.push(nextRoute);
3373
+ sortRoutes(routes);
3305
3374
  }
3306
3375
  }
3307
3376
 
@@ -3338,6 +3407,8 @@
3338
3407
  const ownsLoader = !loader;
3339
3408
  const cleanups = new Set();
3340
3409
  let destroyed = false;
3410
+ let navigationVersion = 0;
3411
+ let activeNavigation;
3341
3412
 
3342
3413
  const api = {
3343
3414
  mode,
@@ -3377,7 +3448,7 @@
3377
3448
  },
3378
3449
 
3379
3450
  match(url) {
3380
- return routes.match(url);
3451
+ return routes.match(resolveUrl(url));
3381
3452
  },
3382
3453
 
3383
3454
  prefetch(url) {
@@ -3402,7 +3473,7 @@
3402
3473
  return null;
3403
3474
  }
3404
3475
 
3405
- const target = toUrl(url);
3476
+ const target = resolveUrl(url);
3406
3477
  if (mode === "ssr-spa") {
3407
3478
  return fetchRoutePartial(target, options);
3408
3479
  }
@@ -3414,6 +3485,7 @@
3414
3485
  return;
3415
3486
  }
3416
3487
  destroyed = true;
3488
+ activeNavigation?.controller.abort(new Error("Router has been destroyed."));
3417
3489
  for (const cleanup of cleanups) {
3418
3490
  cleanup();
3419
3491
  }
@@ -3453,24 +3525,37 @@
3453
3525
  async function renderLocalRoutePartial(target, options = {}) {
3454
3526
  const matched = api.match(target);
3455
3527
  if (!matched) {
3528
+ beginNavigation(target, null);
3456
3529
  setNoRouteError(target);
3457
3530
  return null;
3458
3531
  }
3459
3532
 
3533
+ const navigation = beginNavigation(target, matched);
3460
3534
  setMatchedRouterState(target, matched, { pending: true, error: null });
3461
3535
 
3462
3536
  try {
3463
3537
  if (!matched.route?.partial || !partials?.resolve?.(matched.route.partial)) {
3464
3538
  const error = new Error(`Route "${target.pathname}" does not have a registered partial.`);
3465
- setRouterState({ pending: false, error });
3539
+ if (isActiveNavigation(navigation)) {
3540
+ setRouterState({ pending: false, error });
3541
+ }
3466
3542
  return null;
3467
3543
  }
3468
3544
 
3469
- const result = await partials.render(matched.route.partial, matched.params, contextFor(matched));
3470
- await applyNavigationResult(result, target, options);
3545
+ const result = await partials.render(matched.route.partial, matched.params, contextFor(matched, navigation));
3546
+ if (!isActiveNavigation(navigation)) {
3547
+ return null;
3548
+ }
3549
+ await applyNavigationResult(result, target, options, navigation);
3550
+ if (!isActiveNavigation(navigation)) {
3551
+ return null;
3552
+ }
3471
3553
  setRouterState({ pending: false, error: null });
3472
3554
  return result;
3473
3555
  } catch (error) {
3556
+ if (!isActiveNavigation(navigation)) {
3557
+ return null;
3558
+ }
3474
3559
  setRouterState({ pending: false, error });
3475
3560
  throw error;
3476
3561
  }
@@ -3478,26 +3563,43 @@
3478
3563
 
3479
3564
  async function fetchRoutePartial(target, options = {}) {
3480
3565
  const matched = api.match(target);
3566
+ const navigation = beginNavigation(target, matched);
3481
3567
  setMatchedRouterState(target, matched, { pending: true, error: null });
3482
3568
 
3483
3569
  try {
3484
- const result = await fetchRoute(target.href);
3485
- await applyNavigationResult(result, target, options);
3570
+ const result = await fetchRoute(target.href, { signal: navigation.abort });
3571
+ if (!isActiveNavigation(navigation)) {
3572
+ return null;
3573
+ }
3574
+ await applyNavigationResult(result, target, options, navigation);
3575
+ if (!isActiveNavigation(navigation)) {
3576
+ return null;
3577
+ }
3486
3578
  setRouterState({ pending: false, error: null });
3487
3579
  return result;
3488
3580
  } catch (error) {
3581
+ if (!isActiveNavigation(navigation)) {
3582
+ return null;
3583
+ }
3489
3584
  setRouterState({ pending: false, error });
3490
3585
  throw error;
3491
3586
  }
3492
3587
  }
3493
3588
 
3494
- async function applyNavigationResult(result, target, options) {
3589
+ async function applyNavigationResult(result, target, options, navigation) {
3590
+ if (!isActiveNavigation(navigation)) {
3591
+ return;
3592
+ }
3495
3593
  await applyServerResult(result, {
3496
3594
  signals: signalRegistry,
3497
3595
  loader: loaderInstance,
3498
3596
  router: api,
3499
- cache
3597
+ cache,
3598
+ abort: navigation?.abort
3500
3599
  });
3600
+ if (!isActiveNavigation(navigation)) {
3601
+ return;
3602
+ }
3501
3603
  if (result?.html != null && !result.boundary && !result.redirect) {
3502
3604
  loaderInstance.swap(boundary, result.html);
3503
3605
  }
@@ -3511,14 +3613,15 @@
3511
3613
  documentRef.defaultView?.history?.pushState?.({}, "", target.href);
3512
3614
  }
3513
3615
 
3514
- async function fetchRoute(url, { prefetch = false } = {}) {
3616
+ async function fetchRoute(url, { prefetch = false, signal } = {}) {
3515
3617
  if (typeof fetchImpl !== "function") {
3516
3618
  throw new Error("Router navigation requires a partial registry or fetch.");
3517
3619
  }
3518
3620
  const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
3519
3621
  headers: {
3520
3622
  accept: "application/json, text/html"
3521
- }
3623
+ },
3624
+ signal
3522
3625
  });
3523
3626
  if (!response.ok) {
3524
3627
  throw new Error(`Route "${url}" failed with ${response.status}.`);
@@ -3533,7 +3636,7 @@
3533
3636
  return { boundary, html: await response.text() };
3534
3637
  }
3535
3638
 
3536
- function contextFor(matched) {
3639
+ function contextFor(matched, navigation) {
3537
3640
  return {
3538
3641
  params: matched.params,
3539
3642
  route: matched.route,
@@ -3543,8 +3646,26 @@
3543
3646
  loader: loaderInstance,
3544
3647
  server,
3545
3648
  cache,
3546
- abort: undefined
3649
+ abort: navigation?.abort
3650
+ };
3651
+ }
3652
+
3653
+ function beginNavigation(target, matched) {
3654
+ activeNavigation?.controller.abort(new Error(`Router navigation superseded by ${target.pathname}${target.search}.`));
3655
+ const controller = new AbortController();
3656
+ const navigation = {
3657
+ id: ++navigationVersion,
3658
+ controller,
3659
+ abort: controller.signal,
3660
+ target,
3661
+ matched
3547
3662
  };
3663
+ activeNavigation = navigation;
3664
+ return navigation;
3665
+ }
3666
+
3667
+ function isActiveNavigation(navigation) {
3668
+ return !destroyed && navigation && activeNavigation?.id === navigation.id && !navigation.abort.aborted;
3548
3669
  }
3549
3670
 
3550
3671
  function updateStateFromLocation() {
@@ -3581,7 +3702,14 @@
3581
3702
  }
3582
3703
 
3583
3704
  function currentUrl() {
3584
- return toUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
3705
+ return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
3706
+ }
3707
+
3708
+ function resolveUrl(url) {
3709
+ if (url instanceof URL) {
3710
+ return url;
3711
+ }
3712
+ return new URL(String(url), documentRef.defaultView?.location?.href ?? "http://localhost/");
3585
3713
  }
3586
3714
 
3587
3715
  function assertActive() {
@@ -3598,6 +3726,7 @@
3598
3726
  pattern,
3599
3727
  regex,
3600
3728
  keys,
3729
+ score: routeScore(pattern),
3601
3730
  definition: normalized
3602
3731
  };
3603
3732
  }
@@ -3666,6 +3795,28 @@
3666
3795
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3667
3796
  }
3668
3797
 
3798
+ function sortRoutes(routes) {
3799
+ routes.sort((left, right) => right.score - left.score || right.pattern.length - left.pattern.length);
3800
+ }
3801
+
3802
+ function routeScore(pattern) {
3803
+ if (pattern === "*") {
3804
+ return -1;
3805
+ }
3806
+ return pattern
3807
+ .split("/")
3808
+ .filter(Boolean)
3809
+ .reduce((score, segment) => {
3810
+ if (segment === "*") {
3811
+ return score;
3812
+ }
3813
+ if (segment.startsWith(":")) {
3814
+ return score + 2;
3815
+ }
3816
+ return score + 4;
3817
+ }, pattern === "/" ? 3 : 0);
3818
+ }
3819
+
3669
3820
  function assertPattern(pattern) {
3670
3821
  if (typeof pattern !== "string" || (pattern !== "*" && !pattern.startsWith("/"))) {
3671
3822
  throw new TypeError("Route pattern must be a path string or \"*\".");
@@ -3749,7 +3900,7 @@
3749
3900
  let started = false;
3750
3901
  let destroyed = false;
3751
3902
 
3752
- applySnapshot(signals, browserCache, options.snapshot);
3903
+ applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
3753
3904
  attachServerCache(server, serverCache);
3754
3905
 
3755
3906
  const runtime = {
@@ -3917,6 +4068,38 @@
3917
4068
 
3918
4069
  const Async = defineApp();
3919
4070
 
4071
+ function readSnapshot(root = globalThis.document, { attributes } = {}) {
4072
+ const attributeConfig = normalizeAttributeConfig(attributes);
4073
+ const snapshotAttr = attributeName(attributeConfig, "async", "snapshot");
4074
+ const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
4075
+ const rootNode = root ?? documentRef;
4076
+ if (!rootNode?.querySelectorAll && !documentRef?.querySelectorAll) {
4077
+ return {};
4078
+ }
4079
+
4080
+ for (const searchRoot of new Set([rootNode, documentRef])) {
4081
+ if (!searchRoot?.querySelectorAll) {
4082
+ continue;
4083
+ }
4084
+ for (const script of searchRoot.querySelectorAll("script[type='application/json'], script")) {
4085
+ if (!script.hasAttribute?.(snapshotAttr)) {
4086
+ continue;
4087
+ }
4088
+ const source = script.textContent?.trim() ?? "";
4089
+ if (!source) {
4090
+ return {};
4091
+ }
4092
+ try {
4093
+ return JSON.parse(source);
4094
+ } catch (cause) {
4095
+ throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
4096
+ }
4097
+ }
4098
+ }
4099
+
4100
+ return {};
4101
+ }
4102
+
3920
4103
  function applyUseToRuntime(runtime, normalized) {
3921
4104
  applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
3922
4105
  applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
@@ -4077,7 +4260,7 @@
4077
4260
  function escapeScriptJson(value) {
4078
4261
  return JSON.stringify(value).replaceAll("<", "\\u003c");
4079
4262
  }
4080
- return { defineApp, createApp, Async };
4263
+ return { defineApp, createApp, readSnapshot, Async };
4081
4264
  })();
4082
4265
 
4083
4266
  const __delayModule = (() => {
@@ -4118,6 +4301,7 @@
4118
4301
  const { Async: Async } = __appModule;
4119
4302
  const { createApp: createApp } = __appModule;
4120
4303
  const { defineApp: defineApp } = __appModule;
4304
+ const { readSnapshot: readSnapshot } = __appModule;
4121
4305
  const { attributeName: attributeName } = __attributesModule;
4122
4306
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
4123
4307
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
@@ -4143,7 +4327,7 @@
4143
4327
  const { createSignalRegistry: createSignalRegistry } = __signalsModule;
4144
4328
  const { effect: effect } = __signalsModule;
4145
4329
  const { signal: signal } = __signalsModule;
4146
- 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 };
4330
+ 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 };
4147
4331
  assertNoUmdNamespaceConflicts(api, Async);
4148
4332
  Object.assign(Async, api);
4149
4333
  Async.Async = Async;