@async/framework 0.6.0 → 0.8.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.
@@ -105,7 +105,8 @@
105
105
  router: context.router,
106
106
  loader: context.loader,
107
107
  cache: context.cache,
108
- abort: activeAbort
108
+ abort: activeAbort,
109
+ scheduler: context.scheduler
109
110
  });
110
111
  }
111
112
  return server;
@@ -119,6 +120,9 @@
119
120
  get cache() {
120
121
  return registry._context?.().cache;
121
122
  },
123
+ get scheduler() {
124
+ return registry._context?.().scheduler;
125
+ },
122
126
  get version() {
123
127
  return runVersion;
124
128
  },
@@ -195,11 +199,20 @@
195
199
  _bindRegistry(nextRegistry, nextId) {
196
200
  registry = nextRegistry;
197
201
  registeredId = nextId;
198
- queueMicrotask(() => {
202
+ const start = () => {
199
203
  if (registry === nextRegistry && status === "idle") {
200
204
  state.refresh();
201
205
  }
202
- });
206
+ };
207
+ const scheduler = registry._context?.().scheduler;
208
+ if (scheduler) {
209
+ scheduler.enqueue("async", start, {
210
+ scope: registeredId,
211
+ key: `asyncSignal:${registeredId}:initial`
212
+ });
213
+ } else {
214
+ queueMicrotask(start);
215
+ }
203
216
  },
204
217
 
205
218
  _dispose() {
@@ -235,11 +248,26 @@
235
248
  for (const dependency of dependencies) {
236
249
  const dependencyId = String(dependency).split(".")[0];
237
250
  if (dependencyId && dependencyId !== registeredId) {
238
- dependencyCleanups.add(registry.subscribe(dependency, () => state.refresh()));
251
+ dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
239
252
  }
240
253
  }
241
254
  }
242
255
 
256
+ function scheduleRefresh() {
257
+ if (activeAbort && !activeAbort.aborted) {
258
+ activeAbort.cancel(new Error(`Async signal "${registeredId}" dependency changed.`));
259
+ }
260
+ const scheduler = registry?._context?.().scheduler;
261
+ if (!scheduler) {
262
+ state.refresh();
263
+ return;
264
+ }
265
+ scheduler.enqueue("async", () => state.refresh(), {
266
+ scope: registeredId,
267
+ key: `asyncSignal:${registeredId}:refresh`
268
+ });
269
+ }
270
+
243
271
  function notify() {
244
272
  for (const subscriber of [...subscribers]) {
245
273
  subscriber(state);
@@ -547,6 +575,7 @@
547
575
  const registryStore = registry ?? createRegistryStore();
548
576
  const definitions = registryStore._map(type);
549
577
  const entries = registryStore._map(`${type}.entries`);
578
+ const pending = new Map();
550
579
 
551
580
  const registryApi = attachRegistryInspection({
552
581
  register(id, definition = defineCache()) {
@@ -608,19 +637,37 @@
608
637
  if (cached !== undefined) {
609
638
  return cached;
610
639
  }
611
- const value = await fn();
612
- registryApi.set(key, value, options);
613
- return value;
640
+ if (pending.has(key)) {
641
+ return pending.get(key);
642
+ }
643
+ let promise;
644
+ promise = Promise.resolve()
645
+ .then(fn)
646
+ .then((value) => {
647
+ if (pending.get(key) === promise) {
648
+ registryApi.set(key, value, options);
649
+ }
650
+ return value;
651
+ })
652
+ .finally(() => {
653
+ if (pending.get(key) === promise) {
654
+ pending.delete(key);
655
+ }
656
+ });
657
+ pending.set(key, promise);
658
+ return promise;
614
659
  },
615
660
 
616
661
  delete(key) {
617
662
  assertKey(key);
663
+ pending.delete(key);
618
664
  return entries.delete(key);
619
665
  },
620
666
 
621
667
  clear(prefix) {
622
668
  if (prefix === undefined) {
623
669
  entries.clear();
670
+ pending.clear();
624
671
  return registryApi;
625
672
  }
626
673
  for (const key of [...entries.keys()]) {
@@ -628,6 +675,11 @@
628
675
  entries.delete(key);
629
676
  }
630
677
  }
678
+ for (const key of [...pending.keys()]) {
679
+ if (key.startsWith(prefix)) {
680
+ pending.delete(key);
681
+ }
682
+ }
631
683
  return registryApi;
632
684
  },
633
685
 
@@ -852,7 +904,8 @@
852
904
  server: registry._context?.().server,
853
905
  router: registry._context?.().router,
854
906
  loader: registry._context?.().loader,
855
- cache: registry._context?.().cache
907
+ cache: registry._context?.().cache,
908
+ scheduler: registry._context?.().scheduler
856
909
  }));
857
910
  });
858
911
  }
@@ -880,6 +933,8 @@
880
933
  const registryCleanups = new Map();
881
934
  const runtimeContext = {};
882
935
  const boundEntries = new Set();
936
+ let subscriptionCounter = 0;
937
+ let effectCounter = 0;
883
938
 
