@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.ts CHANGED
@@ -283,8 +283,217 @@ const __asyncSignalModule = (() => {
283
283
  return { asyncSignal, isAsyncSignal };
284
284
  })();
285
285
 
286
+ const __lazyRegistryModule = (() => {
287
+ const descriptorTypes = new Set(["handler", "component", "asyncSignal", "partial", "route"]);
288
+ const defaultBaseUrl = "_async";
289
+
290
+ function defineRegistrySnapshot(snapshot = {}) {
291
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
292
+ throw new TypeError("defineRegistrySnapshot(snapshot) requires an object.");
293
+ }
294
+ return snapshot;
295
+ }
296
+
297
+ function createLazyRegistry(options = {}) {
298
+ const registryAssets = normalizeRegistryAssets(options.registryAssets ?? options.assets);
299
+ const importModule = options.importModule ?? ((url) => import(url));
300
+ const moduleCache = new Map();
301
+ const exportCache = new Map();
302
+
303
+ return {
304
+ registryAssets,
305
+
306
+ resolveUrl(type, id, descriptor) {
307
+ return resolveDescriptorUrl(type, id, descriptor, registryAssets);
308
+ },
309
+
310
+ async resolve(type, id, descriptor) {
311
+ if (!isLazyDescriptor(descriptor)) {
312
+ return descriptor;
313
+ }
314
+ const cacheKey = `${type}:${id}`;
315
+ if (exportCache.has(cacheKey)) {
316
+ return exportCache.get(cacheKey);
317
+ }
318
+
319
+ const resolved = resolveDescriptorUrl(type, id, descriptor, registryAssets);
320
+ let modulePromise = moduleCache.get(resolved.moduleUrl);
321
+ if (!modulePromise) {
322
+ modulePromise = Promise.resolve(importModule(resolved.moduleUrl));
323
+ moduleCache.set(resolved.moduleUrl, modulePromise);
324
+ }
325
+ const module = await modulePromise;
326
+ const value = resolveExport(module, resolved.exportNames, type, id);
327
+ exportCache.set(cacheKey, value);
328
+ return value;
329
+ },
330
+
331
+ inspect() {
332
+ return {
333
+ registryAssets,
334
+ modules: [...moduleCache.keys()],
335
+ exports: [...exportCache.keys()]
336
+ };
337
+ }
338
+ };
339
+ }
340
+
341
+ function normalizeRegistryAssets(options = {}) {
342
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultBaseUrl);
343
+ const paths = {
344
+ component: "component",
345
+ handler: "handler",
346
+ asyncSignal: "asyncSignal",
347
+ partial: "partial",
348
+ route: "route",
349
+ ...(options.paths ?? {})
350
+ };
351
+
352
+ for (const [type, value] of Object.entries(paths)) {
353
+ if (!descriptorTypes.has(type)) {
354
+ continue;
355
+ }
356
+ if (typeof value !== "string" || value.length === 0) {
357
+ throw new TypeError(`Registry asset path for "${type}" must be a non-empty string.`);
358
+ }
359
+ }
360
+
361
+ return {
362
+ baseUrl,
363
+ paths
364
+ };
365
+ }
366
+
367
+ function isLazyDescriptor(value) {
368
+ return Boolean(
369
+ value &&
370
+ typeof value === "object" &&
371
+ !Array.isArray(value) &&
372
+ typeof value.url === "string"
373
+ );
374
+ }
375
+
376
+ function sameRegistryValue(left, right) {
377
+ if (left === right) {
378
+ return true;
379
+ }
380
+ if (isLazyDescriptor(left) && isLazyDescriptor(right)) {
381
+ return stableStringify(left) === stableStringify(right);
382
+ }
383
+ return false;
384
+ }
385
+
386
+ function publicRegistryValue(value, id) {
387
+ if (isLazyDescriptor(value)) {
388
+ return { ...value };
389
+ }
390
+ return { id };
391
+ }
392
+
393
+ function resolveDescriptorUrl(type, id, descriptor, registryAssets) {
394
+ if (!descriptorTypes.has(type)) {
395
+ throw new Error(`Registry type "${type}" does not support lazy descriptors.`);
396
+ }
397
+ if (!isLazyDescriptor(descriptor)) {
398
+ throw new TypeError(`Registry descriptor for "${type}:${id}" requires a url.`);
399
+ }
400
+
401
+ const { path, hash } = splitHash(descriptor.url);
402
+ const moduleUrl = resolveModuleUrl(type, path, registryAssets);
403
+ const exportNames = hash
404
+ ? [hash]
405
+ : inferredExportNames(id, path);
406
+
407
+ return {
408
+ moduleUrl,
409
+ exportNames,
410
+ url: hash ? `${moduleUrl}#${hash}` : moduleUrl
411
+ };
412
+ }
413
+
414
+ function resolveModuleUrl(type, path, registryAssets) {
415
+ if (isAbsoluteUrl(path) || path.startsWith("/") || path.startsWith("./") || path.startsWith("../")) {
416
+ return path;
417
+ }
418
+ const typePath = registryAssets.paths[type] ?? type;
419
+ return joinUrl(registryAssets.baseUrl, typePath, path);
420
+ }
421
+
422
+ function resolveExport(module, exportNames, type, id) {
423
+ for (const name of exportNames) {
424
+ if (name in module) {
425
+ return module[name];
426
+ }
427
+ }
428
+ throw new Error(`Lazy ${type} "${id}" did not export ${exportNames.map((name) => `"${name}"`).join(", ")}.`);
429
+ }
430
+
431
+ function inferredExportNames(id, path) {
432
+ const names = [];
433
+ const leaf = id.split(".").filter(Boolean).at(-1);
434
+ const basename = path
435
+ .split("/")
436
+ .filter(Boolean)
437
+ .at(-1)
438
+ ?.replace(/\.[^.]+$/, "");
439
+ for (const name of [leaf, basename, "default"]) {
440
+ if (name && !names.includes(name)) {
441
+ names.push(name);
442
+ }
443
+ }
444
+ return names;
445
+ }
446
+
447
+ function splitHash(url) {
448
+ const index = url.indexOf("#");
449
+ if (index === -1) {
450
+ return { path: url, hash: "" };
451
+ }
452
+ return {
453
+ path: url.slice(0, index),
454
+ hash: url.slice(index + 1)
455
+ };
456
+ }
457
+
458
+ function normalizeBaseUrl(baseUrl) {
459
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) {
460
+ throw new TypeError("registryAssets.baseUrl must be a non-empty string.");
461
+ }
462
+ if (isAbsoluteUrl(baseUrl) || baseUrl.startsWith("/") || baseUrl.startsWith("./") || baseUrl.startsWith("../")) {
463
+ return stripTrailingSlash(baseUrl);
464
+ }
465
+ return `/${stripSlashes(baseUrl)}`;
466
+ }
467
+
468
+ function joinUrl(...parts) {
469
+ const [first, ...rest] = parts;
470
+ return [stripTrailingSlash(first), ...rest.map(stripSlashes)].filter(Boolean).join("/");
471
+ }
472
+
473
+ function stripSlashes(value) {
474
+ return String(value).replace(/^\/+|\/+$/g, "");
475
+ }
476
+
477
+ function stripTrailingSlash(value) {
478
+ return String(value).replace(/\/+$/g, "");
479
+ }
480
+
481
+ function isAbsoluteUrl(value) {
482
+ return /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
483
+ }
484
+
485
+ function stableStringify(value) {
486
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
487
+ return JSON.stringify(value);
488
+ }
489
+ return JSON.stringify(Object.keys(value).sort().map((key) => [key, value[key]]));
490
+ }
491
+ return { defineRegistrySnapshot, createLazyRegistry, normalizeRegistryAssets, isLazyDescriptor, sameRegistryValue, publicRegistryValue };
492
+ })();
493
+
286
494
  const __registryStoreModule = (() => {
287
- const declarationTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
495
+ const { publicRegistryValue } = __lazyRegistryModule;
496
+ const declarationTypes = new Set(["signal", "handler", "server", "partial", "route", "component", "asyncSignal"]);
288
497
  const cacheTypes = new Set(["cache.browser", "cache.server"]);
289
498
  const cacheEntryTypes = new Set(["cache.browser.entries", "cache.server.entries"]);
290
499
  const allTypes = new Set([...declarationTypes, ...cacheTypes, ...cacheEntryTypes]);
@@ -371,11 +580,12 @@ const __registryStoreModule = (() => {
371
580
  const snapshotTarget = snapshotOptions.target ?? target;
372
581
  return {
373
582
  signal: snapshotSignals(backing.signal),
374
- handler: snapshotDescriptors(backing.handler, "handler"),
375
- server: snapshotDescriptors(backing.server, "server"),
376
- partial: snapshotDescriptors(backing.partial, "partial"),
583
+ handler: snapshotDescriptors(backing.handler),
584
+ server: snapshotDescriptors(backing.server),
585
+ partial: snapshotDescriptors(backing.partial),
377
586
  route: snapshotPlain(backing.route),
378
- component: snapshotDescriptors(backing.component, "component"),
587
+ component: snapshotDescriptors(backing.component),
588
+ asyncSignal: snapshotDescriptors(backing.asyncSignal),
379
589
  cache: {
380
590
  browser: snapshotPlain(backing.cache.browser),
381
591
  server: snapshotPlain(backing.cache.server)
@@ -395,6 +605,7 @@ const __registryStoreModule = (() => {
395
605
  partial: Object.fromEntries(backing.partial),
396
606
  route: Object.fromEntries(backing.route),
397
607
  component: Object.fromEntries(backing.component),
608
+ asyncSignal: Object.fromEntries(backing.asyncSignal),
398
609
  cache: {
399
610
  browser: Object.fromEntries(backing.cache.browser),
400
611
  server: Object.fromEntries(backing.cache.server)
@@ -454,6 +665,7 @@ const __registryStoreModule = (() => {
454
665
  partial: new Map(),
455
666
  route: new Map(),
456
667
  component: new Map(),
668
+ asyncSignal: new Map(),
457
669
  cache: {
458
670
  browser: new Map(),
459
671
  server: new Map()
@@ -472,6 +684,7 @@ const __registryStoreModule = (() => {
472
684
  registry.registerMany("partial", initial.partial);
473
685
  registry.registerMany("route", initial.route);
474
686
  registry.registerMany("component", initial.component);
687
+ registry.registerMany("asyncSignal", initial.asyncSignal);
475
688
  registry.registerMany("cache.browser", initial.cache?.browser);
476
689
  registry.registerMany("cache.server", initial.cache?.server);
477
690
 
@@ -499,7 +712,7 @@ const __registryStoreModule = (() => {
499
712
 
500
713
  function publicValue(type, id, value, options) {
501
714
  if (type === "server" && options.target === "browser") {
502
- return { id, kind: "server" };
715
+ return publicRegistryValue(value, id);
503
716
  }
504
717
  if (cacheEntryTypes.has(type)) {
505
718
  return value?.value;
@@ -519,10 +732,10 @@ const __registryStoreModule = (() => {
519
732
  return snapshot;
520
733
  }
521
734
 
522
- function snapshotDescriptors(map, kind) {
735
+ function snapshotDescriptors(map) {
523
736
  const snapshot = {};
524
- for (const id of map.keys()) {
525
- snapshot[id] = { id, kind };
737
+ for (const [id, value] of map) {
738
+ snapshot[id] = publicRegistryValue(value, id);
526
739
  }
527
740
  return snapshot;
528
741
  }
@@ -799,6 +1012,7 @@ const __attributesModule = (() => {
799
1012
  const __signalsModule = (() => {
800
1013
  const { asyncSignal: createAsyncSignal, isAsyncSignal } = __asyncSignalModule;
801
1014
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1015
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
802
1016
  const signalKind = Symbol.for("@async/framework.signal");
803
1017
  const computedKind = Symbol.for("@async/framework.computed");
804
1018
  const effectKind = Symbol.for("@async/framework.effect");
@@ -920,6 +1134,8 @@ const __signalsModule = (() => {
920
1134
  const registryStore = options.registry ?? createRegistryStore();
921
1135
  const type = options.type ?? "signal";
922
1136
  const entries = registryStore._map(type);
1137
+ const asyncDescriptors = registryStore._map("asyncSignal");
1138
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
923
1139
  const registryCleanups = new Map();
924
1140
  const runtimeContext = {};
925
1141
  const boundEntries = new Set();
@@ -960,6 +1176,7 @@ const __signalsModule = (() => {
960
1176
 
961
1177
  ensure(id, initial) {
962
1178
  assertId(id);
1179
+ materializeAsyncSignal(id);
963
1180
  if (!entries.has(id)) {
964
1181
  registry.register(id, createSignal(initial));
965
1182
  }
@@ -967,18 +1184,18 @@ const __signalsModule = (() => {
967
1184
  },
968
1185
 
969
1186
  has(id) {
970
- return entries.has(id);
1187
+ return entries.has(id) || asyncDescriptors.has(id);
971
1188
  },
972
1189
 
973
1190
  get(path) {
974
- const parsed = parsePath(path, entries);
1191
+ const parsed = parseRegistryPath(path);
975
1192
  track(parsed.path);
976
1193
  const entry = requireEntry(entries, parsed.id);
977
1194
  return readEntry(entry, parsed.parts);
978
1195
  },
979
1196
 
980
1197
  set(path, value) {
981
- const parsed = parsePath(path, entries);
1198
+ const parsed = parseRegistryPath(path);
982
1199
  const entry = requireEntry(entries, parsed.id);
983
1200
  if (parsed.parts.length === 0) {
984
1201
  return entry.set(value);
@@ -997,6 +1214,7 @@ const __signalsModule = (() => {
997
1214
 
998
1215
  ref(id) {
999
1216
  assertId(id);
1217
+ materializeAsyncSignal(id);
1000
1218
  return createRef(registry, id);
1001
1219
  },
1002
1220
 
@@ -1004,7 +1222,7 @@ const __signalsModule = (() => {
1004
1222
  if (typeof fn !== "function") {
1005
1223
  throw new TypeError("subscribe(path, fn) requires a function.");
1006
1224
  }
1007
- const parsed = parsePath(path, entries);
1225
+ const parsed = parseRegistryPath(path);
1008
1226
  const entry = requireEntry(entries, parsed.id);
1009
1227
  const subscriptionId = ++subscriptionCounter;
1010
1228
  return entry.subscribe(() => {
@@ -1110,6 +1328,7 @@ const __signalsModule = (() => {
1110
1328
  },
1111
1329
 
1112
1330
  _entry(id) {
1331
+ materializeAsyncSignal(id);
1113
1332
  return requireEntry(entries, id);
1114
1333
  },
1115
1334
 
@@ -1149,6 +1368,42 @@ const __signalsModule = (() => {
1149
1368
  }
1150
1369
  }
1151
1370
 
1371
+ function parseRegistryPath(path) {
1372
+ if (typeof path !== "string" || path.length === 0) {
1373
+ throw new TypeError("Signal path must be a non-empty string.");
1374
+ }
1375
+ const segments = path.split(".");
1376
+ for (let end = segments.length; end > 0; end -= 1) {
1377
+ const id = segments.slice(0, end).join(".");
1378
+ if (entries.has(id) || asyncDescriptors.has(id)) {
1379
+ materializeAsyncSignal(id);
1380
+ return { id, parts: segments.slice(end), path };
1381
+ }
1382
+ }
1383
+ const [id, ...parts] = segments;
1384
+ return { id, parts, path };
1385
+ }
1386
+
1387
+ function materializeAsyncSignal(id) {
1388
+ if (entries.has(id) || !asyncDescriptors.has(id)) {
1389
+ return;
1390
+ }
1391
+ const descriptor = asyncDescriptors.get(id);
1392
+ if (!isLazyDescriptor(descriptor) && typeof descriptor !== "function") {
1393
+ throw new TypeError(`Async signal "${id}" must be a function or lazy descriptor.`);
1394
+ }
1395
+ const loader = async function runLazyAsyncSignal(...args) {
1396
+ const resolved = await lazyRegistry.resolve("asyncSignal", id, descriptor);
1397
+ if (typeof resolved !== "function") {
1398
+ throw new TypeError(`Async signal "${id}" did not resolve to a function.`);
1399
+ }
1400
+ return resolved.apply(this, args);
1401
+ };
1402
+ const entry = createAsyncSignal(id, loader);
1403
+ entries.set(id, entry);
1404
+ bindEntry(id, entry);
1405
+ }
1406
+
1152
1407
  function scheduleCallback(fn, options = {}) {
1153
1408
  const scheduler = options.scheduler;
1154
1409
  if (!scheduler || options.phase === "sync") {
@@ -1514,6 +1769,7 @@ const __componentModule = (() => {
1514
1769
  const { attributeName } = __attributesModule;
1515
1770
  const { escapeHtml, rawHtml, renderTemplate } = __htmlModule;
1516
1771
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1772
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
1517
1773
  const componentKind = Symbol.for("@async/framework.component");
1518
1774
  let componentCounter = 0;
1519
1775
 
@@ -1534,13 +1790,15 @@ const __componentModule = (() => {
1534
1790
  const registryStore = options.registry ?? createRegistryStore();
1535
1791
  const type = options.type ?? "component";
1536
1792
  const entries = registryStore._map(type);
1793
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
1794
+ const lazyComponents = new Map();
1537
1795
 
1538
1796
  const registry = attachRegistryInspection({
1539
1797
  register(id, Component) {
1540
1798
  if (typeof id !== "string" || id.length === 0) {
1541
1799
  throw new TypeError("Component id must be a non-empty string.");
1542
1800
  }
1543
- if (!isComponent(Component) && typeof Component !== "function") {
1801
+ if (!isComponent(Component) && typeof Component !== "function" && !isLazyDescriptor(Component)) {
1544
1802
  throw new TypeError(`Component "${id}" must be a component function.`);
1545
1803
  }
1546
1804
  if (entries.has(id)) {
@@ -1561,6 +1819,7 @@ const __componentModule = (() => {
1561
1819
  if (typeof id !== "string" || id.length === 0) {
1562
1820
  throw new TypeError("Component id must be a non-empty string.");
1563
1821
  }
1822
+ lazyComponents.delete(id);
1564
1823
  return entries.delete(id);
1565
1824
  },
1566
1825
 
@@ -1568,7 +1827,20 @@ const __componentModule = (() => {
1568
1827
  if (typeof id !== "string" || id.length === 0) {
1569
1828
  throw new TypeError("Component id must be a non-empty string.");
1570
1829
  }
1571
- return entries.get(id);
1830
+ const Component = entries.get(id);
1831
+ if (!isLazyDescriptor(Component)) {
1832
+ return Component;
1833
+ }
1834
+ if (!lazyComponents.has(id)) {
1835
+ lazyComponents.set(id, async function LazyComponent(...args) {
1836
+ const resolved = await lazyRegistry.resolve(type, id, Component);
1837
+ if (typeof resolved !== "function") {
1838
+ throw new TypeError(`Component "${id}" did not resolve to a function.`);
1839
+ }
1840
+ return resolved.apply(this, args);
1841
+ });
1842
+ }
1843
+ return lazyComponents.get(id);
1572
1844
  },
1573
1845
 
1574
1846
  _adoptMany() {
@@ -2233,6 +2505,7 @@ const __serverModule = (() => {
2233
2505
  const __handlersModule = (() => {
2234
2506
  const { applyServerResult, defaultInput, resolveServerCommandArguments, unwrapServerResult } = __serverModule;
2235
2507
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
2508
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
2236
2509
  const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
2237
2510
  const builtInHandlers = {
2238
2511
  prevent: preventDefault,
@@ -2253,11 +2526,13 @@ const __handlersModule = (() => {
2253
2526
  const registryStore = options.registry ?? createRegistryStore();
2254
2527
  const type = options.type ?? "handler";
2255
2528
  const handlers = registryStore._map(type);
2529
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
2530
+ const lazyHandlers = new Map();
2256
2531
 
2257
2532
  const registry = attachRegistryInspection({
2258
2533
  register(id, fn) {
2259
2534
  assertId(id);
2260
- if (typeof fn !== "function") {
2535
+ if (typeof fn !== "function" && !isLazyDescriptor(fn)) {
2261
2536
  throw new TypeError(`Handler "${id}" must be a function.`);
2262
2537
  }
2263
2538
  if (handlers.has(id)) {
@@ -2276,12 +2551,26 @@ const __handlersModule = (() => {
2276
2551
 
2277
2552
  unregister(id) {
2278
2553
  assertId(id);
2554
+ lazyHandlers.delete(id);
2279
2555
  return handlers.delete(id);
2280
2556
  },
2281
2557
 
2282
2558
  resolve(id) {
2283
2559
  assertId(id);
2284
- return handlers.get(id);
2560
+ const handler = handlers.get(id);
2561
+ if (!isLazyDescriptor(handler)) {
2562
+ return handler;
2563
+ }
2564
+ if (!lazyHandlers.has(id)) {
2565
+ lazyHandlers.set(id, async function runLazyHandler(...args) {
2566
+ const resolved = await lazyRegistry.resolve(type, id, handler);
2567
+ if (typeof resolved !== "function") {
2568
+ throw new TypeError(`Handler "${id}" did not resolve to a function.`);
2569
+ }
2570
+ return resolved.apply(this, args);
2571
+ });
2572
+ }
2573
+ return lazyHandlers.get(id);
2285
2574
  },
2286
2575
 
2287
2576
  async run(ref, context = {}) {
@@ -2595,6 +2884,10 @@ const __schedulerModule = (() => {
2595
2884
  return api;
2596
2885
  },
2597
2886
 
2887
+ isScopeDestroyed(scope) {
2888
+ return scope !== undefined && destroyedScopes.has(scope);
2889
+ },
2890
+
2598
2891
  inspect() {
2599
2892
  const counts = {};
2600
2893
  for (const [phase, queue] of queues) {
@@ -3067,7 +3360,7 @@ const __loaderModule = (() => {
3067
3360
  if (renderingBoundaries.has(boundary)) {
3068
3361
  continue;
3069
3362
  }
3070
- const id = readAttribute(boundary, attributeConfig, "async", "boundary");
3363
+ const id = boundaryIdFor(boundary, attributeConfig);
3071
3364
  if (id == null) {
3072
3365
  continue;
3073
3366
  }
@@ -3384,19 +3677,26 @@ const __loaderModule = (() => {
3384
3677
  function collectBoundaryTemplates(boundary, id, attributeConfig) {
3385
3678
  const templates = {};
3386
3679
  for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
3387
- if (readAttribute(template, attributeConfig, "async", "loading") === id) {
3680
+ if (templateMatchesState(template, "loading", id, boundary, attributeConfig)) {
3388
3681
  templates.loading = template;
3389
3682
  }
3390
- if (readAttribute(template, attributeConfig, "async", "ready") === id) {
3683
+ if (templateMatchesState(template, "ready", id, boundary, attributeConfig)) {
3391
3684
  templates.ready = template;
3392
3685
  }
3393
- if (readAttribute(template, attributeConfig, "async", "error") === id) {
3686
+ if (templateMatchesState(template, "error", id, boundary, attributeConfig)) {
3394
3687
  templates.error = template;
3395
3688
  }
3396
3689
  }
3397
3690
  return templates;
3398
3691
  }
3399
3692
 
3693
+ function templateMatchesState(template, state, id, boundary, attributeConfig) {
3694
+ if (readAttribute(template, attributeConfig, "async", state) === id) {
3695
+ return true;
3696
+ }
3697
+ return isAsyncSuspense(boundary) && template.hasAttribute?.(state);
3698
+ }
3699
+
3400
3700
  function chooseBoundaryTemplate(templates, status) {
3401
3701
  if (status === "ready") {
3402
3702
  return templates.ready ?? templates.loading ?? templates.error;
@@ -3444,13 +3744,24 @@ const __loaderModule = (() => {
3444
3744
 
3445
3745
  function findBoundary(root, boundaryId, attributeConfig) {
3446
3746
  for (const element of elementsIn(root)) {
3447
- if (readAttribute(element, attributeConfig, "async", "boundary") === String(boundaryId)) {
3747
+ if (boundaryIdFor(element, attributeConfig) === String(boundaryId)) {
3448
3748
  return element;
3449
3749
  }
3450
3750
  }
3451
3751
  return null;
3452
3752
  }
3453
3753
 
3754
+ function boundaryIdFor(element, attributeConfig) {
3755
+ if (isAsyncSuspense(element) && element.hasAttribute?.("for")) {
3756
+ return element.getAttribute("for");
3757
+ }
3758
+ return readAttribute(element, attributeConfig, "async", "boundary");
3759
+ }
3760
+
3761
+ function isAsyncSuspense(element) {
3762
+ return element?.tagName === "ASYNC-SUSPENSE";
3763
+ }
3764
+
3454
3765
  function toFragment(value, documentRef) {
3455
3766
  if (value?.nodeType === 11) {
3456
3767
  return value;
@@ -3483,15 +3794,18 @@ const __loaderModule = (() => {
3483
3794
  const __partialsModule = (() => {
3484
3795
  const { isTemplateResult, renderTemplate } = __htmlModule;
3485
3796
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
3797
+ const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
3486
3798
  function createPartialRegistry(initialMap = {}, options = {}) {
3487
3799
  const registryStore = options.registry ?? createRegistryStore();
3488
3800
  const type = options.type ?? "partial";
3489
3801
  const entries = registryStore._map(type);
3802
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
3803
+ const lazyPartials = new Map();
3490
3804
 
3491
3805
  const registry = attachRegistryInspection({
3492
3806
  register(id, fn) {
3493
3807
  assertId(id);
3494
- if (typeof fn !== "function") {
3808
+ if (typeof fn !== "function" && !isLazyDescriptor(fn)) {
3495
3809
  throw new TypeError(`Partial "${id}" must be a function.`);
3496
3810
  }
3497
3811
  if (entries.has(id)) {
@@ -3510,12 +3824,26 @@ const __partialsModule = (() => {
3510
3824
 
3511
3825
  unregister(id) {
3512
3826
  assertId(id);
3827
+ lazyPartials.delete(id);
3513
3828
  return entries.delete(id);
3514
3829
  },
3515
3830
 
3516
3831
  resolve(id) {
3517
3832
  assertId(id);
3518
- return entries.get(id);
3833
+ const partial = entries.get(id);
3834
+ if (!isLazyDescriptor(partial)) {
3835
+ return partial;
3836
+ }
3837
+ if (!lazyPartials.has(id)) {
3838
+ lazyPartials.set(id, async function runLazyPartial(...args) {
3839
+ const resolved = await lazyRegistry.resolve(type, id, partial);
3840
+ if (typeof resolved !== "function") {
3841
+ throw new TypeError(`Partial "${id}" did not resolve to a function.`);
3842
+ }
3843
+ return resolved.apply(this, args);
3844
+ });
3845
+ }
3846
+ return lazyPartials.get(id);
3519
3847
  },
3520
3848
 
3521
3849
  async render(id, props = {}, context = {}) {
@@ -4185,7 +4513,8 @@ const __appModule = (() => {
4185
4513
  const { createSignal, createSignalRegistry } = __signalsModule;
4186
4514
  const { createRegistryStore } = __registryStoreModule;
4187
4515
  const { attributeName, normalizeAttributeConfig } = __attributesModule;
4188
- const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
4516
+ const { createLazyRegistry, defineRegistrySnapshot, sameRegistryValue } = __lazyRegistryModule;
4517
+ const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component", "asyncSignal"]);
4189
4518
 
4190
4519
  function defineApp(initial, options = {}) {
4191
4520
  const registry = createRegistryStore(undefined, { target: "browser" });
@@ -4214,6 +4543,27 @@ const __appModule = (() => {
4214
4543
  return runtime;
4215
4544
  },
4216
4545
 
4546
+ attachRoot(root) {
4547
+ return ensureRuntime(app).attachRoot(root);
4548
+ },
4549
+
4550
+ detachRoot(root) {
4551
+ return app.runtime?.detachRoot(root) ?? app;
4552
+ },
4553
+
4554
+ applySnapshot(snapshot, snapshotOptions = {}) {
4555
+ if (app.runtime) {
4556
+ app.runtime.applySnapshot(snapshot, snapshotOptions);
4557
+ return app;
4558
+ }
4559
+ appendSnapshotDeclarations(registry, snapshot, snapshotOptions);
4560
+ return app;
4561
+ },
4562
+
4563
+ inspectRoots() {
4564
+ return app.runtime?.inspectRoots() ?? { count: 0, roots: [] };
4565
+ },
4566
+
4217
4567
  _attach(runtime) {
4218
4568
  runtimes.add(runtime);
4219
4569
  return () => app._detach(runtime);
@@ -4239,23 +4589,32 @@ const __appModule = (() => {
4239
4589
  });
4240
4590
  const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
4241
4591
  const attributes = normalizeAttributeConfig(options.attributes);
4592
+ const lazyRegistry = options.lazyRegistry ?? createLazyRegistry({
4593
+ registryAssets: options.registryAssets,
4594
+ importModule: options.importModule
4595
+ });
4242
4596
  const registry = options.registry ?? app.registry.view({ target });
4243
- const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
4244
- const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
4597
+ const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal", lazyRegistry });
4598
+ const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler", lazyRegistry });
4245
4599
  const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
4246
4600
  const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
4247
4601
  const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
4248
4602
  const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
4249
- const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
4603
+ const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial", lazyRegistry });
4250
4604
  const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
4251
- const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
4605
+ const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component", lazyRegistry });
4606
+ const hasStartupRoot = options.loader || Object.hasOwn(options, "root");
4607
+ const startupRoot = hasStartupRoot ? options.root : null;
4252
4608
  let loader = options.loader;
4253
4609
  let router = options.router;
4610
+ let routerStarted = false;
4254
4611
  let detach = () => {};
4255
4612
  let started = false;
4256
4613
  let destroyed = false;
4614
+ const rootLoaders = new Map();
4257
4615
 
4258
- applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
4616
+ const snapshotRoot = startupRoot ?? globalThis.document;
4617
+ const initialSnapshot = options.snapshot ?? (target === "browser" ? readSnapshot(snapshotRoot, { attributes }) : undefined);
4259
4618
  attachServerCache(server, serverCache);
4260
4619
 
4261
4620
  const runtime = {
@@ -4284,54 +4643,112 @@ const __appModule = (() => {
4284
4643
  started = true;
4285
4644
 
4286
4645
  if (target !== "server") {
4287
- loader = loader ?? Loader({
4288
- root: options.root,
4289
- signals,
4290
- handlers,
4291
- server,
4292
- cache: browserCache,
4293
- scheduler,
4294
- attributes
4295
- });
4296
- runtime.loader = loader;
4297
-
4298
4646
  configureServerContext({ cache: browserCache });
4299
4647
  signals._setContext?.({ server, loader, cache: browserCache, scheduler });
4300
4648
 
4301
- loader.start();
4649
+ if (loader) {
4650
+ registerRootLoader(loader.root, loader);
4651
+ loader.start();
4652
+ startRouterFor(loader.root);
4653
+ } else if (startupRoot != null) {
4654
+ runtime.attachRoot(startupRoot);
4655
+ }
4656
+ } else {
4657
+ configureServerContext({ cache: serverCache });
4658
+ signals._setContext?.({ server, cache: serverCache, scheduler });
4659
+ }
4660
+
4661
+ return runtime;
4662
+ },
4302
4663
 
4303
- if (router !== false && (router || shouldStartRouter(routes, options))) {
4304
- router = router ?? createRouter({
4305
- mode: options.mode ?? "ssr-spa",
4306
- root: options.root,
4307
- boundary: options.boundary ?? "route",
4308
- routes,
4309
- loader,
4664
+ use(typeOrModule, entries) {
4665
+ app.use(typeOrModule, entries);
4666
+ return runtime;
4667
+ },
4668
+
4669
+ attachRoot(root) {
4670
+ assertActive();
4671
+ if (target === "server") {
4672
+ throw new Error("Server runtimes cannot attach DOM roots.");
4673
+ }
4674
+ if (!root) {
4675
+ throw new TypeError("runtime.attachRoot(root) requires a root.");
4676
+ }
4677
+ if (rootLoaders.has(root)) {
4678
+ return runtime;
4679
+ }
4680
+
4681
+ const rootLoader = rootLoaders.size === 0 && loader
4682
+ ? loader
4683
+ : Loader({
4684
+ root,
4310
4685
  signals,
4311
4686
  handlers,
4312
4687
  server,
4313
4688
  cache: browserCache,
4314
- partials,
4315
4689
  scheduler,
4316
- fetch: options.fetch,
4317
- routeEndpoint: options.routeEndpoint,
4318
4690
  attributes
4319
4691
  });
4320
- runtime.router = router;
4321
- loader.router = router;
4322
- configureServerContext({ cache: browserCache, router });
4323
- router.start();
4692
+ registerRootLoader(root, rootLoader);
4693
+ rootLoader.start();
4694
+ configureServerContext({ cache: browserCache });
4695
+ signals._setContext?.({ server, loader: runtime.loader, cache: browserCache, scheduler });
4696
+ startRouterFor(root);
4697
+ return runtime;
4698
+ },
4699
+
4700
+ detachRoot(root) {
4701
+ assertActive();
4702
+ if (target === "server") {
4703
+ return runtime;
4704
+ }
4705
+ if (root == null) {
4706
+ for (const rootLoader of new Set(rootLoaders.values())) {
4707
+ rootLoader.destroy?.();
4708
+ }
4709
+ rootLoaders.clear();
4710
+ router?.destroy?.();
4711
+ router = undefined;
4712
+ routerStarted = false;
4713
+ loader = undefined;
4714
+ runtime.loader = undefined;
4715
+ runtime.router = undefined;
4716
+ return runtime;
4717
+ }
4718
+ const rootLoader = rootLoaders.get(root);
4719
+ if (!rootLoader) {
4720
+ return runtime;
4721
+ }
4722
+ rootLoader.destroy?.();
4723
+ rootLoaders.delete(root);
4724
+ if (loader === rootLoader) {
4725
+ router?.destroy?.();
4726
+ router = undefined;
4727
+ routerStarted = false;
4728
+ const next = rootLoaders.values().next().value;
4729
+ loader = next;
4730
+ runtime.loader = next;
4731
+ runtime.router = undefined;
4732
+ if (next) {
4733
+ startRouterFor(next.root);
4324
4734
  }
4325
- } else {
4326
- configureServerContext({ cache: serverCache });
4327
- signals._setContext?.({ server, cache: serverCache, scheduler });
4328
4735
  }
4329
-
4330
4736
  return runtime;
4331
4737
  },
4332
4738
 
4333
- use(typeOrModule, entries) {
4334
- app.use(typeOrModule, entries);
4739
+ inspectRoots() {
4740
+ return {
4741
+ count: rootLoaders.size,
4742
+ roots: [...rootLoaders].map(([root, rootLoader]) => ({
4743
+ root,
4744
+ loader: rootLoader,
4745
+ primary: rootLoader === loader
4746
+ }))
4747
+ };
4748
+ },
4749
+
4750
+ applySnapshot(snapshot, snapshotOptions = {}) {
4751
+ applySnapshotToRuntime(runtime, snapshot, snapshotOptions);
4335
4752
  return runtime;
4336
4753
  },
4337
4754
 
@@ -4393,7 +4810,14 @@ const __appModule = (() => {
4393
4810
  destroyed = true;
4394
4811
  detach();
4395
4812
  router?.destroy?.();
4396
- loader?.destroy?.();
4813
+ const destroyedLoaders = new Set(rootLoaders.values());
4814
+ for (const rootLoader of destroyedLoaders) {
4815
+ rootLoader.destroy?.();
4816
+ }
4817
+ rootLoaders.clear();
4818
+ if (loader && !destroyedLoaders.has(loader)) {
4819
+ loader?.destroy?.();
4820
+ }
4397
4821
  signals.destroy?.();
4398
4822
  if (ownsScheduler) {
4399
4823
  scheduler.destroy();
@@ -4407,10 +4831,49 @@ const __appModule = (() => {
4407
4831
 
4408
4832
  server.cache = serverCache;
4409
4833
  runtime.server.cache = serverCache;
4834
+ runtime.applySnapshot(initialSnapshot, { strict: options.strictSnapshots ?? true });
4410
4835
  detach = app._attach(runtime);
4411
4836
 
4412
4837
  return runtime;
4413
4838
 
4839
+ function registerRootLoader(root, rootLoader) {
4840
+ rootLoaders.set(root, rootLoader);
4841
+ if (!loader) {
4842
+ loader = rootLoader;
4843
+ runtime.loader = rootLoader;
4844
+ }
4845
+ rootLoader.server = server;
4846
+ rootLoader.cache = browserCache;
4847
+ rootLoader.scheduler = scheduler;
4848
+ }
4849
+
4850
+ function startRouterFor(root) {
4851
+ if (router === false || routerStarted || !(router || shouldStartRouter(routes, options)) || !runtime.loader) {
4852
+ return;
4853
+ }
4854
+ router = router ?? createRouter({
4855
+ mode: options.mode ?? "ssr-spa",
4856
+ root,
4857
+ boundary: options.boundary ?? "route",
4858
+ routes,
4859
+ loader: runtime.loader,
4860
+ signals,
4861
+ handlers,
4862
+ server,
4863
+ cache: browserCache,
4864
+ partials,
4865
+ scheduler,
4866
+ fetch: options.fetch,
4867
+ routeEndpoint: options.routeEndpoint,
4868
+ attributes
4869
+ });
4870
+ runtime.router = router;
4871
+ runtime.loader.router = router;
4872
+ configureServerContext({ cache: browserCache, router });
4873
+ router.start();
4874
+ routerStarted = true;
4875
+ }
4876
+
4414
4877
  function configureServerContext(extra = {}) {
4415
4878
  const cache = isLocalServerRegistry(server) ? serverCache : extra.cache;
4416
4879
  server._setContext?.({
@@ -4453,6 +4916,7 @@ const __appModule = (() => {
4453
4916
  return {};
4454
4917
  }
4455
4918
 
4919
+ const merged = {};
4456
4920
  for (const searchRoot of new Set([rootNode, documentRef])) {
4457
4921
  if (!searchRoot?.querySelectorAll) {
4458
4922
  continue;
@@ -4463,17 +4927,19 @@ const __appModule = (() => {
4463
4927
  }
4464
4928
  const source = script.textContent?.trim() ?? "";
4465
4929
  if (!source) {
4466
- return {};
4930
+ continue;
4467
4931
  }
4932
+ let parsed;
4468
4933
  try {
4469
- return JSON.parse(source);
4934
+ parsed = JSON.parse(source);
4470
4935
  } catch (cause) {
4471
4936
  throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
4472
4937
  }
4938
+ mergeSnapshot(merged, parsed, { strict: true });
4473
4939
  }
4474
4940
  }
4475
4941
 
4476
- return {};
4942
+ return merged;
4477
4943
  }
4478
4944
 
4479
4945
  function applyUseToRuntime(runtime, normalized) {
@@ -4483,10 +4949,22 @@ const __appModule = (() => {
4483
4949
  applyRegistryUse(runtime.partials, runtime.registry, normalized.partial);
4484
4950
  applyRegistryUse(runtime.routes, runtime.registry, normalized.route);
4485
4951
  applyRegistryUse(runtime.components, runtime.registry, normalized.component);
4952
+ applyRegistryStoreUse(runtime.registry, "asyncSignal", normalized.asyncSignal);
4486
4953
  applyRegistryUse(runtime.browser.cache, runtime.registry, normalized.cache.browser);
4487
4954
  applyRegistryUse(runtime.server.cache, runtime.registry, normalized.cache.server);
4488
4955
  }
4489
4956
 
4957
+ function applyRegistryStoreUse(registry, type, entries) {
4958
+ if (!entries || Object.keys(entries).length === 0) {
4959
+ return;
4960
+ }
4961
+ for (const [id, value] of Object.entries(entries)) {
4962
+ if (!registry.has(type, id)) {
4963
+ registry.register(type, id, value);
4964
+ }
4965
+ }
4966
+ }
4967
+
4490
4968
  function applyRegistryUse(registry, runtimeRegistry, entries) {
4491
4969
  if (!entries || Object.keys(entries).length === 0) {
4492
4970
  return;
@@ -4506,6 +4984,7 @@ const __appModule = (() => {
4506
4984
  partial: {},
4507
4985
  route: {},
4508
4986
  component: {},
4987
+ asyncSignal: {},
4509
4988
  cache: {
4510
4989
  browser: {},
4511
4990
  server: {}
@@ -4561,11 +5040,128 @@ const __appModule = (() => {
4561
5040
  return Boolean(value && typeof value.use === "function" && typeof value.snapshot === "function" && value.registry);
4562
5041
  }
4563
5042
 
4564
- function applySnapshot(signals, browserCache, snapshot = {}) {
4565
- for (const [path, value] of Object.entries(snapshot.signals ?? {})) {
4566
- setOrRegisterSignal(signals, path, value);
5043
+ function ensureRuntime(app) {
5044
+ if (!app.runtime) {
5045
+ app.start();
5046
+ }
5047
+ return app.runtime;
5048
+ }
5049
+
5050
+ function applySnapshotToRuntime(runtime, snapshot = {}, options = {}) {
5051
+ const normalized = normalizeSnapshot(snapshot);
5052
+ for (const [path, value] of Object.entries(normalized.signal)) {
5053
+ setOrRegisterSignal(runtime.signals, path, value);
5054
+ }
5055
+ runtime.browser.cache.restore(normalized.cache.browser);
5056
+ mergeRegistryEntries(runtime, "handler", normalized.handler, runtime.handlers, options);
5057
+ mergeRegistryEntries(runtime, "server", normalized.server, runtime.server, options);
5058
+ mergeRegistryEntries(runtime, "partial", normalized.partial, runtime.partials, options);
5059
+ mergeRegistryEntries(runtime, "route", normalized.route, runtime.routes, options);
5060
+ mergeRegistryEntries(runtime, "component", normalized.component, runtime.components, options);
5061
+ mergeRegistryEntries(runtime, "asyncSignal", normalized.asyncSignal, null, options);
5062
+ return runtime;
5063
+ }
5064
+
5065
+ function appendSnapshotDeclarations(registry, snapshot = {}, options = {}) {
5066
+ const normalized = normalizeSnapshot(snapshot);
5067
+ for (const [id, value] of Object.entries(normalized.signal)) {
5068
+ registerSnapshotEntry(registry, "signal", id, createSignal(value), options);
5069
+ }
5070
+ for (const type of ["handler", "server", "partial", "route", "component", "asyncSignal"]) {
5071
+ for (const [id, value] of Object.entries(normalized[type])) {
5072
+ registerSnapshotEntry(registry, type, id, value, options);
5073
+ }
5074
+ }
5075
+ }
5076
+
5077
+ function mergeRegistryEntries(runtime, type, entries, concreteRegistry, options = {}) {
5078
+ if (!entries || Object.keys(entries).length === 0) {
5079
+ return;
5080
+ }
5081
+ for (const [id, value] of Object.entries(entries)) {
5082
+ registerSnapshotEntry(runtime.registry, type, id, value, options);
5083
+ }
5084
+ concreteRegistry?._adoptMany?.(entries);
5085
+ }
5086
+
5087
+ function registerSnapshotEntry(registry, type, id, value, options = {}) {
5088
+ const strict = options.strict ?? true;
5089
+ const map = registry._map(type);
5090
+ if (map.has(id)) {
5091
+ if (sameRegistryValue(map.get(id), value) || sameSnapshotValue(map.get(id), value)) {
5092
+ return;
5093
+ }
5094
+ if (strict) {
5095
+ throw new Error(`${type} "${id}" is already registered with a different value.`);
5096
+ }
5097
+ return;
5098
+ }
5099
+ registry.set(type, id, value);
5100
+ }
5101
+
5102
+ function normalizeSnapshot(snapshot = {}) {
5103
+ const normalized = {
5104
+ signal: {
5105
+ ...(snapshot.signals ?? {}),
5106
+ ...(snapshot.signal ?? {})
5107
+ },
5108
+ handler: { ...(snapshot.handler ?? {}) },
5109
+ server: { ...(snapshot.server ?? {}) },
5110
+ partial: { ...(snapshot.partial ?? {}) },
5111
+ route: { ...(snapshot.route ?? {}) },
5112
+ component: { ...(snapshot.component ?? {}) },
5113
+ asyncSignal: { ...(snapshot.asyncSignal ?? {}) },
5114
+ cache: {
5115
+ browser: {
5116
+ ...(snapshot.entries?.browser ?? {}),
5117
+ ...(snapshot.cache?.browser ?? {})
5118
+ }
5119
+ }
5120
+ };
5121
+ return normalized;
5122
+ }
5123
+
5124
+ function mergeSnapshot(target, source, options = {}) {
5125
+ const normalized = normalizeSnapshot(defineRegistrySnapshot(source));
5126
+ target.signal = {
5127
+ ...(target.signal ?? target.signals ?? {}),
5128
+ ...normalized.signal
5129
+ };
5130
+ target.signals = target.signal;
5131
+ target.cache = {
5132
+ ...(target.cache ?? {}),
5133
+ browser: {
5134
+ ...(target.cache?.browser ?? {}),
5135
+ ...normalized.cache.browser
5136
+ }
5137
+ };
5138
+ for (const type of ["handler", "server", "partial", "route", "component", "asyncSignal"]) {
5139
+ target[type] = target[type] ?? {};
5140
+ for (const [id, value] of Object.entries(normalized[type])) {
5141
+ if (Object.hasOwn(target[type], id)) {
5142
+ if (sameRegistryValue(target[type][id], value) || sameSnapshotValue(target[type][id], value)) {
5143
+ continue;
5144
+ }
5145
+ if (options.strict ?? true) {
5146
+ throw new Error(`${type} "${id}" is already declared with a different value.`);
5147
+ }
5148
+ continue;
5149
+ }
5150
+ target[type][id] = value;
5151
+ }
5152
+ }
5153
+ return target;
5154
+ }
5155
+
5156
+ function sameSnapshotValue(left, right) {
5157
+ if (left === right) {
5158
+ return true;
5159
+ }
5160
+ try {
5161
+ return JSON.stringify(left) === JSON.stringify(right);
5162
+ } catch {
5163
+ return false;
4567
5164
  }
4568
- browserCache.restore(snapshot.cache?.browser);
4569
5165
  }
4570
5166
 
4571
5167
  function setOrRegisterSignal(signals, path, value) {
@@ -4710,6 +5306,312 @@ const __appModule = (() => {
4710
5306
  return { defineApp, createApp, readSnapshot, Async };
4711
5307
  })();
4712
5308
 
5309
+ const __boundaryReceiverModule = (() => {
5310
+ const defaultRecentLimit = 50;
5311
+
5312
+ function createBoundaryReceiver(options = {}) {
5313
+ const loader = options.loader;
5314
+ const signals = options.signals ?? loader?.signals;
5315
+ const cache = options.cache ?? loader?.cache;
5316
+ const scheduler = options.scheduler ?? loader?.scheduler;
5317
+ const router = options.router ?? loader?.router;
5318
+ const recentLimit = options.recentLimit ?? defaultRecentLimit;
5319
+ const throwOnError = options.throwOnError === true;
5320
+ const onApply = typeof options.onApply === "function" ? options.onApply : undefined;
5321
+ const onIgnore = typeof options.onIgnore === "function" ? options.onIgnore : undefined;
5322
+ const onError = typeof options.onError === "function" ? options.onError : undefined;
5323
+ const isScopeDestroyed = typeof options.isScopeDestroyed === "function"
5324
+ ? options.isScopeDestroyed
5325
+ : (scope) => scheduler?.isScopeDestroyed?.(scope) ?? scheduler?.inspectDestroyed?.(scope) ?? false;
5326
+
5327
+ if (!loader || typeof loader.swap !== "function") {
5328
+ throw new TypeError("createBoundaryReceiver(...) requires a loader with swap(boundary, html).");
5329
+ }
5330
+ if (!Number.isInteger(recentLimit) || recentLimit < 0) {
5331
+ throw new TypeError("createBoundaryReceiver(...) recentLimit must be a non-negative integer.");
5332
+ }
5333
+
5334
+ const boundaries = new Map();
5335
+ const recent = [];
5336
+ let destroyed = false;
5337
+
5338
+ const receiver = {
5339
+ async apply(patch) {
5340
+ if (destroyed) {
5341
+ throw new Error("Boundary receiver has been destroyed.");
5342
+ }
5343
+
5344
+ const normalized = validatePatch(patch);
5345
+ const record = boundaryRecord(normalized.boundary);
5346
+ if (normalized.seq <= record.lastSeq) {
5347
+ const result = {
5348
+ status: "ignored-stale",
5349
+ boundary: normalized.boundary,
5350
+ seq: normalized.seq,
5351
+ lastSeq: record.lastSeq
5352
+ };
5353
+ record.ignored += 1;
5354
+ record.lastStatus = result.status;
5355
+ remember(result);
5356
+ onIgnore?.(result, patch);
5357
+ return result;
5358
+ }
5359
+
5360
+ if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
5361
+ const result = {
5362
+ status: "ignored-destroyed",
5363
+ boundary: normalized.boundary,
5364
+ seq: normalized.seq,
5365
+ parentScope: normalized.parentScope
5366
+ };
5367
+ record.ignored += 1;
5368
+ record.lastStatus = result.status;
5369
+ remember(result);
5370
+ onIgnore?.(result, patch);
5371
+ return result;
5372
+ }
5373
+
5374
+ record.lastSeq = normalized.seq;
5375
+
5376
+ if (Object.hasOwn(normalized, "error")) {
5377
+ const error = toStableError(normalized.error);
5378
+ const result = {
5379
+ status: "errored",
5380
+ boundary: normalized.boundary,
5381
+ seq: normalized.seq,
5382
+ error
5383
+ };
5384
+ record.errored += 1;
5385
+ record.lastStatus = result.status;
5386
+ remember(result);
5387
+ onError?.(error, result, patch);
5388
+ if (throwOnError) {
5389
+ throw error;
5390
+ }
5391
+ return result;
5392
+ }
5393
+
5394
+ if (normalized.signals) {
5395
+ if (!signals || typeof signals.set !== "function") {
5396
+ throw new Error("Boundary patch includes signals, but no signal registry is available.");
5397
+ }
5398
+ for (const [path, value] of Object.entries(normalized.signals)) {
5399
+ signals.set(path, value);
5400
+ }
5401
+ }
5402
+
5403
+ if (normalized.cache?.browser) {
5404
+ if (!cache || typeof cache.restore !== "function") {
5405
+ throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
5406
+ }
5407
+ cache.restore(normalized.cache.browser);
5408
+ }
5409
+
5410
+ if (normalized.html != null) {
5411
+ loader.swap(normalized.boundary, normalized.html);
5412
+ }
5413
+
5414
+ await flushScheduler(scheduler, normalized.scope);
5415
+
5416
+ if (normalized.redirect) {
5417
+ await followRedirect(normalized.redirect, router, loader);
5418
+ const result = {
5419
+ status: "redirected",
5420
+ boundary: normalized.boundary,
5421
+ seq: normalized.seq,
5422
+ redirect: normalized.redirect
5423
+ };
5424
+ record.applied += 1;
5425
+ record.lastStatus = result.status;
5426
+ remember(result);
5427
+ onApply?.(result, patch);
5428
+ return result;
5429
+ }
5430
+
5431
+ const result = {
5432
+ status: "applied",
5433
+ boundary: normalized.boundary,
5434
+ seq: normalized.seq
5435
+ };
5436
+ record.applied += 1;
5437
+ record.lastStatus = result.status;
5438
+ remember(result);
5439
+ onApply?.(result, patch);
5440
+ return result;
5441
+ },
5442
+
5443
+ inspect() {
5444
+ const snapshot = {};
5445
+ for (const [boundary, record] of boundaries) {
5446
+ snapshot[boundary] = {
5447
+ lastSeq: record.lastSeq,
5448
+ applied: record.applied,
5449
+ ignored: record.ignored,
5450
+ lastStatus: record.lastStatus
5451
+ };
5452
+ if (record.errored > 0) {
5453
+ snapshot[boundary].errored = record.errored;
5454
+ }
5455
+ }
5456
+ return {
5457
+ destroyed,
5458
+ boundaries: snapshot,
5459
+ recent: recent.map((entry) => ({ ...entry }))
5460
+ };
5461
+ },
5462
+
5463
+ reset(boundary) {
5464
+ if (boundary === undefined) {
5465
+ boundaries.clear();
5466
+ recent.length = 0;
5467
+ return receiver;
5468
+ }
5469
+ assertBoundary(boundary);
5470
+ boundaries.delete(boundary);
5471
+ for (let index = recent.length - 1; index >= 0; index -= 1) {
5472
+ if (recent[index].boundary === boundary) {
5473
+ recent.splice(index, 1);
5474
+ }
5475
+ }
5476
+ return receiver;
5477
+ },
5478
+
5479
+ destroy() {
5480
+ destroyed = true;
5481
+ boundaries.clear();
5482
+ recent.length = 0;
5483
+ }
5484
+ };
5485
+
5486
+ return receiver;
5487
+
5488
+ function boundaryRecord(boundary) {
5489
+ if (!boundaries.has(boundary)) {
5490
+ boundaries.set(boundary, {
5491
+ lastSeq: -Infinity,
5492
+ applied: 0,
5493
+ ignored: 0,
5494
+ errored: 0,
5495
+ lastStatus: undefined
5496
+ });
5497
+ }
5498
+ return boundaries.get(boundary);
5499
+ }
5500
+
5501
+ function remember(result) {
5502
+ if (recentLimit === 0) {
5503
+ return;
5504
+ }
5505
+ recent.push(toRecentEntry(result));
5506
+ while (recent.length > recentLimit) {
5507
+ recent.shift();
5508
+ }
5509
+ }
5510
+ }
5511
+
5512
+ function validatePatch(patch) {
5513
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
5514
+ throw new TypeError("receiver.apply(patch) requires a boundary patch object.");
5515
+ }
5516
+
5517
+ assertBoundary(patch.boundary);
5518
+ if (typeof patch.seq !== "number" || !Number.isFinite(patch.seq)) {
5519
+ throw new TypeError("Boundary patch seq must be a finite number.");
5520
+ }
5521
+
5522
+ if (patch.signals !== undefined && !isPlainObject(patch.signals)) {
5523
+ throw new TypeError("Boundary patch signals must be an object.");
5524
+ }
5525
+ if (patch.cache !== undefined && !isPlainObject(patch.cache)) {
5526
+ throw new TypeError("Boundary patch cache must be an object.");
5527
+ }
5528
+ if (patch.cache?.browser !== undefined && !isPlainObject(patch.cache.browser)) {
5529
+ throw new TypeError("Boundary patch cache.browser must be an object.");
5530
+ }
5531
+ if (patch.redirect !== undefined && (typeof patch.redirect !== "string" || patch.redirect.length === 0)) {
5532
+ throw new TypeError("Boundary patch redirect must be a non-empty string.");
5533
+ }
5534
+ if (patch.parentScope !== undefined && typeof patch.parentScope !== "string") {
5535
+ throw new TypeError("Boundary patch parentScope must be a string.");
5536
+ }
5537
+ if (patch.scope !== undefined && typeof patch.scope !== "string") {
5538
+ throw new TypeError("Boundary patch scope must be a string.");
5539
+ }
5540
+
5541
+ const hasHtml = Object.hasOwn(patch, "html") && patch.html != null;
5542
+ const hasSignals = patch.signals && Object.keys(patch.signals).length > 0;
5543
+ const hasBrowserCache = patch.cache?.browser && Object.keys(patch.cache.browser).length > 0;
5544
+ const hasRedirect = Boolean(patch.redirect);
5545
+ const hasError = Object.hasOwn(patch, "error");
5546
+ if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError) {
5547
+ throw new TypeError("Boundary patch must include html, signals, cache.browser, redirect, or error.");
5548
+ }
5549
+
5550
+ return patch;
5551
+ }
5552
+
5553
+ function assertBoundary(boundary) {
5554
+ if (typeof boundary !== "string" || boundary.length === 0) {
5555
+ throw new TypeError("Boundary patch boundary must be a non-empty string.");
5556
+ }
5557
+ }
5558
+
5559
+ async function flushScheduler(scheduler, scope) {
5560
+ if (!scheduler) {
5561
+ return;
5562
+ }
5563
+ if (scope !== undefined && typeof scheduler.flushScope === "function") {
5564
+ await scheduler.flushScope(scope);
5565
+ return;
5566
+ }
5567
+ if (typeof scheduler.flush === "function") {
5568
+ await scheduler.flush();
5569
+ }
5570
+ }
5571
+
5572
+ async function followRedirect(redirect, router, loader) {
5573
+ if (router && typeof router.navigate === "function") {
5574
+ await router.navigate(redirect);
5575
+ return;
5576
+ }
5577
+ const location = loader?.root?.ownerDocument?.defaultView?.location ?? globalThis.location;
5578
+ location?.assign?.(redirect);
5579
+ }
5580
+
5581
+ function toStableError(value) {
5582
+ if (value instanceof Error) {
5583
+ return value;
5584
+ }
5585
+ if (value && typeof value === "object" && typeof value.message === "string") {
5586
+ return Object.assign(new Error(value.message), value);
5587
+ }
5588
+ return new Error(String(value));
5589
+ }
5590
+
5591
+ function toRecentEntry(result) {
5592
+ const entry = {
5593
+ boundary: result.boundary,
5594
+ seq: result.seq,
5595
+ status: result.status
5596
+ };
5597
+ if (result.status === "ignored-stale") {
5598
+ entry.lastSeq = result.lastSeq;
5599
+ }
5600
+ if (result.status === "ignored-destroyed" && result.parentScope !== undefined) {
5601
+ entry.parentScope = result.parentScope;
5602
+ }
5603
+ if (result.status === "redirected") {
5604
+ entry.redirect = result.redirect;
5605
+ }
5606
+ return entry;
5607
+ }
5608
+
5609
+ function isPlainObject(value) {
5610
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
5611
+ }
5612
+ return { createBoundaryReceiver };
5613
+ })();
5614
+
4713
5615
  const __delayModule = (() => {
4714
5616
  function delay(ms, signal) {
4715
5617
  if (signal?.aborted) {
@@ -4744,6 +5646,72 @@ const __delayModule = (() => {
4744
5646
  return { delay };
4745
5647
  })();
4746
5648
 
5649
+ const __elementsModule = (() => {
5650
+ const { Async } = __appModule;
5651
+ function defineAsyncContainerElement(options = {}) {
5652
+ const tagName = options.tagName ?? "async-container";
5653
+ const registry = options.customElements ?? globalThis.customElements;
5654
+ if (!registry) {
5655
+ throw new Error("defineAsyncContainerElement(...) requires customElements.");
5656
+ }
5657
+ const existing = registry.get(tagName);
5658
+ if (existing) {
5659
+ return existing;
5660
+ }
5661
+ const app = options.app ?? options.Async ?? Async;
5662
+ const HTMLElementBase = options.HTMLElement ?? options.window?.HTMLElement ?? globalThis.HTMLElement;
5663
+ if (!HTMLElementBase) {
5664
+ throw new Error("defineAsyncContainerElement(...) requires HTMLElement.");
5665
+ }
5666
+
5667
+ class AsyncContainerElement extends HTMLElementBase {
5668
+ connectedCallback() {
5669
+ if (this.__asyncAttached) {
5670
+ return;
5671
+ }
5672
+ const runtime = app.runtime ?? app.start?.();
5673
+ runtime?.attachRoot?.(this);
5674
+ this.__asyncRuntime = runtime;
5675
+ this.__asyncAttached = true;
5676
+ }
5677
+
5678
+ disconnectedCallback() {
5679
+ if (!this.__asyncAttached) {
5680
+ return;
5681
+ }
5682
+ this.__asyncRuntime?.detachRoot?.(this);
5683
+ this.__asyncRuntime = undefined;
5684
+ this.__asyncAttached = false;
5685
+ }
5686
+ }
5687
+
5688
+ registry.define(tagName, AsyncContainerElement);
5689
+ return AsyncContainerElement;
5690
+ }
5691
+
5692
+ function defineAsyncSuspenseElement(options = {}) {
5693
+ const tagName = options.tagName ?? "async-suspense";
5694
+ const registry = options.customElements ?? globalThis.customElements;
5695
+ if (!registry) {
5696
+ throw new Error("defineAsyncSuspenseElement(...) requires customElements.");
5697
+ }
5698
+ const existing = registry.get(tagName);
5699
+ if (existing) {
5700
+ return existing;
5701
+ }
5702
+ const HTMLElementBase = options.HTMLElement ?? options.window?.HTMLElement ?? globalThis.HTMLElement;
5703
+ if (!HTMLElementBase) {
5704
+ throw new Error("defineAsyncSuspenseElement(...) requires HTMLElement.");
5705
+ }
5706
+
5707
+ class AsyncSuspenseElement extends HTMLElementBase {}
5708
+
5709
+ registry.define(tagName, AsyncSuspenseElement);
5710
+ return AsyncSuspenseElement;
5711
+ }
5712
+ return { defineAsyncContainerElement, defineAsyncSuspenseElement };
5713
+ })();
5714
+
4747
5715
  const { asyncSignal: asyncSignal } = __asyncSignalModule;
4748
5716
  const { Async: Async } = __appModule;
4749
5717
  const { createApp: createApp } = __appModule;
@@ -4751,14 +5719,19 @@ const { defineApp: defineApp } = __appModule;
4751
5719
  const { readSnapshot: readSnapshot } = __appModule;
4752
5720
  const { attributeName: attributeName } = __attributesModule;
4753
5721
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
5722
+ const { createBoundaryReceiver: createBoundaryReceiver } = __boundaryReceiverModule;
4754
5723
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
4755
5724
  const { defineCache: defineCache } = __cacheModule;
4756
5725
  const { component: component } = __componentModule;
4757
5726
  const { createComponentRegistry: createComponentRegistry } = __componentModule;
4758
5727
  const { defineComponent: defineComponent } = __componentModule;
4759
5728
  const { delay: delay } = __delayModule;
5729
+ const { defineAsyncContainerElement: defineAsyncContainerElement } = __elementsModule;
5730
+ const { defineAsyncSuspenseElement: defineAsyncSuspenseElement } = __elementsModule;
4760
5731
  const { createHandlerRegistry: createHandlerRegistry } = __handlersModule;
4761
5732
  const { html: html } = __htmlModule;
5733
+ const { createLazyRegistry: createLazyRegistry } = __lazyRegistryModule;
5734
+ const { defineRegistrySnapshot: defineRegistrySnapshot } = __lazyRegistryModule;
4762
5735
  const { Loader: Loader } = __loaderModule;
4763
5736
  const { AsyncLoader: AsyncLoader } = __loaderModule;
4764
5737
  const { createPartialRegistry: createPartialRegistry } = __partialsModule;
@@ -4778,4 +5751,4 @@ const { createSignalRegistry: createSignalRegistry } = __signalsModule;
4778
5751
  const { effect: effect } = __signalsModule;
4779
5752
  const { signal: signal } = __signalsModule;
4780
5753
 
4781
- export { 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 };
5754
+ export { 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 };