@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.0 - 2026-06-17
6
+
7
+ - Split browser and server entrypoints with `@async/framework/browser`,
8
+ `@async/framework/server`, browser-only CDN bundles, and server-only local
9
+ function registry exports.
10
+ - Added `createRequestContextStore(...)` for Node request-scoped server
11
+ function context using `AsyncLocalStorage`.
12
+ - Added the Layer 1.5 scheduler for deterministic signal binding, lifecycle,
13
+ effect, async, and post-flush phases.
14
+ - Added browser microtask scheduling by default and manual scheduler flushing for
15
+ server render paths.
16
+ - Threaded `this.scheduler` through loader, handler, component, async signal,
17
+ server, router, and partial contexts.
18
+
19
+ ## 0.7.0 - 2026-06-17
20
+
21
+ - Added router navigation abort/version guards so stale route partials cannot
22
+ clobber newer navigations, and route partial contexts receive `this.abort`.
23
+ - Added ranked route matching so static and dynamic routes win over wildcard
24
+ fallbacks regardless of registration order.
25
+ - Added `readSnapshot(...)` and automatic browser activation from SSR snapshot
26
+ scripts.
27
+ - Fixed repeated server result application when proxy/server envelopes pass
28
+ through namespace or handler callers.
29
+ - Added in-flight `cache.getOrSet(...)` deduplication and clear proxy errors for
30
+ `File`, `Blob`, and `FormData` values that the JSON transport cannot send.
31
+ - Changed `framework.ts` from a source facade to a bundled TypeScript source
32
+ entrypoint.
33
+
3
34
  ## 0.6.0 - 2026-06-17
4
35
 
5
36
  - Added `Loader` as the canonical public loader factory, including
package/README.md CHANGED
@@ -53,6 +53,8 @@ primitives. It keeps the runtime small and explicit:
53
53
  - Handlers live in a registry and run through delegated DOM events.
54
54
  - Async signals use native `AbortSignal` cancellation and suppress stale async
55
55
  completions.
56
+ - A small scheduler batches signal-driven DOM bindings, lifecycle callbacks,
57
+ effects, and async refreshes without adding a render loop.
56
58
  - Browser and server cache declarations are structurally split.
57
59
  - Boundaries can be swapped out of order and rescanned, which keeps server
58
60
  streaming and partial HTML replacement simple.
@@ -68,7 +70,7 @@ next level on every app.
68
70
 
69
71
  | Layer | Name | Requirement | Purpose |
70
72
  | --- | --- | --- | --- |
71
- | 1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |
73
+ | 1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, scheduler, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |
72
74
  | 2 | App/server layer | Light server integration. No app compiler required. | `Async.use(...)`, router modes, server function proxy, partial registry, SSR output, browser activation, and split browser/server cache. |
73
75
  | 3 | Authoring build | Build step required. | JSX, ESM, and TypeScript authoring that lowers into Layer 1 HTML attributes and Layer 2 registries. |
74
76
  | 4 | Chunk and resumability metadata | Build metadata required. | Lazy module manifests, visibility/prefetch hints, resource graphs, and resumability records that the bootloader can consume. |
@@ -90,18 +92,18 @@ and package lifecycle tooling. Browser consumers import ESM directly.
90
92
 
91
93
  ## CDN
92
94
 
93
- The package ships root CDN artifacts for UNPKG and can be loaded without a
95
+ The package ships browser CDN artifacts for UNPKG and can be loaded without a
94
96
  build step. Use `@latest` for quick prototypes, and pin an exact version in
95
97
  production:
96
98
 
97
99
  | File | Format | Use |
98
100
  | --- | --- | --- |