884
939
  const registry = attachRegistryInspection({
885
940
  register(id, signalLike) {
@@ -955,17 +1010,21 @@
955
1010
  return createRef(registry, id);
956
1011
  },
957
1012
 
958
- subscribe(path, fn) {
1013
+ subscribe(path, fn, options = {}) {
959
1014
  if (typeof fn !== "function") {
960
1015
  throw new TypeError("subscribe(path, fn) requires a function.");
961
1016
  }
962
1017
  const parsed = parsePath(path, entries);
963
1018
  const entry = requireEntry(entries, parsed.id);
1019
+ const subscriptionId = ++subscriptionCounter;
964
1020
  return entry.subscribe(() => {
965
- fn(registry.get(parsed.path), {
1021
+ scheduleCallback(() => fn(registry.get(parsed.path), {
966
1022
  id: parsed.id,
967
1023
  path: parsed.path,
968
1024
  signal: entry
1025
+ }), {
1026
+ ...options,
1027
+ key: options.key ?? `signal:${parsed.path}:${subscriptionId}`
969
1028
  });
970
1029
  });
971
1030
  },
@@ -983,10 +1042,12 @@
983
1042
  return registry.ref(id);
984
1043
  },
985
1044
 
986
- effect(fn) {
1045
+ effect(fn, options = {}) {
987
1046
  let cleanup;
988
1047
  let dependencyCleanups = [];
989
1048
  let stopped = false;
1049
+ const scheduler = options.scheduler;
1050
+ const effectId = ++effectCounter;
990
1051
 
991
1052
  const run = () => {
992
1053
  if (stopped) {
@@ -1005,10 +1066,22 @@
1005
1066
  server: runtimeContext.server,
1006
1067
  router: runtimeContext.router,
1007
1068
  loader: runtimeContext.loader,
1008
- cache: runtimeContext.cache
1069
+ cache: runtimeContext.cache,
1070
+ scheduler: runtimeContext.scheduler
1009
1071
  }));
1010
1072
  cleanup = outcome.value;
1011
- dependencyCleanups = outcome.dependencies.map((dependency) => registry.subscribe(dependency, run));
1073
+ dependencyCleanups = outcome.dependencies.map((dependency) => registry.subscribe(dependency, scheduleRun));
1074
+ };
1075
+
1076
+ const scheduleRun = () => {
1077
+ if (!scheduler) {
1078
+ run();
1079
+ return;
1080
+ }
1081
+ scheduler.enqueue(options.phase ?? "effect", run, {
1082
+ scope: options.scope,
1083
+ key: options.key ?? `effect:${effectId}`
1084
+ });
1012
1085
  };
1013
1086
 
1014
1087
  run();
@@ -1085,6 +1158,17 @@
1085
1158
  registryCleanups.set(id, cleanup);
1086
1159
  }
1087
1160
  }
1161
+
1162
+ function scheduleCallback(fn, options = {}) {
1163
+ const scheduler = options.scheduler;
1164
+ if (!scheduler || options.phase === "sync") {
1165
+ return fn();
1166
+ }
1167
+ return scheduler.enqueue(options.phase ?? "effect", fn, {
1168
+ scope: options.scope,
1169
+ key: options.key
1170
+ });
1171
+ }
1088
1172
  }
1089
1173
 
1090
1174
  function normalizeSignal(signalLike) {
@@ -1551,10 +1635,15 @@
1551
1635
  html,
1552
1636
  attach(target) {
1553
1637
  for (const hook of attachHooks) {
1554
- const cleanup = hook(target);
1555
- if (typeof cleanup === "function") {
1556
- cleanups.push(cleanup);
1557
- }
1638
+ runtime.scheduler?.enqueue("lifecycle", () => {
1639
+ const cleanup = hook(target);
1640
+ if (typeof cleanup === "function") {
1641
+ cleanups.push(cleanup);
1642
+ }
1643
+ }, {
1644
+ scope,
1645
+ key: `attach:${attachHooks.indexOf(hook)}`
1646
+ }) ?? runAttachHook(hook, target);
1558
1647
  }
1559
1648
  },
1560
1649
  mount(target) {
@@ -1562,7 +1651,17 @@
1562
1651
  },
1563
1652
  visible(target, observeVisible) {
1564
1653
  for (const hook of visibleHooks) {
1565
- const cleanup = observeVisible(target, hook);
1654
+ const cleanup = observeVisible(target, () => {
1655
+ runtime.scheduler?.enqueue("lifecycle", () => {
1656
+ const hookCleanup = hook(target);
1657
+ if (typeof hookCleanup === "function") {
1658
+ cleanups.push(hookCleanup);
1659
+ }
1660
+ }, {
1661
+ scope,
1662
+ key: `visible:${visibleHooks.indexOf(hook)}`
1663
+ }) ?? runVisibleHook(hook, target);
1664
+ });
1566
1665
  if (typeof cleanup === "function") {
1567
1666
  cleanups.push(cleanup);
1568
1667
  }
@@ -1572,6 +1671,7 @@
1572
1671
  while (destroyHooks.length > 0) {
1573
1672
  destroyHooks.pop()?.();
1574
1673
  }
1674
+ runtime.scheduler?.markScopeDestroyed(scope);
1575
1675
  while (cleanups.length > 0) {
1576
1676
  cleanups.pop()?.();
1577
1677
  }
@@ -1580,10 +1680,24 @@
1580
1680
  }
1581
1681
  }
1582
1682
  };
1683
+
1684
+ function runAttachHook(hook, target) {
1685
+ const cleanup = hook(target);
1686
+ if (typeof cleanup === "function") {
1687
+ cleanups.push(cleanup);
1688
+ }
1689
+ }
1690
+
1691
+ function runVisibleHook(hook, target) {
1692
+ const cleanup = hook(target);
1693
+ if (typeof cleanup === "function") {
1694
+ cleanups.push(cleanup);
1695
+ }
1696
+ }
1583
1697
  }
1584
1698
 
1585
1699
  function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
1586
- const { signals, handlers, loader, server, router, cache } = runtime;
1700
+ const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
1587
1701
  const generatedHandlers = new WeakMap();
1588
1702
  let generatedHandlerCounter = 0;
1589
1703
  let generatedSignalCounter = 0;
@@ -1595,6 +1709,7 @@
1595
1709
  server,
1596
1710
  router,
1597
1711
  cache,
1712
+ scheduler,
1598
1713
 
1599
1714
  signal(name, initial) {
1600
1715
  if (arguments.length === 1) {
@@ -1639,7 +1754,11 @@
1639
1754
  },
1640
1755
 
1641
1756
  effect(fn) {
1642
- const cleanup = signals.effect(() => fn.call(context));
1757
+ const cleanup = signals.effect(() => fn.call(context), {
1758
+ scheduler,
1759
+ phase: "effect",
1760
+ scope
1761
+ });
1643
1762
  cleanups.push(cleanup);
1644
1763
  return cleanup;
1645
1764
  },
@@ -1761,90 +1880,9 @@
1761
1880
  })();
1762
1881
 
