@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
3
19
  ## 0.7.0 - 2026-06-17
4
20
 
5
21
  - Added router navigation abort/version guards so stale route partials cannot
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` | Bundled TypeScript source | 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,
@@ -218,7 +224,16 @@ import {
218
224
  readSnapshot,
219
225
  route,
220
226
  signal
221
- } 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";
222
237
  ```
223
238
 
224
239
  `Loader` is the canonical loader factory. `AsyncLoader` remains as a
@@ -373,6 +388,49 @@ signals.set("product.title", "Headphones");
373
388
 
374
389
  `signal(...)` remains a compatibility alias for `createSignal(...)`.
375
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
+
376
434
  ### Async Signals
377
435
 
378
436
  Async signals add loading state, error state, versions, refresh, and cancel to a
@@ -401,6 +459,7 @@ The async function context includes:
401
459
  | `this.id` | Current async signal id |
402
460
  | `this.version` | Run version |
403
461
  | `this.abort` | Native `AbortSignal` with non-enumerable `cancel(reason?)` |
462
+ | `this.scheduler` | Current runtime scheduler |
404
463
  | `this.refresh()` | Start a new run |
405
464
 
406
465
  `this.abort` can be passed directly to `fetch` or to `delay`:
@@ -619,6 +678,10 @@ Server registries run locally on the server and proxies call an HTTP endpoint
619
678
  from the browser. Both expose the same dotted call shape.
620
679
 
621
680
  ```js
681
+ import {
682
+ createServerRegistry
683
+ } from "@async/framework/server";
684
+
622
685
  const server = createServerRegistry({
623
686
  "cart.add"(productId, quantity) {
624
687
  return {
@@ -634,6 +697,10 @@ const server = createServerRegistry({
634
697
  Client proxy:
635
698
 
636
699
  ```js
