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