1763
1882
  const __serverModule = (() => {
1764
- const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1765
1883
  const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
1766
-
1767
- function createServerRegistry(initialMap = {}, options = {}) {
1768
- const registryStore = options.registry ?? createRegistryStore();
1769
- const type = options.type ?? "server";
1770
- const entries = registryStore._map(type);
1771
- const defaults = {};
1772
-
1773
- const registry = attachRegistryInspection({
1774
- register(id, fn) {
1775
- assertServerId(id);
1776
- if (typeof fn !== "function") {
1777
- throw new TypeError(`Server function "${id}" must be a function.`);
1778
- }
1779
- if (entries.has(id)) {
1780
- throw new Error(`Server function "${id}" is already registered.`);
1781
- }
1782
- entries.set(id, fn);
1783
- return id;
1784
- },
1785
-
1786
- registerMany(map) {
1787
- for (const [id, fn] of Object.entries(map ?? {})) {
1788
- registry.register(id, fn);
1789
- }
1790
- return registry;
1791
- },
1792
-
1793
- unregister(id) {
1794
- assertServerId(id);
1795
- return entries.delete(id);
1796
- },
1797
-
1798
- resolve(id) {
1799
- assertServerId(id);
1800
- return entries.get(id);
1801
- },
1802
-
1803
- async run(id, args = [], context = {}) {
1804
- assertServerId(id);
1805
- const fn = registry.resolve(id);
1806
- if (!fn) {
1807
- throw new Error(`Server function "${id}" is not registered.`);
1808
- }
1809
-
1810
- let runContext;
1811
- const server = createServerNamespace((childId, childArgs, childContext = {}) => {
1812
- return registry.run(childId, childArgs, { ...runContext, ...childContext });
1813
- }, {}, () => runContext);
1814
-
1815
- const mergedContext = {
1816
- ...defaults,
1817
- ...context,
1818
- cache: defaults.cache ?? context.cache
1819
- };
1820
-
1821
- runContext = {
1822
- ...mergedContext,
1823
- id,
1824
- args,
1825
- input: mergedContext.input,
1826
- signals: createSignalReader(mergedContext.signals),
1827
- abort: mergedContext.abort,
1828
- cache: mergedContext.cache,
1829
- server
1830
- };
1831
-
1832
- return fn.call(runContext, ...args);
1833
- },
1834
-
1835
- _setContext(context = {}) {
1836
- Object.assign(defaults, context);
1837
- return registry;
1838
- },
1839
-
1840
- _adoptMany() {
1841
- return registry;
1842
- }
1843
- }, registryStore, type);
1844
-
1845
- registry.registerMany(initialMap);
1846
- return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
1847
- }
1884
+ const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
1885
+ const appliedServerValues = new WeakSet();
1848
1886
 
1849
1887
  function createServerProxy({
1850
1888
  endpoint = "/__async/server",
@@ -1853,13 +1891,14 @@
1853
1891
  loader,
1854
1892
  router,
1855
1893
  cache,
1894
+ scheduler,
1856
1895
  headers = {}
1857
1896
  } = {}) {
1858
1897
  if (typeof fetchImpl !== "function") {
1859
1898
  throw new TypeError("createServerProxy(...) requires fetch to be available.");
1860
1899
  }
1861
1900
 
1862
- const defaults = { signals, loader, router, cache };
1901
+ const defaults = { signals, loader, router, cache, scheduler };
1863
1902
 
1864
1903
  async function run(id, args = [], context = {}) {
1865
1904
  assertServerId(id);
@@ -1869,6 +1908,7 @@
1869
1908
  input: context.input ?? defaultInput(runContext),
1870
1909
  signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
1871
1910
  };
1911
+ assertJsonTransportable(body);
1872
1912
 
1873
1913
  const response = await fetchImpl(joinEndpoint(endpoint, id), {
1874
1914
  method: "POST",
@@ -1886,7 +1926,7 @@
1886
1926
 
1887
1927
  const result = await readServerResponse(response);
1888
1928
  await applyServerResult(result, runContext);
1889
- return unwrapServerResult(result);
1929
+ return markAppliedServerValue(unwrapServerResult(result));
1890
1930
  }
1891
1931
 
1892
1932
  return createServerNamespace(run, {
@@ -1921,6 +1961,9 @@
1921
1961
  if (!isServerEnvelope(result)) {
1922
1962
  return result;
1923
1963
  }
1964
+ if (result[appliedServerResult] || appliedServerValues.has(result)) {
1965
+ return result;
1966
+ }
1924
1967
 
1925
1968
  if (result.signals && context.signals) {
1926
1969
  for (const [path, value] of Object.entries(result.signals)) {
@@ -1944,6 +1987,12 @@
1944
1987
  throw toError(result.error);
1945
1988
  }
1946
1989
 
1990
+ Object.defineProperty(result, appliedServerResult, {
1991
+ configurable: true,
1992
+ enumerable: false,
1993
+ value: true
1994
+ });
1995
+
1947
1996
  return result;
1948
1997
  }
1949
1998
 
@@ -1954,6 +2003,13 @@
1954
2003
  return result;
1955
2004
  }
1956
2005
 
2006
+ function markAppliedServerValue(value) {
2007
+ if (value && typeof value === "object") {
2008
+ appliedServerValues.add(value);
2009
+ }
2010
+ return value;
2011
+ }
2012
+
1957
2013
  function defaultInput(context = {}) {
1958
2014
  const form = findForm(context);
1959
2015
  if (form) {
@@ -2131,6 +2187,30 @@
2131
2187
  return output;
2132
2188
  }
2133
2189
 
2190
+ function assertJsonTransportable(value, seen = new Set()) {
2191
+ if (value == null || typeof value !== "object") {
2192
+ return;
2193
+ }
2194
+ if (seen.has(value)) {
2195
+ return;
2196
+ }
2197
+ seen.add(value);
2198
+
2199
+ const tag = Object.prototype.toString.call(value);
2200
+ if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
2201
+ throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
2202
+ }
2203
+ if (Array.isArray(value)) {
2204
+ for (const item of value) {
2205
+ assertJsonTransportable(item, seen);
2206
+ }
2207
+ return;
2208
+ }
2209
+ for (const item of Object.values(value)) {
2210
+ assertJsonTransportable(item, seen);
2211
+ }
2212
+ }
2213
+
2134
2214
  function joinEndpoint(endpoint, id) {
2135
2215
  return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
2136
2216
  }
@@ -2157,7 +2237,7 @@
2157
2237
  throw new TypeError("Server function id must be a non-empty string.");
2158
2238
  }
2159
2239
  }
2160
- return { createServerRegistry, createServerProxy, resolveServerCommandArguments, applyServerResult, unwrapServerResult, defaultInput };
2240
+ return { createServerProxy, resolveServerCommandArguments, applyServerResult, unwrapServerResult, defaultInput, createServerNamespace, createSignalReader, assertServerId };
2161
2241
  })();
2162
2242
 
2163
2243
  const __handlersModule = (() => {
@@ -2360,18 +2440,321 @@
2360
2440
  return { createHandlerRegistry, parseHandlerRef, isHandlerToken };
2361
2441
  })();
2362
2442
 
2443
+ const __schedulerModule = (() => {
2444
+ const defaultPhases = ["binding", "lifecycle", "effect", "async", "post"];
2445
+
2446
+ function createScheduler(options = {}) {
2447
+ const phases = [...(options.phases ?? defaultPhases)];
2448
+ const queues = new Map(phases.map((phase) => [phase, []]));
2449
+ const keyedJobs = new Map();
2450
+ const destroyedScopes = new Set();
2451
+ const objectScopeIds = new WeakMap();
2452
+ const onError = typeof options.onError === "function" ? options.onError : undefined;
2453
+ const maxDepth = options.maxDepth ?? 100;
2454
+ const strategy = options.strategy ?? "microtask";
2455
+ let destroyed = false;
2456
+ let flushing = false;
2457
+ let scheduled = false;
2458
+ let batchDepth = 0;
2459
+ let jobCounter = 0;
2460
+ let scopeCounter = 0;
2461
+
2462
+ const api = {
2463
+ strategy,
2464
+ phases,
2465
+
2466
+ batch(fn) {
2467
+ if (typeof fn !== "function") {
2468
+ throw new TypeError("scheduler.batch(fn) requires a function.");
2469
+ }
2470
+ assertActive();
2471
+ batchDepth += 1;
2472
+ let asyncBatch = false;
2473
+ try {
2474
+ const value = fn();
2475
+ if (value && typeof value.then === "function") {
2476
+ asyncBatch = true;
2477
+ return value.finally(() => {
2478
+ batchDepth -= 1;
2479
+ requestFlush();
2480
+ });
2481
+ }
2482
+ return value;
2483
+ } finally {
2484
+ if (!asyncBatch && batchDepth > 0) {
2485
+ batchDepth -= 1;
2486
+ requestFlush();
2487
+ }
2488
+ }
2489
+ },
2490
+
2491
+ enqueue(phase, fn, options = {}) {
2492
+ assertActive();
2493
+ assertPhase(phase);
2494
+ if (typeof fn !== "function") {
2495
+ throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
2496
+ }
2497
+ const scope = options.scope;
2498
+ if (scope !== undefined && destroyedScopes.has(scope)) {
2499
+ return noop;
2500
+ }
2501
+
2502
+ const dedupeKey = options.key === undefined ? undefined : `${phase}:${scopeKey(scope)}:${String(options.key)}`;
2503
+ if (dedupeKey && keyedJobs.has(dedupeKey)) {
2504
+ return keyedJobs.get(dedupeKey).cancel;
2505
+ }
2506
+
2507
+ const job = {
2508
+ id: ++jobCounter,
2509
+ phase,
2510
+ fn,
2511
+ scope,
2512
+ boundary: options.boundary,
2513
+ key: dedupeKey,
2514
+ canceled: false,
2515
+ cancel() {
2516
+ job.canceled = true;
2517
+ if (job.key) {
2518
+ keyedJobs.delete(job.key);
2519
+ }
2520
+ }
2521
+ };
2522
+ queues.get(phase).push(job);
2523
+ if (job.key) {
2524
+ keyedJobs.set(job.key, job);
2525
+ }
2526
+ requestFlush();
2527
+ return job.cancel;
2528
+ },
2529
+
2530
+ afterFlush(fn, options = {}) {
2531
+ return api.enqueue("post", fn, options);
2532
+ },
2533
+
2534
+ async flush() {
2535
+ assertActive();
2536
+ if (flushing) {
2537
+ return;
2538
+ }
2539
+ scheduled = false;
2540
+ flushing = true;
2541
+ let depth = 0;
2542
+ try {
2543
+ while (hasJobs()) {
2544
+ depth += 1;
2545
+ if (depth > maxDepth) {
2546
+ throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
2547
+ }
2548
+ for (const phase of phases) {
2549
+ await flushPhase(phase);
2550
+ }
2551
+ }
2552
+ } finally {
2553
+ flushing = false;
2554
+ if (hasJobs()) {
2555
+ requestFlush();
2556
+ }
2557
+ }
2558
+ },
2559
+
2560
+ async flushScope(scope) {
2561
+ assertActive();
2562
+ if (flushing) {
2563
+ return;
2564
+ }
2565
+ scheduled = false;
2566
+ flushing = true;
2567
+ let depth = 0;
2568
+ try {
2569
+ while (hasJobsForScope(scope)) {
2570
+ depth += 1;
2571
+ if (depth > maxDepth) {
2572
+ throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
2573
+ }
2574
+ for (const phase of phases) {
2575
+ await flushPhase(phase, scope);
2576
+ }
2577
+ }
2578
+ } finally {
2579
+ flushing = false;
2580
+ if (hasJobs()) {
2581
+ requestFlush();
2582
+ }
2583
+ }
2584
+ },
2585
+
2586
+ cancelScope(scope) {
2587
+ if (scope === undefined) {
2588
+ return api;
2589
+ }
2590
+ for (const queue of queues.values()) {
2591
+ for (const job of queue) {
2592
+ if (job.scope === scope) {
2593
+ job.cancel();
2594
+ }
2595
+ }
2596
+ }
2597
+ return api;
2598
+ },
2599
+
2600
+ markScopeDestroyed(scope) {
2601
+ if (scope !== undefined) {
2602
+ destroyedScopes.add(scope);
2603
+ api.cancelScope(scope);
2604
+ }
2605
+ return api;
2606
+ },
2607
+
2608
+ inspect() {
2609
+ const counts = {};
2610
+ for (const [phase, queue] of queues) {
2611
+ counts[phase] = queue.filter((job) => !job.canceled).length;
2612
+ }
2613
+ return {
2614
+ strategy,
2615
+ phases: [...phases],
2616
+ pending: counts,
2617
+ scopesDestroyed: destroyedScopes.size,
2618
+ flushing,
2619
+ scheduled
2620
+ };
2621
+ },
2622
+
2623
+ destroy() {
2624
+ destroyed = true;
2625
+ for (const queue of queues.values()) {
2626
+ for (const job of queue) {
2627
+ job.cancel();
2628
+ }
2629
+ queue.length = 0;
2630
+ }
2631
+ keyedJobs.clear();
2632
+ destroyedScopes.clear();
2633
+ }
2634
+ };
2635
+
2636
+ return api;
2637
+
2638
+ function requestFlush() {
2639
+ if (strategy === "manual" || destroyed || flushing || batchDepth > 0 || scheduled) {
2640
+ return;
2641
+ }
2642
+ scheduled = true;
2643
+ scheduleMicrotask(() => {
2644
+ if (!destroyed) {
2645
+ void api.flush();
2646
+ }
2647
+ });
2648
+ }
2649
+
2650
+ async function flushPhase(phase, scope) {
2651
+ const queue = queues.get(phase);
2652
+ const remaining = [];
2653
+ const runnable = [];
2654
+
2655
+ for (const job of queue.splice(0)) {
2656
+ if (job.canceled) {
2657
+ continue;
2658
+ }
2659
+ if (scope !== undefined && job.scope !== scope) {
2660
+ remaining.push(job);
2661
+ continue;
2662
+ }
2663
+ runnable.push(job);
2664
+ }
2665
+
2666
+ queue.push(...remaining);
2667
+
2668
+ for (const job of runnable) {
2669
+ if (job.key) {
2670
+ keyedJobs.delete(job.key);
2671
+ }
2672
+ if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
2673
+ continue;
2674
+ }
2675
+ try {
2676
+ await job.fn();
2677
+ } catch (error) {
2678
+ if (onError) {
2679
+ onError(error, job);
2680
+ } else {
2681
+ throw error;
2682
+ }
2683
+ }
2684
+ }
2685
+ }
2686
+
2687
+ function hasJobs() {
2688
+ for (const queue of queues.values()) {
2689
+ if (queue.some((job) => !job.canceled)) {
2690
+ return true;
2691
+ }
2692
+ }
2693
+ return false;
2694
+ }
2695
+
2696
+ function hasJobsForScope(scope) {
2697
+ for (const queue of queues.values()) {
2698
+ if (queue.some((job) => !job.canceled && job.scope === scope)) {
2699
+ return true;
2700
+ }
2701
+ }
2702
+ return false;
2703
+ }
2704
+
2705
+ function assertActive() {
2706
+ if (destroyed) {
2707
+ throw new Error("Scheduler has been destroyed.");
2708
+ }
2709
+ }
2710
+
2711
+ function assertPhase(phase) {
2712
+ if (!queues.has(phase)) {
2713
+ throw new Error(`Unknown scheduler phase "${phase}".`);
2714
+ }
2715
+ }
2716
+
2717
+ function scopeKey(scope) {
2718
+ if (scope === undefined) {
2719
+ return "global";
2720
+ }
2721
+ if ((typeof scope === "object" && scope !== null) || typeof scope === "function") {
2722
+ if (!objectScopeIds.has(scope)) {
2723
+ objectScopeIds.set(scope, `scope:${++scopeCounter}`);
2724
+ }
2725
+ return objectScopeIds.get(scope);
2726
+ }
2727
+ return String(scope);
2728
+ }
2729
+ }
2730
+
2731
+ function scheduleMicrotask(fn) {
2732
+ if (typeof queueMicrotask === "function") {
2733
+ queueMicrotask(fn);
2734
+ return;
2735
+ }
2736
+ Promise.resolve().then(fn);
2737
+ }
2738
+
2739
+ function noop() {}
2740
+ return { createScheduler };
2741
+ })();
2742
+
2363
2743
  const __loaderModule = (() => {
2364
2744
  const { renderComponent } = __componentModule;
2365
2745
  const { createHandlerRegistry } = __handlersModule;
2746
+ const { createScheduler } = __schedulerModule;
2366
2747
  const { createSignalRegistry, isSignalRef } = __signalsModule;
2367
2748
  const { matchAttribute, normalizeAttributeConfig, readAttribute } = __attributesModule;
2368
2749
  const inlineBindingPrefix = "__async:inline:";
2369
2750
 
2370
- function Loader({ root, signals, handlers, server, router, cache, attributes } = {}) {
2751
+ function Loader({ root, signals, handlers, server, router, cache, attributes, scheduler } = {}) {
2371
2752
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
2372
2753
  const rootNode = root ?? documentRef;
2373
2754
  const signalRegistry = signals ?? createSignalRegistry();
2374
2755
  const handlerRegistry = handlers ?? createHandlerRegistry();
2756
+ const schedulerInstance = scheduler ?? createScheduler();
2757
+ const ownsScheduler = !scheduler;
2375
2758
  const attributeConfig = normalizeAttributeConfig(attributes);
2376
2759
  const cleanups = new Set();
2377
2760
  const eventBindings = new WeakMap();
@@ -2392,6 +2775,7 @@
2392
2775
  server,
2393
2776
  router,
2394
2777
  cache,
2778
+ scheduler: schedulerInstance,
2395
2779
  attributes: attributeConfig,
2396
2780
 
2397
2781
  start() {
@@ -2431,6 +2815,7 @@
2431
2815
  server: api.server,
2432
2816
  router: api.router,
2433
2817
  cache: api.cache,
2818
+ scheduler: schedulerInstance,
2434
2819
  attributes: attributeConfig
2435
2820
  });
2436
2821
  cleanupChildren(target);
@@ -2451,6 +2836,9 @@
2451
2836
  runCleanup(cleanup);
2452
2837
  }
2453
2838
  cleanups.clear();
2839
+ if (ownsScheduler) {
2840
+ schedulerInstance.destroy();
2841
+ }
2454
2842
  },
2455
2843
 
2456
2844
  _observeVisible(target, fn) {
@@ -2468,13 +2856,14 @@
2468
2856
  }
2469
2857
  };
