@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/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) {