99
- | `framework.js` | ESM | Readable browser module bundle |
100
- | `framework.min.js` | ESM | Compact browser module bundle |
101
- | `framework.umd.js` | UMD | Readable script-tag/CommonJS-style bundle |
102
- | `framework.umd.min.js` | UMD | Compact script-tag/CommonJS-style bundle and default CDN file |
103
- | `framework.ts` | TypeScript source facade | TS-aware runtimes and higher-layer tooling |
104
- | `framework.d.ts` | Type declarations | TypeScript declarations for the public API |
101
+ | `browser.js` | ESM | Readable browser module bundle |
102
+ | `browser.min.js` | ESM | Compact browser module bundle |
103
+ | `browser.umd.js` | UMD | Readable script-tag/CommonJS-style bundle |
104
+ | `browser.umd.min.js` | UMD | Compact script-tag/CommonJS-style bundle and default CDN file |
105
+ | `browser.ts` | Bundled TypeScript source | TS-aware runtimes and higher-layer tooling |
106
+ | `browser.d.ts` | Type declarations | TypeScript declarations for the browser API |
105
107
 
106
108
  ```html
107
109
  <main async:container>
@@ -113,7 +115,7 @@ production:
113
115
  import {
114
116
  Async,
115
117
  createSignal
116
- } from "https://unpkg.com/@async/framework@latest/framework.js";
118
+ } from "https://unpkg.com/@async/framework@latest/browser.js";
117
119
 
118
120
  Async.use({
119
121
  signal: {
@@ -136,7 +138,7 @@ For a plain script tag, use the UMD bundle. In this UMD-only global form,
136
138
  call `Async.Loader(...)` directly.
137
139
 
138
140
  ```html
139
- <script src="https://unpkg.com/@async/framework@latest/framework.umd.min.js"></script>
141
+ <script src="https://unpkg.com/@async/framework@latest/browser.umd.min.js"></script>
140
142
  <script>