2470
2858
 
2471
- signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
2859
+ signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache, scheduler: schedulerInstance });
2472
2860
  api.server?._setContext?.({
2473
2861
  signals: signalRegistry,
2474
2862
  handlers: handlerRegistry,
2475
2863
  loader: api,
2476
2864
  router: api.router,
2477
- cache: api.cache
2865
+ cache: api.cache,
2866
+ scheduler: schedulerInstance
2478
2867
  });
2479
2868
 
2480
2869
  function bindEventAttributes(scope) {
@@ -2506,18 +2895,19 @@
2506
2895
 
2507
2896
  const listener = async (event) => {
2508
2897
  try {
2509
- await handlerRegistry.run(ref, {
2898
+ await schedulerInstance.batch(() => handlerRegistry.run(ref, {
2510
2899
  signals: signalRegistry,
2511
2900
  handlers: handlerRegistry,
2512
2901
  loader: api,
2513
2902
  server: api.server,
2514
2903
  router: api.router,
2515
2904
  cache: api.cache,
2905
+ scheduler: schedulerInstance,
2516
2906
  event,
2517
2907
  element,
2518
2908
  el: element,
2519
2909
  root: rootNode
2520
- });
2910
+ }));
2521
2911
  } catch (error) {
2522
2912
  dispatchAsyncError(element, error);
2523
2913
  }
@@ -2633,7 +3023,12 @@
2633
3023
 
2634
3024
  const read = () => readBinding(path, options);
2635
3025
  apply(read());
2636
- addCleanup(subscribeBinding(path, () => apply(read())), element);
3026
+ addCleanup(subscribeBinding(path, () => {
3027
+ schedulerInstance.enqueue("binding", () => apply(read()), {
3028
+ scope: element,
3029
+ key
3030
+ });
3031
+ }), element);
2637
3032
  }
2638
3033
 
2639
3034
  function bindValueWriter(element, path) {
@@ -2694,7 +3089,12 @@
2694
3089
  const state = {
2695
3090
  id,
2696
3091
  templates,
2697
- cleanup: signalRegistry.subscribe(`${id}.$status`, () => renderBoundary(boundary))
3092
+ cleanup: signalRegistry.subscribe(`${id}.$status`, () => {
3093
+ schedulerInstance.enqueue("binding", () => renderBoundary(boundary), {
3094
+ scope: boundary,
3095
+ key: `boundary:${id}`
3096
+ });
3097
+ })
2698
3098
  };
2699
3099
  boundaryState.set(boundary, state);
2700
3100
  addCleanup(state.cleanup, boundary);
@@ -2734,7 +3134,7 @@
2734
3134
  }
2735
3135
  mountedElements.add(element);
2736
3136
  for (const ref of refs) {
2737
- runPseudo(element, ref);
3137
+ scheduleLifecycle(element, () => runPseudo(element, ref), `attach:${ref}`);
2738
3138
  }
2739
3139
  }
