@async/framework 0.7.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.
@@ -94,7 +94,8 @@ const __asyncSignalModule = (() => {
94
94
  router: context.router,
95
95
  loader: context.loader,
96
96
  cache: context.cache,
97
- abort: activeAbort
97
+ abort: activeAbort,
98
+ scheduler: context.scheduler
98
99
  });
99
100
  }
100
101
  return server;
@@ -108,6 +109,9 @@ const __asyncSignalModule = (() => {
108
109
  get cache() {
109
110
  return registry._context?.().cache;
110
111
  },
112
+ get scheduler() {
113
+ return registry._context?.().scheduler;
114
+ },
111
115
  get version() {
112
116
  return runVersion;
113
117
  },
@@ -184,11 +188,20 @@ const __asyncSignalModule = (() => {
184
188
  _bindRegistry(nextRegistry, nextId) {
185
189
  registry = nextRegistry;
186
190
  registeredId = nextId;
187
- queueMicrotask(() => {
191
+ const start = () => {
188
192
  if (registry === nextRegistry && status === "idle") {
189
193
  state.refresh();
190
194
  }
191
- });
195
+ };
196
+ const scheduler = registry._context?.().scheduler;
197
+ if (scheduler) {
198
+ scheduler.enqueue("async", start, {
199
+ scope: registeredId,
200
+ key: `asyncSignal:${registeredId}:initial`
201
+ });
202
+ } else {
203
+ queueMicrotask(start);
204
+ }
192
205
  },
193
206
 
194
207
  _dispose() {
@@ -224,11 +237,26 @@ const __asyncSignalModule = (() => {
224
237
  for (const dependency of dependencies) {
225
238
  const dependencyId = String(dependency).split(".")[0];
226
239
  if (dependencyId && dependencyId !== registeredId) {
227
- dependencyCleanups.add(registry.subscribe(dependency, () => state.refresh()));
240
+ dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
228
241
  }
229
242
  }
230
243
  }
231
244
 
245
+ function scheduleRefresh() {
246
+ if (activeAbort && !activeAbort.aborted) {
247
+ activeAbort.cancel(new Error(`Async signal "${registeredId}" dependency changed.`));
248
+ }
249
+ const scheduler = registry?._context?.().scheduler;
250
+ if (!scheduler) {
251
+ state.refresh();
252
+ return;
253
+ }
254
+ scheduler.enqueue("async", () => state.refresh(), {
255
+ scope: registeredId,
256
+ key: `asyncSignal:${registeredId}:refresh`
257
+ });
258
+ }
259
+
232
260
  function notify() {
233
261
  for (const subscriber of [...subscribers]) {
234
262
  subscriber(state);
@@ -865,7 +893,8 @@ const __signalsModule = (() => {
865
893
  server: registry._context?.().server,
866
894
  router: registry._context?.().router,
867
895
  loader: registry._context?.().loader,
868
- cache: registry._context?.().cache
896
+ cache: registry._context?.().cache,
897
+ scheduler: registry._context?.().scheduler
869
898
  }));
870
899
  });
871
900
  }
@@ -893,6 +922,8 @@ const __signalsModule = (() => {
893
922
  const registryCleanups = new Map();
894
923
  const runtimeContext = {};
895
924
  const boundEntries = new Set();
925
+ let subscriptionCounter = 0;
926
+ let effectCounter = 0;
896
927
 
897
928
  const registry = attachRegistryInspection({
898
929
  register(id, signalLike) {
@@ -968,17 +999,21 @@ const __signalsModule = (() => {
968
999
  return createRef(registry, id);
969
1000
  },
970
1001
 
971
- subscribe(path, fn) {
1002
+ subscribe(path, fn, options = {}) {
972
1003
  if (typeof fn !== "function") {
973
1004
  throw new TypeError("subscribe(path, fn) requires a function.");
974
1005
  }
975
1006
  const parsed = parsePath(path, entries);
976
1007
  const entry = requireEntry(entries, parsed.id);
1008
+ const subscriptionId = ++subscriptionCounter;
977
1009
  return entry.subscribe(() => {
978
- fn(registry.get(parsed.path), {
1010
+ scheduleCallback(() => fn(registry.get(parsed.path), {
979
1011
  id: parsed.id,
980
1012
  path: parsed.path,
981
1013
  signal: entry
1014
+ }), {
1015
+ ...options,
1016
+ key: options.key ?? `signal:${parsed.path}:${subscriptionId}`
982
1017
  });
983
1018
  });
984
1019
  },
@@ -996,10 +1031,12 @@ const __signalsModule = (() => {
996
1031
  return registry.ref(id);
997
1032
  },
998
1033
 
999
- effect(fn) {
1034
+ effect(fn, options = {}) {
1000
1035
  let cleanup;
1001
1036
  let dependencyCleanups = [];
1002
1037
  let stopped = false;
1038
+ const scheduler = options.scheduler;
1039
+ const effectId = ++effectCounter;
1003
1040
 
1004
1041
  const run = () => {
1005
1042
  if (stopped) {
@@ -1018,10 +1055,22 @@ const __signalsModule = (() => {
1018
1055
  server: runtimeContext.server,
1019
1056
  router: runtimeContext.router,
1020
1057
  loader: runtimeContext.loader,
1021
- cache: runtimeContext.cache
1058
+ cache: runtimeContext.cache,
1059
+ scheduler: runtimeContext.scheduler
1022
1060
  }));
1023
1061
  cleanup = outcome.value;
1024
- dependencyCleanups = outcome.dependencies.map((dependency) => registry.subscribe(dependency, run));
1062
+ dependencyCleanups = outcome.dependencies.map((dependency) => registry.subscribe(dependency, scheduleRun));
1063
+ };
1064
+
1065
+ const scheduleRun = () => {
1066
+ if (!scheduler) {
1067
+ run();
1068
+ return;
1069
+ }
1070
+ scheduler.enqueue(options.phase ?? "effect", run, {
1071
+ scope: options.scope,
1072
+ key: options.key ?? `effect:${effectId}`
1073
+ });
1025
1074
  };
1026
1075
 
1027
1076
  run();
@@ -1098,6 +1147,17 @@ const __signalsModule = (() => {
1098
1147
  registryCleanups.set(id, cleanup);
1099
1148
  }
1100
1149
  }
1150
+
1151
+ function scheduleCallback(fn, options = {}) {
1152
+ const scheduler = options.scheduler;
1153
+ if (!scheduler || options.phase === "sync") {
1154
+ return fn();
1155
+ }
1156
+ return scheduler.enqueue(options.phase ?? "effect", fn, {
1157
+ scope: options.scope,
1158
+ key: options.key
1159
+ });
1160
+ }
1101
1161
  }
1102
1162
 
1103
1163
  function normalizeSignal(signalLike) {
@@ -1564,10 +1624,15 @@ const __componentModule = (() => {
1564
1624
  html,
1565
1625
  attach(target) {
1566
1626
  for (const hook of attachHooks) {
1567
- const cleanup = hook(target);
1568
- if (typeof cleanup === "function") {
1569
- cleanups.push(cleanup);
1570
- }
1627
+ runtime.scheduler?.enqueue("lifecycle", () => {
1628
+ const cleanup = hook(target);
1629
+ if (typeof cleanup === "function") {
1630
+ cleanups.push(cleanup);
1631
+ }
1632
+ }, {
1633
+ scope,
1634
+ key: `attach:${attachHooks.indexOf(hook)}`
1635
+ }) ?? runAttachHook(hook, target);
1571
1636
  }
1572
1637
  },
1573
1638
  mount(target) {
@@ -1575,7 +1640,17 @@ const __componentModule = (() => {
1575
1640
  },
1576
1641
  visible(target, observeVisible) {
1577
1642
  for (const hook of visibleHooks) {
1578
- const cleanup = observeVisible(target, hook);
1643
+ const cleanup = observeVisible(target, () => {
1644
+ runtime.scheduler?.enqueue("lifecycle", () => {
1645
+ const hookCleanup = hook(target);
1646
+ if (typeof hookCleanup === "function") {
1647
+ cleanups.push(hookCleanup);
1648
+ }
1649
+ }, {
1650
+ scope,
1651
+ key: `visible:${visibleHooks.indexOf(hook)}`
1652
+ }) ?? runVisibleHook(hook, target);
1653
+ });
1579
1654
  if (typeof cleanup === "function") {
1580
1655
  cleanups.push(cleanup);
1581
1656
  }
@@ -1585,6 +1660,7 @@ const __componentModule = (() => {
1585
1660
  while (destroyHooks.length > 0) {
1586
1661
  destroyHooks.pop()?.();
1587
1662
  }
1663
+ runtime.scheduler?.markScopeDestroyed(scope);
1588
1664
  while (cleanups.length > 0) {
1589
1665
  cleanups.pop()?.();
1590
1666
  }
@@ -1593,10 +1669,24 @@ const __componentModule = (() => {
1593
1669
  }
1594
1670
  }
1595
1671
  };
1672
+
1673
+ function runAttachHook(hook, target) {
1674
+ const cleanup = hook(target);
1675
+ if (typeof cleanup === "function") {
1676
+ cleanups.push(cleanup);
1677
+ }
1678
+ }
1679
+
1680
+ function runVisibleHook(hook, target) {
1681
+ const cleanup = hook(target);
1682
+ if (typeof cleanup === "function") {
1683
+ cleanups.push(cleanup);
1684
+ }
1685
+ }
1596
1686
  }
1597
1687
 
1598
1688
  function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
1599
- const { signals, handlers, loader, server, router, cache } = runtime;
1689
+ const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
1600
1690
  const generatedHandlers = new WeakMap();
1601
1691
  let generatedHandlerCounter = 0;
1602
1692
  let generatedSignalCounter = 0;
@@ -1608,6 +1698,7 @@ const __componentModule = (() => {
1608
1698
  server,
1609
1699
  router,
1610
1700
  cache,
1701
+ scheduler,
1611
1702
 
1612
1703
  signal(name, initial) {
1613
1704
  if (arguments.length === 1) {
@@ -1652,7 +1743,11 @@ const __componentModule = (() => {
1652
1743
  },
1653
1744
 
1654
1745
  effect(fn) {
1655
- const cleanup = signals.effect(() => fn.call(context));
1746
+ const cleanup = signals.effect(() => fn.call(context), {
1747
+ scheduler,
1748
+ phase: "effect",
1749
+ scope
1750
+ });
1656
1751
  cleanups.push(cleanup);
1657
1752
  return cleanup;
1658
1753
  },
@@ -1774,93 +1869,10 @@ const __componentModule = (() => {
1774
1869
  })();
1775
1870
 
1776
1871
  const __serverModule = (() => {
1777
- const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1778
1872
  const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
1779
1873
  const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
1780
1874
  const appliedServerValues = new WeakSet();
1781
1875
 
1782
- function createServerRegistry(initialMap = {}, options = {}) {
1783
- const registryStore = options.registry ?? createRegistryStore();
1784
- const type = options.type ?? "server";
1785
- const entries = registryStore._map(type);
1786
- const defaults = {};
1787
-
1788
- const registry = attachRegistryInspection({
1789
- register(id, fn) {
1790
- assertServerId(id);
1791
- if (typeof fn !== "function") {
1792
- throw new TypeError(`Server function "${id}" must be a function.`);
1793
- }
1794
- if (entries.has(id)) {
1795
- throw new Error(`Server function "${id}" is already registered.`);
1796
- }
1797
- entries.set(id, fn);
1798
- return id;
1799
- },
1800
-
1801
- registerMany(map) {
1802
- for (const [id, fn] of Object.entries(map ?? {})) {
1803
- registry.register(id, fn);
1804
- }
1805
- return registry;
1806
- },
1807
-
1808
- unregister(id) {
1809
- assertServerId(id);
1810
- return entries.delete(id);
1811
- },
1812
-
1813
- resolve(id) {
1814
- assertServerId(id);
1815
- return entries.get(id);
1816
- },
1817
-
1818
- async run(id, args = [], context = {}) {
1819
- assertServerId(id);
1820
- const fn = registry.resolve(id);
1821
- if (!fn) {
1822
- throw new Error(`Server function "${id}" is not registered.`);
1823
- }
1824
-
1825
- let runContext;
1826
- const server = createServerNamespace((childId, childArgs, childContext = {}) => {
1827
- return registry.run(childId, childArgs, { ...runContext, ...childContext });
1828
- }, {}, () => runContext);
1829
-
1830
- const mergedContext = {
1831
- ...defaults,
1832
- ...context,
1833
- cache: defaults.cache ?? context.cache
1834
- };
1835
-
1836
- runContext = {
1837
- ...mergedContext,
1838
- id,
1839
- args,
1840
- input: mergedContext.input,
1841
- signals: createSignalReader(mergedContext.signals),
1842
- abort: mergedContext.abort,
1843
- cache: mergedContext.cache,
1844
- server
1845
- };
1846
-
1847
- return fn.call(runContext, ...args);
1848
- },
1849
-
1850
- _setContext(context = {}) {
1851
- Object.assign(defaults, context);
1852
- return registry;
1853
- },
1854
-
1855
- _adoptMany() {
1856
- return registry;
1857
- }
1858
- }, registryStore, type);
1859
-
1860
- registry.registerMany(initialMap);
1861
- return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
1862
- }
1863
-
1864
1876
  function createServerProxy({
1865
1877
  endpoint = "/__async/server",
1866
1878
  fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
@@ -1868,13 +1880,14 @@ const __serverModule = (() => {
1868
1880
  loader,
1869
1881
  router,
1870
1882
  cache,
1883
+ scheduler,
1871
1884
  headers = {}
1872
1885
  } = {}) {
1873
1886
  if (typeof fetchImpl !== "function") {
1874
1887
  throw new TypeError("createServerProxy(...) requires fetch to be available.");
1875
1888
  }
1876
1889
 
1877
- const defaults = { signals, loader, router, cache };
1890
+ const defaults = { signals, loader, router, cache, scheduler };
1878
1891
 
1879
1892
  async function run(id, args = [], context = {}) {
1880
1893
  assertServerId(id);
@@ -2213,7 +2226,7 @@ const __serverModule = (() => {
2213
2226
  throw new TypeError("Server function id must be a non-empty string.");
2214
2227
  }
2215
2228
  }
2216
- return { createServerRegistry, createServerProxy, resolveServerCommandArguments, applyServerResult, unwrapServerResult, defaultInput };
2229
+ return { createServerProxy, resolveServerCommandArguments, applyServerResult, unwrapServerResult, defaultInput, createServerNamespace, createSignalReader, assertServerId };
2217
2230
  })();
2218
2231
 
2219
2232
  const __handlersModule = (() => {
@@ -2416,18 +2429,321 @@ const __handlersModule = (() => {
2416
2429
  return { createHandlerRegistry, parseHandlerRef, isHandlerToken };
2417
2430
  })();
2418
2431
 
2432
+ const __schedulerModule = (() => {
2433
+ const defaultPhases = ["binding", "lifecycle", "effect", "async", "post"];
2434
+
2435
+ function createScheduler(options = {}) {
2436
+ const phases = [...(options.phases ?? defaultPhases)];
2437
+ const queues = new Map(phases.map((phase) => [phase, []]));
2438
+ const keyedJobs = new Map();
2439
+ const destroyedScopes = new Set();
2440
+ const objectScopeIds = new WeakMap();
2441
+ const onError = typeof options.onError === "function" ? options.onError : undefined;
2442
+ const maxDepth = options.maxDepth ?? 100;
2443
+ const strategy = options.strategy ?? "microtask";
2444
+ let destroyed = false;
2445
+ let flushing = false;
2446
+ let scheduled = false;
2447
+ let batchDepth = 0;
2448
+ let jobCounter = 0;
2449
+ let scopeCounter = 0;
2450
+
2451
+ const api = {
2452
+ strategy,
2453
+ phases,
2454
+
2455
+ batch(fn) {
2456
+ if (typeof fn !== "function") {
2457
+ throw new TypeError("scheduler.batch(fn) requires a function.");
2458
+ }
2459
+ assertActive();
2460
+ batchDepth += 1;
2461
+ let asyncBatch = false;
2462
+ try {
2463
+ const value = fn();
2464
+ if (value && typeof value.then === "function") {
2465
+ asyncBatch = true;
2466
+ return value.finally(() => {
2467
+ batchDepth -= 1;
2468
+ requestFlush();
2469
+ });
2470
+ }
2471
+ return value;
2472
+ } finally {
2473
+ if (!asyncBatch && batchDepth > 0) {
2474
+ batchDepth -= 1;
2475
+ requestFlush();
2476
+ }
2477
+ }
2478
+ },
2479
+
2480
+ enqueue(phase, fn, options = {}) {
2481
+ assertActive();
2482
+ assertPhase(phase);
2483
+ if (typeof fn !== "function") {
2484
+ throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
2485
+ }
2486
+ const scope = options.scope;
2487
+ if (scope !== undefined && destroyedScopes.has(scope)) {
2488
+ return noop;
2489
+ }
2490
+
2491
+ const dedupeKey = options.key === undefined ? undefined : `${phase}:${scopeKey(scope)}:${String(options.key)}`;
2492
+ if (dedupeKey && keyedJobs.has(dedupeKey)) {
2493
+ return keyedJobs.get(dedupeKey).cancel;
2494
+ }
2495
+
2496
+ const job = {
2497
+ id: ++jobCounter,
2498
+ phase,
2499
+ fn,
2500
+ scope,
2501
+ boundary: options.boundary,
2502
+ key: dedupeKey,
2503
+ canceled: false,
2504
+ cancel() {
2505
+ job.canceled = true;
2506
+ if (job.key) {
2507
+ keyedJobs.delete(job.key);
2508
+ }
2509
+ }
2510
+ };
2511
+ queues.get(phase).push(job);
2512
+ if (job.key) {
2513
+ keyedJobs.set(job.key, job);
2514
+ }
2515
+ requestFlush();
2516
+ return job.cancel;
2517
+ },
2518
+
2519
+ afterFlush(fn, options = {}) {
2520
+ return api.enqueue("post", fn, options);
2521
+ },
2522
+
2523
+ async flush() {
2524
+ assertActive();
2525
+ if (flushing) {
2526
+ return;
2527
+ }
2528
+ scheduled = false;
2529
+ flushing = true;
2530
+ let depth = 0;
2531
+ try {
2532
+ while (hasJobs()) {
2533
+ depth += 1;
2534
+ if (depth > maxDepth) {
2535
+ throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
2536
+ }
2537
+ for (const phase of phases) {
2538
+ await flushPhase(phase);
2539
+ }
2540
+ }
2541
+ } finally {
2542
+ flushing = false;
2543
+ if (hasJobs()) {
2544
+ requestFlush();
2545
+ }
2546
+ }
2547
+ },
2548
+
2549
+ async flushScope(scope) {
2550
+ assertActive();
2551
+ if (flushing) {
2552
+ return;
2553
+ }
2554
+ scheduled = false;
2555
+ flushing = true;
2556
+ let depth = 0;
2557
+ try {
2558
+ while (hasJobsForScope(scope)) {
2559
+ depth += 1;
2560
+ if (depth > maxDepth) {
2561
+ throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
2562
+ }
2563
+ for (const phase of phases) {
2564
+ await flushPhase(phase, scope);
2565
+ }
2566
+ }
2567
+ } finally {
2568
+ flushing = false;
2569
+ if (hasJobs()) {
2570
+ requestFlush();
2571
+ }
2572
+ }
2573
+ },
2574
+
2575
+ cancelScope(scope) {
2576
+ if (scope === undefined) {
2577
+ return api;
2578
+ }
2579
+ for (const queue of queues.values()) {
2580
+ for (const job of queue) {
2581
+ if (job.scope === scope) {
2582
+ job.cancel();
2583
+ }
2584
+ }
2585
+ }
2586
+ return api;
2587
+ },
2588
+
2589
+ markScopeDestroyed(scope) {
2590
+ if (scope !== undefined) {
2591
+ destroyedScopes.add(scope);
2592
+ api.cancelScope(scope);
2593
+ }
2594
+ return api;
2595
+ },
2596
+
2597
+ inspect() {
2598
+ const counts = {};
2599
+ for (const [phase, queue] of queues) {
2600
+ counts[phase] = queue.filter((job) => !job.canceled).length;
2601
+ }
2602
+ return {
2603
+ strategy,
2604
+ phases: [...phases],
2605
+ pending: counts,
2606
+ scopesDestroyed: destroyedScopes.size,
2607
+ flushing,
2608
+ scheduled
2609
+ };
2610
+ },
2611
+
2612
+ destroy() {
2613
+ destroyed = true;
2614
+ for (const queue of queues.values()) {
2615
+ for (const job of queue) {
2616
+ job.cancel();
2617
+ }
2618
+ queue.length = 0;
2619
+ }
2620
+ keyedJobs.clear();
2621
+ destroyedScopes.clear();
2622
+ }
2623
+ };
2624
+
2625
+ return api;
2626
+
2627
+ function requestFlush() {
2628
+ if (strategy === "manual" || destroyed || flushing || batchDepth > 0 || scheduled) {
2629
+ return;
2630
+ }
2631
+ scheduled = true;
2632
+ scheduleMicrotask(() => {
2633
+ if (!destroyed) {
2634
+ void api.flush();
2635
+ }
2636
+ });
2637
+ }
2638
+
2639
+ async function flushPhase(phase, scope) {
2640
+ const queue = queues.get(phase);
2641
+ const remaining = [];
2642
+ const runnable = [];
2643
+
2644
+ for (const job of queue.splice(0)) {
2645
+ if (job.canceled) {
2646
+ continue;
2647
+ }
2648
+ if (scope !== undefined && job.scope !== scope) {
2649
+ remaining.push(job);
2650
+ continue;
2651
+ }
2652
+ runnable.push(job);
2653
+ }
2654
+
2655
+ queue.push(...remaining);
2656
+
2657
+ for (const job of runnable) {
2658
+ if (job.key) {
2659
+ keyedJobs.delete(job.key);
2660
+ }
2661
+ if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
2662
+ continue;
2663
+ }
2664
+ try {
2665
+ await job.fn();
2666
+ } catch (error) {
2667
+ if (onError) {
2668
+ onError(error, job);
2669
+ } else {
2670
+ throw error;
2671
+ }
2672
+ }
2673
+ }
2674
+ }
2675
+
2676
+ function hasJobs() {
2677
+ for (const queue of queues.values()) {
2678
+ if (queue.some((job) => !job.canceled)) {
2679
+ return true;
2680
+ }
2681
+ }
2682
+ return false;
2683
+ }
2684
+
2685
+ function hasJobsForScope(scope) {
2686
+ for (const queue of queues.values()) {
2687
+ if (queue.some((job) => !job.canceled && job.scope === scope)) {
2688
+ return true;
2689
+ }
2690
+ }
2691
+ return false;
2692
+ }
2693
+
2694
+ function assertActive() {
2695
+ if (destroyed) {
2696
+ throw new Error("Scheduler has been destroyed.");
2697
+ }
2698
+ }
2699
+
2700
+ function assertPhase(phase) {
2701
+ if (!queues.has(phase)) {
2702
+ throw new Error(`Unknown scheduler phase "${phase}".`);
2703
+ }
2704
+ }
2705
+
2706
+ function scopeKey(scope) {
2707
+ if (scope === undefined) {
2708
+ return "global";
2709
+ }
2710
+ if ((typeof scope === "object" && scope !== null) || typeof scope === "function") {
2711
+ if (!objectScopeIds.has(scope)) {
2712
+ objectScopeIds.set(scope, `scope:${++scopeCounter}`);
2713
+ }
2714
+ return objectScopeIds.get(scope);
2715
+ }
2716
+ return String(scope);
2717
+ }
2718
+ }
2719
+
2720
+ function scheduleMicrotask(fn) {
2721
+ if (typeof queueMicrotask === "function") {
2722
+ queueMicrotask(fn);
2723
+ return;
2724
+ }
2725
+ Promise.resolve().then(fn);
2726
+ }
2727
+
2728
+ function noop() {}
2729
+ return { createScheduler };
2730
+ })();
2731
+
2419
2732
  const __loaderModule = (() => {
2420
2733
  const { renderComponent } = __componentModule;
2421
2734
  const { createHandlerRegistry } = __handlersModule;
2735
+ const { createScheduler } = __schedulerModule;
2422
2736
  const { createSignalRegistry, isSignalRef } = __signalsModule;
2423
2737
  const { matchAttribute, normalizeAttributeConfig, readAttribute } = __attributesModule;
2424
2738
  const inlineBindingPrefix = "__async:inline:";
2425
2739
 
2426
- function Loader({ root, signals, handlers, server, router, cache, attributes } = {}) {
2740
+ function Loader({ root, signals, handlers, server, router, cache, attributes, scheduler } = {}) {
2427
2741
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
2428
2742
  const rootNode = root ?? documentRef;
2429
2743
  const signalRegistry = signals ?? createSignalRegistry();
2430
2744
  const handlerRegistry = handlers ?? createHandlerRegistry();
2745
+ const schedulerInstance = scheduler ?? createScheduler();
2746
+ const ownsScheduler = !scheduler;
2431
2747
  const attributeConfig = normalizeAttributeConfig(attributes);
2432
2748
  const cleanups = new Set();
2433
2749
  const eventBindings = new WeakMap();
@@ -2448,6 +2764,7 @@ const __loaderModule = (() => {
2448
2764
  server,
2449
2765
  router,
2450
2766
  cache,
2767
+ scheduler: schedulerInstance,
2451
2768
  attributes: attributeConfig,
2452
2769
 
2453
2770
  start() {
@@ -2487,6 +2804,7 @@ const __loaderModule = (() => {
2487
2804
  server: api.server,
2488
2805
  router: api.router,
2489
2806
  cache: api.cache,
2807
+ scheduler: schedulerInstance,
2490
2808
  attributes: attributeConfig
2491
2809
  });
2492
2810
  cleanupChildren(target);
@@ -2507,6 +2825,9 @@ const __loaderModule = (() => {
2507
2825
  runCleanup(cleanup);
2508
2826
  }
2509
2827
  cleanups.clear();
2828
+ if (ownsScheduler) {
2829
+ schedulerInstance.destroy();
2830
+ }
2510
2831
  },
2511
2832
 
2512
2833
  _observeVisible(target, fn) {
@@ -2524,13 +2845,14 @@ const __loaderModule = (() => {
2524
2845
  }
2525
2846
  };
2526
2847
 
2527
- signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
2848
+ signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache, scheduler: schedulerInstance });
2528
2849
  api.server?._setContext?.({
2529
2850
  signals: signalRegistry,
2530
2851
  handlers: handlerRegistry,
2531
2852
  loader: api,
2532
2853
  router: api.router,
2533
- cache: api.cache
2854
+ cache: api.cache,
2855
+ scheduler: schedulerInstance
2534
2856
  });
2535
2857
 
2536
2858
  function bindEventAttributes(scope) {
@@ -2562,18 +2884,19 @@ const __loaderModule = (() => {
2562
2884
 
2563
2885
  const listener = async (event) => {
2564
2886
  try {
2565
- await handlerRegistry.run(ref, {
2887
+ await schedulerInstance.batch(() => handlerRegistry.run(ref, {
2566
2888
  signals: signalRegistry,
2567
2889
  handlers: handlerRegistry,
2568
2890
  loader: api,
2569
2891
  server: api.server,
2570
2892
  router: api.router,
2571
2893
  cache: api.cache,
2894
+ scheduler: schedulerInstance,
2572
2895
  event,
2573
2896
  element,
2574
2897
  el: element,
2575
2898
  root: rootNode
2576
- });
2899
+ }));
2577
2900
  } catch (error) {
2578
2901
  dispatchAsyncError(element, error);
2579
2902
  }
@@ -2689,7 +3012,12 @@ const __loaderModule = (() => {
2689
3012
 
2690
3013
  const read = () => readBinding(path, options);
2691
3014
  apply(read());
2692
- addCleanup(subscribeBinding(path, () => apply(read())), element);
3015
+ addCleanup(subscribeBinding(path, () => {
3016
+ schedulerInstance.enqueue("binding", () => apply(read()), {
3017
+ scope: element,
3018
+ key
3019
+ });
3020
+ }), element);
2693
3021
  }
2694
3022
 
2695
3023
  function bindValueWriter(element, path) {
@@ -2750,7 +3078,12 @@ const __loaderModule = (() => {
2750
3078
  const state = {
2751
3079
  id,
2752
3080
  templates,
2753
- cleanup: signalRegistry.subscribe(`${id}.$status`, () => renderBoundary(boundary))
3081
+ cleanup: signalRegistry.subscribe(`${id}.$status`, () => {
3082
+ schedulerInstance.enqueue("binding", () => renderBoundary(boundary), {
3083
+ scope: boundary,
3084
+ key: `boundary:${id}`
3085
+ });
3086
+ })
2754
3087
  };
2755
3088
  boundaryState.set(boundary, state);
2756
3089
  addCleanup(state.cleanup, boundary);
@@ -2790,7 +3123,7 @@ const __loaderModule = (() => {
2790
3123
  }
2791
3124
  mountedElements.add(element);
2792
3125
  for (const ref of refs) {
2793
- runPseudo(element, ref);
3126
+ scheduleLifecycle(element, () => runPseudo(element, ref), `attach:${ref}`);
2794
3127
  }
2795
3128
  }
2796
3129
 
@@ -2803,7 +3136,7 @@ const __loaderModule = (() => {
2803
3136
  continue;
2804
3137
  }
2805
3138
  visibleElements.add(element);
2806
- addCleanup(observeVisible(element, () => runPseudo(element, ref)), element);
3139
+ addCleanup(observeVisible(element, () => scheduleLifecycle(element, () => runPseudo(element, ref), `visible:${ref}`)), element);
2807
3140
  }
2808
3141
  }
2809
3142
 
@@ -2827,6 +3160,7 @@ const __loaderModule = (() => {
2827
3160
  server: api.server,
2828
3161
  router: api.router,
2829
3162
  cache: api.cache,
3163
+ scheduler: schedulerInstance,
2830
3164
  element,
2831
3165
  el: element,
2832
3166
  root: rootNode
@@ -2845,10 +3179,13 @@ const __loaderModule = (() => {
2845
3179
  const ownerWindow = target.ownerDocument?.defaultView ?? globalThis;
2846
3180
  const Observer = ownerWindow.IntersectionObserver ?? globalThis.IntersectionObserver;
2847
3181
  if (!Observer) {
2848
- queueMicrotask(() => {
3182
+ schedulerInstance.enqueue("lifecycle", () => {
2849
3183
  if (!destroyed) {
2850
3184
  fn(target);
2851
3185
  }
3186
+ }, {
3187
+ scope: target,
3188
+ key: "visible:fallback"
2852
3189
  });
2853
3190
  return () => {};
2854
3191
  }
@@ -2903,6 +3240,7 @@ const __loaderModule = (() => {
2903
3240
  }
2904
3241
  for (const element of elementsIn(node)) {
2905
3242
  runScopedCleanups(element);
3243
+ schedulerInstance.markScopeDestroyed(element);
2906
3244
  }
2907
3245
  }
2908
3246
 
@@ -2926,6 +3264,13 @@ const __loaderModule = (() => {
2926
3264
  }
2927
3265
  }
2928
3266
 
3267
+ function scheduleLifecycle(element, fn, key) {
3268
+ schedulerInstance.enqueue("lifecycle", fn, {
3269
+ scope: element,
3270
+ key
3271
+ });
3272
+ }
3273
+
2929
3274
  return api;
2930
3275
  }
2931
3276
 
@@ -3256,6 +3601,7 @@ const __partialsModule = (() => {
3256
3601
  const __routerModule = (() => {
3257
3602
  const { Loader } = __loaderModule;
3258
3603
  const { createHandlerRegistry } = __handlersModule;
3604
+ const { createScheduler } = __schedulerModule;
3259
3605
  const { createSignalRegistry } = __signalsModule;
3260
3606
  const { applyServerResult } = __serverModule;
3261
3607
  const { createRegistryStore } = __registryStoreModule;
@@ -3376,12 +3722,15 @@ const __routerModule = (() => {
3376
3722
  partials,
3377
3723
  fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
3378
3724
  routeEndpoint = "/__async/route",
3379
- attributes
3725
+ attributes,
3726
+ scheduler
3380
3727
  } = {}) {
3381
3728
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
3382
3729
  const rootNode = root ?? documentRef;
3383
3730
  const signalRegistry = signals ?? loader?.signals ?? createSignalRegistry();
3384
3731
  const handlerRegistry = handlers ?? loader?.handlers ?? createHandlerRegistry();
3732
+ const schedulerInstance = scheduler ?? loader?.scheduler ?? createScheduler();
3733
+ const ownsScheduler = !scheduler && !loader?.scheduler;
3385
3734
  const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
3386
3735
  const loaderInstance =
3387
3736
  loader ??
@@ -3391,6 +3740,7 @@ const __routerModule = (() => {
3391
3740
  handlers: handlerRegistry,
3392
3741
  server,
3393
3742
  cache,
3743
+ scheduler: schedulerInstance,
3394
3744
  attributes: attributeConfig
3395
3745
  });
3396
3746
  const ownsLoader = !loader;
@@ -3410,12 +3760,13 @@ const __routerModule = (() => {
3410
3760
  server,
3411
3761
  cache,
3412
3762
  partials,
3763
+ scheduler: schedulerInstance,
3413
3764
  attributes: attributeConfig,
3414
3765
 
3415
3766
  start() {
3416
3767
  assertActive();
3417
3768
  loaderInstance.router = api;
3418
- signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache });
3769
+ signalRegistry._setContext?.({ router: api, loader: loaderInstance, server, cache, scheduler: schedulerInstance });
3419
3770
  if (ownsLoader) {
3420
3771
  loaderInstance.start();
3421
3772
  }
@@ -3479,6 +3830,9 @@ const __routerModule = (() => {
3479
3830
  cleanup();
3480
3831
  }
3481
3832
  cleanups.clear();
3833
+ if (ownsScheduler) {
3834
+ schedulerInstance.destroy();
3835
+ }
3482
3836
  }
3483
3837
  };
3484
3838
 
@@ -3584,13 +3938,16 @@ const __routerModule = (() => {
3584
3938
  loader: loaderInstance,
3585
3939
  router: api,
3586
3940
  cache,
3941
+ scheduler: schedulerInstance,
3587
3942
  abort: navigation?.abort
3588
3943
  });
3944
+ await schedulerInstance.flush();
3589
3945
  if (!isActiveNavigation(navigation)) {
3590
3946
  return;
3591
3947
  }
3592
3948
  if (result?.html != null && !result.boundary && !result.redirect) {
3593
3949
  loaderInstance.swap(boundary, result.html);
3950
+ await schedulerInstance.flush();
3594
3951
  }
3595
3952
  if (result?.redirect || options.history === false) {
3596
3953
  return;
@@ -3635,6 +3992,7 @@ const __routerModule = (() => {
3635
3992
  loader: loaderInstance,
3636
3993
  server,
3637
3994
  cache,
3995
+ scheduler: schedulerInstance,
3638
3996
  abort: navigation?.abort
3639
3997
  };
3640
3998
  }
@@ -3821,15 +4179,17 @@ const __appModule = (() => {
3821
4179
  const { Loader } = __loaderModule;
3822
4180
  const { createPartialRegistry } = __partialsModule;
3823
4181
  const { createRouteRegistry, createRouter } = __routerModule;
3824
- const { createServerRegistry } = __serverModule;
4182
+ const { createScheduler } = __schedulerModule;
4183
+ const { createServerNamespace } = __serverModule;
3825
4184
  const { createSignal, createSignalRegistry } = __signalsModule;
3826
4185
  const { createRegistryStore } = __registryStoreModule;
3827
4186
  const { attributeName, normalizeAttributeConfig } = __attributesModule;
3828
4187
  const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
3829
4188
 
3830
- function defineApp(initial) {
4189
+ function defineApp(initial, options = {}) {
3831
4190
  const registry = createRegistryStore(undefined, { target: "browser" });
3832
4191
  const runtimes = new Set();
4192
+ const createRuntime = options.createRuntime ?? createApp;
3833
4193
 
3834
4194
  const app = {
3835
4195
  registry,
@@ -3848,7 +4208,7 @@ const __appModule = (() => {
3848
4208
  },
3849
4209
 
3850
4210
  start(options = {}) {
3851
- const runtime = createApp(app, options).start();
4211
+ const runtime = createRuntime(app, options).start();
3852
4212
  app.runtime = runtime;
3853
4213
  return runtime;
3854
4214
  },
@@ -3873,13 +4233,18 @@ const __appModule = (() => {
3873
4233
  function createApp(appOrDefinition = Async, options = {}) {
3874
4234
  const app = isAppHub(appOrDefinition) ? appOrDefinition : defineApp(appOrDefinition ?? {});
3875
4235
  const target = options.target ?? "browser";
4236
+ const scheduler = options.scheduler ?? options.loader?.scheduler ?? createScheduler({
4237
+ strategy: target === "server" ? "manual" : "microtask"
4238
+ });
4239
+ const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
3876
4240
  const attributes = normalizeAttributeConfig(options.attributes);
3877
4241
  const registry = options.registry ?? app.registry.view({ target });
3878
4242
  const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
3879
4243
  const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
3880
4244
  const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
3881
4245
  const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
3882
- const server = options.server ?? createServerRegistry(undefined, { registry, type: "server" });
4246
+ const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
4247
+ const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
3883
4248
  const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
3884
4249
  const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
3885
4250
  const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
@@ -3907,6 +4272,7 @@ const __appModule = (() => {
3907
4272
  },
3908
4273
  loader,
3909
4274
  router,
4275
+ scheduler,
3910
4276
  attributes,
3911
4277
 
3912
4278
  start() {
@@ -3923,12 +4289,13 @@ const __appModule = (() => {
3923
4289
  handlers,
3924
4290
  server,
3925
4291
  cache: browserCache,
4292
+ scheduler,
3926
4293
  attributes
3927
4294
  });
3928
4295
  runtime.loader = loader;
3929
4296
 
3930
4297
  configureServerContext({ cache: browserCache });
3931
- signals._setContext?.({ server, loader, cache: browserCache });
4298
+ signals._setContext?.({ server, loader, cache: browserCache, scheduler });
3932
4299
 
3933
4300
  loader.start();
3934
4301
 
@@ -3944,6 +4311,7 @@ const __appModule = (() => {
3944
4311
  server,
3945
4312
  cache: browserCache,
3946
4313
  partials,
4314
+ scheduler,
3947
4315
  fetch: options.fetch,
3948
4316
  routeEndpoint: options.routeEndpoint,
3949
4317
  attributes
@@ -3955,7 +4323,7 @@ const __appModule = (() => {
3955
4323
  }
3956
4324
  } else {
3957
4325
  configureServerContext({ cache: serverCache });
3958
- signals._setContext?.({ server, cache: serverCache });
4326
+ signals._setContext?.({ server, cache: serverCache, scheduler });
3959
4327
  }
3960
4328
 
3961
4329
  return runtime;
@@ -3969,9 +4337,10 @@ const __appModule = (() => {
3969
4337
  async render(url) {
3970
4338
  assertActive();
3971
4339
  configureServerContext({ cache: serverCache });
3972
- signals._setContext?.({ server, cache: serverCache });
4340
+ signals._setContext?.({ server, cache: serverCache, scheduler });
3973
4341
  const matched = routes.match(url);
3974
4342
  if (!matched) {
4343
+ await scheduler.flush();
3975
4344
  return {
3976
4345
  html: renderDocument("", { status: 404, signals, browserCache, boundary: options.boundary ?? "route", attributes }),
3977
4346
  status: 404,
@@ -3991,8 +4360,8 @@ const __appModule = (() => {
3991
4360
  cache: serverCache,
3992
4361
  browserCache,
3993
4362
  partials,
3994
- request: options.request,
3995
- locals: options.locals
4363
+ scheduler,
4364
+ ...currentRequestContext()
3996
4365
  })
3997
4366
  : { html: "" };
3998
4367
 
@@ -4005,6 +4374,8 @@ const __appModule = (() => {
4005
4374
  browserCache.restore(result.cache.browser);
4006
4375
  }
4007
4376
 
4377
+ await scheduler.flush();
4378
+
4008
4379
  const status = result.status ?? 200;
4009
4380
  return {
4010
4381
  html: renderDocument(result.html, { status, signals, browserCache, boundary: result.boundary ?? options.boundary ?? "route", attributes }),
@@ -4023,6 +4394,9 @@ const __appModule = (() => {
4023
4394
  router?.destroy?.();
4024
4395
  loader?.destroy?.();
4025
4396
  signals.destroy?.();
4397
+ if (ownsScheduler) {
4398
+ scheduler.destroy();
4399
+ }
4026
4400
  },
4027
4401
 
4028
4402
  _applyUse(normalized) {
@@ -4043,11 +4417,23 @@ const __appModule = (() => {
4043
4417
  loader,
4044
4418
  router,
4045
4419
  cache,
4046
- request: options.request,
4047
- locals: options.locals
4420
+ scheduler,
4421
+ requestContext: options.requestContext,
4422
+ ...currentRequestContext()
4048
4423
  });
4049
4424
  }
4050
4425
 
4426
+ function currentRequestContext() {
4427
+ const context = readRequestContextLike(options.requestContext);
4428
+ return {
4429
+ requestContext: context,
4430
+ request: context.request ?? options.request,
4431
+ headers: context.headers ?? options.headers,
4432
+ cookies: context.cookies ?? options.cookies,
4433
+ locals: context.locals ?? options.locals
4434
+ };
4435
+ }
4436
+
4051
4437
  function assertActive() {
4052
4438
  if (destroyed) {
4053
4439
  throw new Error("Async app runtime has been destroyed.");
@@ -4201,6 +4587,77 @@ const __appModule = (() => {
4201
4587
  }
4202
4588
  }
4203
4589
 
4590
+ function createServerReferenceRegistry(initialMap = {}, options = {}) {
4591
+ const registry = options.registry ?? createRegistryStore();
4592
+ const type = options.type ?? "server";
4593
+ const defaults = {};
4594
+
4595
+ const reference = {
4596
+ registry,
4597
+
4598
+ register(id, value) {
4599
+ registry.register(type, id, value);
4600
+ return id;
4601
+ },
4602
+
4603
+ registerMany(map) {
4604
+ for (const [id, value] of Object.entries(map ?? {})) {
4605
+ reference.register(id, value);
4606
+ }
4607
+ return reference;
4608
+ },
4609
+
4610
+ unregister(id) {
4611
+ return registry.unregister(type, id);
4612
+ },
4613
+
4614
+ resolve() {
4615
+ return undefined;
4616
+ },
4617
+
4618
+ async run(id) {
4619
+ throw new Error(`Server command "${id}" cannot run without a server proxy or server registry.`);
4620
+ },
4621
+
4622
+ keys() {
4623
+ return registry.keys(type);
4624
+ },
4625
+
4626
+ entries() {
4627
+ return registry.entries(type);
4628
+ },
4629
+
4630
+ inspect() {
4631
+ return registry.entries(type);
4632
+ },
4633
+
4634
+ _setContext(context = {}) {
4635
+ Object.assign(defaults, context);
4636
+ return reference;
4637
+ },
4638
+
4639
+ _adoptMany() {
4640
+ return reference;
4641
+ }
4642
+ };
4643
+
4644
+ reference.registerMany(initialMap);
4645
+ return createServerNamespace((id, args, context) => reference.run(id, args, context), reference, () => defaults);
4646
+ }
4647
+
4648
+ function readRequestContextLike(store) {
4649
+ if (!store) {
4650
+ return {};
4651
+ }
4652
+ if (typeof store.get === "function") {
4653
+ return store.get() ?? {};
4654
+ }
4655
+ if (typeof store.getStore === "function") {
4656
+ return store.getStore() ?? {};
4657
+ }
4658
+ return {};
4659
+ }
4660
+
4204
4661
  function normalizeEntries(type, entries = {}) {
4205
4662
  if (type !== "signal") {
4206
4663
  return { ...(entries ?? {}) };
@@ -4309,12 +4766,15 @@ const { createRouteRegistry: createRouteRegistry } = __routerModule;
4309
4766
  const { createRouter: createRouter } = __routerModule;
4310
4767
  const { defineRoute: defineRoute } = __routerModule;
4311
4768
  const { route: route } = __routerModule;
4769
+ const { createScheduler: createScheduler } = __schedulerModule;
4770
+ const { applyServerResult: applyServerResult } = __serverModule;
4312
4771
  const { createServerProxy: createServerProxy } = __serverModule;
4313
- const { createServerRegistry: createServerRegistry } = __serverModule;
4772
+ const { resolveServerCommandArguments: resolveServerCommandArguments } = __serverModule;
4773
+ const { unwrapServerResult: unwrapServerResult } = __serverModule;
4314
4774
  const { computed: computed } = __signalsModule;
4315
4775
  const { createSignal: createSignal } = __signalsModule;
4316
4776
  const { createSignalRegistry: createSignalRegistry } = __signalsModule;
4317
4777
  const { effect: effect } = __signalsModule;
4318
4778
  const { signal: signal } = __signalsModule;
4319
4779
 
4320
- 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 };
4780
+ export { 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 };