@async/framework 0.8.0 → 0.10.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
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.10.0 - 2026-06-17
6
+
7
+ - Added rootless browser startup plus `attachRoot`, `detachRoot`,
8
+ `inspectRoots`, and streamed `applySnapshot(...)` support for advanced
9
+ build-step bootstrapping.
10
+ - Added compact lazy registry descriptors with `_async` asset resolution,
11
+ inferred exports, and lazy handler, partial, component, and async-signal
12
+ materialization.
13
+ - Added optional `async-container` and `async-suspense` custom elements while
14
+ preserving the existing `async:container`, `async:boundary`, and
15
+ `this.suspense(...)` Layer 1 APIs.
16
+
17
+ ## 0.9.0 - 2026-06-17
18
+
19
+ - Added `createBoundaryReceiver(...)` for optional out-of-order boundary patch
20
+ delivery with per-boundary sequence checks, signal/cache effect ordering,
21
+ scheduler flushing, redirects, and destroyed parent-scope filtering.
22
+
5
23
  ## 0.8.0 - 2026-06-17
6
24
 
7
25
  - Split browser and server entrypoints with `@async/framework/browser`,
package/README.md CHANGED
@@ -187,6 +187,117 @@ You can also use an import map so app code imports `@async/framework` by name:
187
187
  </script>
188
188
  ```
189
189
 
190
+ ## Advanced Build-Step Runtime
191
+
192
+ Layer 1 still works with no build step. A build step can optimize the same
193
+ runtime by emitting SSR HTML plus compact registry descriptors. The browser can
194
+ start in the document head, apply snapshots, and wait for a root to appear:
195
+
196
+ ```html
197
+ <script type="importmap">
198
+ {
199
+ "imports": {
200
+ "@async/framework": "https://unpkg.com/@async/framework@latest/browser.js"
201
+ }
202
+ }
203
+ </script>
204
+
205
+ <script type="application/json" async:snapshot>
206
+ {
207
+ "signal": {
208
+ "productId": "sku-1"
209
+ },
210
+ "handler": {
211
+ "cart.add": { "url": "cart.add.js" }
212
+ },
213
+ "component": {
214
+ "ProductCard": { "url": "ProductCard.js" }
215
+ },
216
+ "asyncSignal": {
217
+ "product.load": { "url": "product.load.js" }
218
+ }
219
+ }
220
+ </script>
221
+
222
+ <script type="module">
223
+ import {
224
+ Async,
225
+ defineAsyncContainerElement,
226
+ defineAsyncSuspenseElement,
227
+ readSnapshot
228
+ } from "@async/framework";
229
+
230
+ Async.start({
231
+ snapshot: readSnapshot(document),
232
+ registryAssets: { baseUrl: "_async" }
233
+ });
234
+
235
+ defineAsyncContainerElement();
236
+ defineAsyncSuspenseElement();
237
+ </script>
238
+ ```
239
+
240
+ `Async.start()` defaults to rootless browser startup. It creates registries,
241
+ applies snapshots, and prepares the scheduler/server proxy context without
242
+ scanning DOM. Attach a root later with `Async.attachRoot(root)` or by using
243
+ `<async-container>`:
244
+
245
+ ```html
246
+ <async-container>
247
+ <button type="button" on:click="cart.add">Add</button>
248
+ </async-container>
249
+ ```
250
+
251
+ Descriptor URLs are relative to a type folder under `registryAssets.baseUrl`.
252
+ The default is:
253
+
254
+ ```js
255
+ {
256
+ baseUrl: "_async",
257
+ paths: {
258
+ component: "component",
259
+ handler: "handler",
260
+ asyncSignal: "asyncSignal",
261
+ partial: "partial",
262
+ route: "route"
263
+ }
264
+ }
265
+ ```
266
+
267
+ So this descriptor:
268
+
269
+ ```json
270
+ { "url": "ProductCard.js#ProductCard" }
271
+ ```
272
+
273
+ resolves as:
274
+
275
+ ```txt
276
+ /_async/component/ProductCard.js#ProductCard
277
+ ```
278
+
279
+ If `#export` is omitted, Async tries the registry id leaf, then the file
280
+ basename, then `default`.
281
+
282
+ For declarative async boundaries, use `<async-suspense>` or keep using
283
+ `this.suspense(...)` inside components:
284
+
285
+ ```html
286
+ <async-suspense for="product.load">
287
+ <template loading>Loading...</template>
288
+ <template ready>
289
+ <h1 signal:text="product.load.title"></h1>
290
+ </template>
291
+ <template error>
292
+ <p signal:text="product.load.$error.message"></p>
293
+ </template>
294
+ </async-suspense>
295
+ ```
296
+
297
+ The build layer can hide `createBoundaryReceiver(...)` setup, but streaming is
298
+ still explicit boundary patches: boundary id, sequence number, HTML, signal
299
+ patches, and browser-cache patches. Async does not ship a component resume graph.
300
+
190
301
  ## Core API