2740
3140
 
@@ -2747,7 +3147,7 @@
2747
3147
  continue;
2748
3148
  }
2749
3149
  visibleElements.add(element);
2750
- addCleanup(observeVisible(element, () => runPseudo(element, ref)), element);
3150
+ addCleanup(observeVisible(element, () => scheduleLifecycle(element, () => runPseudo(element, ref), `visible:${ref}`)), element);
2751
3151
  }
2752
3152
  }
2753
3153
 
@@ -2771,6 +3171,7 @@
2771
3171
  server: api.server,
2772
3172
  router: api.router,
2773
3173
  cache: api.cache,
3174
+ scheduler: schedulerInstance,
2774
3175
  element,
2775
3176
  el: element,
2776
3177
  root: rootNode
@@ -2789,10 +3190,13 @@
2789
3190
  const ownerWindow = target.ownerDocument?.defaultView ?? globalThis;
2790
3191
  const Observer = ownerWindow.IntersectionObserver ?? globalThis.IntersectionObserver;
2791
3192
  if (!Observer) {
2792
- queueMicrotask(() => {
3193
+ schedulerInstance.enqueue("lifecycle", () => {
2793
3194
  if (!destroyed) {
2794
3195
  fn(target);
2795
3196
  }
3197
+ }, {
3198
+ scope: target,
3199
+ key: "visible:fallback"
2796
3200
  });
2797
3201
  return () => {};
2798
3202
  }
@@ -2847,6 +3251,7 @@
2847
3251
  }
2848
3252
  for (const element of elementsIn(node)) {
2849
3253
  runScopedCleanups(element);
3254
+ schedulerInstance.markScopeDestroyed(element);
2850
3255
  }
2851
3256
  }
2852
3257
 
@@ -2870,6 +3275,13 @@
2870
3275
  }
2871
3276
  }
2872
3277
 
3278
+ function scheduleLifecycle(element, fn, key) {
3279
+ schedulerInstance.enqueue("lifecycle", fn, {
3280
+ scope: element,
3281
+ key
3282
+ });
3283
+ }
3284
+
2873
3285
  return api;
2874
3286
  }
2875
3287
 
@@ -3200,6 +3612,7 @@
3200
3612
  const __routerModule = (() => {
3201
3613
  const { Loader } = __loaderModule;
3202
3614
  const { createHandlerRegistry } = __handlersModule;
3615
+ const { createScheduler } = __schedulerModule;
3203
3616
  const { createSignalRegistry } = __signalsModule;
3204
3617
  const { applyServerResult } = __serverModule;
3205
3618
  const { createRegistryStore } = __registryStoreModule;
@@ -3230,6 +3643,7 @@
3230
3643
  const nextRoute = normalizeRoute(pattern, definition);
3231
3644
  entries.set(pattern, nextRoute.definition);
3232
3645
  routes.push(nextRoute);
3646
+ sortRoutes(routes);
3233
3647
  return nextRoute;
3234
3648
  },
3235
3649
 
@@ -3302,6 +3716,7 @@
3302
3716
  const nextRoute = normalizeRoute(pattern, definition);
3303
3717
  entries.set(pattern, nextRoute.definition);
3304
3718
  routes.push(nextRoute);
3719
+ sortRoutes(routes);
3305
3720
  }
3306
3721
  }
3307
3722
 
@@ -3318,12 +3733,15 @@
3318
3733
  partials,
3319
3734
  fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
3320
3735
  routeEndpoint = "/__async/route",
3321
- attributes
3736
+ attributes,
3737
+ scheduler
3322
3738
  } = {}) {
3323
3739
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
3324
3740
  const rootNode = root ?? documentRef;
3325
3741
  const signalRegistry = signals ?? loader?.signals ?? createSignalRegistry();
3326
3742
  const handlerRegistry = handlers ?? loader?.handlers ?? createHandlerRegistry();
3743
+ const schedulerInstance = scheduler ?? loader?.scheduler ?? createScheduler();
3744
+ const ownsScheduler = !scheduler && !loader?.scheduler;
3327
3745
  const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
3328
3746
  const loaderInstance =
3329
3747
  loader ??
@@ -3333,11 +3751,14 @@
3333
3751
  handlers: handlerRegistry,
3334
3752
  server,
3335
3753
  cache,
3754
+ scheduler: schedulerInstance,
3336
3755
  attributes: attributeConfig
3337
3756
  });