141
143
  Async.use({
142
144
  signal: {
@@ -159,7 +161,7 @@ You can also use an import map so app code imports `@async/framework` by name:
159
161
  <script type="importmap">
160
162
  {
161
163
  "imports": {
162
- "@async/framework": "https://unpkg.com/@async/framework@latest/framework.js"
164
+ "@async/framework": "https://unpkg.com/@async/framework@latest/browser.js"
163
165
  }
164
166
  }
165
167
  </script>
@@ -187,6 +189,10 @@ You can also use an import map so app code imports `@async/framework` by name:
187
189
 
188
190
  ## Core API
189
191
 
192
+ For npm consumers, `@async/framework` uses conditional exports: browser-aware
193
+ tooling receives the browser entry, while Node receives the server-capable
194
+ entry. Use explicit subpaths when the target matters.
195
+
190
196
  ```js
191
197
  import {
192
198
  Async,
@@ -204,8 +210,8 @@ import {
204
210
  createRegistryStore,
205
211
  createRouteRegistry,
206
212
  createRouter,
213
+ createScheduler,
207
214
  createServerProxy,
208
- createServerRegistry,
209
215
  createSignalRegistry,
210
216
  defineAttributeConfig,
211
217
  defineApp,
@@ -215,9 +221,19 @@ import {
215
221
  delay,
216
222
  effect,
217
223
  html,
224
+ readSnapshot,
218
225
  route,
219
226
  signal
220
- } from "@async/framework";
227
+ } from "@async/framework/browser";
228
+ ```
229
+
230
+ Server-only APIs live behind the server entry:
231
+
232
+ ```js
233
+ import {
234
+ createRequestContextStore,
235
+ createServerRegistry
236
+ } from "@async/framework/server";
221
237
  ```
222
238
 
223
239
  `Loader` is the canonical loader factory. `AsyncLoader` remains as a
@@ -372,6 +388,49 @@ signals.set("product.title", "Headphones");
372
388
 
373
389
  `signal(...)` remains a compatibility alias for `createSignal(...)`.
374
390
 
391
+ ### Scheduler
392
+
393
+ The scheduler is the Layer 1.5 ordering engine. Signal writes are still
394
+ synchronous:
395
+
396
+ ```js
397
+ signals.set("count", 3);
398
+ signals.get("count");
399
+ // 3
400
+ ```
401
+
402
+ DOM bindings, component lifecycle callbacks, component effects, and async signal
403
+ refreshes are scheduled through deterministic phases:
404
+
405
+ ```txt
406
+ binding -> lifecycle -> effect -> async -> post
407
+ ```
408
+
409
+ Browser runtimes use a microtask scheduler by default. Server runtimes use a
410
+ manual scheduler and drain it during `runtime.render(...)`.
411
+
412
+ ```js
413
+ import {
414
+ createScheduler
415
+ } from "@async/framework";
416
+
417
+ const scheduler = createScheduler({
418
+ strategy: "manual"
419
+ });
420
+
421
+ const runtime = Async.start({
422
+ root: document,
423
+ scheduler
424
+ });
425
+
426
+ signals.set("count", 1);
427
+ await scheduler.flush();
428
+ ```
429
+
430
+ Most apps do not need to call the scheduler directly. It is exposed for tests,
431
+ custom runtimes, streaming receivers, and higher layers that need explicit flush
432
+ boundaries.
433
+
375
434
  ### Async Signals
376
435
 
377
436
  Async signals add loading state, error state, versions, refresh, and cancel to a
@@ -400,6 +459,7 @@ The async function context includes:
400
459
  | `this.id` | Current async signal id |
401
460
  | `this.version` | Run version |
402
461
  | `this.abort` | Native `AbortSignal` with non-enumerable `cancel(reason?)` |
462
+ | `this.scheduler` | Current runtime scheduler |
403
463
  | `this.refresh()` | Start a new run |
404
464
 
405
465
  `this.abort` can be passed directly to `fetch` or to `delay`:
@@ -411,6 +471,10 @@ await delay(250, this.abort);
411
471
  If a dependency read through `this.signals.get(...)` changes, the async signal
412
472
  reruns and the previous run is aborted.
413
473
 
474
+ Dependency reads are captured while the async signal function starts running.
475
+ Read signal dependencies before the first `await`; reads that happen later are
476
+ ordinary reads and do not create refresh subscriptions.
477
+
414
478
  ## HTML Protocol
415
479
 
416
480
  Loader scans regular HTML attributes:
@@ -614,6 +678,10 @@ Server registries run locally on the server and proxies call an HTTP endpoint
614
678
  from the browser. Both expose the same dotted call shape.
615
679
 
616
680
  ```js
681
+ import {
682
+ createServerRegistry
683
+ } from "@async/framework/server";
684
+
617
685
  const server = createServerRegistry({
618
686
  "cart.add"(productId, quantity) {
619
687
  return {
@@ -629,6 +697,10 @@ const server = createServerRegistry({
629
697
  Client proxy:
630
698
 
631
699
  ```js
700
+ import {
701
+ createServerProxy
702
+ } from "@async/framework/browser";
703
+
632
704
  const server = createServerProxy({
633
705
  endpoint: "/__async/server",
634
706
  signals,
@@ -819,11 +891,17 @@ hydrate, diff, patch, or rerender:
819
891
  ```js
820
892
  createApp(browserApp, {
821
893
  root: document,
822
- snapshot,
823
894
  server: createServerProxy({ endpoint: "/__async/server" })
824
895
  }).start();
825
896
  ```
826
897
 
898
+ If an `async:snapshot` script is present under the root or document,
899
+ `createApp(...)` reads it automatically. You can also inspect it directly:
900
+
901
+ ```js
902
+ const snapshot = readSnapshot(document);
903
+ ```
904
+
827
905
  ## Components
828
906
 
829
907
  Components are scoped fragment functions. They return strings or `html`
@@ -1011,7 +1089,7 @@ pnpm run release:check
1011
1089
  such as signals, handlers, server functions, partials, routes, and components.
1012
1090
  It writes `.async/registry-manifest.json` plus a per-file cache at
1013
1091
  `.async/registry-lint-cache.json`, skips generated root bundles such as
1014
- `framework.umd.min.js`, and fails only when the same registry type and id are
1092
+ `browser.umd.min.js`, and fails only when the same registry type and id are
1015
1093
  declared with different normalized content. Duplicate declarations with the
1016
1094
  same content are reported as dedupe candidates, not errors.
1017
1095
 
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/build-framework-bundle.js. Do not edit by hand.
2
- // Public type declarations for @async/framework.
2
+ // Browser type declarations for @async/framework/browser.
3
3
 
4
4
  export type RuntimeTarget = "browser" | "server";
5
5
  export type RouterMode = "csr" | "spa" | "ssr" | "ssr-spa" | "mpa";
@@ -40,6 +40,51 @@ export interface TemplateResult {
40
40
  readonly values: readonly unknown[];
41
41
  }
42
42
 
43
+ export type SchedulerStrategy = "microtask" | "manual";
44
+ export type SchedulerPhase = "binding" | "lifecycle" | "effect" | "async" | "post" | string;
45
+ export interface SchedulerJob {
46
+ id: number;
47
+ phase: SchedulerPhase;
48
+ scope?: unknown;
49
+ boundary?: string;
50
+ key?: string;
51
+ canceled: boolean;
52
+ cancel(): void;
53
+ }
54
+ export interface SchedulerOptions {
55
+ strategy?: SchedulerStrategy;
56
+ phases?: SchedulerPhase[];
57
+ maxDepth?: number;
58
+ onError?(error: unknown, job: SchedulerJob): void;
59
+ }
60
+ export interface SchedulerInspection {
61
+ strategy: SchedulerStrategy;
62
+ phases: SchedulerPhase[];
63
+ pending: Record<string, number>;
64
+ scopesDestroyed: number;
65
+ flushing: boolean;
66
+ scheduled: boolean;
67
+ }
68
+ export interface Scheduler {
69
+ strategy: SchedulerStrategy;
70
+ phases: SchedulerPhase[];
71
+ batch<T>(fn: () => T): T;
72
+ enqueue(phase: SchedulerPhase, job: () => MaybePromise<unknown>, options?: { scope?: unknown; boundary?: string; key?: string }): Cleanup;
73
+ flush(): Promise<void>;
74
+ flushScope(scope: unknown): Promise<void>;
75
+ afterFlush(job: () => MaybePromise<unknown>, options?: { scope?: unknown; boundary?: string; key?: string }): Cleanup;
76
+ cancelScope(scope: unknown): this;
77
+ markScopeDestroyed(scope: unknown): this;
78
+ destroy(): void;
79
+ inspect(): SchedulerInspection;
80
+ }
81
+
82
+ export interface RequestContextStore {
83
+ run<T>(context: Record<string, unknown> | undefined, fn: () => T): T;
84
+ get(): Record<string, unknown> | undefined;
85
+ snapshot(): Record<string, unknown>;
86
+ }
87
+
43
88
  export interface Signal<T = unknown> {
44
89
  readonly kind: "signal";
45
90
  value: T;
@@ -113,6 +158,7 @@ export interface AsyncSignalContext {
113
158
  router?: Router;
114
159
  loader?: LoaderInstance;
115
160
  cache?: CacheRegistry;
161
+ scheduler?: Scheduler;
116
162
  refresh(): Promise<unknown>;
117
163
  }
118
164
 
@@ -149,6 +195,7 @@ export interface HandlerContext {
149
195
  loader?: LoaderInstance;
150
196
  router?: Router;
151
197
  cache?: CacheRegistry;
198
+ scheduler?: Scheduler;
152
199
  event?: Event;
153
200
  element?: Element;
154
201
  el?: Element;
@@ -192,6 +239,7 @@ export interface ServerContext {
192
239
  locals?: unknown;
193
240
  abort?: AbortSignal;
194
241
  cache?: CacheRegistry;
242
+ scheduler?: Scheduler;
195
243
  server: ServerNamespace;
196
244
  [key: string]: unknown;
197
245
  }
@@ -216,6 +264,7 @@ export interface ServerProxyOptions {
216
264
  loader?: LoaderInstance;
217
265
  router?: Router;
218
266
  cache?: CacheRegistry;
267
+ scheduler?: Scheduler;
219
268
  headers?: Record<string, string>;
220
269
  }
221
270
 
@@ -262,6 +311,8 @@ export interface PartialContext {
262
311
  cache?: CacheRegistry;
263
312
  browserCache?: CacheRegistry;
264
313
  partials: PartialRegistry;
314
+ abort?: AbortSignal;
315
+ scheduler?: Scheduler;
265
316
  request?: Request;
266
317
  locals?: unknown;
267
318
  [key: string]: unknown;
@@ -314,6 +365,7 @@ export interface RouterOptions {
314
365
  fetch?: typeof fetch;
315
366
  routeEndpoint?: string;
316
367
  attributes?: AttributeConfig;
368
+ scheduler?: Scheduler;
317
369
  }
318
370
 
319
371
  export interface Router {
@@ -327,6 +379,7 @@ export interface Router {
327
379
  server?: ServerNamespace;
328
380
  cache?: CacheRegistry;
329
381
  partials?: PartialRegistry;
382
+ scheduler: Scheduler;
330
383
  attributes: NormalizedAttributeConfig;
331
384
  start(): this;
332
385
  match(url: string | URL): RouteMatch | null;
@@ -345,6 +398,7 @@ export interface ComponentContext {
345
398
  server?: ServerNamespace;
346
399
  router?: Router;
347
400
  cache?: CacheRegistry;
401
+ scheduler?: Scheduler;
348
402
  signal<T = unknown>(initial: T): SignalRef<T>;
349
403
  signal<T = unknown>(name: string, initial: T): SignalRef<T>;
350
404
  computed<T = unknown>(name: string, fn: (this: ComponentContext) => T): SignalRef<T>;
@@ -381,6 +435,7 @@ export interface LoaderOptions {
381
435
  server?: ServerNamespace;
382
436
  router?: Router;
383
437
  cache?: CacheRegistry;
438
+ scheduler?: Scheduler;
384
439
  attributes?: AttributeConfig;
385
440
  }
386
441
 
@@ -391,6 +446,7 @@ export interface LoaderInstance {
391
446
  server?: ServerNamespace;
392
447
  router?: Router;
393
448
  cache?: CacheRegistry;
449
+ scheduler: Scheduler;
394
450
  attributes: NormalizedAttributeConfig;
395
451
  start(): this;
396
452
  scan(rootOrFragment?: Document | Element | DocumentFragment): this;
@@ -472,6 +528,8 @@ export interface CreateAppOptions extends LoaderOptions {
472
528
  routeEndpoint?: string;
473
529
  request?: Request;
474
530
  locals?: unknown;
531
+ requestContext?: RequestContextStore;
532
+ scheduler?: Scheduler;
475
533
  }
476
534
 
477
535
  export interface RenderResult {
@@ -494,6 +552,7 @@ export interface AppRuntime {
494
552
  browser: { cache: CacheRegistry };
495
553
  loader?: LoaderInstance;
496
554
  router?: Router;
555
+ scheduler: Scheduler;
497
556
  attributes: NormalizedAttributeConfig;
498
557
  start(): this;
499
558
  use(type: Parameters<AppHub["use"]>[0], entries?: unknown): this;
@@ -506,6 +565,7 @@ export interface AsyncNamespace extends AppHub {
506
565
  asyncSignal: typeof asyncSignal;
507
566
  createApp: typeof createApp;
508
567
  defineApp: typeof defineApp;
568
+ readSnapshot: typeof readSnapshot;
509
569
  attributeName: typeof attributeName;
510
570
  defineAttributeConfig: typeof defineAttributeConfig;
511
571
  createCacheRegistry: typeof createCacheRegistry;
@@ -522,10 +582,13 @@ export interface AsyncNamespace extends AppHub {
522
582
  createRegistryStore: typeof createRegistryStore;
523
583
  createRouteRegistry: typeof createRouteRegistry;
524
584
  createRouter: typeof createRouter;
585
+ createScheduler: typeof createScheduler;
525
586
  defineRoute: typeof defineRoute;
526
587
  route: typeof route;
588
+ applyServerResult: typeof applyServerResult;
527
589
  createServerProxy: typeof createServerProxy;
528
- createServerRegistry: typeof createServerRegistry;
590
+ resolveServerCommandArguments: typeof resolveServerCommandArguments;
591
+ unwrapServerResult: typeof unwrapServerResult;
529
592
  computed: typeof computed;
530
593
  createSignal: typeof createSignal;
531
594
  createSignalRegistry: typeof createSignalRegistry;
@@ -537,6 +600,7 @@ export declare function asyncSignal<T = unknown>(id: string, fn: AsyncSignalFunc
537
600
  export declare const Async: AppHub;
538
601
  export declare function createApp(appOrDefinition?: AppHub | AppDefinition, options?: CreateAppOptions): AppRuntime;
539
602
  export declare function defineApp(initial?: AppDefinition): AppHub;
603
+ export declare function readSnapshot(root?: Document | Element, options?: { attributes?: AttributeConfig }): { signals?: Record<string, unknown>; cache?: { browser?: Record<string, unknown> } };
540
604
  export declare function attributeName(attributes: AttributeConfig | undefined, type: keyof NormalizedAttributeConfig, name: string): string;
541
605
  export declare function defineAttributeConfig(config?: AttributeConfig): NormalizedAttributeConfig;
542
606
  export declare function createCacheRegistry(initialMap?: Record<string, CacheDefinition | CacheDefinitionOptions>, options?: { now?: () => number; registry?: RegistryStore; type?: "cache.browser" | "cache.server" }): CacheRegistry;
@@ -553,11 +617,14 @@ export declare function createPartialRegistry(initialMap?: Record<string, Partia
553
617
  export declare function createRegistryStore(initial?: AppDefinition, options?: { target?: RuntimeTarget; backing?: unknown }): RegistryStore;
554
618
  export declare function createRouteRegistry(initialMap?: Record<string, RouteDefinition | string>, options?: { registry?: RegistryStore; type?: "route" }): RouteRegistry;
555
619
  export declare function createRouter(options?: RouterOptions): Router;
620
+ export declare function createScheduler(options?: SchedulerOptions): Scheduler;
556
621
  export declare function defineRoute(partial: string, options?: Omit<RouteDefinition, "partial">): RouteDefinition;
557
622
  export declare const route: typeof defineRoute;
623
+ export declare function applyServerResult(result: unknown, context?: Record<string, unknown>): Promise<unknown>;
558
624
  export declare function createServerProxy(options?: ServerProxyOptions): ServerNamespace;
559
- export declare function createServerRegistry(initialMap?: Record<string, ServerFunction>, options?: { registry?: RegistryStore; type?: "server" }): ServerNamespace;
560
- export declare function computed<T = unknown>(fn: (this: { signals: SignalRegistry; id: string; server?: ServerNamespace; router?: Router; loader?: LoaderInstance; cache?: CacheRegistry }) => T): ComputedSignal<T>;
625
+ export declare function resolveServerCommandArguments(args: Array<{ type: "local"; name: string } | { type: "signal"; path: string }>, context?: Record<string, unknown>): { args: unknown[]; signalValues: Record<string, unknown>; signalPaths: string[] };
626
+ export declare function unwrapServerResult<T = unknown>(result: ServerResult<T>): T | ServerResult<T>;
627
+ export declare function computed<T = unknown>(fn: (this: { signals: SignalRegistry; id: string; server?: ServerNamespace; router?: Router; loader?: LoaderInstance; cache?: CacheRegistry; scheduler?: Scheduler }) => T): ComputedSignal<T>;
561
628
  export declare function createSignal<T = unknown>(initial: T): Signal<T>;
562
629
  export declare function createSignalRegistry(initialMap?: SignalMap, options?: { registry?: RegistryStore; type?: "signal" }): SignalRegistry;
563
630
  export declare function effect(fn: () => unknown): EffectDefinition;