@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/browser.umd.js CHANGED
@@ -293,8 +293,217 @@
293
293
  return { asyncSignal, isAsyncSignal };
294
294
  })();
295
295
 
296
+ const __lazyRegistryModule = (() => {
297
+ const descriptorTypes = new Set(["handler", "component", "asyncSignal", "partial", "route"]);
298
+ const defaultBaseUrl = "_async";
299
+
300
+ function defineRegistrySnapshot(snapshot = {}) {
301
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
302
+ throw new TypeError("defineRegistrySnapshot(snapshot) requires an object.");
303
+ }
304
+ return snapshot;
305
+ }
306
+
307
+ function createLazyRegistry(options = {}) {
308
+ const registryAssets = normalizeRegistryAssets(options.registryAssets ?? options.assets);
309
+ const importModule = options.importModule ?? ((url) => import(url));
310
+ const moduleCache = new Map();
311
+ const exportCache = new Map();
312
+
313
+ return {
314
+ registryAssets,
315
+
316
+ resolveUrl(type, id, descriptor) {
317
+ return resolveDescriptorUrl(type, id, descriptor, registryAssets);
318
+ },
319
+
320
+ async resolve(type, id, descriptor) {
321
+ if (!isLazyDescriptor(descriptor)) {
322
+ return descriptor;
323
+ }
324
+ const cacheKey = `${type}:${id}`;
325
+ if (exportCache.has(cacheKey)) {
326
+ return exportCache.get(cacheKey);
327
+ }
328
+
329
+ const resolved = resolveDescriptorUrl(type, id, descriptor, registryAssets);
330
+ let modulePromise = moduleCache.get(resolved.moduleUrl);
331
+ if (!modulePromise) {
332
+ modulePromise = Promise.resolve(importModule(resolved.moduleUrl));
333
+ moduleCache.set(resolved.moduleUrl, modulePromise);
334
+ }
335
+ const module = await modulePromise;
336
+ const value = resolveExport(module, resolved.exportNames, type, id);
337
+ exportCache.set(cacheKey, value);
338
+ return value;
339
+ },
340
+
341
+ inspect() {
342
+ return {
343
+ registryAssets,
344
+ modules: [...moduleCache.keys()],
345
+ exports: [...exportCache.keys()]
346
+ };
347
+ }
348
+ };
349
+ }
350
+
351
+ function normalizeRegistryAssets(options = {}) {
352
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultBaseUrl);
353
+ const paths = {
354
+ component: "component",
355
+ handler: "handler",
356
+ asyncSignal: "asyncSignal",
357
+ partial: "partial",
358
+ route: "route",
359
+ ...(options.paths ?? {})
360
+ };
361
+
362
+ for (const [type, value] of Object.entries(paths)) {
363
+ if (!descriptorTypes.has(type)) {
364
+ continue;
365
+ }
366
+ if (typeof value !== "string" || value.length === 0) {
367
+ throw new TypeError(`Registry asset path for "${type}" must be a non-empty string.`);
368
+ }
369
+ }
370
+
371
+ return {
372
+ baseUrl,
373
+ paths
374
+ };
375
+ }
376
+
377
+ function isLazyDescriptor(value) {
378
+ return Boolean(
379
+ value &&
380
+ typeof value === "object" &&
381
+ !Array.isArray(value) &&
382
+ typeof value.url === "string"
383
+ );
384
+ }
385
+
386
+ function sameRegistryValue(left, right) {
387
+ if (left === right) {
388
+ return true;
389
+ }
390
+ if (isLazyDescriptor(left) && isLazyDescriptor(right)) {
391
+ return stableStringify(left) === stableStringify(right);
392
+ }
393
+ return false;
394
+ }
395
+
396
+ function publicRegistryValue(value, id) {
397
+ if (isLazyDescriptor(value)) {
398
+ return { ...value };
399
+ }
400
+ return { id };
401
+ }
402
+
403
+ function resolveDescriptorUrl(type, id, descriptor, registryAssets) {
404
+ if (!descriptorTypes.has(type)) {
405
+ throw new Error(`Registry type "${type}" does not support lazy descriptors.`);
406
+ }
407
+ if (!isLazyDescriptor(descriptor)) {
408
+ throw new TypeError(`Registry descriptor for "${type}:${id}" requires a url.`);
409
+ }
410
+
411
+ const { path, hash } = splitHash(descriptor.url);
412
+ const moduleUrl = resolveModuleUrl(type, path, registryAssets);
413
+ const exportNames = hash
414
+ ? [hash]
415
+ : inferredExportNames(id, path);
416
+
417
+ return {
418
+ moduleUrl,
419
+ exportNames,
420
+ url: hash ? `${moduleUrl}#${hash}` : moduleUrl
421
+ };
422
+ }
423
+
424
+ function resolveModuleUrl(type, path, registryAssets) {
425
+ if (isAbsoluteUrl(path) || path.startsWith("/") || path.startsWith("./") || path.startsWith("../")) {
426
+ return path;
427
+ }
428
+ const typePath = registryAssets.paths[type] ?? type;
429
+ return joinUrl(registryAssets.baseUrl, typePath, path);
430
+ }
431
+
432
+ function resolveExport(module, exportNames, type, id) {
433
+ for (const name of exportNames) {
434
+ if (name in module) {
435
+ return module[name];
436
+ }
437
+ }
438
+ throw new Error(`Lazy ${type} "${id}" did not export ${exportNames.map((name) => `"${name}"`).join(", ")}.`);
439
+ }
440
+
441
+ function inferredExportNames(id, path) {
442
+ const names = [];
443
+ const leaf = id.split(".").filter(Boolean).at(-1);
444
+ const basename = path
445
+ .split("/")
446
+ .filter(Boolean)
447
+ .at(-1)
448
+ ?.replace(/\.[^.]+$/, "");
449
+ for (const name of [leaf, basename, "default"]) {
450
+ if (name && !names.includes(name)) {
451
+ names.push(name);
452
+ }
453
+ }
454
+ return names;
455
+ }
456
+
457
+ function splitHash(url) {
458
+ const index = url.indexOf("#");
459
+ if (index === -1) {
460
+ return { path: url, hash: "" };
461
+ }
462
+ return {
463
+ path: url.slice(0, index),
464
+ hash: url.slice(index + 1)
465
+ };
466
+ }
467
+
468
+ function normalizeBaseUrl(baseUrl) {
469
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) {
470
+ throw new TypeError("registryAssets.baseUrl must be a non-empty string.");
471
+ }
472
+ if (isAbsoluteUrl(baseUrl) || baseUrl.startsWith("/") || baseUrl.startsWith("./") || baseUrl.startsWith("../")) {
473
+ return stripTrailingSlash(baseUrl);
474
+ }
475
+ return `/${stripSlashes(baseUrl)}`;
476
+ }
477
+
478
+ function joinUrl(...parts) {
479
+ const [first, ...rest] = parts;
480
+ return [stripTrailingSlash(first), ...rest.map(stripSlashes)].filter(Boolean).join("/");
481
+ }
482
+
483
+ function stripSlashes(value) {
484
+ return String(value).replace(/^\/+|\/+$/g, "");
485
+ }
486
+
487
+ function stripTrailingSlash(value) {
488
+ return String(value).replace(/\/+$/g, "");
489
+ }
490
+
491
+ function isAbsoluteUrl(value) {
492
+ return /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
493
+ }
494
+
495
+ function stableStringify(value) {
496
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
497
+ return JSON.stringify(value);
498
+ }
499
+ return JSON.stringify(Object.keys(value).sort().map((key) => [key, value[key]]));
500
+ }
501
+ return { defineRegistrySnapshot, createLazyRegistry, normalizeRegistryAssets, isLazyDescriptor, sameRegistryValue, publicRegistryValue };
502
+ })();
503
+
296
504
  const __registryStoreModule = (() => {
297
- const declarationTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
505
+ const { publicRegistryValue } = __lazyRegistryModule;
506
+ const declarationTypes = new Set(["signal", "handler", "server", "partial", "route", "component", "asyncSignal"]);
298
507
  const cacheTypes = new Set(["cache.browser", "cache.server"]);
299
508
  const cacheEntryTypes = new Set(["cache.browser.entries", "cache.server.entries"]);
300
509
  const allTypes = new Set([...declarationTypes, ...cacheTypes, ...cacheEntryTypes]);
@@ -381,11 +590,12 @@
381
590
  const snapshotTarget = snapshotOptions.target ?? target;
382
591
  return {
383
592
  signal: snapshotSignals(backing.signal),
384
- handler: snapshotDescriptors(backing.handler, "handler"),
385
- server: snapshotDescriptors(backing.server, "server"),
386
- partial: snapshotDescriptors(backing.partial, "partial"),
593
+ handler: snapshotDescriptors(backing.handler),
594
+ server: snapshotDescriptors(backing.server),
595
+ partial: snapshotDescriptors(backing.partial),
387
596
  route: snapshotPlain(backing.route),
388
- component: snapshotDescriptors(backing.component, "component"),
597
+ component: snapshotDescriptors(backing.component),
598
+ asyncSignal: snapshotDescriptors(backing.asyncSignal),
389
599
  cache: {
390
600
  browser: snapshotPlain(backing.cache.browser),
391
601
  server: snapshotPlain(backing.cache.server)
@@ -405,6 +615,7 @@
405
615
  partial: Object.fromEntries(backing.partial),
406
616
  route: Object.fromEntries(backing.route),
407
617
  component: Object.fromEntries(backing.component),
618
+ asyncSignal: Object.fromEntries(backing.asyncSignal),
408
619
  cache: {
409
620
  browser: Object.fromEntries(backing.cache.browser),
410
621
  server: Object.fromEntries(backing.cache.server)
@@ -464,6 +675,7 @@
464
675
  partial: new Map(),
465
676
  route: new Map(),
466
677
  component: new Map(),
678
+ asyncSignal: new Map(),
467
679
  cache: {
468
680
  browser: new Map(),
469
681
  server: new Map()
@@ -482,6 +694,7 @@
482
694
  registry.registerMany("partial", initial.partial);
483
695
  registry.registerMany("route", initial.route);
484
696
  registry.registerMany("component", initial.component);
697
+ registry.registerMany("asyncSignal", initial.asyncSignal);
485
698
  registry.registerMany("cache.browser", initial.cache?.browser);
486
699
  registry.registerMany("cache.server", initial.cache?.server);
487
700
 
@@ -509,7 +722,7 @@
509
722
 
510
723
  function publicValue(type, id, value, options) {
511
724
  if (type === "server" && options.target === "browser") {
512
- return { id, kind: "server" };
725
+ return publicRegistryValue(value, id);
513
726
  }
514
727
  if (cacheEntryTypes.has(type)) {
515
728
  return value?.value;
@@ -529,10 +742,10 @@
529
742
  return snapshot;
530
743
  }
531
744
 
532
- function snapshotDescriptors(map, kind) {
745
+ function snapshotDescriptors(map) {
533
746
  const snapshot = {};
534
- for (const id of map.keys()) {
535
- snapshot[id] = { id, kind };
747
+ for (const [id, value] of map) {
748
+ snapshot[id] = publicRegistryValue(value, id);
536
749
  }
537
750
  return snapshot;
538
751
  }
@@ -809,6 +1022,7 @@
809
1022
  const __signalsModule = (() => {
810
1023
  const { asyncSignal: createAsyncSignal, isAsyncSignal } = __asyncSignalModule;
811
1024
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1025
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
812
1026
  const signalKind = Symbol.for("@async/framework.signal");
813
1027
  const computedKind = Symbol.for("@async/framework.computed");
814
1028
  const effectKind = Symbol.for("@async/framework.effect");
@@ -930,6 +1144,8 @@
930
1144
  const registryStore = options.registry ?? createRegistryStore();
931
1145
  const type = options.type ?? "signal";
932
1146
  const entries = registryStore._map(type);
1147
+ const asyncDescriptors = registryStore._map("asyncSignal");
1148
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
933
1149
  const registryCleanups = new Map();
934
1150
  const runtimeContext = {};
935
1151
  const boundEntries = new Set();
@@ -970,6 +1186,7 @@
970
1186
 
971
1187
  ensure(id, initial) {
972
1188
  assertId(id);
1189
+ materializeAsyncSignal(id);
973
1190
  if (!entries.has(id)) {
974
1191
  registry.register(id, createSignal(initial));
975
1192
  }
@@ -977,18 +1194,18 @@
977
1194
  },
978
1195
 
979
1196
  has(id) {
980
- return entries.has(id);
1197
+ return entries.has(id) || asyncDescriptors.has(id);
981
1198
  },
982
1199
 
983
1200
  get(path) {
984
- const parsed = parsePath(path, entries);
1201
+ const parsed = parseRegistryPath(path);
985
1202
  track(parsed.path);
986
1203
  const entry = requireEntry(entries, parsed.id);
987
1204
  return readEntry(entry, parsed.parts);
988
1205
  },
989
1206
 
990
1207
  set(path, value) {
991
- const parsed = parsePath(path, entries);
1208
+ const parsed = parseRegistryPath(path);
992
1209
  const entry = requireEntry(entries, parsed.id);
993
1210
  if (parsed.parts.length === 0) {
994
1211
  return entry.set(value);
@@ -1007,6 +1224,7 @@
1007
1224
 
1008
1225
  ref(id) {
1009
1226
  assertId(id);
1227
+ materializeAsyncSignal(id);
1010
1228
  return createRef(registry, id);
1011
1229
  },
1012
1230
 
@@ -1014,7 +1232,7 @@
1014
1232
  if (typeof fn !== "function") {
1015
1233
  throw new TypeError("subscribe(path, fn) requires a function.");
1016
1234
  }
1017
- const parsed = parsePath(path, entries);
1235
+ const parsed = parseRegistryPath(path);
1018
1236
  const entry = requireEntry(entries, parsed.id);
1019
1237
  const subscriptionId = ++subscriptionCounter;
1020
1238
  return entry.subscribe(() => {
@@ -1120,6 +1338,7 @@
1120
1338
  },
1121
1339
 
1122
1340
  _entry(id) {
1341
+ materializeAsyncSignal(id);
1123
1342
  return requireEntry(entries, id);
1124
1343
  },
1125
1344
 
@@ -1159,6 +1378,42 @@
1159
1378
  }
1160
1379
  }
1161
1380
 
1381
+ function parseRegistryPath(path) {
1382
+ if (typeof path !== "string" || path.length === 0) {
1383
+ throw new TypeError("Signal path must be a non-empty string.");
1384
+ }
1385
+ const segments = path.split(".");
1386
+ for (let end = segments.length; end > 0; end -= 1) {
1387
+ const id = segments.slice(0, end).join(".");
1388
+ if (entries.has(id) || asyncDescriptors.has(id)) {
1389
+ materializeAsyncSignal(id);
1390
+ return { id, parts: segments.slice(end), path };
1391
+ }
1392
+ }
1393
+ const [id, ...parts] = segments;
1394
+ return { id, parts, path };
1395
+ }
1396
+
1397
+ function materializeAsyncSignal(id) {
1398
+ if (entries.has(id) || !asyncDescriptors.has(id)) {
1399
+ return;
1400
+ }
1401
+ const descriptor = asyncDescriptors.get(id);
1402
+ if (!isLazyDescriptor(descriptor) && typeof descriptor !== "function") {
1403
+ throw new TypeError(`Async signal "${id}" must be a function or lazy descriptor.`);
1404
+ }
1405
+ const loader = async function runLazyAsyncSignal(...args) {
1406
+ const resolved = await lazyRegistry.resolve("asyncSignal", id, descriptor);
1407
+ if (typeof resolved !== "function") {
1408
+ throw new TypeError(`Async signal "${id}" did not resolve to a function.`);
1409
+ }
1410
+ return resolved.apply(this, args);
1411
+ };
1412
+ const entry = createAsyncSignal(id, loader);
1413
+ entries.set(id, entry);
1414
+ bindEntry(id, entry);
1415
+ }
1416
+
1162
1417
  function scheduleCallback(fn, options = {}) {
1163
1418
  const scheduler = options.scheduler;
1164
1419
  if (!scheduler || options.phase === "sync") {
@@ -1524,6 +1779,7 @@
1524
1779
  const { attributeName } = __attributesModule;
1525
1780
  const { escapeHtml, rawHtml, renderTemplate } = __htmlModule;
1526
1781
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1782
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
1527
1783
  const componentKind = Symbol.for("@async/framework.component");
1528
1784
  let componentCounter = 0;
1529
1785
 
@@ -1544,13 +1800,15 @@
1544
1800
  const registryStore = options.registry ?? createRegistryStore();
1545
1801
  const type = options.type ?? "component";
1546
1802
  const entries = registryStore._map(type);
1803
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
1804
+ const lazyComponents = new Map();
1547
1805
 
1548
1806
  const registry = attachRegistryInspection({
1549
1807
  register(id, Component) {
1550
1808
  if (typeof id !== "string" || id.length === 0) {
1551
1809
  throw new TypeError("Component id must be a non-empty string.");
1552
1810
  }
1553
- if (!isComponent(Component) && typeof Component !== "function") {
1811
+ if (!isComponent(Component) && typeof Component !== "function" && !isLazyDescriptor(Component)) {
1554
1812
  throw new TypeError(`Component "${id}" must be a component function.`);
1555
1813
  }
1556
1814
  if (entries.has(id)) {
@@ -1571,6 +1829,7 @@
1571
1829
  if (typeof id !== "string" || id.length === 0) {
1572
1830
  throw new TypeError("Component id must be a non-empty string.");
1573
1831
  }
1832
+ lazyComponents.delete(id);
1574
1833
  return entries.delete(id);
1575
1834
  },
1576
1835
 
@@ -1578,7 +1837,20 @@
1578
1837
  if (typeof id !== "string" || id.length === 0) {
1579
1838
  throw new TypeError("Component id must be a non-empty string.");
1580
1839
  }
1581
- return entries.get(id);
1840
+ const Component = entries.get(id);
1841
+ if (!isLazyDescriptor(Component)) {
1842
+ return Component;
1843
+ }
1844
+ if (!lazyComponents.has(id)) {
1845
+ lazyComponents.set(id, async function LazyComponent(...args) {
1846
+ const resolved = await lazyRegistry.resolve(type, id, Component);
1847
+ if (typeof resolved !== "function") {
1848
+ throw new TypeError(`Component "${id}" did not resolve to a function.`);
1849
+ }
1850
+ return resolved.apply(this, args);
1851
+ });
1852
+ }
1853
+ return lazyComponents.get(id);
1582
1854
  },
1583
1855
 
1584
1856
  _adoptMany() {
@@ -2243,6 +2515,7 @@
2243
2515
  const __handlersModule = (() => {
2244
2516
  const { applyServerResult, defaultInput, resolveServerCommandArguments, unwrapServerResult } = __serverModule;
2245
2517
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
2518
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
2246
2519
  const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
2247
2520
  const builtInHandlers = {
2248
2521
  prevent: preventDefault,
@@ -2263,11 +2536,13 @@
2263
2536
  const registryStore = options.registry ?? createRegistryStore();
2264
2537
  const type = options.type ?? "handler";
2265
2538
  const handlers = registryStore._map(type);
2539
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
2540
+ const lazyHandlers = new Map();
2266
2541
 
2267
2542
  const registry = attachRegistryInspection({
2268
2543
  register(id, fn) {
2269
2544
  assertId(id);
2270
- if (typeof fn !== "function") {
2545
+ if (typeof fn !== "function" && !isLazyDescriptor(fn)) {
2271
2546
  throw new TypeError(`Handler "${id}" must be a function.`);
2272
2547
  }
2273
2548
  if (handlers.has(id)) {
@@ -2286,12 +2561,26 @@
2286
2561
 
2287
2562
  unregister(id) {
2288
2563
  assertId(id);
2564
+ lazyHandlers.delete(id);
2289
2565
  return handlers.delete(id);
2290
2566
  },
2291
2567
 
2292
2568
  resolve(id) {
2293
2569
  assertId(id);
2294
- return handlers.get(id);
2570
+ const handler = handlers.get(id);
2571
+ if (!isLazyDescriptor(handler)) {
2572
+ return handler;
2573
+ }
2574
+ if (!lazyHandlers.has(id)) {
2575
+ lazyHandlers.set(id, async function runLazyHandler(...args) {
2576
+ const resolved = await lazyRegistry.resolve(type, id, handler);
2577
+ if (typeof resolved !== "function") {
2578
+ throw new TypeError(`Handler "${id}" did not resolve to a function.`);
2579
+ }
2580
+ return resolved.apply(this, args);
2581
+ });
2582
+ }
2583
+ return lazyHandlers.get(id);
2295
2584
  },
2296
2585
 
2297
2586
  async run(ref, context = {}) {
@@ -2605,6 +2894,10 @@
2605
2894
  return api;
2606
2895
  },
2607
2896
 
2897
+ isScopeDestroyed(scope) {
2898
+ return scope !== undefined && destroyedScopes.has(scope);
2899
+ },
2900
+
2608
2901
  inspect() {
2609
2902
  const counts = {};
2610
2903
  for (const [phase, queue] of queues) {
@@ -3077,7 +3370,7 @@
3077
3370
  if (renderingBoundaries.has(boundary)) {
3078
3371
  continue;
3079
3372
  }
3080
- const id = readAttribute(boundary, attributeConfig, "async", "boundary");
3373
+ const id = boundaryIdFor(boundary, attributeConfig);
3081
3374
  if (id == null) {
3082
3375
  continue;
3083
3376
  }
@@ -3394,19 +3687,26 @@
3394
3687
  function collectBoundaryTemplates(boundary, id, attributeConfig) {
3395
3688
  const templates = {};
3396
3689
  for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
3397
- if (readAttribute(template, attributeConfig, "async", "loading") === id) {
3690
+ if (templateMatchesState(template, "loading", id, boundary, attributeConfig)) {
3398
3691
  templates.loading = template;
3399
3692
  }
3400
- if (readAttribute(template, attributeConfig, "async", "ready") === id) {
3693
+ if (templateMatchesState(template, "ready", id, boundary, attributeConfig)) {
3401
3694
  templates.ready = template;
3402
3695
  }
3403
- if (readAttribute(template, attributeConfig, "async", "error") === id) {
3696
+ if (templateMatchesState(template, "error", id, boundary, attributeConfig)) {
3404
3697
  templates.error = template;
3405
3698
  }
3406
3699
  }
3407
3700
  return templates;
3408
3701
  }
3409
3702
 
3703
+ function templateMatchesState(template, state, id, boundary, attributeConfig) {
3704
+ if (readAttribute(template, attributeConfig, "async", state) === id) {
3705
+ return true;
3706
+ }
3707
+ return isAsyncSuspense(boundary) && template.hasAttribute?.(state);
3708
+ }
3709
+
3410
3710
  function chooseBoundaryTemplate(templates, status) {
3411
3711
  if (status === "ready") {
3412
3712
  return templates.ready ?? templates.loading ?? templates.error;
@@ -3454,13 +3754,24 @@
3454
3754
 
3455
3755
  function findBoundary(root, boundaryId, attributeConfig) {
3456
3756
  for (const element of elementsIn(root)) {
3457
- if (readAttribute(element, attributeConfig, "async", "boundary") === String(boundaryId)) {
3757
+ if (boundaryIdFor(element, attributeConfig) === String(boundaryId)) {
3458
3758
  return element;
3459
3759
  }
3460
3760
  }
3461
3761
  return null;
3462
3762
  }
3463
3763
 
3764
+ function boundaryIdFor(element, attributeConfig) {
3765
+ if (isAsyncSuspense(element) && element.hasAttribute?.("for")) {
3766
+ return element.getAttribute("for");
3767
+ }
3768
+ return readAttribute(element, attributeConfig, "async", "boundary");
3769
+ }
3770
+
3771
+ function isAsyncSuspense(element) {
3772
+ return element?.tagName === "ASYNC-SUSPENSE";
3773
+ }
3774
+
3464
3775
  function toFragment(value, documentRef) {
3465
3776
  if (value?.nodeType === 11) {
3466
3777
  return value;
@@ -3493,15 +3804,18 @@
3493
3804
  const __partialsModule = (() => {
3494
3805
  const { isTemplateResult, renderTemplate } = __htmlModule;
3495
3806
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
3807
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
3496
3808
  function createPartialRegistry(initialMap = {}, options = {}) {
3497
3809
  const registryStore = options.registry ?? createRegistryStore();
3498
3810
  const type = options.type ?? "partial";
3499
3811
  const entries = registryStore._map(type);
3812
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
3813
+ const lazyPartials = new Map();
3500
3814
 
3501
3815
  const registry = attachRegistryInspection({
3502
3816
  register(id, fn) {
3503
3817
  assertId(id);
3504
- if (typeof fn !== "function") {
3818
+ if (typeof fn !== "function" && !isLazyDescriptor(fn)) {
3505
3819
  throw new TypeError(`Partial "${id}" must be a function.`);
3506
3820
  }
3507
3821
  if (entries.has(id)) {
@@ -3520,12 +3834,26 @@
3520
3834
 
3521
3835
  unregister(id) {
3522
3836
  assertId(id);
3837
+ lazyPartials.delete(id);
3523
3838
  return entries.delete(id);
3524
3839
  },
3525
3840
 
3526
3841
  resolve(id) {
3527
3842
  assertId(id);
3528
- return entries.get(id);
3843
+ const partial = entries.get(id);
3844
+ if (!isLazyDescriptor(partial)) {
3845
+ return partial;
3846
+ }
3847
+ if (!lazyPartials.has(id)) {
3848
+ lazyPartials.set(id, async function runLazyPartial(...args) {
3849
+ const resolved = await lazyRegistry.resolve(type, id, partial);
3850
+ if (typeof resolved !== "function") {
3851
+ throw new TypeError(`Partial "${id}" did not resolve to a function.`);
3852
+ }
3853
+ return resolved.apply(this, args);
3854
+ });
3855
+ }
3856
+ return lazyPartials.get(id);
3529
3857
  },
3530
3858
 
3531
3859
  async render(id, props = {}, context = {}) {
@@ -4195,7 +4523,8 @@
4195
4523
  const { createSignal, createSignalRegistry } = __signalsModule;
4196
4524
  const { createRegistryStore } = __registryStoreModule;
4197
4525
  const { attributeName, normalizeAttributeConfig } = __attributesModule;
4198
- const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
4526
+ const { createLazyRegistry, defineRegistrySnapshot, sameRegistryValue } = __lazyRegistryModule;
4527
+ const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component", "asyncSignal"]);
4199
4528
 
4200
4529
  function defineApp(initial, options = {}) {
4201
4530
  const registry = createRegistryStore(undefined, { target: "browser" });
@@ -4224,6 +4553,27 @@
4224
4553
  return runtime;
4225
4554
  },
4226
4555
 
4556
+ attachRoot(root) {
4557
+ return ensureRuntime(app).attachRoot(root);
4558
+ },
4559
+
4560
+ detachRoot(root) {
4561
+ return app.runtime?.detachRoot(root) ?? app;
4562
+ },
4563
+
4564
+ applySnapshot(snapshot, snapshotOptions = {}) {
4565
+ if (app.runtime) {
4566
+ app.runtime.applySnapshot(snapshot, snapshotOptions);
4567
+ return app;
4568
+ }
4569
+ appendSnapshotDeclarations(registry, snapshot, snapshotOptions);
4570
+ return app;
4571
+ },
4572
+
4573
+ inspectRoots() {
4574
+ return app.runtime?.inspectRoots() ?? { count: 0, roots: [] };
4575
+ },
4576
+
4227
4577
  _attach(runtime) {
4228
4578
  runtimes.add(runtime);
4229
4579
  return () => app._detach(runtime);
@@ -4249,23 +4599,32 @@
4249
4599
  });
4250
4600
  const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
4251
4601
  const attributes = normalizeAttributeConfig(options.attributes);
4602
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry({
4603
+ registryAssets: options.registryAssets,
4604
+ importModule: options.importModule
4605
+ });
4252
4606
  const registry = options.registry ?? app.registry.view({ target });
4253
- const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
4254
- const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
4607
+ const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal", lazyRegistry });
4608
+ const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler", lazyRegistry });
4255
4609
  const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
4256
4610
  const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
4257
4611
  const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
4258
4612
  const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
4259
- const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
4613
+ const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial", lazyRegistry });
4260
4614
  const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
4261
- const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
4615
+ const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component", lazyRegistry });
4616
+ const hasStartupRoot = options.loader || Object.hasOwn(options, "root");
4617
+ const startupRoot = hasStartupRoot ? options.root : null;
4262
4618
  let loader = options.loader;
4263
4619
  let router = options.router;
4620
+ let routerStarted = false;
4264
4621
  let detach = () => {};
4265
4622
  let started = false;
4266
4623
  let destroyed = false;
4624
+ const rootLoaders = new Map();
4267
4625
 
4268
- applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
4626
+ const snapshotRoot = startupRoot ?? globalThis.document;
4627
+ const initialSnapshot = options.snapshot ?? (target === "browser" ? readSnapshot(snapshotRoot, { attributes }) : undefined);
4269
4628
  attachServerCache(server, serverCache);
4270
4629
 
4271
4630
  const runtime = {
@@ -4294,54 +4653,112 @@
4294
4653
  started = true;
4295
4654
 
4296
4655
  if (target !== "server") {
4297
- loader = loader ?? Loader({
4298
- root: options.root,
4299
- signals,
4300
- handlers,
4301
- server,
4302
- cache: browserCache,
4303
- scheduler,
4304
- attributes
4305
- });
4306
- runtime.loader = loader;
4307
-
4308
4656
  configureServerContext({ cache: browserCache });
4309
4657
  signals._setContext?.({ server, loader, cache: browserCache, scheduler });
4310
4658
 
4311
- loader.start();
4659
+ if (loader) {
4660
+ registerRootLoader(loader.root, loader);
4661
+ loader.start();
4662
+ startRouterFor(loader.root);
4663
+ } else if (startupRoot != null) {
4664
+ runtime.attachRoot(startupRoot);
4665
+ }
4666
+ } else {
4667
+ configureServerContext({ cache: serverCache });
4668
+ signals._setContext?.({ server, cache: serverCache, scheduler });
4669
+ }
4670
+
4671
+ return runtime;
4672
+ },
4312
4673
 
4313
- if (router !== false && (router || shouldStartRouter(routes, options))) {
4314
- router = router ?? createRouter({
4315
- mode: options.mode ?? "ssr-spa",
4316
- root: options.root,
4317
- boundary: options.boundary ?? "route",
4318
- routes,
4319
- loader,
4674
+ use(typeOrModule, entries) {
4675
+ app.use(typeOrModule, entries);
4676
+ return runtime;
4677
+ },
4678
+
4679
+ attachRoot(root) {
4680
+ assertActive();
4681
+ if (target === "server") {
4682
+ throw new Error("Server runtimes cannot attach DOM roots.");
4683
+ }
4684
+ if (!root) {
4685
+ throw new TypeError("runtime.attachRoot(root) requires a root.");
4686
+ }
4687
+ if (rootLoaders.has(root)) {
4688
+ return runtime;
4689
+ }
4690
+
4691
+ const rootLoader = rootLoaders.size === 0 && loader
4692
+ ? loader
4693
+ : Loader({
4694
+ root,
4320
4695
  signals,
4321
4696
  handlers,
4322
4697
  server,
4323
4698
  cache: browserCache,
4324
- partials,
4325
4699
  scheduler,
4326
- fetch: options.fetch,
4327
- routeEndpoint: options.routeEndpoint,
4328
4700
  attributes
4329
4701
  });
4330
- runtime.router = router;
4331
- loader.router = router;
4332
- configureServerContext({ cache: browserCache, router });
4333
- router.start();
4702
+ registerRootLoader(root, rootLoader);
4703
+ rootLoader.start();
4704
+ configureServerContext({ cache: browserCache });
4705
+ signals._setContext?.({ server, loader: runtime.loader, cache: browserCache, scheduler });
4706
+ startRouterFor(root);
4707
+ return runtime;
4708
+ },
4709
+
4710
+ detachRoot(root) {
4711
+ assertActive();
4712
+ if (target === "server") {
4713
+ return runtime;
4714
+ }
4715
+ if (root == null) {
4716
+ for (const rootLoader of new Set(rootLoaders.values())) {
4717
+ rootLoader.destroy?.();
4718
+ }
4719
+ rootLoaders.clear();
4720
+ router?.destroy?.();
4721
+ router = undefined;
4722
+ routerStarted = false;
4723
+ loader = undefined;
4724
+ runtime.loader = undefined;
4725
+ runtime.router = undefined;
4726
+ return runtime;
4727
+ }
4728
+ const rootLoader = rootLoaders.get(root);
4729
+ if (!rootLoader) {
4730
+ return runtime;
4731
+ }
4732
+ rootLoader.destroy?.();
4733
+ rootLoaders.delete(root);
4734
+ if (loader === rootLoader) {
4735
+ router?.destroy?.();
4736
+ router = undefined;
4737
+ routerStarted = false;
4738
+ const next = rootLoaders.values().next().value;
4739
+ loader = next;
4740
+ runtime.loader = next;
4741
+ runtime.router = undefined;
4742
+ if (next) {
4743
+ startRouterFor(next.root);
4334
4744
  }
4335
- } else {
4336
- configureServerContext({ cache: serverCache });
4337
- signals._setContext?.({ server, cache: serverCache, scheduler });
4338
4745
  }
4339
-
4340
4746
  return runtime;
4341
4747
  },
4342
4748
 
4343
- use(typeOrModule, entries) {
4344
- app.use(typeOrModule, entries);
4749
+ inspectRoots() {
4750
+ return {
4751
+ count: rootLoaders.size,
4752
+ roots: [...rootLoaders].map(([root, rootLoader]) => ({
4753
+ root,
4754
+ loader: rootLoader,
4755
+ primary: rootLoader === loader
4756
+ }))
4757
+ };
4758
+ },
4759
+
4760
+ applySnapshot(snapshot, snapshotOptions = {}) {
4761
+ applySnapshotToRuntime(runtime, snapshot, snapshotOptions);
4345
4762
  return runtime;
4346
4763
  },
4347
4764
 
@@ -4403,7 +4820,14 @@
4403
4820
  destroyed = true;
4404
4821
  detach();
4405
4822
  router?.destroy?.();
4406
- loader?.destroy?.();
4823
+ const destroyedLoaders = new Set(rootLoaders.values());
4824
+ for (const rootLoader of destroyedLoaders) {
4825
+ rootLoader.destroy?.();
4826
+ }
4827
+ rootLoaders.clear();
4828
+ if (loader && !destroyedLoaders.has(loader)) {
4829
+ loader?.destroy?.();
4830
+ }
4407
4831
  signals.destroy?.();
4408
4832
  if (ownsScheduler) {
4409
4833
  scheduler.destroy();
@@ -4417,10 +4841,49 @@
4417
4841
 
4418
4842
  server.cache = serverCache;
4419
4843
  runtime.server.cache = serverCache;
4844
+ runtime.applySnapshot(initialSnapshot, { strict: options.strictSnapshots ?? true });
4420
4845
  detach = app._attach(runtime);
4421
4846
 
4422
4847
  return runtime;
4423
4848
 
4849
+ function registerRootLoader(root, rootLoader) {
4850
+ rootLoaders.set(root, rootLoader);
4851
+ if (!loader) {
4852
+ loader = rootLoader;
4853
+ runtime.loader = rootLoader;
4854
+ }
4855
+ rootLoader.server = server;
4856
+ rootLoader.cache = browserCache;
4857
+ rootLoader.scheduler = scheduler;
4858
+ }
4859
+
4860
+ function startRouterFor(root) {
4861
+ if (router === false || routerStarted || !(router || shouldStartRouter(routes, options)) || !runtime.loader) {
4862
+ return;
4863
+ }
4864
+ router = router ?? createRouter({
4865
+ mode: options.mode ?? "ssr-spa",
4866
+ root,
4867
+ boundary: options.boundary ?? "route",
4868
+ routes,
4869
+ loader: runtime.loader,
4870
+ signals,
4871
+ handlers,
4872
+ server,
4873
+ cache: browserCache,
4874
+ partials,
4875
+ scheduler,
4876
+ fetch: options.fetch,
4877
+ routeEndpoint: options.routeEndpoint,
4878
+ attributes
4879
+ });
4880
+ runtime.router = router;
4881
+ runtime.loader.router = router;
4882
+ configureServerContext({ cache: browserCache, router });
4883
+ router.start();
4884
+ routerStarted = true;
4885
+ }
4886
+
4424
4887
  function configureServerContext(extra = {}) {
4425
4888
  const cache = isLocalServerRegistry(server) ? serverCache : extra.cache;
4426
4889
  server._setContext?.({
@@ -4463,6 +4926,7 @@
4463
4926
  return {};
4464
4927
  }
4465
4928
 
4929
+ const merged = {};
4466
4930
  for (const searchRoot of new Set([rootNode, documentRef])) {
4467
4931
  if (!searchRoot?.querySelectorAll) {
4468
4932
  continue;
@@ -4473,17 +4937,19 @@
4473
4937
  }
4474
4938
  const source = script.textContent?.trim() ?? "";
4475
4939
  if (!source) {
4476
- return {};
4940
+ continue;
4477
4941
  }
4942
+ let parsed;
4478
4943
  try {
4479
- return JSON.parse(source);
4944
+ parsed = JSON.parse(source);
4480
4945
  } catch (cause) {
4481
4946
  throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
4482
4947
  }
4948
+ mergeSnapshot(merged, parsed, { strict: true });
4483
4949
  }
4484
4950
  }
4485
4951
 
4486
- return {};
4952
+ return merged;
4487
4953
  }
4488
4954
 
4489
4955
  function applyUseToRuntime(runtime, normalized) {
@@ -4493,10 +4959,22 @@
4493
4959
  applyRegistryUse(runtime.partials, runtime.registry, normalized.partial);
4494
4960
  applyRegistryUse(runtime.routes, runtime.registry, normalized.route);
4495
4961
  applyRegistryUse(runtime.components, runtime.registry, normalized.component);
4962
+ applyRegistryStoreUse(runtime.registry, "asyncSignal", normalized.asyncSignal);
4496
4963
  applyRegistryUse(runtime.browser.cache, runtime.registry, normalized.cache.browser);
4497
4964
  applyRegistryUse(runtime.server.cache, runtime.registry, normalized.cache.server);
4498
4965
  }
4499
4966
 
4967
+ function applyRegistryStoreUse(registry, type, entries) {
4968
+ if (!entries || Object.keys(entries).length === 0) {
4969
+ return;
4970
+ }
4971
+ for (const [id, value] of Object.entries(entries)) {
4972
+ if (!registry.has(type, id)) {
4973
+ registry.register(type, id, value);
4974
+ }
4975
+ }
4976
+ }
4977
+
4500
4978
  function applyRegistryUse(registry, runtimeRegistry, entries) {
4501
4979
  if (!entries || Object.keys(entries).length === 0) {
4502
4980
  return;
@@ -4516,6 +4994,7 @@
4516
4994
  partial: {},
4517
4995
  route: {},
4518
4996
  component: {},
4997
+ asyncSignal: {},
4519
4998
  cache: {
4520
4999
  browser: {},
4521
5000
  server: {}
@@ -4571,11 +5050,128 @@
4571
5050
  return Boolean(value && typeof value.use === "function" && typeof value.snapshot === "function" && value.registry);
4572
5051
  }
4573
5052
 
4574
- function applySnapshot(signals, browserCache, snapshot = {}) {
4575
- for (const [path, value] of Object.entries(snapshot.signals ?? {})) {
4576
- setOrRegisterSignal(signals, path, value);
5053
+ function ensureRuntime(app) {
5054
+ if (!app.runtime) {
5055
+ app.start();
5056
+ }
5057
+ return app.runtime;
5058
+ }
5059
+
5060
+ function applySnapshotToRuntime(runtime, snapshot = {}, options = {}) {
5061
+ const normalized = normalizeSnapshot(snapshot);
5062
+ for (const [path, value] of Object.entries(normalized.signal)) {
5063
+ setOrRegisterSignal(runtime.signals, path, value);
5064
+ }
5065
+ runtime.browser.cache.restore(normalized.cache.browser);
5066
+ mergeRegistryEntries(runtime, "handler", normalized.handler, runtime.handlers, options);
5067
+ mergeRegistryEntries(runtime, "server", normalized.server, runtime.server, options);
5068
+ mergeRegistryEntries(runtime, "partial", normalized.partial, runtime.partials, options);
5069
+ mergeRegistryEntries(runtime, "route", normalized.route, runtime.routes, options);
5070
+ mergeRegistryEntries(runtime, "component", normalized.component, runtime.components, options);
5071
+ mergeRegistryEntries(runtime, "asyncSignal", normalized.asyncSignal, null, options);
5072
+ return runtime;
5073
+ }
5074
+
5075
+ function appendSnapshotDeclarations(registry, snapshot = {}, options = {}) {
5076
+ const normalized = normalizeSnapshot(snapshot);
5077
+ for (const [id, value] of Object.entries(normalized.signal)) {
5078
+ registerSnapshotEntry(registry, "signal", id, createSignal(value), options);
5079
+ }
5080
+ for (const type of ["handler", "server", "partial", "route", "component", "asyncSignal"]) {
5081
+ for (const [id, value] of Object.entries(normalized[type])) {
5082
+ registerSnapshotEntry(registry, type, id, value, options);
5083
+ }
5084
+ }
5085
+ }
5086
+
5087
+ function mergeRegistryEntries(runtime, type, entries, concreteRegistry, options = {}) {
5088
+ if (!entries || Object.keys(entries).length === 0) {
5089
+ return;
5090
+ }
5091
+ for (const [id, value] of Object.entries(entries)) {
5092
+ registerSnapshotEntry(runtime.registry, type, id, value, options);
5093
+ }
5094
+ concreteRegistry?._adoptMany?.(entries);
5095
+ }
5096
+
5097
+ function registerSnapshotEntry(registry, type, id, value, options = {}) {
5098
+ const strict = options.strict ?? true;
5099
+ const map = registry._map(type);
5100
+ if (map.has(id)) {
5101
+ if (sameRegistryValue(map.get(id), value) || sameSnapshotValue(map.get(id), value)) {
5102
+ return;
5103
+ }
5104
+ if (strict) {
5105
+ throw new Error(`${type} "${id}" is already registered with a different value.`);
5106
+ }
5107
+ return;
5108
+ }
5109
+ registry.set(type, id, value);
5110
+ }
5111
+
5112
+ function normalizeSnapshot(snapshot = {}) {
5113
+ const normalized = {
5114
+ signal: {
5115
+ ...(snapshot.signals ?? {}),
5116
+ ...(snapshot.signal ?? {})
5117
+ },
5118
+ handler: { ...(snapshot.handler ?? {}) },
5119
+ server: { ...(snapshot.server ?? {}) },
5120
+ partial: { ...(snapshot.partial ?? {}) },
5121
+ route: { ...(snapshot.route ?? {}) },
5122
+ component: { ...(snapshot.component ?? {}) },
5123
+ asyncSignal: { ...(snapshot.asyncSignal ?? {}) },
5124
+ cache: {
5125
+ browser: {
5126
+ ...(snapshot.entries?.browser ?? {}),
5127
+ ...(snapshot.cache?.browser ?? {})
5128
+ }
5129
+ }
5130
+ };
5131
+ return normalized;
5132
+ }
5133
+
5134
+ function mergeSnapshot(target, source, options = {}) {
5135
+ const normalized = normalizeSnapshot(defineRegistrySnapshot(source));
5136
+ target.signal = {
5137
+ ...(target.signal ?? target.signals ?? {}),
5138
+ ...normalized.signal
5139
+ };
5140
+ target.signals = target.signal;
5141
+ target.cache = {
5142
+ ...(target.cache ?? {}),
5143
+ browser: {
5144
+ ...(target.cache?.browser ?? {}),
5145
+ ...normalized.cache.browser
5146
+ }
5147
+ };
5148
+ for (const type of ["handler", "server", "partial", "route", "component", "asyncSignal"]) {
5149
+ target[type] = target[type] ?? {};
5150
+ for (const [id, value] of Object.entries(normalized[type])) {
5151
+ if (Object.hasOwn(target[type], id)) {
5152
+ if (sameRegistryValue(target[type][id], value) || sameSnapshotValue(target[type][id], value)) {
5153
+ continue;
5154
+ }
5155
+ if (options.strict ?? true) {
5156
+ throw new Error(`${type} "${id}" is already declared with a different value.`);
5157
+ }
5158
+ continue;
5159
+ }
5160
+ target[type][id] = value;
5161
+ }
5162
+ }
5163
+ return target;
5164
+ }
5165
+
5166
+ function sameSnapshotValue(left, right) {
5167
+ if (left === right) {
5168
+ return true;
5169
+ }
5170
+ try {
5171
+ return JSON.stringify(left) === JSON.stringify(right);
5172
+ } catch {
5173
+ return false;
4577
5174
  }
4578
- browserCache.restore(snapshot.cache?.browser);
4579
5175
  }
4580
5176
 
4581
5177
  function setOrRegisterSignal(signals, path, value) {
@@ -4720,6 +5316,312 @@
4720
5316
  return { defineApp, createApp, readSnapshot, Async };
4721
5317
  })();
4722
5318
 
5319
+ const __boundaryReceiverModule = (() => {
5320
+ const defaultRecentLimit = 50;
5321
+
5322
+ function createBoundaryReceiver(options = {}) {
5323
+ const loader = options.loader;
5324
+ const signals = options.signals ?? loader?.signals;
5325
+ const cache = options.cache ?? loader?.cache;
5326
+ const scheduler = options.scheduler ?? loader?.scheduler;
5327
+ const router = options.router ?? loader?.router;
5328
+ const recentLimit = options.recentLimit ?? defaultRecentLimit;
5329
+ const throwOnError = options.throwOnError === true;
5330
+ const onApply = typeof options.onApply === "function" ? options.onApply : undefined;
5331
+ const onIgnore = typeof options.onIgnore === "function" ? options.onIgnore : undefined;
5332
+ const onError = typeof options.onError === "function" ? options.onError : undefined;
5333
+ const isScopeDestroyed = typeof options.isScopeDestroyed === "function"
5334
+ ? options.isScopeDestroyed
5335
+ : (scope) => scheduler?.isScopeDestroyed?.(scope) ?? scheduler?.inspectDestroyed?.(scope) ?? false;
5336
+
5337
+ if (!loader || typeof loader.swap !== "function") {
5338
+ throw new TypeError("createBoundaryReceiver(...) requires a loader with swap(boundary, html).");
5339
+ }
5340
+ if (!Number.isInteger(recentLimit) || recentLimit < 0) {
5341
+ throw new TypeError("createBoundaryReceiver(...) recentLimit must be a non-negative integer.");
5342
+ }
5343
+
5344
+ const boundaries = new Map();
5345
+ const recent = [];
5346
+ let destroyed = false;
5347
+
5348
+ const receiver = {
5349
+ async apply(patch) {
5350
+ if (destroyed) {
5351
+ throw new Error("Boundary receiver has been destroyed.");
5352
+ }
5353
+
5354
+ const normalized = validatePatch(patch);
5355
+ const record = boundaryRecord(normalized.boundary);
5356
+ if (normalized.seq <= record.lastSeq) {
5357
+ const result = {
5358
+ status: "ignored-stale",
5359
+ boundary: normalized.boundary,
5360
+ seq: normalized.seq,
5361
+ lastSeq: record.lastSeq
5362
+ };
5363
+ record.ignored += 1;
5364
+ record.lastStatus = result.status;
5365
+ remember(result);
5366
+ onIgnore?.(result, patch);
5367
+ return result;
5368
+ }
5369
+
5370
+ if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
5371
+ const result = {
5372
+ status: "ignored-destroyed",
5373
+ boundary: normalized.boundary,
5374
+ seq: normalized.seq,
5375
+ parentScope: normalized.parentScope
5376
+ };
5377
+ record.ignored += 1;
5378
+ record.lastStatus = result.status;
5379
+ remember(result);
5380
+ onIgnore?.(result, patch);
5381
+ return result;
5382
+ }
5383
+
5384
+ record.lastSeq = normalized.seq;
5385
+
5386
+ if (Object.hasOwn(normalized, "error")) {
5387
+ const error = toStableError(normalized.error);
5388
+ const result = {
5389
+ status: "errored",
5390
+ boundary: normalized.boundary,
5391
+ seq: normalized.seq,
5392
+ error
5393
+ };
5394
+ record.errored += 1;
5395
+ record.lastStatus = result.status;
5396
+ remember(result);
5397
+ onError?.(error, result, patch);
5398
+ if (throwOnError) {
5399
+ throw error;
5400
+ }
5401
+ return result;
5402
+ }
5403
+
5404
+ if (normalized.signals) {
5405
+ if (!signals || typeof signals.set !== "function") {
5406
+ throw new Error("Boundary patch includes signals, but no signal registry is available.");
5407
+ }
5408
+ for (const [path, value] of Object.entries(normalized.signals)) {
5409
+ signals.set(path, value);
5410
+ }
5411
+ }
5412
+
5413
+ if (normalized.cache?.browser) {
5414
+ if (!cache || typeof cache.restore !== "function") {
5415
+ throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
5416
+ }
5417
+ cache.restore(normalized.cache.browser);
5418
+ }
5419
+
5420
+ if (normalized.html != null) {
5421
+ loader.swap(normalized.boundary, normalized.html);
5422
+ }
5423
+
5424
+ await flushScheduler(scheduler, normalized.scope);
5425
+
5426
+ if (normalized.redirect) {
5427
+ await followRedirect(normalized.redirect, router, loader);
5428
+ const result = {
5429
+ status: "redirected",
5430
+ boundary: normalized.boundary,
5431
+ seq: normalized.seq,
5432
+ redirect: normalized.redirect
5433
+ };
5434
+ record.applied += 1;
5435
+ record.lastStatus = result.status;
5436
+ remember(result);
5437
+ onApply?.(result, patch);
5438
+ return result;
5439
+ }
5440
+
5441
+ const result = {
5442
+ status: "applied",
5443
+ boundary: normalized.boundary,
5444
+ seq: normalized.seq
5445
+ };
5446
+ record.applied += 1;
5447
+ record.lastStatus = result.status;
5448
+ remember(result);
5449
+ onApply?.(result, patch);
5450
+ return result;
5451
+ },
5452
+
5453
+ inspect() {
5454
+ const snapshot = {};
5455
+ for (const [boundary, record] of boundaries) {
5456
+ snapshot[boundary] = {
5457
+ lastSeq: record.lastSeq,
5458
+ applied: record.applied,
5459
+ ignored: record.ignored,
5460
+ lastStatus: record.lastStatus
5461
+ };
5462
+ if (record.errored > 0) {
5463
+ snapshot[boundary].errored = record.errored;
5464
+ }
5465
+ }
5466
+ return {
5467
+ destroyed,
5468
+ boundaries: snapshot,
5469
+ recent: recent.map((entry) => ({ ...entry }))
5470
+ };
5471
+ },
5472
+
5473
+ reset(boundary) {
5474
+ if (boundary === undefined) {
5475
+ boundaries.clear();
5476
+ recent.length = 0;
5477
+ return receiver;
5478
+ }
5479
+ assertBoundary(boundary);
5480
+ boundaries.delete(boundary);
5481
+ for (let index = recent.length - 1; index >= 0; index -= 1) {
5482
+ if (recent[index].boundary === boundary) {
5483
+ recent.splice(index, 1);
5484
+ }
5485
+ }
5486
+ return receiver;
5487
+ },
5488
+
5489
+ destroy() {
5490
+ destroyed = true;
5491
+ boundaries.clear();
5492
+ recent.length = 0;
5493
+ }
5494
+ };
5495
+
5496
+ return receiver;
5497
+
5498
+ function boundaryRecord(boundary) {
5499
+ if (!boundaries.has(boundary)) {
5500
+ boundaries.set(boundary, {
5501
+ lastSeq: -Infinity,
5502
+ applied: 0,
5503
+ ignored: 0,
5504
+ errored: 0,
5505
+ lastStatus: undefined
5506
+ });
5507
+ }
5508
+ return boundaries.get(boundary);
5509
+ }
5510
+
5511
+ function remember(result) {
5512
+ if (recentLimit === 0) {
5513
+ return;
5514
+ }
5515
+ recent.push(toRecentEntry(result));
5516
+ while (recent.length > recentLimit) {
5517
+ recent.shift();
5518
+ }
5519
+ }
5520
+ }
5521
+
5522
+ function validatePatch(patch) {
5523
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
5524
+ throw new TypeError("receiver.apply(patch) requires a boundary patch object.");
5525
+ }
5526
+
5527
+ assertBoundary(patch.boundary);
5528
+ if (typeof patch.seq !== "number" || !Number.isFinite(patch.seq)) {
5529
+ throw new TypeError("Boundary patch seq must be a finite number.");
5530
+ }
5531
+
5532
+ if (patch.signals !== undefined && !isPlainObject(patch.signals)) {
5533
+ throw new TypeError("Boundary patch signals must be an object.");
5534
+ }
5535
+ if (patch.cache !== undefined && !isPlainObject(patch.cache)) {
5536
+ throw new TypeError("Boundary patch cache must be an object.");
5537
+ }
5538
+ if (patch.cache?.browser !== undefined && !isPlainObject(patch.cache.browser)) {
5539
+ throw new TypeError("Boundary patch cache.browser must be an object.");
5540
+ }
5541
+ if (patch.redirect !== undefined && (typeof patch.redirect !== "string" || patch.redirect.length === 0)) {
5542
+ throw new TypeError("Boundary patch redirect must be a non-empty string.");
5543
+ }
5544
+ if (patch.parentScope !== undefined && typeof patch.parentScope !== "string") {
5545
+ throw new TypeError("Boundary patch parentScope must be a string.");
5546
+ }
5547
+ if (patch.scope !== undefined && typeof patch.scope !== "string") {
5548
+ throw new TypeError("Boundary patch scope must be a string.");
5549
+ }
5550
+
5551
+ const hasHtml = Object.hasOwn(patch, "html") && patch.html != null;
5552
+ const hasSignals = patch.signals && Object.keys(patch.signals).length > 0;
5553
+ const hasBrowserCache = patch.cache?.browser && Object.keys(patch.cache.browser).length > 0;
5554
+ const hasRedirect = Boolean(patch.redirect);
5555
+ const hasError = Object.hasOwn(patch, "error");
5556
+ if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError) {
5557
+ throw new TypeError("Boundary patch must include html, signals, cache.browser, redirect, or error.");
5558
+ }
5559
+
5560
+ return patch;
5561
+ }
5562
+
5563
+ function assertBoundary(boundary) {
5564
+ if (typeof boundary !== "string" || boundary.length === 0) {
5565
+ throw new TypeError("Boundary patch boundary must be a non-empty string.");
5566
+ }
5567
+ }
5568
+
5569
+ async function flushScheduler(scheduler, scope) {
5570
+ if (!scheduler) {
5571
+ return;
5572
+ }
5573
+ if (scope !== undefined && typeof scheduler.flushScope === "function") {
5574
+ await scheduler.flushScope(scope);
5575
+ return;
5576
+ }
5577
+ if (typeof scheduler.flush === "function") {
5578
+ await scheduler.flush();
5579
+ }
5580
+ }
5581
+
5582
+ async function followRedirect(redirect, router, loader) {
5583
+ if (router && typeof router.navigate === "function") {
5584
+ await router.navigate(redirect);
5585
+ return;
5586
+ }
5587
+ const location = loader?.root?.ownerDocument?.defaultView?.location ?? globalThis.location;
5588
+ location?.assign?.(redirect);
5589
+ }
5590
+
5591
+ function toStableError(value) {
5592
+ if (value instanceof Error) {
5593
+ return value;
5594
+ }
5595
+ if (value && typeof value === "object" && typeof value.message === "string") {
5596
+ return Object.assign(new Error(value.message), value);
5597
+ }
5598
+ return new Error(String(value));
5599
+ }
5600
+
5601
+ function toRecentEntry(result) {
5602
+ const entry = {
5603
+ boundary: result.boundary,
5604
+ seq: result.seq,
5605
+ status: result.status
5606
+ };
5607
+ if (result.status === "ignored-stale") {
5608
+ entry.lastSeq = result.lastSeq;
5609
+ }
5610
+ if (result.status === "ignored-destroyed" && result.parentScope !== undefined) {
5611
+ entry.parentScope = result.parentScope;
5612
+ }
5613
+ if (result.status === "redirected") {
5614
+ entry.redirect = result.redirect;
5615
+ }
5616
+ return entry;
5617
+ }
5618
+
5619
+ function isPlainObject(value) {
5620
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
5621
+ }
5622
+ return { createBoundaryReceiver };
5623
+ })();
5624
+
4723
5625
  const __delayModule = (() => {
4724
5626
  function delay(ms, signal) {
4725
5627
  if (signal?.aborted) {
@@ -4754,6 +5656,72 @@
4754
5656
  return { delay };
4755
5657
  })();
4756
5658
 
5659
+ const __elementsModule = (() => {
5660
+ const { Async } = __appModule;
5661
+ function defineAsyncContainerElement(options = {}) {
5662
+ const tagName = options.tagName ?? "async-container";
5663
+ const registry = options.customElements ?? globalThis.customElements;
5664
+ if (!registry) {
5665
+ throw new Error("defineAsyncContainerElement(...) requires customElements.");
5666
+ }
5667
+ const existing = registry.get(tagName);
5668
+ if (existing) {
5669
+ return existing;
5670
+ }
5671
+ const app = options.app ?? options.Async ?? Async;
5672
+ const HTMLElementBase = options.HTMLElement ?? options.window?.HTMLElement ?? globalThis.HTMLElement;
5673
+ if (!HTMLElementBase) {
5674
+ throw new Error("defineAsyncContainerElement(...) requires HTMLElement.");
5675
+ }
5676
+
5677
+ class AsyncContainerElement extends HTMLElementBase {
5678
+ connectedCallback() {
5679
+ if (this.__asyncAttached) {
5680
+ return;
5681
+ }
5682
+ const runtime = app.runtime ?? app.start?.();
5683
+ runtime?.attachRoot?.(this);
5684
+ this.__asyncRuntime = runtime;
5685
+ this.__asyncAttached = true;
5686
+ }
5687
+
5688
+ disconnectedCallback() {
5689
+ if (!this.__asyncAttached) {
5690
+ return;
5691
+ }
5692
+ this.__asyncRuntime?.detachRoot?.(this);
5693
+ this.__asyncRuntime = undefined;
5694
+ this.__asyncAttached = false;
5695
+ }
5696
+ }
5697
+
5698
+ registry.define(tagName, AsyncContainerElement);
5699
+ return AsyncContainerElement;
5700
+ }
5701
+
5702
+ function defineAsyncSuspenseElement(options = {}) {
5703
+ const tagName = options.tagName ?? "async-suspense";
5704
+ const registry = options.customElements ?? globalThis.customElements;
5705
+ if (!registry) {
5706
+ throw new Error("defineAsyncSuspenseElement(...) requires customElements.");
5707
+ }
5708
+ const existing = registry.get(tagName);
5709
+ if (existing) {
5710
+ return existing;
5711
+ }
5712
+ const HTMLElementBase = options.HTMLElement ?? options.window?.HTMLElement ?? globalThis.HTMLElement;
5713
+ if (!HTMLElementBase) {
5714
+ throw new Error("defineAsyncSuspenseElement(...) requires HTMLElement.");
5715
+ }
5716
+
5717
+ class AsyncSuspenseElement extends HTMLElementBase {}
5718
+
5719
+ registry.define(tagName, AsyncSuspenseElement);
5720
+ return AsyncSuspenseElement;
5721
+ }
5722
+ return { defineAsyncContainerElement, defineAsyncSuspenseElement };
5723
+ })();
5724
+
4757
5725
  const { asyncSignal: asyncSignal } = __asyncSignalModule;
4758
5726
  const { Async: Async } = __appModule;
4759
5727
  const { createApp: createApp } = __appModule;
@@ -4761,14 +5729,19 @@
4761
5729
  const { readSnapshot: readSnapshot } = __appModule;
4762
5730
  const { attributeName: attributeName } = __attributesModule;
4763
5731
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
5732
+ const { createBoundaryReceiver: createBoundaryReceiver } = __boundaryReceiverModule;
4764
5733
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
4765
5734
  const { defineCache: defineCache } = __cacheModule;
4766
5735
  const { component: component } = __componentModule;
4767
5736
  const { createComponentRegistry: createComponentRegistry } = __componentModule;
4768
5737
  const { defineComponent: defineComponent } = __componentModule;
4769
5738
  const { delay: delay } = __delayModule;
5739
+ const { defineAsyncContainerElement: defineAsyncContainerElement } = __elementsModule;
5740
+ const { defineAsyncSuspenseElement: defineAsyncSuspenseElement } = __elementsModule;
4770
5741
  const { createHandlerRegistry: createHandlerRegistry } = __handlersModule;
4771
5742
  const { html: html } = __htmlModule;
5743
+ const { createLazyRegistry: createLazyRegistry } = __lazyRegistryModule;
5744
+ const { defineRegistrySnapshot: defineRegistrySnapshot } = __lazyRegistryModule;
4772
5745
  const { Loader: Loader } = __loaderModule;
4773
5746
  const { AsyncLoader: AsyncLoader } = __loaderModule;
4774
5747
  const { createPartialRegistry: createPartialRegistry } = __partialsModule;
@@ -4787,7 +5760,7 @@
4787
5760
  const { createSignalRegistry: createSignalRegistry } = __signalsModule;
4788
5761
  const { effect: effect } = __signalsModule;
4789
5762
  const { signal: signal } = __signalsModule;
4790
- const api = { asyncSignal, Async, createApp, defineApp, readSnapshot, attributeName, defineAttributeConfig, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, createHandlerRegistry, html, Loader, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createScheduler, applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult, computed, createSignal, createSignalRegistry, effect, signal };
5763
+ const api = { asyncSignal, Async, createApp, defineApp, readSnapshot, attributeName, defineAttributeConfig, createBoundaryReceiver, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, defineAsyncContainerElement, defineAsyncSuspenseElement, createHandlerRegistry, html, createLazyRegistry, defineRegistrySnapshot, Loader, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createScheduler, applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult, computed, createSignal, createSignalRegistry, effect, signal };
4791
5764
  assertNoUmdNamespaceConflicts(api, Async);
4792
5765
  Object.assign(Async, api);
4793
5766
  Async.Async = Async;