3338
3757
  const ownsLoader = !loader;
3339
3758
  const cleanups = new Set();
3340
3759
  let destroyed = false;
3760
+ let navigationVersion = 0;
3761
+ let activeNavigation;
3341
3762
 
3342
3763
  const api = {
3343
3764
  mode,
@@ -3350,12 +3771,13 @@
3350
3771
  server,
3351
3772
  cache,
3352
3773
  partials,
3774
+ scheduler: schedulerInstance,
3353
3775
  attributes: attributeConfig,
3354
3776
 
3355
3777
  start() {
3356
3778
  assertActive();
3357
3779
  loaderInstance.router = api;
3358
- signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache });
3780
+ signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache, scheduler: schedulerInstance });
3359
3781
  if (ownsLoader) {
3360
3782
  loaderInstance.start();
3361
3783
  }
@@ -3377,7 +3799,7 @@
3377
3799
  },
3378
3800
 
3379
3801
  match(url) {
3380
- return routes.match(url);
3802
+ return routes.match(resolveUrl(url));
3381
3803
  },
3382
3804
 
3383
3805
  prefetch(url) {
@@ -3402,7 +3824,7 @@
3402
3824
  return null;
3403
3825
  }
3404
3826
 
3405
- const target = toUrl(url);
3827
+ const target = resolveUrl(url);
3406
3828
  if (mode === "ssr-spa") {
3407
3829
  return fetchRoutePartial(target, options);
3408
3830
  }
@@ -3414,10 +3836,14 @@
3414
3836
  return;
3415
3837
  }
3416
3838
  destroyed = true;
3839
+ activeNavigation?.controller.abort(new Error("Router has been destroyed."));
3417
3840
  for (const cleanup of cleanups) {
3418
3841
  cleanup();
3419
3842
  }
3420
3843
  cleanups.clear();
3844
+ if (ownsScheduler) {
3845
+ schedulerInstance.destroy();
3846
+ }
3421
3847
  }
3422
3848
  };
3423
3849
 