191
302
 
192
303
  For npm consumers, `@async/framework` uses conditional exports: browser-aware
@@ -202,6 +313,7 @@ import {
202
313
  createApp,
203
314
  createCacheRegistry,
204
315
  createComponentRegistry,
316
+ createLazyRegistry,
205
317
  component,
206
318
  computed,
207
319
  createSignal,
@@ -213,10 +325,13 @@ import {
213
325
  createScheduler,
214
326
  createServerProxy,
215
327
  createSignalRegistry,
328
+ defineAsyncContainerElement,
329
+ defineAsyncSuspenseElement,
216
330
  defineAttributeConfig,
217
331
  defineApp,
218
332
  defineCache,
219
333
  defineComponent,
334
+ defineRegistrySnapshot,
220
335
  defineRoute,
221
336
  delay,
222
337
  effect,
@@ -1050,6 +1165,47 @@ loader.swap(
1050
1165
  `swap(boundaryId, fragmentOrTemplate)` replaces the boundary contents and
1051
1166
  rescans the inserted fragment.
1052
1167
 
1168
+ When boundary patches can arrive independently, use `createBoundaryReceiver`.
1169
+ It keeps per-boundary sequence state, applies signal/cache effects before the
1170
+ HTML swap, flushes scheduled bindings, and ignores stale child patches after a
1171
+ parent scope is destroyed.
1172
+
1173
+ ```js
1174
+ import { createBoundaryReceiver } from "@async/framework/browser";
1175
+
1176
+ const receiver = createBoundaryReceiver({
1177
+ loader: runtime.loader,
1178
+ signals: runtime.signals,
1179
+ cache: runtime.browser.cache,
1180
+ scheduler: runtime.scheduler,
1181
+ router: runtime.router
1182
+ });
1183
+
1184
+ await receiver.apply({
1185
+ boundary: "product",
1186
+ seq: 1,
1187
+ signals: {
1188
+ product: { title: "Keyboard" }
1189
+ },
1190
+ cache: {
1191
+ browser: {
1192
+ "product:sku-1": { title: "Keyboard" }
1193
+ }
1194
+ },
1195
+ html: `
1196
+ <article>
1197
+ <h1 signal:text="product.title"></h1>
1198
+ <button type="button" on:click="server.cart.add(productId)">Add</button>
1199
+ </article>
1200
+ `
1201
+ });
1202
+ ```
1203
+
1204
+ Sequence numbers are tracked per boundary: `hero` patch `10` can apply before
1205
+ `reviews` patch `2`, while a later `hero` patch `9` is ignored. The receiver
1206
+ does not add transport management, a transaction log, hydration, or component
1207
+ rerendering.
1208
+
1053
1209
  ## Examples
1054
1210
 
1055
1211
  | Example | Shows |
package/browser.d.ts CHANGED
@@ -13,6 +13,7 @@ export type RegistryType =
13
13
  | "partial"
14
14
  | "route"
15
15
  | "component"
16
+ | "asyncSignal"
16
17
  | "cache.browser"
17
18
  | "cache.server"
18
19
  | "cache.browser.entries"
@@ -34,6 +35,20 @@ export interface NormalizedAttributeConfig {
34
35
 
35
36
  export type TemplatePrimitive = string | number | boolean | null | undefined;
36
37
  export type TemplateLike = TemplateResult | TemplatePrimitive | Node | TemplateLike[];
38
+ export interface LazyDescriptor {
39
+ url: string;
40
+ [key: string]: unknown;
41
+ }
42
+ export interface RegistryAssetsConfig {
43
+ baseUrl?: string;
44
+ paths?: Partial<Record<"component" | "handler" | "asyncSignal" | "partial" | "route", string>>;
45
+ }
46
+ export interface LazyRegistry {
47
+ registryAssets: Required<Pick<RegistryAssetsConfig, "baseUrl">> & { paths: Record<string, string> };
48
+ resolveUrl(type: "component" | "handler" | "asyncSignal" | "partial" | "route", id: string, descriptor: LazyDescriptor): { moduleUrl: string; exportNames: string[]; url: string };
49
+ resolve<T = unknown>(type: "component" | "handler" | "asyncSignal" | "partial" | "route", id: string, descriptor: LazyDescriptor): Promise<T>;
50
+ inspect(): { registryAssets: unknown; modules: string[]; exports: string[] };
51
+ }
37
52
 
38
53
  export interface TemplateResult {
39
54
  readonly strings: TemplateStringsArray;
@@ -75,6 +90,7 @@ export interface Scheduler {
75
90
  afterFlush(job: () => MaybePromise<unknown>, options?: { scope?: unknown; boundary?: string; key?: string }): Cleanup;
76
91
  cancelScope(scope: unknown): this;
77
92
  markScopeDestroyed(scope: unknown): this;
93
+ isScopeDestroyed(scope: unknown): boolean;
78
94
  destroy(): void;
79
95
  inspect(): SchedulerInspection;
80
96
  }
@@ -208,8 +224,8 @@ export interface HandlerContext {
208
224
  export type HandlerFunction = (this: HandlerContext, context: HandlerContext) => MaybePromise<unknown>;
209
225
 
210
226
  export interface HandlerRegistry extends RegistryInspection<HandlerFunction> {
211
- register(id: string, fn: HandlerFunction): string;
212
- registerMany(map?: Record<string, HandlerFunction>): this;
227
+ register(id: string, fn: HandlerFunction | LazyDescriptor): string;
228
+ registerMany(map?: Record<string, HandlerFunction | LazyDescriptor>): this;
213
229
  unregister(id: string): boolean;
214
230
  resolve(id: string): HandlerFunction | undefined;
215
231
  run(ref: string, context?: Partial<HandlerContext>): Promise<unknown[]>;
@@ -321,8 +337,8 @@ export interface PartialContext {
321
337
  export type PartialFunction = (this: PartialContext, props: Record<string, unknown>) => MaybePromise<TemplateLike | ServerEnvelope>;
322
338
 
323
339
  export interface PartialRegistry extends RegistryInspection<PartialFunction> {
324
- register(id: string, fn: PartialFunction): string;
325
- registerMany(map?: Record<string, PartialFunction>): this;
340
+ register(id: string, fn: PartialFunction | LazyDescriptor): string;
341
+ registerMany(map?: Record<string, PartialFunction | LazyDescriptor>): this;
326
342
  unregister(id: string): boolean;
327
343
  resolve(id: string): PartialFunction | undefined;
328
344
  render(id: string, props?: Record<string, unknown>, context?: Partial<PartialContext>): Promise<ServerEnvelope>;
@@ -422,8 +438,8 @@ export interface SuspenseViews {
422
438
  }
423
439
 
424
440
  export interface ComponentRegistry extends RegistryInspection<ComponentFunction> {
425
- register(id: string, Component: ComponentFunction): string;
426
- registerMany(map?: Record<string, ComponentFunction>): this;
441
+ register(id: string, Component: ComponentFunction | LazyDescriptor): string;
442
+ registerMany(map?: Record<string, ComponentFunction | LazyDescriptor>): this;
427
443
  unregister(id: string): boolean;
428
444
  resolve(id: string): ComponentFunction | undefined;
429
445
  }
@@ -458,6 +474,53 @@ export interface LoaderInstance {
458
474
  export type AsyncLoaderOptions = LoaderOptions;
459
475
  export type AsyncLoaderInstance = LoaderInstance;
460
476
 
477
+ export interface BoundaryPatch {
478
+ boundary: string;
479
+ seq: number;
480
+ html?: TemplateLike;
481
+ signals?: Record<string, unknown>;
482
+ cache?: { browser?: Record<string, unknown> };
483
+ redirect?: string;
484
+ error?: unknown;
485
+ parentScope?: string;
486
+ scope?: string;
487
+ meta?: Record<string, unknown>;
488
+ }
489
+
490
+ export type BoundaryApplyResult =
491
+ | { status: "applied"; boundary: string; seq: number }
492
+ | { status: "ignored-stale"; boundary: string; seq: number; lastSeq: number }
493
+ | { status: "ignored-destroyed"; boundary: string; seq: number; parentScope?: string }
494
+ | { status: "redirected"; boundary: string; seq: number; redirect: string }
495
+ | { status: "errored"; boundary: string; seq: number; error: Error };
496
+
497
+ export interface BoundaryReceiverInspection {
498
+ destroyed: boolean;
499
+ boundaries: Record<string, { lastSeq: number; applied: number; ignored: number; errored?: number; lastStatus?: BoundaryApplyResult["status"] }>;
500
+ recent: Array<{ boundary: string; seq: number; status: BoundaryApplyResult["status"]; lastSeq?: number; parentScope?: string; redirect?: string }>;
501
+ }
502
+
503
+ export interface BoundaryReceiverOptions {
504
+ loader: LoaderInstance;
505
+ signals?: SignalRegistry;
506
+ cache?: CacheRegistry;
507
+ scheduler?: Scheduler;
508
+ router?: Router;
509
+ onApply?(result: BoundaryApplyResult, patch: BoundaryPatch): void;
510
+ onIgnore?(result: BoundaryApplyResult, patch: BoundaryPatch): void;
511
+ onError?(error: Error, result: BoundaryApplyResult, patch: BoundaryPatch): void;
512
+ throwOnError?: boolean;
513
+ recentLimit?: number;
514
+ isScopeDestroyed?(scope: string): boolean;
515
+ }
516
+
517
+ export interface BoundaryReceiver {
518
+ apply(patch: BoundaryPatch): Promise<BoundaryApplyResult>;
519
+ inspect(): BoundaryReceiverInspection;
520
+ reset(boundary?: string): this;
521
+ destroy(): void;
522
+ }
523
+
461
524
  export interface RegistryStore {
462
525
  target: RuntimeTarget;
463
526
  register(type: RegistryType, id: string, value: unknown): string;
@@ -476,22 +539,24 @@ export interface RegistryStore {
476
539
 
477
540
  export interface RegistrySnapshot {
478
541
  signal: Record<string, unknown>;
479
- handler: Record<string, { id: string; kind: "handler" }>;
480
- server: Record<string, { id: string; kind: "server" }>;
481
- partial: Record<string, { id: string; kind: "partial" }>;
542
+ handler: Record<string, { id?: string } | LazyDescriptor>;
543
+ server: Record<string, { id?: string } | LazyDescriptor>;
544
+ partial: Record<string, { id?: string } | LazyDescriptor>;
482
545
  route: Record<string, RouteDefinition>;
483
- component: Record<string, { id: string; kind: "component" }>;
546
+ component: Record<string, { id?: string } | LazyDescriptor>;
547
+ asyncSignal: Record<string, { id?: string } | LazyDescriptor>;
484
548
  cache: { browser: Record<string, CacheDefinition>; server: Record<string, CacheDefinition> };
485
549
  entries: { browser: Record<string, unknown>; server: Record<string, unknown> };
486
550
  }
487
551
 
488
552
  export interface AppDefinition {
489
553
  signal?: SignalMap;
490
- handler?: Record<string, HandlerFunction>;
554
+ handler?: Record<string, HandlerFunction | LazyDescriptor>;
491
555
  server?: Record<string, ServerFunction>;
492
- partial?: Record<string, PartialFunction>;
556
+ partial?: Record<string, PartialFunction | LazyDescriptor>;
493
557
  route?: Record<string, RouteDefinition | string>;
494
- component?: Record<string, ComponentFunction>;
558
+ component?: Record<string, ComponentFunction | LazyDescriptor>;
559
+ asyncSignal?: Record<string, AsyncSignalFunction | LazyDescriptor>;
495
560
  cache?: {
496
561
  browser?: Record<string, CacheDefinition | CacheDefinitionOptions>;
497
562
  server?: Record<string, CacheDefinition | CacheDefinitionOptions>;
@@ -499,25 +564,43 @@ export interface AppDefinition {
499
564
  entries?: { browser?: Record<string, unknown>; server?: Record<string, unknown> };
500
565
  }
501
566
 
567
+ export interface RegistryRuntimeSnapshot extends AppDefinition {
568
+ signals?: Record<string, unknown>;
569
+ }
570
+
571
+ export interface RootInspection {
572
+ count: number;
573
+ roots: Array<{ root: Document | Element | DocumentFragment; loader: LoaderInstance; primary: boolean }>;
574
+ }
575
+
502
576
  export interface AppHub {
503
577
  registry: RegistryStore;
504
578
  runtime?: AppRuntime;
505
579
  use(type: "signal", entries: SignalMap): this;
506
- use(type: "handler", entries: Record<string, HandlerFunction>): this;
580
+ use(type: "handler", entries: Record<string, HandlerFunction | LazyDescriptor>): this;
507
581
  use(type: "server", entries: Record<string, ServerFunction>): this;
508
- use(type: "partial", entries: Record<string, PartialFunction>): this;
582
+ use(type: "partial", entries: Record<string, PartialFunction | LazyDescriptor>): this;
509
583
  use(type: "route", entries: Record<string, RouteDefinition | string>): this;
510
- use(type: "component", entries: Record<string, ComponentFunction>): this;
584
+ use(type: "component", entries: Record<string, ComponentFunction | LazyDescriptor>): this;
585
+ use(type: "asyncSignal", entries: Record<string, AsyncSignalFunction | LazyDescriptor>): this;
511
586
  use(moduleObject: AppDefinition): this;
512
587
  snapshot(): AppDefinition;
513
588
  start(options?: CreateAppOptions): AppRuntime;
589
+ attachRoot(root: Document | Element | DocumentFragment): AppRuntime;
590
+ detachRoot(root?: Document | Element | DocumentFragment): this;
591
+ applySnapshot(snapshot: RegistryRuntimeSnapshot, options?: { strict?: boolean }): this;
592
+ inspectRoots(): RootInspection;
514
593
  }
515
594
 
516
595
  export interface CreateAppOptions extends LoaderOptions {
517
596
  target?: RuntimeTarget;
518
597
  mode?: RouterMode;
519
598
  boundary?: string;
520
- snapshot?: { signals?: Record<string, unknown>; cache?: { browser?: Record<string, unknown> } };
599
+ snapshot?: RegistryRuntimeSnapshot;
600
+ registryAssets?: RegistryAssetsConfig;
601
+ importModule?: (url: string) => MaybePromise<Record<string, unknown>>;
602
+ lazyRegistry?: LazyRegistry;
603
+ strictSnapshots?: boolean;
521
604
  registry?: RegistryStore;
522
605
  loader?: LoaderInstance;
523
606
  router?: Router | false;
@@ -556,6 +639,10 @@ export interface AppRuntime {
556
639
  attributes: NormalizedAttributeConfig;
557
640
  start(): this;
558
641
  use(type: Parameters<AppHub["use"]>[0], entries?: unknown): this;
642
+ attachRoot(root: Document | Element | DocumentFragment): this;
643
+ detachRoot(root?: Document | Element | DocumentFragment): this;
644
+ applySnapshot(snapshot: RegistryRuntimeSnapshot, options?: { strict?: boolean }): this;
645
+ inspectRoots(): RootInspection;
559
646
  render(url: string | URL): Promise<RenderResult>;
560
647
  destroy(): void;
561
648
  }
@@ -566,13 +653,22 @@ export interface AsyncNamespace extends AppHub {
566
653
  createApp: typeof createApp;
567
654
  defineApp: typeof defineApp;
568
655
  readSnapshot: typeof readSnapshot;
656
+ attachRoot: AppHub["attachRoot"];
657
+ detachRoot: AppHub["detachRoot"];
658
+ applySnapshot: AppHub["applySnapshot"];
659
+ inspectRoots: AppHub["inspectRoots"];
569
660
  attributeName: typeof attributeName;
570
661
  defineAttributeConfig: typeof defineAttributeConfig;
662
+ createBoundaryReceiver: typeof createBoundaryReceiver;
571
663
  createCacheRegistry: typeof createCacheRegistry;
572
664
  defineCache: typeof defineCache;
573
665
  component: typeof component;
574
666
  createComponentRegistry: typeof createComponentRegistry;
575
667
  defineComponent: typeof defineComponent;
668
+ defineAsyncContainerElement: typeof defineAsyncContainerElement;
669
+ defineAsyncSuspenseElement: typeof defineAsyncSuspenseElement;
670
+ defineRegistrySnapshot: typeof defineRegistrySnapshot;
671
+ createLazyRegistry: typeof createLazyRegistry;
576
672
  delay: typeof delay;
577
673
  createHandlerRegistry: typeof createHandlerRegistry;
578
674
  html: typeof html;
@@ -600,14 +696,19 @@ export declare function asyncSignal<T = unknown>(id: string, fn: AsyncSignalFunc
600
696
  export declare const Async: AppHub;
601
697
  export declare function createApp(appOrDefinition?: AppHub | AppDefinition, options?: CreateAppOptions): AppRuntime;
602
698
  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> } };
699
+ export declare function readSnapshot(root?: Document | Element, options?: { attributes?: AttributeConfig }): RegistryRuntimeSnapshot;
604
700
  export declare function attributeName(attributes: AttributeConfig | undefined, type: keyof NormalizedAttributeConfig, name: string): string;
605
701
  export declare function defineAttributeConfig(config?: AttributeConfig): NormalizedAttributeConfig;
702
+ export declare function createBoundaryReceiver(options: BoundaryReceiverOptions): BoundaryReceiver;
606
703
  export declare function createCacheRegistry(initialMap?: Record<string, CacheDefinition | CacheDefinitionOptions>, options?: { now?: () => number; registry?: RegistryStore; type?: "cache.browser" | "cache.server" }): CacheRegistry;
607
704
  export declare function defineCache(options?: CacheDefinitionOptions): CacheDefinition;
608
705
  export declare function component<TProps extends Record<string, unknown> = Record<string, unknown>>(fn: ComponentFunction<TProps>): ComponentFunction<TProps>;
609
706
  export declare function createComponentRegistry(initialMap?: Record<string, ComponentFunction>, options?: { registry?: RegistryStore; type?: "component" }): ComponentRegistry;
610
707
  export declare function defineComponent<TProps extends Record<string, unknown> = Record<string, unknown>>(fn: ComponentFunction<TProps>): ComponentFunction<TProps>;
708
+ export declare function defineAsyncContainerElement(options?: { tagName?: string; app?: AppHub; Async?: AppHub; customElements?: CustomElementRegistry; HTMLElement?: typeof HTMLElement; window?: Window }): CustomElementConstructor;
709
+ export declare function defineAsyncSuspenseElement(options?: { tagName?: string; customElements?: CustomElementRegistry; HTMLElement?: typeof HTMLElement; window?: Window }): CustomElementConstructor;
710
+ export declare function defineRegistrySnapshot<T extends RegistryRuntimeSnapshot>(snapshot?: T): T;
711
+ export declare function createLazyRegistry(options?: { registryAssets?: RegistryAssetsConfig; assets?: RegistryAssetsConfig; importModule?: (url: string) => MaybePromise<Record<string, unknown>> }): LazyRegistry;
611
712
  export declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
612
713
  export declare function createHandlerRegistry(initialMap?: Record<string, HandlerFunction>, options?: { registry?: RegistryStore; type?: "handler" }): HandlerRegistry;
613
714
  export declare function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;