700
+ import {
701
+ createServerProxy
702
+ } from "@async/framework/browser";
703
+
637
704
  const server = createServerProxy({
638
705
  endpoint: "/__async/server",
639
706
  signals,
@@ -1022,7 +1089,7 @@ pnpm run release:check
1022
1089
  such as signals, handlers, server functions, partials, routes, and components.
1023
1090
  It writes `.async/registry-manifest.json` plus a per-file cache at
1024
1091
  `.async/registry-lint-cache.json`, skips generated root bundles such as
1025
- `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
1026
1093
  declared with different normalized content. Duplicate declarations with the
1027
1094
  same content are reported as dedupe candidates, not errors.
1028
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
 
@@ -263,6 +312,7 @@ export interface PartialContext {
263
312
  browserCache?: CacheRegistry;
264
313
  partials: PartialRegistry;
265
314
  abort?: AbortSignal;
315
+ scheduler?: Scheduler;
266
316
  request?: Request;
267
317
  locals?: unknown;
268
318
  [key: string]: unknown;
@@ -315,6 +365,7 @@ export interface RouterOptions {
315
365
  fetch?: typeof fetch;
316
366
  routeEndpoint?: string;
317
367
  attributes?: AttributeConfig;
368
+ scheduler?: Scheduler;
318
369
  }
319
370
 
320
371
  export interface Router {
@@ -328,6 +379,7 @@ export interface Router {
328
379
  server?: ServerNamespace;
329
380
  cache?: CacheRegistry;
330
381
  partials?: PartialRegistry;
382
+ scheduler: Scheduler;
331
383
  attributes: NormalizedAttributeConfig;
332
384
  start(): this;
333
385
  match(url: string | URL): RouteMatch | null;
@@ -346,6 +398,7 @@ export interface ComponentContext {
346
398
  server?: ServerNamespace;
347
399
  router?: Router;
348
400
  cache?: CacheRegistry;
401
+ scheduler?: Scheduler;
349
402
  signal<T = unknown>(initial: T): SignalRef<T>;
350
403
  signal<T = unknown>(name: string, initial: T): SignalRef<T>;
351
404
  computed<T = unknown>(name: string, fn: (this: ComponentContext) => T): SignalRef<T>;
@@ -382,6 +435,7 @@ export interface LoaderOptions {
382
435
  server?: ServerNamespace;
383
436
  router?: Router;
384
437
  cache?: CacheRegistry;
438
+ scheduler?: Scheduler;
385
439
  attributes?: AttributeConfig;
386
440
  }
387
441
 
@@ -392,6 +446,7 @@ export interface LoaderInstance {
392
446
  server?: ServerNamespace;
393
447
  router?: Router;
394
448
  cache?: CacheRegistry;
449
+ scheduler: Scheduler;
395
450
  attributes: NormalizedAttributeConfig;
396
451
  start(): this;
397
452
  scan(rootOrFragment?: Document | Element | DocumentFragment): this;
@@ -473,6 +528,8 @@ export interface CreateAppOptions extends LoaderOptions {
473
528
  routeEndpoint?: string;
474
529
  request?: Request;
475
530
  locals?: unknown;
531
+ requestContext?: RequestContextStore;
532
+ scheduler?: Scheduler;
476
533
  }
477
534
 
478
535
  export interface RenderResult {
@@ -495,6 +552,7 @@ export interface AppRuntime {
495
552
  browser: { cache: CacheRegistry };
496
553
  loader?: LoaderInstance;
497
554
  router?: Router;
555
+ scheduler: Scheduler;
498
556
  attributes: NormalizedAttributeConfig;
499
557
  start(): this;
500
558
  use(type: Parameters<AppHub["use"]>[0], entries?: unknown): this;
@@ -524,10 +582,13 @@ export interface AsyncNamespace extends AppHub {
524
582
  createRegistryStore: typeof createRegistryStore;
525
583
  createRouteRegistry: typeof createRouteRegistry;
526
584
  createRouter: typeof createRouter;
585
+ createScheduler: typeof createScheduler;
527
586
  defineRoute: typeof defineRoute;
528
587
  route: typeof route;
588
+ applyServerResult: typeof applyServerResult;
529
589
  createServerProxy: typeof createServerProxy;
530
- createServerRegistry: typeof createServerRegistry;
590
+ resolveServerCommandArguments: typeof resolveServerCommandArguments;
591
+ unwrapServerResult: typeof unwrapServerResult;
531
592
  computed: typeof computed;
532
593
  createSignal: typeof createSignal;
533
594
  createSignalRegistry: typeof createSignalRegistry;
@@ -556,11 +617,14 @@ export declare function createPartialRegistry(initialMap?: Record<string, Partia
556
617
  export declare function createRegistryStore(initial?: AppDefinition, options?: { target?: RuntimeTarget; backing?: unknown }): RegistryStore;
557
618
  export declare function createRouteRegistry(initialMap?: Record<string, RouteDefinition | string>, options?: { registry?: RegistryStore; type?: "route" }): RouteRegistry;
558
619
  export declare function createRouter(options?: RouterOptions): Router;
620
+ export declare function createScheduler(options?: SchedulerOptions): Scheduler;
559
621
  export declare function defineRoute(partial: string, options?: Omit<RouteDefinition, "partial">): RouteDefinition;
560
622
  export declare const route: typeof defineRoute;
623
+ export declare function applyServerResult(result: unknown, context?: Record<string, unknown>): Promise<unknown>;
561
624
  export declare function createServerProxy(options?: ServerProxyOptions): ServerNamespace;
562
- export declare function createServerRegistry(initialMap?: Record<string, ServerFunction>, options?: { registry?: RegistryStore; type?: "server" }): ServerNamespace;
563
- 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>;
564
628
  export declare function createSignal<T = unknown>(initial: T): Signal<T>;
565
629
  export declare function createSignalRegistry(initialMap?: SignalMap, options?: { registry?: RegistryStore; type?: "signal" }): SignalRegistry;
566
630
  export declare function effect(fn: () => unknown): EffectDefinition;