@@ -3453,24 +3879,37 @@
3453
3879
  async function renderLocalRoutePartial(target, options = {}) {
3454
3880
  const matched = api.match(target);
3455
3881
  if (!matched) {
3882
+ beginNavigation(target, null);
3456
3883
  setNoRouteError(target);
3457
3884
  return null;
3458
3885
  }
3459
3886
 
3887
+ const navigation = beginNavigation(target, matched);
3460
3888
  setMatchedRouterState(target, matched, { pending: true, error: null });
3461
3889
 
3462
3890
  try {
3463
3891
  if (!matched.route?.partial || !partials?.resolve?.(matched.route.partial)) {
3464
3892
  const error = new Error(`Route "${target.pathname}" does not have a registered partial.`);
3465
- setRouterState({ pending: false, error });
3893
+ if (isActiveNavigation(navigation)) {
3894
+ setRouterState({ pending: false, error });
3895
+ }
3466
3896
  return null;
3467
3897
  }
3468
3898
 
3469
- const result = await partials.render(matched.route.partial, matched.params, contextFor(matched));
3470
- await applyNavigationResult(result, target, options);
3899
+ const result = await partials.render(matched.route.partial, matched.params, contextFor(matched, navigation));
3900
+ if (!isActiveNavigation(navigation)) {
3901
+ return null;
3902
+ }
3903
+ await applyNavigationResult(result, target, options, navigation);
3904
+ if (!isActiveNavigation(navigation)) {
3905
+ return null;
3906
+ }
3471
3907
  setRouterState({ pending: false, error: null });
3472
3908
  return result;
3473
3909
  } catch (error) {
3910
+ if (!isActiveNavigation(navigation)) {
3911
+ return null;
3912
+ }
3474
3913
  setRouterState({ pending: false, error });
3475
3914
  throw error;
3476
3915
  }
@@ -3478,28 +3917,48 @@
3478
3917
 
3479
3918
  async function fetchRoutePartial(target, options = {}) {
3480
3919
  const matched = api.match(target);
3920
+ const navigation = beginNavigation(target, matched);
3481
3921
  setMatchedRouterState(target, matched, { pending: true, error: null });
3482
3922
 
3483
3923
  try {
3484
- const result = await fetchRoute(target.href);
3485
- await applyNavigationResult(result, target, options);
3924
+ const result = await fetchRoute(target.href, { signal: navigation.abort });
3925
+ if (!isActiveNavigation(navigation)) {
3926
+ return null;
3927
+ }
3928
+ await applyNavigationResult(result, target, options, navigation);
3929
+ if (!isActiveNavigation(navigation)) {
3930
+ return null;
3931
+ }
3486
3932
  setRouterState({ pending: false, error: null });
3487
3933
  return result;
3488
3934
  } catch (error) {
3935
+ if (!isActiveNavigation(navigation)) {
3936
+ return null;
3937
+ }
3489
3938
  setRouterState({ pending: false, error });
3490
3939
  throw error;
3491
3940
  }
3492
3941
  }
3493
3942
 
3494
- async function applyNavigationResult(result, target, options) {
3943
+ async function applyNavigationResult(result, target, options, navigation) {
3944
+ if (!isActiveNavigation(navigation)) {
3945
+ return;
3946
+ }
3495
3947
  await applyServerResult(result, {
3496
3948
  signals: signalRegistry,
3497
3949
  loader: loaderInstance,
3498
3950
  router: api,
3499
- cache
3951
+ cache,
3952
+ scheduler: schedulerInstance,
3953
+ abort: navigation?.abort
3500
3954
  });
3955
+ await schedulerInstance.flush();
3956
+ if (!isActiveNavigation(navigation)) {
3957
+ return;
3958
+ }
3501
3959
  if (result?.html != null && !result.boundary && !result.redirect) {
3502
3960
  loaderInstance.swap(boundary, result.html);
3961
+ await schedulerInstance.flush();
3503
3962
  }
3504
3963
  if (result?.redirect || options.history === false) {
3505
3964
  return;
@@ -3511,14 +3970,15 @@
3511
3970
  documentRef.defaultView?.history?.pushState?.({}, "", target.href);
3512
3971
  }
3513
3972
 
3514
- async function fetchRoute(url, { prefetch = false } = {}) {
3973
+ async function fetchRoute(url, { prefetch = false, signal } = {}) {
3515
3974
  if (typeof fetchImpl !== "function") {
3516
3975
  throw new Error("Router navigation requires a partial registry or fetch.");
3517
3976
  }
3518
3977
  const response = await fetchImpl(`${routeEndpoint}?to=${encodeURIComponent(String(url))}`, {
3519
3978
  headers: {
3520
3979
  accept: "application/json, text/html"
3521
- }
3980
+ },
3981
+ signal
3522
3982
  });
3523
3983
  if (!response.ok) {
3524
3984
  throw new Error(`Route "${url}" failed with ${response.status}.`);
@@ -3533,7 +3993,7 @@
3533
3993
  return { boundary, html: await response.text() };
3534
3994
  }
3535
3995
 
3536
- function contextFor(matched) {
3996
+ function contextFor(matched, navigation) {
3537
3997
  return {
3538
3998
  params: matched.params,
3539
3999
  route: matched.route,
@@ -3543,8 +4003,27 @@
3543
4003
  loader: loaderInstance,
3544
4004
  server,
3545
4005
  cache,
3546
- abort: undefined
4006
+ scheduler: schedulerInstance,
4007
+ abort: navigation?.abort
4008
+ };
4009
+ }
4010
+
4011
+ function beginNavigation(target, matched) {
4012
+ activeNavigation?.controller.abort(new Error(`Router navigation superseded by ${target.pathname}${target.search}.`));
4013
+ const controller = new AbortController();
4014
+ const navigation = {
4015
+ id: ++navigationVersion,
4016
+ controller,
4017
+ abort: controller.signal,
4018
+ target,
4019
+ matched
3547
4020
  };
4021
+ activeNavigation = navigation;
4022
+ return navigation;
4023
+ }
4024
+
4025
+ function isActiveNavigation(navigation) {
4026
+ return !destroyed && navigation && activeNavigation?.id === navigation.id && !navigation.abort.aborted;
3548
4027
  }
3549
4028
 
3550
4029
  function updateStateFromLocation() {
@@ -3581,7 +4060,14 @@
3581
4060
  }
3582
4061
 
3583
4062
  function currentUrl() {
3584
- return toUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
4063
+ return resolveUrl(documentRef.defaultView?.location?.href ?? "http://localhost/");
4064
+ }
4065
+
4066
+ function resolveUrl(url) {
4067
+ if (url instanceof URL) {
4068
+ return url;
4069
+ }
4070
+ return new URL(String(url), documentRef.defaultView?.location?.href ?? "http://localhost/");
3585
4071
  }
3586
4072
 
3587
4073
  function assertActive() {
@@ -3598,6 +4084,7 @@
3598
4084
  pattern,
3599
4085
  regex,
3600
4086
  keys,
4087
+ score: routeScore(pattern),
3601
4088
  definition: normalized
3602
4089
  };
3603
4090
  }
@@ -3666,6 +4153,28 @@
3666
4153
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3667
4154
  }
3668
4155
 
4156
+ function sortRoutes(routes) {
4157
+ routes.sort((left, right) => right.score - left.score || right.pattern.length - left.pattern.length);
4158
+ }
4159
+
4160
+ function routeScore(pattern) {
4161
+ if (pattern === "*") {
4162
+ return -1;
4163
+ }
4164
+ return pattern
4165
+ .split("/")
4166
+ .filter(Boolean)
4167
+ .reduce((score, segment) => {
4168
+ if (segment === "*") {
4169
+ return score;
4170
+ }
4171
+ if (segment.startsWith(":")) {
4172
+ return score + 2;
4173
+ }
4174
+ return score + 4;
4175
+ }, pattern === "/" ? 3 : 0);
4176
+ }
4177
+
3669
4178
  function assertPattern(pattern) {
3670
4179
  if (typeof pattern !== "string" || (pattern !== "*" && !pattern.startsWith("/"))) {
3671
4180
  throw new TypeError("Route pattern must be a path string or \"*\".");
@@ -3681,15 +4190,17 @@
3681
4190
  const { Loader } = __loaderModule;
3682
4191
  const { createPartialRegistry } = __partialsModule;
3683
4192
  const { createRouteRegistry, createRouter } = __routerModule;
3684
- const { createServerRegistry } = __serverModule;
4193
+ const { createScheduler } = __schedulerModule;
4194
+ const { createServerNamespace } = __serverModule;
3685
4195
  const { createSignal, createSignalRegistry } = __signalsModule;
3686
4196
  const { createRegistryStore } = __registryStoreModule;
3687
4197
  const { attributeName, normalizeAttributeConfig } = __attributesModule;
3688
4198
  const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
3689
4199
 
3690
- function defineApp(initial) {
4200
+ function defineApp(initial, options = {}) {
3691
4201
  const registry = createRegistryStore(undefined, { target: "browser" });
3692
4202
  const runtimes = new Set();
4203
+ const createRuntime = options.createRuntime ?? createApp;
3693
4204
 
3694
4205
  const app = {
3695
4206
  registry,
@@ -3708,7 +4219,7 @@
3708
4219
  },
3709
4220
 
3710
4221
  start(options = {}) {
3711
- const runtime = createApp(app, options).start();
4222
+ const runtime = createRuntime(app, options).start();
3712
4223
  app.runtime = runtime;
3713
4224
  return runtime;
3714
4225
  },
@@ -3733,13 +4244,18 @@
3733
4244
  function createApp(appOrDefinition = Async, options = {}) {
3734
4245
  const app = isAppHub(appOrDefinition) ? appOrDefinition : defineApp(appOrDefinition ?? {});
3735
4246
  const target = options.target ?? "browser";
4247
+ const scheduler = options.scheduler ?? options.loader?.scheduler ?? createScheduler({
4248
+ strategy: target === "server" ? "manual" : "microtask"
4249
+ });
4250
+ const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
3736
4251
  const attributes = normalizeAttributeConfig(options.attributes);
3737
4252
  const registry = options.registry ?? app.registry.view({ target });
3738
4253
  const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
3739
4254
  const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
3740
4255
  const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
3741
4256
  const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
3742
- const server = options.server ?? createServerRegistry(undefined, { registry, type: "server" });
4257
+ const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
4258
+ const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
3743
4259
  const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
3744
4260
  const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
3745
4261
  const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
@@ -3749,7 +4265,7 @@
3749
4265
  let started = false;
3750
4266
  let destroyed = false;
3751
4267
 
3752
- applySnapshot(signals, browserCache, options.snapshot);
4268
+ applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
3753
4269
  attachServerCache(server, serverCache);
3754
4270
 
3755
4271
  const runtime = {
@@ -3767,6 +4283,7 @@
3767
4283
  },
3768
4284
  loader,
3769
4285
  router,
4286
+ scheduler,
3770
4287
  attributes,
3771
4288
 
3772
4289
  start() {
@@ -3783,12 +4300,13 @@
3783
4300
  handlers,
3784
4301
  server,
3785
4302
  cache: browserCache,
4303
+ scheduler,
3786
4304
  attributes
3787
4305
  });
3788
4306
  runtime.loader = loader;
3789
4307
 
3790
4308
  configureServerContext({ cache: browserCache });
3791
- signals._setContext?.({ server, loader, cache: browserCache });
4309
+ signals._setContext?.({ server, loader, cache: browserCache, scheduler });
3792
4310
 
3793
4311
  loader.start();
3794
4312
 
@@ -3804,6 +4322,7 @@
3804
4322
  server,
3805
4323
  cache: browserCache,
3806
4324
  partials,
4325
+ scheduler,
3807
4326
  fetch: options.fetch,
3808
4327
  routeEndpoint: options.routeEndpoint,
3809
4328
  attributes
@@ -3815,7 +4334,7 @@
3815
4334
  }
3816
4335
  } else {
3817
4336
  configureServerContext({ cache: serverCache });
3818
- signals._setContext?.({ server, cache: serverCache });
4337
+ signals._setContext?.({ server, cache: serverCache, scheduler });
3819
4338
  }
3820
4339
 
3821
4340
  return runtime;
@@ -3829,9 +4348,10 @@
3829
4348
  async render(url) {
3830
4349
  assertActive();
3831
4350
  configureServerContext({ cache: serverCache });
3832
- signals._setContext?.({ server, cache: serverCache });
4351
+ signals._setContext?.({ server, cache: serverCache, scheduler });
3833
4352
  const matched = routes.match(url);
3834
4353
  if (!matched) {
4354
+ await scheduler.flush();
3835
4355
  return {
3836
4356
  html: renderDocument("", { status: 404, signals, browserCache, boundary: options.boundary ?? "route", attributes }),
3837
4357
  status: 404,
@@ -3851,8 +4371,8 @@
3851
4371
  cache: serverCache,
3852
4372
  browserCache,
3853
4373
  partials,
3854
- request: options.request,
3855
- locals: options.locals
4374
+ scheduler,
4375
+ ...currentRequestContext()
3856
4376
  })
3857
4377
  : { html: "" };
3858
4378
 
@@ -3865,6 +4385,8 @@
3865
4385
  browserCache.restore(result.cache.browser);
3866
4386
  }
3867
4387
 
4388
+ await scheduler.flush();
4389
+
3868
4390
  const status = result.status ?? 200;
3869
4391
  return {
3870
4392
  html: renderDocument(result.html, { status, signals, browserCache, boundary: result.boundary ?? options.boundary ?? "route", attributes }),
@@ -3883,6 +4405,9 @@
3883
4405
  router?.destroy?.();
3884
4406
  loader?.destroy?.();
3885
4407
  signals.destroy?.();
4408
+ if (ownsScheduler) {
4409
+ scheduler.destroy();
4410
+ }
3886
4411
  },
3887
4412
 
3888
4413
  _applyUse(normalized) {
@@ -3903,11 +4428,23 @@
3903
4428
  loader,
3904
4429
  router,
3905
4430
  cache,
3906
- request: options.request,
3907
- locals: options.locals
4431
+ scheduler,
4432
+ requestContext: options.requestContext,
4433
+ ...currentRequestContext()
3908
4434
  });
3909
4435
  }
3910
4436
 
4437
+ function currentRequestContext() {
4438
+ const context = readRequestContextLike(options.requestContext);
4439
+ return {
4440
+ requestContext: context,
4441
+ request: context.request ?? options.request,
4442
+ headers: context.headers ?? options.headers,
4443
+ cookies: context.cookies ?? options.cookies,
4444
+ locals: context.locals ?? options.locals
4445
+ };
4446
+ }
4447
+
3911
4448
  function assertActive() {
3912
4449
  if (destroyed) {
3913
4450
  throw new Error("Async app runtime has been destroyed.");
@@ -3917,6 +4454,38 @@
3917
4454
 
3918
4455
  const Async = defineApp();
3919
4456
 
4457
+ function readSnapshot(root = globalThis.document, { attributes } = {}) {
4458
+ const attributeConfig = normalizeAttributeConfig(attributes);
4459
+ const snapshotAttr = attributeName(attributeConfig, "async", "snapshot");
4460
+ const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
4461
+ const rootNode = root ?? documentRef;
4462
+ if (!rootNode?.querySelectorAll && !documentRef?.querySelectorAll) {
4463
+ return {};
4464
+ }
4465
+
4466
+ for (const searchRoot of new Set([rootNode, documentRef])) {
4467
+ if (!searchRoot?.querySelectorAll) {
4468
+ continue;
4469
+ }
4470
+ for (const script of searchRoot.querySelectorAll("script[type='application/json'], script")) {
4471
+ if (!script.hasAttribute?.(snapshotAttr)) {
4472
+ continue;
4473
+ }
4474
+ const source = script.textContent?.trim() ?? "";
4475
+ if (!source) {
4476
+ return {};
4477
+ }
4478
+ try {
4479
+ return JSON.parse(source);
4480
+ } catch (cause) {
4481
+ throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
4482
+ }
4483
+ }
4484
+ }
4485
+
4486
+ return {};
4487
+ }
4488
+
3920
4489
  function applyUseToRuntime(runtime, normalized) {
3921
4490
  applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
3922
4491
  applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
@@ -4029,6 +4598,77 @@
4029
4598
  }
4030
4599
  }
4031
4600
 
4601
+ function createServerReferenceRegistry(initialMap = {}, options = {}) {
4602
+ const registry = options.registry ?? createRegistryStore();
4603
+ const type = options.type ?? "server";
4604
+ const defaults = {};
4605
+
4606
+ const reference = {
4607
+ registry,
4608
+
4609
+ register(id, value) {
4610
+ registry.register(type, id, value);
4611
+ return id;
4612
+ },
4613
+
4614
+ registerMany(map) {
4615
+ for (const [id, value] of Object.entries(map ?? {})) {
4616
+ reference.register(id, value);
4617
+ }
4618
+ return reference;
4619
+ },
4620
+
4621
+ unregister(id) {
4622
+ return registry.unregister(type, id);
4623
+ },
4624
+
4625
+ resolve() {
4626
+ return undefined;
4627
+ },
4628
+
4629
+ async run(id) {
4630
+ throw new Error(`Server command "${id}" cannot run without a server proxy or server registry.`);
4631
+ },
4632
+
4633
+ keys() {
4634
+ return registry.keys(type);
4635
+ },
4636
+
4637
+ entries() {
4638
+ return registry.entries(type);
4639
+ },
4640
+
4641
+ inspect() {
4642
+ return registry.entries(type);
4643
+ },
4644
+
4645
+ _setContext(context = {}) {
4646
+ Object.assign(defaults, context);
4647
+ return reference;
4648
+ },
4649
+
4650
+ _adoptMany() {
4651
+ return reference;
4652
+ }
4653
+ };
4654
+
4655
+ reference.registerMany(initialMap);
4656
+ return createServerNamespace((id, args, context) => reference.run(id, args, context), reference, () => defaults);
4657
+ }
4658
+
4659
+ function readRequestContextLike(store) {
4660
+ if (!store) {
4661
+ return {};
4662
+ }
4663
+ if (typeof store.get === "function") {
4664
+ return store.get() ?? {};
4665
+ }
4666
+ if (typeof store.getStore === "function") {
4667
+ return store.getStore() ?? {};
4668
+ }
4669
+ return {};
4670
+ }
4671
+
4032
4672
  function normalizeEntries(type, entries = {}) {
4033
4673
  if (type !== "signal") {
4034
4674
  return { ...(entries ?? {}) };
@@ -4077,7 +4717,7 @@
4077
4717
  function escapeScriptJson(value) {
4078
4718
  return JSON.stringify(value).replaceAll("<", "\\u003c");
4079
4719
  }
4080
- return { defineApp, createApp, Async };
4720
+ return { defineApp, createApp, readSnapshot, Async };
4081
4721
  })();
4082
4722
 
4083
4723
  const __delayModule = (() => {
@@ -4118,6 +4758,7 @@
4118
4758
  const { Async: Async } = __appModule;
4119
4759
  const { createApp: createApp } = __appModule;
4120
4760
  const { defineApp: defineApp } = __appModule;
4761
+ const { readSnapshot: readSnapshot } = __appModule;
4121
4762
  const { attributeName: attributeName } = __attributesModule;
4122
4763
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
4123
4764
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
@@ -4136,14 +4777,17 @@
4136
4777
  const { createRouter: createRouter } = __routerModule;
4137
4778
  const { defineRoute: defineRoute } = __routerModule;
4138
4779
  const { route: route } = __routerModule;
4780
+ const { createScheduler: createScheduler } = __schedulerModule;
4781
+ const { applyServerResult: applyServerResult } = __serverModule;
4139
4782
  const { createServerProxy: createServerProxy } = __serverModule;
4140
- const { createServerRegistry: createServerRegistry } = __serverModule;
4783
+ const { resolveServerCommandArguments: resolveServerCommandArguments } = __serverModule;
4784
+ const { unwrapServerResult: unwrapServerResult } = __serverModule;
4141
4785
  const { computed: computed } = __signalsModule;
4142
4786
  const { createSignal: createSignal } = __signalsModule;
4143
4787
  const { createSignalRegistry: createSignalRegistry } = __signalsModule;
4144
4788
  const { effect: effect } = __signalsModule;
4145
4789
  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 };
4790
+ 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, createScheduler, applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult, computed, createSignal, createSignalRegistry, effect, signal };
4147
4791
  assertNoUmdNamespaceConflicts(api, Async);
4148
4792
  Object.assign(Async, api);
4149
4793
  Async.Async = Async;