@async/framework 0.11.15 → 0.11.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -6262,7 +6262,19 @@ const __serverEntryModule = (() => {
6262
6262
  })();
6263
6263
 
6264
6264
  const __boundaryReceiverModule = (() => {
6265
+ const { normalizeAttributeConfig, readAttribute } = __attributesModule;
6266
+ const { renderTemplate } = __htmlModule;
6265
6267
  const defaultRecentLimit = 50;
6268
+ const builtBackpatchAttribute = "data-async-backpatch";
6269
+ const pendingTargetAttribute = "data-pending-id";
6270
+ const revealOrders = new Set(["as-ready", "forwards", "backwards", "together"]);
6271
+ const revealTails = new Set(["collapsed", "hidden"]);
6272
+ const structuralAttributeNames = new Set(["innerhtml", "outerhtml", "textcontent", "children", "childnodes"]);
6273
+
6274
+ const AsyncStream = Object.freeze({
6275
+ applyScript,
6276
+ applyCurrentScript
6277
+ });
6266
6278
 
6267
6279
  function createBoundaryReceiver(options = {}) {
6268
6280
  const loader = options.loader;
@@ -6270,6 +6282,7 @@ const __boundaryReceiverModule = (() => {
6270
6282
  const cache = options.cache ?? loader?.cache;
6271
6283
  const scheduler = options.scheduler ?? loader?.scheduler;
6272
6284
  const router = options.router ?? loader?.router;
6285
+ const attributes = normalizeAttributeConfig(options.attributes ?? loader?.attributes);
6273
6286
  const recentLimit = options.recentLimit ?? defaultRecentLimit;
6274
6287
  const throwOnError = options.throwOnError === true;
6275
6288
  const onApply = typeof options.onApply === "function" ? options.onApply : undefined;
@@ -6287,6 +6300,7 @@ const __boundaryReceiverModule = (() => {
6287
6300
  }
6288
6301
 
6289
6302
  const boundaries = new Map();
6303
+ const revealGroups = new Map();
6290
6304
  const recent = [];
6291
6305
  let destroyed = false;
6292
6306
 
@@ -6335,6 +6349,7 @@ const __boundaryReceiverModule = (() => {
6335
6349
  return {
6336
6350
  destroyed,
6337
6351
  boundaries: snapshot,
6352
+ reveal: inspectRevealGroups(revealGroups),
6338
6353
  recent: recent.map((entry) => ({ ...entry }))
6339
6354
  };
6340
6355
  },
@@ -6342,6 +6357,7 @@ const __boundaryReceiverModule = (() => {
6342
6357
  reset(boundary) {
6343
6358
  if (boundary === undefined) {
6344
6359
  boundaries.clear();
6360
+ revealGroups.clear();
6345
6361
  recent.length = 0;
6346
6362
  return receiver;
6347
6363
  }
@@ -6352,12 +6368,20 @@ const __boundaryReceiverModule = (() => {
6352
6368
  recent.splice(index, 1);
6353
6369
  }
6354
6370
  }
6371
+ for (const group of revealGroups.values()) {
6372
+ for (const [index, item] of group.pending) {
6373
+ if (item.normalized.boundary === boundary) {
6374
+ group.pending.delete(index);
6375
+ }
6376
+ }
6377
+ }
6355
6378
  return receiver;
6356
6379
  },
6357
6380
 
6358
6381
  destroy() {
6359
6382
  destroyed = true;
6360
6383
  boundaries.clear();
6384
+ revealGroups.clear();
6361
6385
  recent.length = 0;
6362
6386
  }
6363
6387
  };
@@ -6365,32 +6389,10 @@ const __boundaryReceiverModule = (() => {
6365
6389
  return receiver;
6366
6390
 
6367
6391
  async function applyBoundaryPatch(record, normalized, patch) {
6368
- if (normalized.seq <= record.lastSeq) {
6369
- const result = {
6370
- status: "ignored-stale",
6371
- boundary: normalized.boundary,
6372
- seq: normalized.seq,
6373
- lastSeq: record.lastSeq
6374
- };
6375
- record.ignored += 1;
6376
- record.lastStatus = result.status;
6377
- remember(result);
6378
- onIgnore?.(result, patch);
6379
- return result;
6380
- }
6381
-
6382
- if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
6383
- const result = {
6384
- status: "ignored-destroyed",
6385
- boundary: normalized.boundary,
6386
- seq: normalized.seq,
6387
- parentScope: normalized.parentScope
6388
- };
6389
- record.ignored += 1;
6390
- record.lastStatus = result.status;
6391
- remember(result);
6392
- onIgnore?.(result, patch);
6393
- return result;
6392
+ const ignored = preflightIgnoredResult(record, normalized);
6393
+ if (ignored) {
6394
+ rememberIgnored(record, ignored, patch);
6395
+ return ignored;
6394
6396
  }
6395
6397
 
6396
6398
  if (Object.hasOwn(normalized, "error")) {
@@ -6412,35 +6414,91 @@ const __boundaryReceiverModule = (() => {
6412
6414
  return result;
6413
6415
  }
6414
6416
 
6415
- if (normalized.signals) {
6416
- if (!signals || typeof signals.set !== "function") {
6417
- throw new Error("Boundary patch includes signals, but no signal registry is available.");
6418
- }
6419
- for (const [path, value] of Object.entries(normalized.signals)) {
6420
- signals.set(path, value);
6421
- }
6417
+ applyStateEffects(normalized);
6418
+
6419
+ if (normalized.reveal) {
6420
+ return await applyRevealPatch(record, normalized, patch);
6422
6421
  }
6423
6422
 
6424
- if (normalized.cache?.browser) {
6425
- if (!cache || typeof cache.restore !== "function") {
6426
- throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
6423
+ return await commitBoundaryPatch(record, normalized, patch, { stateApplied: true });
6424
+ }
6425
+
6426
+ async function applyRevealPatch(record, normalized, patch) {
6427
+ const group = revealGroup(normalized.reveal);
6428
+ const index = normalized.reveal.index;
6429
+ if (group.committed.has(index)) {
6430
+ throw new TypeError(`Reveal group "${group.id}" already committed index ${index}.`);
6431
+ }
6432
+ if (group.pending.has(index)) {
6433
+ throw new TypeError(`Reveal group "${group.id}" already has a pending patch for index ${index}.`);
6434
+ }
6435
+
6436
+ const item = { record, normalized, patch };
6437
+ group.pending.set(index, item);
6438
+ const ready = takeReadyRevealItems(group);
6439
+ if (!ready.includes(item)) {
6440
+ const result = {
6441
+ status: "buffered",
6442
+ boundary: normalized.boundary,
6443
+ seq: normalized.seq,
6444
+ reveal: revealResultMetadata(normalized.reveal)
6445
+ };
6446
+ record.lastStatus = result.status;
6447
+ remember(result);
6448
+ updateRevealTail(group);
6449
+ return result;
6450
+ }
6451
+
6452
+ let currentResult;
6453
+ for (const readyItem of ready) {
6454
+ const result = await commitBoundaryPatch(readyItem.record, readyItem.normalized, readyItem.patch, {
6455
+ stateApplied: true
6456
+ });
6457
+ group.committed.add(readyItem.normalized.reveal.index);
6458
+ if (readyItem === item) {
6459
+ currentResult = result;
6427
6460
  }
6428
- cache.restore(normalized.cache.browser);
6461
+ }
6462
+ updateRevealTail(group);
6463
+ return currentResult;
6464
+ }
6465
+
6466
+ async function commitBoundaryPatch(record, normalized, patch, options = {}) {
6467
+ const ignored = preflightIgnoredResult(record, normalized);
6468
+ if (ignored) {
6469
+ rememberIgnored(record, ignored, patch);
6470
+ return ignored;
6471
+ }
6472
+
6473
+ if (!options.stateApplied) {
6474
+ applyStateEffects(normalized);
6429
6475
  }
6430
6476
 
6477
+ let boundaryElement;
6478
+ let replacementCount = 0;
6431
6479
  if (normalized.html != null) {
6432
- loader.swap(normalized.boundary, normalized.html);
6480
+ boundaryElement = loader.swap(normalized.boundary, normalized.html);
6481
+ }
6482
+ if (normalized.replace) {
6483
+ boundaryElement ??= findBoundaryElement(loader.root, normalized.boundary, attributes);
6484
+ replacementCount = applyReplacements(boundaryElement, normalized.replace);
6485
+ }
6486
+
6487
+ let attrs;
6488
+ if (normalized.attrs) {
6489
+ boundaryElement ??= findBoundaryElement(loader.root, normalized.boundary, attributes);
6490
+ attrs = applyAttributePatches(boundaryElement, normalized.attrs);
6433
6491
  }
6434
6492
 
6435
6493
  await flushScheduler(scheduler, normalized.scope);
6436
6494
 
6437
6495
  if (normalized.redirect) {
6438
- const result = {
6496
+ const result = withPatchMetadata({
6439
6497
  status: "redirected",
6440
6498
  boundary: normalized.boundary,
6441
6499
  seq: normalized.seq,
6442
6500
  redirect: normalized.redirect
6443
- };
6501
+ }, attrs, replacementCount);
6444
6502
  await followRedirect(normalized.redirect, router, loader);
6445
6503
  record.applied += 1;
6446
6504
  record.lastSeq = normalized.seq;
@@ -6450,11 +6508,11 @@ const __boundaryReceiverModule = (() => {
6450
6508
  return result;
6451
6509
  }
6452
6510
 
6453
- const result = {
6511
+ const result = withPatchMetadata({
6454
6512
  status: "applied",
6455
6513
  boundary: normalized.boundary,
6456
6514
  seq: normalized.seq
6457
- };
6515
+ }, attrs, replacementCount);
6458
6516
  record.applied += 1;
6459
6517
  record.lastSeq = normalized.seq;
6460
6518
  record.lastStatus = result.status;
@@ -6463,6 +6521,157 @@ const __boundaryReceiverModule = (() => {
6463
6521
  return result;
6464
6522
  }
6465
6523
 
6524
+ function applyStateEffects(normalized) {
6525
+ if (normalized.signals) {
6526
+ if (!signals || typeof signals.set !== "function") {
6527
+ throw new Error("Boundary patch includes signals, but no signal registry is available.");
6528
+ }
6529
+ for (const [path, value] of Object.entries(normalized.signals)) {
6530
+ signals.set(path, value);
6531
+ }
6532
+ }
6533
+
6534
+ if (normalized.cache?.browser) {
6535
+ if (!cache || typeof cache.restore !== "function") {
6536
+ throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
6537
+ }
6538
+ cache.restore(normalized.cache.browser);
6539
+ }
6540
+ }
6541
+
6542
+ function applyReplacements(boundaryElement, replacements) {
6543
+ let applied = 0;
6544
+ for (const replacement of replacements) {
6545
+ if (replacement.mode === "boundary") {
6546
+ const target = findBoundaryElement(loader.root, replacement.target, attributes);
6547
+ if (!containsOrEquals(boundaryElement, target)) {
6548
+ throw new Error(`Boundary replacement target "${replacement.target}" is outside boundary "${boundaryIdFor(boundaryElement, attributes)}".`);
6549
+ }
6550
+ loader.swap(replacement.target, replacement.html);
6551
+ applied += 1;
6552
+ continue;
6553
+ }
6554
+
6555
+ const target = findUniqueScopedElement(
6556
+ boundaryElement,
6557
+ (element) => element.getAttribute?.(pendingTargetAttribute) === replacement.target,
6558
+ `Pending replacement target "${replacement.target}"`
6559
+ );
6560
+ const fragment = toFragment(replacement.html, ownerDocumentOf(boundaryElement));
6561
+ const inserted = [...fragment.childNodes];
6562
+ target.replaceWith(fragment);
6563
+ for (const node of inserted) {
6564
+ if (node.nodeType === 1 || node.nodeType === 11) {
6565
+ loader.scan?.(node);
6566
+ }
6567
+ }
6568
+ applied += 1;
6569
+ }
6570
+ return applied;
6571
+ }
6572
+
6573
+ function applyAttributePatches(boundaryElement, attrs) {
6574
+ let applied = 0;
6575
+ for (const attr of attrs.items) {
6576
+ const target = attrs.kind === "built"
6577
+ ? resolveBuiltAttrTarget(boundaryElement, attr.target)
6578
+ : resolveNamedAttrTarget(boundaryElement, attr.target);
6579
+ setPatchedAttribute(target, attr.name, attr.value);
6580
+ applied += 1;
6581
+ }
6582
+ return {
6583
+ applied,
6584
+ ignored: 0
6585
+ };
6586
+ }
6587
+
6588
+ function resolveBuiltAttrTarget(boundaryElement, targetIndex) {
6589
+ const targets = scopedElements(boundaryElement)
6590
+ .filter((element) => element.hasAttribute?.(builtBackpatchAttribute));
6591
+ const target = targets[targetIndex];
6592
+ if (!target) {
6593
+ throw new Error(`Built attribute patch target ${targetIndex} was not found in boundary "${boundaryIdFor(boundaryElement, attributes)}".`);
6594
+ }
6595
+ return target;
6596
+ }
6597
+
6598
+ function resolveNamedAttrTarget(boundaryElement, targetName) {
6599
+ return findUniqueScopedElement(
6600
+ boundaryElement,
6601
+ (element) => readAttribute(element, attributes, "async", "patch") === targetName,
6602
+ `Attribute patch target "${targetName}"`
6603
+ );
6604
+ }
6605
+
6606
+ function findUniqueScopedElement(boundaryElement, predicate, label) {
6607
+ const matches = scopedElements(boundaryElement).filter(predicate);
6608
+ if (matches.length === 0) {
6609
+ throw new Error(`${label} was not found.`);
6610
+ }
6611
+ if (matches.length > 1) {
6612
+ throw new Error(`${label} is ambiguous.`);
6613
+ }
6614
+ return matches[0];
6615
+ }
6616
+
6617
+ function scopedElements(boundaryElement) {
6618
+ return elementsIn(boundaryElement)
6619
+ .filter((element) => !isNestedBoundaryElement(element, boundaryElement, attributes));
6620
+ }
6621
+
6622
+ function revealGroup(reveal) {
6623
+ const existing = revealGroups.get(reveal.group);
6624
+ if (existing) {
6625
+ if (existing.count !== reveal.count || existing.order !== reveal.order || existing.tail !== reveal.tail) {
6626
+ throw new TypeError(`Reveal group "${reveal.group}" metadata does not match earlier patches.`);
6627
+ }
6628
+ return existing;
6629
+ }
6630
+
6631
+ const group = {
6632
+ id: reveal.group,
6633
+ count: reveal.count,
6634
+ order: reveal.order,
6635
+ tail: reveal.tail,
6636
+ pending: new Map(),
6637
+ committed: new Set(),
6638
+ nextForward: 0,
6639
+ nextBackward: reveal.count - 1
6640
+ };
6641
+ revealGroups.set(reveal.group, group);
6642
+ return group;
6643
+ }
6644
+
6645
+ function updateRevealTail(group) {
6646
+ if (!group.tail) {
6647
+ return;
6648
+ }
6649
+ const container = findRevealContainer(loader.root, group.id, attributes);
6650
+ if (!container) {
6651
+ return;
6652
+ }
6653
+ const children = directChildBoundaryElements(container, attributes).slice(0, group.count);
6654
+ if (children.length === 0) {
6655
+ return;
6656
+ }
6657
+
6658
+ const pending = [];
6659
+ for (let index = 0; index < children.length; index += 1) {
6660
+ if (!group.committed.has(index)) {
6661
+ pending.push(index);
6662
+ } else {
6663
+ setTailHidden(children[index], false);
6664
+ }
6665
+ }
6666
+
6667
+ const visiblePending = group.tail === "collapsed" && pending.length > 0
6668
+ ? pendingOrderFor(group, pending)[0]
6669
+ : undefined;
6670
+ for (const index of pending) {
6671
+ setTailHidden(children[index], group.tail === "hidden" || index !== visiblePending);
6672
+ }
6673
+ }
6674
+
6466
6675
  function boundaryRecord(boundary) {
6467
6676
  if (!boundaries.has(boundary)) {
6468
6677
  boundaries.set(boundary, {
@@ -6477,6 +6686,34 @@ const __boundaryReceiverModule = (() => {
6477
6686
  return boundaries.get(boundary);
6478
6687
  }
6479
6688
 
6689
+ function preflightIgnoredResult(record, normalized) {
6690
+ if (normalized.seq <= record.lastSeq) {
6691
+ return {
6692
+ status: "ignored-stale",
6693
+ boundary: normalized.boundary,
6694
+ seq: normalized.seq,
6695
+ lastSeq: record.lastSeq
6696
+ };
6697
+ }
6698
+
6699
+ if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
6700
+ return {
6701
+ status: "ignored-destroyed",
6702
+ boundary: normalized.boundary,
6703
+ seq: normalized.seq,
6704
+ parentScope: normalized.parentScope
6705
+ };
6706
+ }
6707
+ return null;
6708
+ }
6709
+
6710
+ function rememberIgnored(record, result, patch) {
6711
+ record.ignored += 1;
6712
+ record.lastStatus = result.status;
6713
+ remember(result);
6714
+ onIgnore?.(result, patch);
6715
+ }
6716
+
6480
6717
  function remember(result) {
6481
6718
  if (recentLimit === 0) {
6482
6719
  return;
@@ -6488,6 +6725,132 @@ const __boundaryReceiverModule = (() => {
6488
6725
  }
6489
6726
  }
6490
6727
 
6728
+ function applyScript(script, options = {}) {
6729
+ if (!script || typeof script !== "object" || typeof script.textContent !== "string") {
6730
+ throw new TypeError("AsyncStream.applyScript(script) requires a JSON script element.");
6731
+ }
6732
+ const attributes = normalizeAttributeConfig(options.attributes ?? options.receiver?.attributes);
6733
+ const patch = parseStreamPatch(script.textContent);
6734
+ const root = options.root ?? script.ownerDocument ?? globalThis.document;
6735
+ const resolved = resolveStreamPatch(patch, { root, attributes });
6736
+ const receiver = resolveReceiver(options);
6737
+ return receiver.apply(resolved);
6738
+ }
6739
+
6740
+ function applyCurrentScript(scriptOrOptions, maybeOptions) {
6741
+ const script = isElementLike(scriptOrOptions)
6742
+ ? scriptOrOptions
6743
+ : globalThis.document?.currentScript;
6744
+ const options = isElementLike(scriptOrOptions) ? maybeOptions ?? {} : scriptOrOptions ?? {};
6745
+ return applyScript(script, options);
6746
+ }
6747
+
6748
+ function resolveReceiver(options = {}) {
6749
+ if (options.receiver && typeof options.receiver.apply === "function") {
6750
+ return options.receiver;
6751
+ }
6752
+ const runtime = options.runtime ?? globalThis.Async?.runtime;
6753
+ const loader = options.loader ?? runtime?.loader;
6754
+ if (!loader) {
6755
+ throw new TypeError("AsyncStream requires receiver, loader, or runtime.loader.");
6756
+ }
6757
+ return createBoundaryReceiver({
6758
+ loader,
6759
+ signals: options.signals ?? runtime?.signals,
6760
+ cache: options.cache ?? runtime?.browser?.cache,
6761
+ scheduler: options.scheduler ?? runtime?.scheduler,
6762
+ router: options.router ?? runtime?.router,
6763
+ attributes: options.attributes ?? runtime?.attributes ?? loader.attributes
6764
+ });
6765
+ }
6766
+
6767
+ function parseStreamPatch(source) {
6768
+ try {
6769
+ return JSON.parse(source);
6770
+ } catch (error) {
6771
+ throw new TypeError(`Async stream patch JSON is invalid: ${error.message}`);
6772
+ }
6773
+ }
6774
+
6775
+ function resolveStreamPatch(patch, { root, attributes }) {
6776
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
6777
+ throw new TypeError("Async stream patch JSON must be an object.");
6778
+ }
6779
+ const resolved = { ...patch };
6780
+ if (patch.replace !== undefined) {
6781
+ resolved.replace = resolveStreamReplacements(patch.replace, root, attributes);
6782
+ }
6783
+ const synthesizedReveal = synthesizeRevealMetadata(resolved, root, attributes);
6784
+ if (patch.reveal !== undefined && synthesizedReveal && !sameRevealMetadata(patch.reveal, synthesizedReveal)) {
6785
+ throw new TypeError("Explicit stream reveal metadata conflicts with DOM reveal metadata.");
6786
+ }
6787
+ if (patch.reveal === undefined && synthesizedReveal) {
6788
+ resolved.reveal = synthesizedReveal;
6789
+ }
6790
+ return resolved;
6791
+ }
6792
+
6793
+ function resolveStreamReplacements(value, root, attributes) {
6794
+ const replacements = Array.isArray(value) ? value : [value];
6795
+ return replacements.map((replacement) => {
6796
+ if (!isPlainObject(replacement)) {
6797
+ throw new TypeError("Stream replacement records must be objects.");
6798
+ }
6799
+ if (replacement.template === undefined || Object.hasOwn(replacement, "html")) {
6800
+ return replacement;
6801
+ }
6802
+ const template = findStreamTemplate(root, replacement.template, attributes);
6803
+ if (!template) {
6804
+ throw new Error(`Stream template "${replacement.template}" was not found.`);
6805
+ }
6806
+ return {
6807
+ ...replacement,
6808
+ html: template.innerHTML
6809
+ };
6810
+ });
6811
+ }
6812
+
6813
+ function findStreamTemplate(root, id, attributes) {
6814
+ if (typeof id !== "string" || id.length === 0) {
6815
+ throw new TypeError("Stream replacement template must be a non-empty string.");
6816
+ }
6817
+ return elementsIn(root)
6818
+ .find((element) => element.tagName === "TEMPLATE" && readAttribute(element, attributes, "async", "stream-template") === id)
6819
+ ?? null;
6820
+ }
6821
+
6822
+ function synthesizeRevealMetadata(patch, root, attributes) {
6823
+ if (typeof patch.boundary !== "string" || patch.boundary.length === 0) {
6824
+ return null;
6825
+ }
6826
+ const boundary = findBoundaryElement(root, patch.boundary, attributes, { required: false });
6827
+ const container = boundary?.parentElement;
6828
+ const group = container ? readAttribute(container, attributes, "async", "reveal") : null;
6829
+ if (!group) {
6830
+ return null;
6831
+ }
6832
+ const children = directChildBoundaryElements(container, attributes);
6833
+ const index = children.indexOf(boundary);
6834
+ if (index === -1) {
6835
+ return null;
6836
+ }
6837
+ return {
6838
+ group,
6839
+ index,
6840
+ count: children.length,
6841
+ order: readAttribute(container, attributes, "async", "reveal-order") || "as-ready",
6842
+ tail: readAttribute(container, attributes, "async", "reveal-tail") || undefined
6843
+ };
6844
+ }
6845
+
6846
+ function sameRevealMetadata(left, right) {
6847
+ return left?.group === right.group &&
6848
+ left?.index === right.index &&
6849
+ left?.count === right.count &&
6850
+ (left?.order ?? "as-ready") === right.order &&
6851
+ left?.tail === right.tail;
6852
+ }
6853
+
6491
6854
  function validatePatch(patch) {
6492
6855
  if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
6493
6856
  throw new TypeError("receiver.apply(patch) requires a boundary patch object.");
@@ -6517,16 +6880,127 @@ const __boundaryReceiverModule = (() => {
6517
6880
  throw new TypeError("Boundary patch scope must be a string.");
6518
6881
  }
6519
6882
 
6883
+ const normalized = { ...patch };
6884
+ if (patch.attrs !== undefined) {
6885
+ normalized.attrs = normalizeAttributePatches(patch.attrs);
6886
+ }
6887
+ if (patch.replace !== undefined) {
6888
+ normalized.replace = normalizeReplacements(patch.replace);
6889
+ }
6890
+ if (patch.reveal !== undefined) {
6891
+ normalized.reveal = normalizeRevealMetadata(patch.reveal);
6892
+ }
6893
+
6520
6894
  const hasHtml = Object.hasOwn(patch, "html") && patch.html != null;
6521
6895
  const hasSignals = patch.signals && Object.keys(patch.signals).length > 0;
6522
6896
  const hasBrowserCache = patch.cache?.browser && Object.keys(patch.cache.browser).length > 0;
6523
6897
  const hasRedirect = Boolean(patch.redirect);
6524
6898
  const hasError = Object.hasOwn(patch, "error");
6525
- if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError) {
6526
- throw new TypeError("Boundary patch must include html, signals, cache.browser, redirect, or error.");
6899
+ const hasAttrs = normalized.attrs?.items.length > 0;
6900
+ const hasReplace = normalized.replace?.length > 0;
6901
+ if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError && !hasAttrs && !hasReplace) {
6902
+ throw new TypeError("Boundary patch must include html, replace, attrs, signals, cache.browser, redirect, or error.");
6527
6903
  }
6528
6904
 
6529
- return patch;
6905
+ return normalized;
6906
+ }
6907
+
6908
+ function normalizeAttributePatches(attrs) {
6909
+ if (!Array.isArray(attrs)) {
6910
+ throw new TypeError("Boundary patch attrs must be an array.");
6911
+ }
6912
+ if (attrs.length === 0) {
6913
+ return { kind: "built", items: [] };
6914
+ }
6915
+ const named = Array.isArray(attrs[0]);
6916
+ if (named) {
6917
+ return {
6918
+ kind: "named",
6919
+ items: attrs.map((tuple) => normalizeNamedAttributePatch(tuple))
6920
+ };
6921
+ }
6922
+ if (attrs.some(Array.isArray)) {
6923
+ throw new TypeError("Boundary patch attrs cannot mix built triples and no-build tuples.");
6924
+ }
6925
+ if (attrs.length % 3 !== 0) {
6926
+ throw new TypeError("Built attribute patch triples must have a length divisible by 3.");
6927
+ }
6928
+ const items = [];
6929
+ for (let index = 0; index < attrs.length; index += 3) {
6930
+ const target = attrs[index];
6931
+ const name = attrs[index + 1];
6932
+ const value = attrs[index + 2];
6933
+ assertBuiltTarget(target);
6934
+ assertAttributeName(name);
6935
+ assertAttributeValue(value);
6936
+ items.push({ target, name, value });
6937
+ }
6938
+ return { kind: "built", items };
6939
+ }
6940
+
6941
+ function normalizeNamedAttributePatch(tuple) {
6942
+ if (!Array.isArray(tuple) || tuple.length !== 3) {
6943
+ throw new TypeError("No-build attribute patches must be [targetName, attrName, value] tuples.");
6944
+ }
6945
+ const [target, name, value] = tuple;
6946
+ if (typeof target !== "string" || target.length === 0) {
6947
+ throw new TypeError("No-build attribute patch target names must be non-empty strings.");
6948
+ }
6949
+ assertAttributeName(name);
6950
+ assertAttributeValue(value);
6951
+ return { target, name, value };
6952
+ }
6953
+
6954
+ function normalizeReplacements(value) {
6955
+ const replacements = Array.isArray(value) ? value : [value];
6956
+ return replacements.map((replacement) => {
6957
+ if (!isPlainObject(replacement)) {
6958
+ throw new TypeError("Boundary patch replace records must be objects.");
6959
+ }
6960
+ if (typeof replacement.target !== "string" || replacement.target.length === 0) {
6961
+ throw new TypeError("Boundary patch replace target must be a non-empty string.");
6962
+ }
6963
+ if (replacement.mode !== undefined && replacement.mode !== "pending" && replacement.mode !== "boundary") {
6964
+ throw new TypeError("Boundary patch replace mode must be \"pending\" or \"boundary\".");
6965
+ }
6966
+ if (!Object.hasOwn(replacement, "html") || replacement.html == null) {
6967
+ throw new TypeError("Boundary patch replace records must include html.");
6968
+ }
6969
+ return {
6970
+ target: replacement.target,
6971
+ html: replacement.html,
6972
+ mode: replacement.mode ?? "pending"
6973
+ };
6974
+ });
6975
+ }
6976
+
6977
+ function normalizeRevealMetadata(reveal) {
6978
+ if (!isPlainObject(reveal)) {
6979
+ throw new TypeError("Boundary patch reveal metadata must be an object.");
6980
+ }
6981
+ const order = reveal.order ?? "as-ready";
6982
+ if (typeof reveal.group !== "string" || reveal.group.length === 0) {
6983
+ throw new TypeError("Reveal group must be a non-empty string.");
6984
+ }
6985
+ if (!Number.isInteger(reveal.count) || reveal.count < 1) {
6986
+ throw new TypeError("Reveal count must be a positive integer.");
6987
+ }
6988
+ if (!Number.isInteger(reveal.index) || reveal.index < 0 || reveal.index >= reveal.count) {
6989
+ throw new TypeError("Reveal index must be an integer from 0 to count - 1.");
6990
+ }
6991
+ if (!revealOrders.has(order)) {
6992
+ throw new TypeError("Reveal order must be as-ready, forwards, backwards, or together.");
6993
+ }
6994
+ if (reveal.tail !== undefined && !revealTails.has(reveal.tail)) {
6995
+ throw new TypeError("Reveal tail must be collapsed or hidden.");
6996
+ }
6997
+ return {
6998
+ group: reveal.group,
6999
+ index: reveal.index,
7000
+ count: reveal.count,
7001
+ order,
7002
+ tail: reveal.tail
7003
+ };
6530
7004
  }
6531
7005
 
6532
7006
  function assertBoundary(boundary) {
@@ -6535,6 +7009,41 @@ const __boundaryReceiverModule = (() => {
6535
7009
  }
6536
7010
  }
6537
7011
 
7012
+ function assertBuiltTarget(target) {
7013
+ if (!Number.isInteger(target) || target < 0) {
7014
+ throw new TypeError("Built attribute patch target indexes must be non-negative integers.");
7015
+ }
7016
+ }
7017
+
7018
+ function assertAttributeName(name) {
7019
+ if (typeof name !== "string" || name.length === 0) {
7020
+ throw new TypeError("Attribute patch names must be non-empty strings.");
7021
+ }
7022
+ const normalized = name.toLowerCase();
7023
+ if (
7024
+ normalized.startsWith("on") ||
7025
+ structuralAttributeNames.has(normalized) ||
7026
+ /[\s<>"'=]/.test(name)
7027
+ ) {
7028
+ throw new TypeError(`Attribute patch name "${name}" is not allowed.`);
7029
+ }
7030
+ }
7031
+
7032
+ function assertAttributeValue(value) {
7033
+ const type = typeof value;
7034
+ if (value != null && type !== "string" && type !== "number" && type !== "boolean") {
7035
+ throw new TypeError("Attribute patch values must be strings, numbers, booleans, null, or undefined.");
7036
+ }
7037
+ }
7038
+
7039
+ function setPatchedAttribute(element, name, value) {
7040
+ if (value === false || value == null) {
7041
+ element.removeAttribute(name);
7042
+ return;
7043
+ }
7044
+ element.setAttribute(name, value === true ? "" : String(value));
7045
+ }
7046
+
6538
7047
  async function flushScheduler(scheduler, scope) {
6539
7048
  if (!scheduler) {
6540
7049
  return;
@@ -6557,6 +7066,90 @@ const __boundaryReceiverModule = (() => {
6557
7066
  location?.assign?.(redirect);
6558
7067
  }
6559
7068
 
7069
+ function takeReadyRevealItems(group) {
7070
+ if (group.order === "as-ready") {
7071
+ return takePendingIndexes(group, [...group.pending.keys()].sort((left, right) => left - right));
7072
+ }
7073
+ if (group.order === "forwards") {
7074
+ const indexes = [];
7075
+ while (group.pending.has(group.nextForward)) {
7076
+ indexes.push(group.nextForward);
7077
+ group.nextForward += 1;
7078
+ }
7079
+ return takePendingIndexes(group, indexes);
7080
+ }
7081
+ if (group.order === "backwards") {
7082
+ const indexes = [];
7083
+ while (group.pending.has(group.nextBackward)) {
7084
+ indexes.push(group.nextBackward);
7085
+ group.nextBackward -= 1;
7086
+ }
7087
+ return takePendingIndexes(group, indexes);
7088
+ }
7089
+ if (group.committed.size + group.pending.size < group.count) {
7090
+ return [];
7091
+ }
7092
+ const indexes = [];
7093
+ for (let index = 0; index < group.count; index += 1) {
7094
+ if (group.pending.has(index)) {
7095
+ indexes.push(index);
7096
+ }
7097
+ }
7098
+ return takePendingIndexes(group, indexes);
7099
+ }
7100
+
7101
+ function takePendingIndexes(group, indexes) {
7102
+ const items = [];
7103
+ for (const index of indexes) {
7104
+ const item = group.pending.get(index);
7105
+ if (item) {
7106
+ group.pending.delete(index);
7107
+ items.push(item);
7108
+ }
7109
+ }
7110
+ return items;
7111
+ }
7112
+
7113
+ function pendingOrderFor(group, pending) {
7114
+ return group.order === "backwards"
7115
+ ? pending.slice().sort((left, right) => right - left)
7116
+ : pending.slice().sort((left, right) => left - right);
7117
+ }
7118
+
7119
+ function inspectRevealGroups(groups) {
7120
+ const inspected = {};
7121
+ for (const [id, group] of groups) {
7122
+ inspected[id] = {
7123
+ count: group.count,
7124
+ order: group.order,
7125
+ tail: group.tail,
7126
+ pending: [...group.pending.keys()].sort((left, right) => left - right),
7127
+ committed: [...group.committed].sort((left, right) => left - right)
7128
+ };
7129
+ }
7130
+ return inspected;
7131
+ }
7132
+
7133
+ function revealResultMetadata(reveal) {
7134
+ return {
7135
+ group: reveal.group,
7136
+ index: reveal.index,
7137
+ count: reveal.count,
7138
+ order: reveal.order,
7139
+ tail: reveal.tail
7140
+ };
7141
+ }
7142
+
7143
+ function withPatchMetadata(result, attrs, replacementCount) {
7144
+ if (attrs) {
7145
+ result.attrs = attrs;
7146
+ }
7147
+ if (replacementCount > 0) {
7148
+ result.replace = { applied: replacementCount };
7149
+ }
7150
+ return result;
7151
+ }
7152
+
6560
7153
  function toStableError(value) {
6561
7154
  if (value instanceof Error) {
6562
7155
  return value;
@@ -6582,13 +7175,118 @@ const __boundaryReceiverModule = (() => {
6582
7175
  if (result.status === "redirected") {
6583
7176
  entry.redirect = result.redirect;
6584
7177
  }
7178
+ if (result.status === "buffered") {
7179
+ entry.reveal = result.reveal;
7180
+ }
7181
+ if (result.attrs) {
7182
+ entry.attrs = { ...result.attrs };
7183
+ }
7184
+ if (result.replace) {
7185
+ entry.replace = { ...result.replace };
7186
+ }
6585
7187
  return entry;
6586
7188
  }
6587
7189
 
7190
+ function findBoundaryElement(root, boundaryId, attributes, options = {}) {
7191
+ const boundary = elementsIn(root)
7192
+ .find((element) => boundaryIdFor(element, attributes) === String(boundaryId));
7193
+ if (!boundary && options.required !== false) {
7194
+ throw new Error(`Boundary "${boundaryId}" was not found.`);
7195
+ }
7196
+ return boundary ?? null;
7197
+ }
7198
+
7199
+ function boundaryIdFor(element, attributes) {
7200
+ if (element?.tagName === "ASYNC-SUSPENSE" && element.hasAttribute?.("for")) {
7201
+ return element.getAttribute("for");
7202
+ }
7203
+ return readAttribute(element, attributes, "async", "boundary");
7204
+ }
7205
+
7206
+ function directChildBoundaryElements(container, attributes) {
7207
+ return [...(container?.children ?? [])]
7208
+ .filter((element) => boundaryIdFor(element, attributes) != null);
7209
+ }
7210
+
7211
+ function findRevealContainer(root, group, attributes) {
7212
+ return elementsIn(root)
7213
+ .find((element) => readAttribute(element, attributes, "async", "reveal") === group)
7214
+ ?? null;
7215
+ }
7216
+
7217
+ function isNestedBoundaryElement(element, boundaryElement, attributes) {
7218
+ if (element === boundaryElement) {
7219
+ return false;
7220
+ }
7221
+ if (boundaryIdFor(element, attributes) != null) {
7222
+ return true;
7223
+ }
7224
+ let parent = element.parentElement;
7225
+ while (parent && parent !== boundaryElement) {
7226
+ if (boundaryIdFor(parent, attributes) != null) {
7227
+ return true;
7228
+ }
7229
+ parent = parent.parentElement;
7230
+ }
7231
+ return false;
7232
+ }
7233
+
7234
+ function setTailHidden(element, hidden) {
7235
+ if (hidden) {
7236
+ element.setAttribute("hidden", "");
7237
+ if ("hidden" in element) {
7238
+ element.hidden = true;
7239
+ }
7240
+ return;
7241
+ }
7242
+ element.removeAttribute("hidden");
7243
+ if ("hidden" in element) {
7244
+ element.hidden = false;
7245
+ }
7246
+ }
7247
+
7248
+ function containsOrEquals(parent, child) {
7249
+ return parent === child || parent.contains?.(child);
7250
+ }
7251
+
7252
+ function ownerDocumentOf(element) {
7253
+ return element.ownerDocument ?? globalThis.document;
7254
+ }
7255
+
7256
+ function toFragment(value, documentRef) {
7257
+ if (value?.nodeType === 11) {
7258
+ return value.cloneNode(true);
7259
+ }
7260
+ if (value?.tagName === "TEMPLATE") {
7261
+ return value.content.cloneNode(true);
7262
+ }
7263
+ if (value?.nodeType) {
7264
+ const fragment = documentRef.createDocumentFragment();
7265
+ fragment.append(value.cloneNode(true));
7266
+ return fragment;
7267
+ }
7268
+ const template = documentRef.createElement("template");
7269
+ template.innerHTML = typeof value === "string" ? value : renderTemplate(value);
7270
+ return template.content.cloneNode(true);
7271
+ }
7272
+
7273
+ function elementsIn(scope) {
7274
+ const elements = [];
7275
+ if (scope?.nodeType === 1) {
7276
+ elements.push(scope);
7277
+ }
7278
+ elements.push(...(scope?.querySelectorAll?.("*") ?? []));
7279
+ return elements;
7280
+ }
7281
+
7282
+ function isElementLike(value) {
7283
+ return Boolean(value?.nodeType === 1 && typeof value.textContent === "string");
7284
+ }
7285
+
6588
7286
  function isPlainObject(value) {
6589
7287
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
6590
7288
  }
6591
- return { createBoundaryReceiver };
7289
+ return { createBoundaryReceiver, AsyncStream };
6592
7290
  })();
6593
7291
 
6594
7292
  const __delayModule = (() => {
@@ -6698,6 +7396,7 @@ const { defineApp: defineApp } = __serverEntryModule;
6698
7396
  const { readSnapshot: readSnapshot } = __serverEntryModule;
6699
7397
  const { attributeName: attributeName } = __attributesModule;
6700
7398
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
7399
+ const { AsyncStream: AsyncStream } = __boundaryReceiverModule;
6701
7400
  const { createBoundaryReceiver: createBoundaryReceiver } = __boundaryReceiverModule;
6702
7401
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
6703
7402
  const { defineCache: defineCache } = __cacheModule;
@@ -6732,4 +7431,4 @@ const { createSignalRegistry: createSignalRegistry } = __signalsModule;
6732
7431
  const { effect: effect } = __signalsModule;
6733
7432
  const { signal: signal } = __signalsModule;
6734
7433
 
6735
- 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, createRequestContextStore, applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult, createServerRegistry, computed, createSignal, createSignalRegistry, effect, signal };
7434
+ export { asyncSignal, Async, createApp, defineApp, readSnapshot, attributeName, defineAttributeConfig, AsyncStream, createBoundaryReceiver, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, defineAsyncContainerElement, defineAsyncSuspenseElement, createHandlerRegistry, html, createLazyRegistry, defineRegistrySnapshot, Loader, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createScheduler, createRequestContextStore, applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult, createServerRegistry, computed, createSignal, createSignalRegistry, effect, signal };