@async/framework 0.9.0 → 0.10.1

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/server.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;
@@ -209,8 +224,8 @@ export interface HandlerContext {
209
224
  export type HandlerFunction = (this: HandlerContext, context: HandlerContext) => MaybePromise<unknown>;
210
225
 
211
226
  export interface HandlerRegistry extends RegistryInspection<HandlerFunction> {
212
- register(id: string, fn: HandlerFunction): string;
213
- registerMany(map?: Record<string, HandlerFunction>): this;
227
+ register(id: string, fn: HandlerFunction | LazyDescriptor): string;
228
+ registerMany(map?: Record<string, HandlerFunction | LazyDescriptor>): this;
214
229
  unregister(id: string): boolean;
215
230
  resolve(id: string): HandlerFunction | undefined;
216
231
  run(ref: string, context?: Partial<HandlerContext>): Promise<unknown[]>;
@@ -322,8 +337,8 @@ export interface PartialContext {
322
337
  export type PartialFunction = (this: PartialContext, props: Record<string, unknown>) => MaybePromise<TemplateLike | ServerEnvelope>;
323
338
 
324
339
  export interface PartialRegistry extends RegistryInspection<PartialFunction> {
325
- register(id: string, fn: PartialFunction): string;
326
- registerMany(map?: Record<string, PartialFunction>): this;
340
+ register(id: string, fn: PartialFunction | LazyDescriptor): string;
341
+ registerMany(map?: Record<string, PartialFunction | LazyDescriptor>): this;
327
342
  unregister(id: string): boolean;
328
343
  resolve(id: string): PartialFunction | undefined;
329
344
  render(id: string, props?: Record<string, unknown>, context?: Partial<PartialContext>): Promise<ServerEnvelope>;
@@ -423,8 +438,8 @@ export interface SuspenseViews {
423
438
  }
424
439
 
425
440
  export interface ComponentRegistry extends RegistryInspection<ComponentFunction> {
426
- register(id: string, Component: ComponentFunction): string;
427
- registerMany(map?: Record<string, ComponentFunction>): this;
441
+ register(id: string, Component: ComponentFunction | LazyDescriptor): string;
442
+ registerMany(map?: Record<string, ComponentFunction | LazyDescriptor>): this;
428
443
  unregister(id: string): boolean;
429
444
  resolve(id: string): ComponentFunction | undefined;
430
445
  }
@@ -524,22 +539,24 @@ export interface RegistryStore {
524
539
 
525
540
  export interface RegistrySnapshot {
526
541
  signal: Record<string, unknown>;
527
- handler: Record<string, { id: string; kind: "handler" }>;
528
- server: Record<string, { id: string; kind: "server" }>;
529
- 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>;
530
545
  route: Record<string, RouteDefinition>;
531
- component: Record<string, { id: string; kind: "component" }>;
546
+ component: Record<string, { id?: string } | LazyDescriptor>;
547
+ asyncSignal: Record<string, { id?: string } | LazyDescriptor>;
532
548
  cache: { browser: Record<string, CacheDefinition>; server: Record<string, CacheDefinition> };
533
549
  entries: { browser: Record<string, unknown>; server: Record<string, unknown> };
534
550
  }
535
551
 
536
552
  export interface AppDefinition {
537
553
  signal?: SignalMap;
538
- handler?: Record<string, HandlerFunction>;
554
+ handler?: Record<string, HandlerFunction | LazyDescriptor>;
539
555
  server?: Record<string, ServerFunction>;
540
- partial?: Record<string, PartialFunction>;
556
+ partial?: Record<string, PartialFunction | LazyDescriptor>;
541
557
  route?: Record<string, RouteDefinition | string>;
542
- component?: Record<string, ComponentFunction>;
558
+ component?: Record<string, ComponentFunction | LazyDescriptor>;
559
+ asyncSignal?: Record<string, AsyncSignalFunction | LazyDescriptor>;
543
560
  cache?: {
544
561
  browser?: Record<string, CacheDefinition | CacheDefinitionOptions>;
545
562
  server?: Record<string, CacheDefinition | CacheDefinitionOptions>;
@@ -547,25 +564,43 @@ export interface AppDefinition {
547
564
  entries?: { browser?: Record<string, unknown>; server?: Record<string, unknown> };
548
565
  }
549
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
+
550
576
  export interface AppHub {
551
577
  registry: RegistryStore;
552
578
  runtime?: AppRuntime;
553
579
  use(type: "signal", entries: SignalMap): this;
554
- use(type: "handler", entries: Record<string, HandlerFunction>): this;
580
+ use(type: "handler", entries: Record<string, HandlerFunction | LazyDescriptor>): this;
555
581
  use(type: "server", entries: Record<string, ServerFunction>): this;
556
- use(type: "partial", entries: Record<string, PartialFunction>): this;
582
+ use(type: "partial", entries: Record<string, PartialFunction | LazyDescriptor>): this;
557
583
  use(type: "route", entries: Record<string, RouteDefinition | string>): this;
558
- 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;
559
586
  use(moduleObject: AppDefinition): this;
560
587
  snapshot(): AppDefinition;
561
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;
562
593
  }
563
594
 
564
595
  export interface CreateAppOptions extends LoaderOptions {
565
596
  target?: RuntimeTarget;
566
597
  mode?: RouterMode;
567
598
  boundary?: string;
568
- 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;
569
604
  registry?: RegistryStore;
570
605
  loader?: LoaderInstance;
571
606
  router?: Router | false;
@@ -604,6 +639,10 @@ export interface AppRuntime {
604
639
  attributes: NormalizedAttributeConfig;
605
640
  start(): this;
606
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;
607
646
  render(url: string | URL): Promise<RenderResult>;
608
647
  destroy(): void;
609
648
  }
@@ -614,6 +653,10 @@ export interface AsyncNamespace extends AppHub {
614
653
  createApp: typeof createApp;
615
654
  defineApp: typeof defineApp;
616
655
  readSnapshot: typeof readSnapshot;
656
+ attachRoot: AppHub["attachRoot"];
657
+ detachRoot: AppHub["detachRoot"];
658
+ applySnapshot: AppHub["applySnapshot"];
659
+ inspectRoots: AppHub["inspectRoots"];
617
660
  attributeName: typeof attributeName;
618
661
  defineAttributeConfig: typeof defineAttributeConfig;
619
662
  createBoundaryReceiver: typeof createBoundaryReceiver;
@@ -622,6 +665,10 @@ export interface AsyncNamespace extends AppHub {
622
665
  component: typeof component;
623
666
  createComponentRegistry: typeof createComponentRegistry;
624
667
  defineComponent: typeof defineComponent;
668
+ defineAsyncContainerElement: typeof defineAsyncContainerElement;
669
+ defineAsyncSuspenseElement: typeof defineAsyncSuspenseElement;
670
+ defineRegistrySnapshot: typeof defineRegistrySnapshot;
671
+ createLazyRegistry: typeof createLazyRegistry;
625
672
  delay: typeof delay;
626
673
  createHandlerRegistry: typeof createHandlerRegistry;
627
674
  html: typeof html;
@@ -651,7 +698,7 @@ export declare function asyncSignal<T = unknown>(id: string, fn: AsyncSignalFunc
651
698
  export declare const Async: AppHub;
652
699
  export declare function createApp(appOrDefinition?: AppHub | AppDefinition, options?: CreateAppOptions): AppRuntime;
653
700
  export declare function defineApp(initial?: AppDefinition): AppHub;
654
- export declare function readSnapshot(root?: Document | Element, options?: { attributes?: AttributeConfig }): { signals?: Record<string, unknown>; cache?: { browser?: Record<string, unknown> } };
701
+ export declare function readSnapshot(root?: Document | Element, options?: { attributes?: AttributeConfig }): RegistryRuntimeSnapshot;
655
702
  export declare function attributeName(attributes: AttributeConfig | undefined, type: keyof NormalizedAttributeConfig, name: string): string;
656
703
  export declare function defineAttributeConfig(config?: AttributeConfig): NormalizedAttributeConfig;
657
704
  export declare function createBoundaryReceiver(options: BoundaryReceiverOptions): BoundaryReceiver;
@@ -660,6 +707,10 @@ export declare function defineCache(options?: CacheDefinitionOptions): CacheDefi
660
707
  export declare function component<TProps extends Record<string, unknown> = Record<string, unknown>>(fn: ComponentFunction<TProps>): ComponentFunction<TProps>;
661
708
  export declare function createComponentRegistry(initialMap?: Record<string, ComponentFunction>, options?: { registry?: RegistryStore; type?: "component" }): ComponentRegistry;
662
709
  export declare function defineComponent<TProps extends Record<string, unknown> = Record<string, unknown>>(fn: ComponentFunction<TProps>): ComponentFunction<TProps>;
710
+ export declare function defineAsyncContainerElement(options?: { tagName?: string; app?: AppHub; Async?: AppHub; customElements?: CustomElementRegistry; HTMLElement?: typeof HTMLElement; window?: Window }): CustomElementConstructor;
711
+ export declare function defineAsyncSuspenseElement(options?: { tagName?: string; customElements?: CustomElementRegistry; HTMLElement?: typeof HTMLElement; window?: Window }): CustomElementConstructor;
712
+ export declare function defineRegistrySnapshot<T extends RegistryRuntimeSnapshot>(snapshot?: T): T;
713
+ export declare function createLazyRegistry(options?: { registryAssets?: RegistryAssetsConfig; assets?: RegistryAssetsConfig; importModule?: (url: string) => MaybePromise<Record<string, unknown>> }): LazyRegistry;
663
714
  export declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
664
715
  export declare function createHandlerRegistry(initialMap?: Record<string, HandlerFunction>, options?: { registry?: RegistryStore; type?: "handler" }): HandlerRegistry;
665
716
  export declare function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
package/src/app.js CHANGED
@@ -9,8 +9,9 @@ import { createServerNamespace } from "./server.js";
9
9
  import { createSignal, createSignalRegistry } from "./signals.js";
10
10
  import { createRegistryStore } from "./registry-store.js";
11
11
  import { attributeName, normalizeAttributeConfig } from "./attributes.js";
12
+ import { createLazyRegistry, defineRegistrySnapshot, sameRegistryValue } from "./lazy-registry.js";
12
13
 
13
- const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
14
+ const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component", "asyncSignal"]);
14
15
 
15
16
  export function defineApp(initial, options = {}) {
16
17
  const registry = createRegistryStore(undefined, { target: "browser" });
@@ -39,6 +40,27 @@ export function defineApp(initial, options = {}) {
39
40
  return runtime;
40
41
  },
41
42
 
43
+ attachRoot(root) {
44
+ return ensureRuntime(app).attachRoot(root);
45
+ },
46
+
47
+ detachRoot(root) {
48
+ return app.runtime?.detachRoot(root) ?? app;
49
+ },
50
+
51
+ applySnapshot(snapshot, snapshotOptions = {}) {
52
+ if (app.runtime) {
53
+ app.runtime.applySnapshot(snapshot, snapshotOptions);
54
+ return app;
55
+ }
56
+ appendSnapshotDeclarations(registry, snapshot, snapshotOptions);
57
+ return app;
58
+ },
59
+
60
+ inspectRoots() {
61
+ return app.runtime?.inspectRoots() ?? { count: 0, roots: [] };
62
+ },
63
+
42
64
  _attach(runtime) {
43
65
  runtimes.add(runtime);
44
66
  return () => app._detach(runtime);
@@ -64,23 +86,32 @@ export function createApp(appOrDefinition = Async, options = {}) {
64
86
  });
65
87
  const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
66
88
  const attributes = normalizeAttributeConfig(options.attributes);
89
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry({
90
+ registryAssets: options.registryAssets,
91
+ importModule: options.importModule
92
+ });
67
93
  const registry = options.registry ?? app.registry.view({ target });
68
- const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
69
- const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
94
+ const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal", lazyRegistry });
95
+ const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler", lazyRegistry });
70
96
  const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
71
97
  const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
72
98
  const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
73
99
  const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
74
- const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
100
+ const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial", lazyRegistry });
75
101
  const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
76
- const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
102
+ const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component", lazyRegistry });
103
+ const hasStartupRoot = options.loader || Object.hasOwn(options, "root");
104
+ const startupRoot = hasStartupRoot ? options.root : null;
77
105
  let loader = options.loader;
78
106
  let router = options.router;
107
+ let routerStarted = false;
79
108
  let detach = () => {};
80
109
  let started = false;
81
110
  let destroyed = false;
111
+ const rootLoaders = new Map();
82
112
 
83
- applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
113
+ const snapshotRoot = startupRoot ?? globalThis.document;
114
+ const initialSnapshot = options.snapshot ?? (target === "browser" ? readSnapshot(snapshotRoot, { attributes }) : undefined);
84
115
  attachServerCache(server, serverCache);
85
116
 
86
117
  const runtime = {
@@ -109,54 +140,112 @@ export function createApp(appOrDefinition = Async, options = {}) {
109
140
  started = true;
110
141
 
111
142
  if (target !== "server") {
112
- loader = loader ?? Loader({
113
- root: options.root,
114
- signals,
115
- handlers,
116
- server,
117
- cache: browserCache,
118
- scheduler,
119
- attributes
120
- });
121
- runtime.loader = loader;
122
-
123
143
  configureServerContext({ cache: browserCache });
124
144
  signals._setContext?.({ server, loader, cache: browserCache, scheduler });
125
145
 
126
- loader.start();
146
+ if (loader) {
147
+ registerRootLoader(loader.root, loader);
148
+ loader.start();
149
+ startRouterFor(loader.root);
150
+ } else if (startupRoot != null) {
151
+ runtime.attachRoot(startupRoot);
152
+ }
153
+ } else {
154
+ configureServerContext({ cache: serverCache });
155
+ signals._setContext?.({ server, cache: serverCache, scheduler });
156
+ }
157
+
158
+ return runtime;
159
+ },
160
+
161
+ use(typeOrModule, entries) {
162
+ app.use(typeOrModule, entries);
163
+ return runtime;
164
+ },
165
+
166
+ attachRoot(root) {
167
+ assertActive();
168
+ if (target === "server") {
169
+ throw new Error("Server runtimes cannot attach DOM roots.");
170
+ }
171
+ if (!root) {
172
+ throw new TypeError("runtime.attachRoot(root) requires a root.");
173
+ }
174
+ if (rootLoaders.has(root)) {
175
+ return runtime;
176
+ }
127
177
 
128
- if (router !== false && (router || shouldStartRouter(routes, options))) {
129
- router = router ?? createRouter({
130
- mode: options.mode ?? "ssr-spa",
131
- root: options.root,
132
- boundary: options.boundary ?? "route",
133
- routes,
134
- loader,
178
+ const rootLoader = rootLoaders.size === 0 && loader
179
+ ? loader
180
+ : Loader({
181
+ root,
135
182
  signals,
136
183
  handlers,
137
184
  server,
138
185
  cache: browserCache,
139
- partials,
140
186
  scheduler,
141
- fetch: options.fetch,
142
- routeEndpoint: options.routeEndpoint,
143
187
  attributes
144
188
  });
145
- runtime.router = router;
146
- loader.router = router;
147
- configureServerContext({ cache: browserCache, router });
148
- router.start();
189
+ registerRootLoader(root, rootLoader);
190
+ rootLoader.start();
191
+ configureServerContext({ cache: browserCache });
192
+ signals._setContext?.({ server, loader: runtime.loader, cache: browserCache, scheduler });
193
+ startRouterFor(root);
194
+ return runtime;
195
+ },
196
+
197
+ detachRoot(root) {
198
+ assertActive();
199
+ if (target === "server") {
200
+ return runtime;
201
+ }
202
+ if (root == null) {
203
+ for (const rootLoader of new Set(rootLoaders.values())) {
204
+ rootLoader.destroy?.();
205
+ }
206
+ rootLoaders.clear();
207
+ router?.destroy?.();
208
+ router = undefined;
209
+ routerStarted = false;
210
+ loader = undefined;
211
+ runtime.loader = undefined;
212
+ runtime.router = undefined;
213
+ return runtime;
214
+ }
215
+ const rootLoader = rootLoaders.get(root);
216
+ if (!rootLoader) {
217
+ return runtime;
218
+ }
219
+ rootLoader.destroy?.();
220
+ rootLoaders.delete(root);
221
+ if (loader === rootLoader) {
222
+ router?.destroy?.();
223
+ router = undefined;
224
+ routerStarted = false;
225
+ const next = rootLoaders.values().next().value;
226
+ loader = next;
227
+ runtime.loader = next;
228
+ runtime.router = undefined;
229
+ if (next) {
230
+ startRouterFor(next.root);
149
231
  }
150
- } else {
151
- configureServerContext({ cache: serverCache });
152
- signals._setContext?.({ server, cache: serverCache, scheduler });
153
232
  }
154
-
155
233
  return runtime;
156
234
  },
157
235
 
158
- use(typeOrModule, entries) {
159
- app.use(typeOrModule, entries);
236
+ inspectRoots() {
237
+ return {
238
+ count: rootLoaders.size,
239
+ roots: [...rootLoaders].map(([root, rootLoader]) => ({
240
+ root,
241
+ loader: rootLoader,
242
+ primary: rootLoader === loader
243
+ }))
244
+ };
245
+ },
246
+
247
+ applySnapshot(snapshot, snapshotOptions = {}) {
248
+ applySnapshotToRuntime(runtime, snapshot, snapshotOptions);
160
249
  return runtime;
161
250
  },
162
251
 
@@ -218,7 +307,14 @@ export function createApp(appOrDefinition = Async, options = {}) {
218
307
  destroyed = true;
219
308
  detach();
220
309
  router?.destroy?.();
221
- loader?.destroy?.();
310
+ const destroyedLoaders = new Set(rootLoaders.values());
311
+ for (const rootLoader of destroyedLoaders) {
312
+ rootLoader.destroy?.();
313
+ }
314
+ rootLoaders.clear();
315
+ if (loader && !destroyedLoaders.has(loader)) {
316
+ loader?.destroy?.();
317
+ }
222
318
  signals.destroy?.();
223
319
  if (ownsScheduler) {
224
320
  scheduler.destroy();
@@ -232,10 +328,49 @@ export function createApp(appOrDefinition = Async, options = {}) {
232
328
 
233
329
  server.cache = serverCache;
234
330
  runtime.server.cache = serverCache;
331
+ runtime.applySnapshot(initialSnapshot, { strict: options.strictSnapshots ?? true });
235
332
  detach = app._attach(runtime);
236
333
 
237
334
  return runtime;
238
335
 
336
+ function registerRootLoader(root, rootLoader) {
337
+ rootLoaders.set(root, rootLoader);
338
+ if (!loader) {
339
+ loader = rootLoader;
340
+ runtime.loader = rootLoader;
341
+ }
342
+ rootLoader.server = server;
343
+ rootLoader.cache = browserCache;
344
+ rootLoader.scheduler = scheduler;
345
+ }
346
+
347
+ function startRouterFor(root) {
348
+ if (router === false || routerStarted || !(router || shouldStartRouter(routes, options)) || !runtime.loader) {
349
+ return;
350
+ }
351
+ router = router ?? createRouter({
352
+ mode: options.mode ?? "ssr-spa",
353
+ root,
354
+ boundary: options.boundary ?? "route",
355
+ routes,
356
+ loader: runtime.loader,
357
+ signals,
358
+ handlers,
359
+ server,
360
+ cache: browserCache,
361
+ partials,
362
+ scheduler,
363
+ fetch: options.fetch,
364
+ routeEndpoint: options.routeEndpoint,
365
+ attributes
366
+ });
367
+ runtime.router = router;
368
+ runtime.loader.router = router;
369
+ configureServerContext({ cache: browserCache, router });
370
+ router.start();
371
+ routerStarted = true;
372
+ }
373
+
239
374
  function configureServerContext(extra = {}) {
240
375
  const cache = isLocalServerRegistry(server) ? serverCache : extra.cache;
241
376
  server._setContext?.({
@@ -278,6 +413,7 @@ export function readSnapshot(root = globalThis.document, { attributes } = {}) {
278
413
  return {};
279
414
  }
280
415
 
416
+ const merged = {};
281
417
  for (const searchRoot of new Set([rootNode, documentRef])) {
282
418
  if (!searchRoot?.querySelectorAll) {
283
419
  continue;
@@ -288,17 +424,19 @@ export function readSnapshot(root = globalThis.document, { attributes } = {}) {
288
424
  }
289
425
  const source = script.textContent?.trim() ?? "";
290
426
  if (!source) {
291
- return {};
427
+ continue;
292
428
  }
429
+ let parsed;
293
430
  try {
294
- return JSON.parse(source);
431
+ parsed = JSON.parse(source);
295
432
  } catch (cause) {
296
433
  throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
297
434
  }
435
+ mergeSnapshot(merged, parsed, { strict: true });
298
436
  }
299
437
  }
300
438
 
301
- return {};
439
+ return merged;
302
440
  }
303
441
 
304
442
  function applyUseToRuntime(runtime, normalized) {
@@ -308,10 +446,22 @@ function applyUseToRuntime(runtime, normalized) {
308
446
  applyRegistryUse(runtime.partials, runtime.registry, normalized.partial);
309
447
  applyRegistryUse(runtime.routes, runtime.registry, normalized.route);
310
448
  applyRegistryUse(runtime.components, runtime.registry, normalized.component);
449
+ applyRegistryStoreUse(runtime.registry, "asyncSignal", normalized.asyncSignal);
311
450
  applyRegistryUse(runtime.browser.cache, runtime.registry, normalized.cache.browser);
312
451
  applyRegistryUse(runtime.server.cache, runtime.registry, normalized.cache.server);
313
452
  }
314
453
 
454
+ function applyRegistryStoreUse(registry, type, entries) {
455
+ if (!entries || Object.keys(entries).length === 0) {
456
+ return;
457
+ }
458
+ for (const [id, value] of Object.entries(entries)) {
459
+ if (!registry.has(type, id)) {
460
+ registry.register(type, id, value);
461
+ }
462
+ }
463
+ }
464
+
315
465
  function applyRegistryUse(registry, runtimeRegistry, entries) {
316
466
  if (!entries || Object.keys(entries).length === 0) {
317
467
  return;
@@ -331,6 +481,7 @@ function emptyDeclarations() {
331
481
  partial: {},
332
482
  route: {},
333
483
  component: {},
484
+ asyncSignal: {},
334
485
  cache: {
335
486
  browser: {},
336
487
  server: {}
@@ -386,11 +537,128 @@ function isAppHub(value) {
386
537
  return Boolean(value && typeof value.use === "function" && typeof value.snapshot === "function" && value.registry);
387
538
  }
388
539
 
389
- function applySnapshot(signals, browserCache, snapshot = {}) {
390
- for (const [path, value] of Object.entries(snapshot.signals ?? {})) {
391
- setOrRegisterSignal(signals, path, value);
540
+ function ensureRuntime(app) {
541
+ if (!app.runtime) {
542
+ app.start();
543
+ }
544
+ return app.runtime;
545
+ }
546
+
547
+ function applySnapshotToRuntime(runtime, snapshot = {}, options = {}) {
548
+ const normalized = normalizeSnapshot(snapshot);
549
+ for (const [path, value] of Object.entries(normalized.signal)) {
550
+ setOrRegisterSignal(runtime.signals, path, value);
551
+ }
552
+ runtime.browser.cache.restore(normalized.cache.browser);
553
+ mergeRegistryEntries(runtime, "handler", normalized.handler, runtime.handlers, options);
554
+ mergeRegistryEntries(runtime, "server", normalized.server, runtime.server, options);
555
+ mergeRegistryEntries(runtime, "partial", normalized.partial, runtime.partials, options);
556
+ mergeRegistryEntries(runtime, "route", normalized.route, runtime.routes, options);
557
+ mergeRegistryEntries(runtime, "component", normalized.component, runtime.components, options);
558
+ mergeRegistryEntries(runtime, "asyncSignal", normalized.asyncSignal, null, options);
559
+ return runtime;
560
+ }
561
+
562
+ function appendSnapshotDeclarations(registry, snapshot = {}, options = {}) {
563
+ const normalized = normalizeSnapshot(snapshot);
564
+ for (const [id, value] of Object.entries(normalized.signal)) {
565
+ registerSnapshotEntry(registry, "signal", id, createSignal(value), options);
566
+ }
567
+ for (const type of ["handler", "server", "partial", "route", "component", "asyncSignal"]) {
568
+ for (const [id, value] of Object.entries(normalized[type])) {
569
+ registerSnapshotEntry(registry, type, id, value, options);
570
+ }
571
+ }
572
+ }
573
+
574
+ function mergeRegistryEntries(runtime, type, entries, concreteRegistry, options = {}) {
575
+ if (!entries || Object.keys(entries).length === 0) {
576
+ return;
577
+ }
578
+ for (const [id, value] of Object.entries(entries)) {
579
+ registerSnapshotEntry(runtime.registry, type, id, value, options);
580
+ }
581
+ concreteRegistry?._adoptMany?.(entries);
582
+ }
583
+
584
+ function registerSnapshotEntry(registry, type, id, value, options = {}) {
585
+ const strict = options.strict ?? true;
586
+ const map = registry._map(type);
587
+ if (map.has(id)) {
588
+ if (sameRegistryValue(map.get(id), value) || sameSnapshotValue(map.get(id), value)) {
589
+ return;
590
+ }
591
+ if (strict) {
592
+ throw new Error(`${type} "${id}" is already registered with a different value.`);
593
+ }
594
+ return;
595
+ }
596
+ registry.set(type, id, value);
597
+ }
598
+
599
+ function normalizeSnapshot(snapshot = {}) {
600
+ const normalized = {
601
+ signal: {
602
+ ...(snapshot.signals ?? {}),
603
+ ...(snapshot.signal ?? {})
604
+ },
605
+ handler: { ...(snapshot.handler ?? {}) },
606
+ server: { ...(snapshot.server ?? {}) },
607
+ partial: { ...(snapshot.partial ?? {}) },
608
+ route: { ...(snapshot.route ?? {}) },
609
+ component: { ...(snapshot.component ?? {}) },
610
+ asyncSignal: { ...(snapshot.asyncSignal ?? {}) },
611
+ cache: {
612
+ browser: {
613
+ ...(snapshot.entries?.browser ?? {}),
614
+ ...(snapshot.cache?.browser ?? {})
615
+ }
616
+ }
617
+ };
618
+ return normalized;
619
+ }
620
+
621
+ function mergeSnapshot(target, source, options = {}) {
622
+ const normalized = normalizeSnapshot(defineRegistrySnapshot(source));
623
+ target.signal = {
624
+ ...(target.signal ?? target.signals ?? {}),
625
+ ...normalized.signal
626
+ };
627
+ target.signals = target.signal;
628
+ target.cache = {
629
+ ...(target.cache ?? {}),
630
+ browser: {
631
+ ...(target.cache?.browser ?? {}),
632
+ ...normalized.cache.browser
633
+ }
634
+ };
635
+ for (const type of ["handler", "server", "partial", "route", "component", "asyncSignal"]) {
636
+ target[type] = target[type] ?? {};
637
+ for (const [id, value] of Object.entries(normalized[type])) {
638
+ if (Object.hasOwn(target[type], id)) {
639
+ if (sameRegistryValue(target[type][id], value) || sameSnapshotValue(target[type][id], value)) {
640
+ continue;
641
+ }
642
+ if (options.strict ?? true) {
643
+ throw new Error(`${type} "${id}" is already declared with a different value.`);
644
+ }
645
+ continue;
646
+ }
647
+ target[type][id] = value;
648
+ }
649
+ }
650
+ return target;
651
+ }
652
+
653
+ function sameSnapshotValue(left, right) {
654
+ if (left === right) {
655
+ return true;
656
+ }
657
+ try {
658
+ return JSON.stringify(left) === JSON.stringify(right);
659
+ } catch {
660
+ return false;
392
661
  }
393
- browserCache.restore(snapshot.cache?.browser);
394
662
  }
395
663
 
396
664
  function setOrRegisterSignal(signals, path, value) {
package/src/browser.js CHANGED
@@ -5,8 +5,10 @@ export { createBoundaryReceiver } from "./boundary-receiver.js";
5
5
  export { createCacheRegistry, defineCache } from "./cache.js";
6
6
  export { component, createComponentRegistry, defineComponent } from "./component.js";
7
7
  export { delay } from "./delay.js";
8
+ export { defineAsyncContainerElement, defineAsyncSuspenseElement } from "./elements.js";
8
9
  export { createHandlerRegistry } from "./handlers.js";
9
10
  export { html } from "./html.js";
11
+ export { createLazyRegistry, defineRegistrySnapshot } from "./lazy-registry.js";
10
12
  export { Loader, AsyncLoader } from "./loader.js";
11
13
  export { createPartialRegistry } from "./partials.js";
12
14
  export { createRegistryStore } from "./registry-store.js";