@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/browser.js CHANGED
@@ -6094,7 +6094,19 @@ const __appModule = (() => {
6094
6094
  })();
6095
6095
 
6096
6096
  const __boundaryReceiverModule = (() => {
6097
+ const { normalizeAttributeConfig, readAttribute } = __attributesModule;
6098
+ const { renderTemplate } = __htmlModule;
6097
6099
  const defaultRecentLimit = 50;
6100
+ const builtBackpatchAttribute = "data-async-backpatch";
6101
+ const pendingTargetAttribute = "data-pending-id";
6102
+ const revealOrders = new Set(["as-ready", "forwards", "backwards", "together"]);
6103
+ const revealTails = new Set(["collapsed", "hidden"]);
6104
+ const structuralAttributeNames = new Set(["innerhtml", "outerhtml", "textcontent", "children", "childnodes"]);
6105
+
6106
+ const AsyncStream = Object.freeze({
6107
+ applyScript,
6108
+ applyCurrentScript
6109
+ });
6098
6110
 
6099
6111
  function createBoundaryReceiver(options = {}) {
6100
6112
  const loader = options.loader;
@@ -6102,6 +6114,7 @@ const __boundaryReceiverModule = (() => {
6102
6114
  const cache = options.cache ?? loader?.cache;
6103
6115
  const scheduler = options.scheduler ?? loader?.scheduler;
6104
6116
  const router = options.router ?? loader?.router;
6117
+ const attributes = normalizeAttributeConfig(options.attributes ?? loader?.attributes);
6105
6118
  const recentLimit = options.recentLimit ?? defaultRecentLimit;
6106
6119
  const throwOnError = options.throwOnError === true;
6107
6120
  const onApply = typeof options.onApply === "function" ? options.onApply : undefined;
@@ -6119,6 +6132,7 @@ const __boundaryReceiverModule = (() => {
6119
6132
  }
6120
6133
 
6121
6134
  const boundaries = new Map();
6135
+ const revealGroups = new Map();
6122
6136
  const recent = [];
6123
6137
  let destroyed = false;
6124
6138
 
@@ -6167,6 +6181,7 @@ const __boundaryReceiverModule = (() => {
6167
6181
  return {
6168
6182
  destroyed,
6169
6183
  boundaries: snapshot,
6184
+ reveal: inspectRevealGroups(revealGroups),
6170
6185
  recent: recent.map((entry) => ({ ...entry }))
6171
6186
  };
6172
6187
  },
@@ -6174,6 +6189,7 @@ const __boundaryReceiverModule = (() => {
6174
6189
  reset(boundary) {
6175
6190
  if (boundary === undefined) {
6176
6191
  boundaries.clear();
6192
+ revealGroups.clear();
6177
6193
  recent.length = 0;
6178
6194
  return receiver;
6179
6195
  }
@@ -6184,12 +6200,20 @@ const __boundaryReceiverModule = (() => {
6184
6200
  recent.splice(index, 1);
6185
6201
  }
6186
6202
  }
6203
+ for (const group of revealGroups.values()) {
6204
+ for (const [index, item] of group.pending) {
6205
+ if (item.normalized.boundary === boundary) {
6206
+ group.pending.delete(index);
6207
+ }
6208
+ }
6209
+ }
6187
6210
  return receiver;
6188
6211
  },
6189
6212
 
6190
6213
  destroy() {
6191
6214
  destroyed = true;
6192
6215
  boundaries.clear();
6216
+ revealGroups.clear();
6193
6217
  recent.length = 0;
6194
6218
  }
6195
6219
  };
@@ -6197,32 +6221,10 @@ const __boundaryReceiverModule = (() => {
6197
6221
  return receiver;
6198
6222
 
6199
6223
  async function applyBoundaryPatch(record, normalized, patch) {
6200
- if (normalized.seq <= record.lastSeq) {
6201
- const result = {
6202
- status: "ignored-stale",
6203
- boundary: normalized.boundary,
6204
- seq: normalized.seq,
6205
- lastSeq: record.lastSeq
6206
- };
6207
- record.ignored += 1;
6208
- record.lastStatus = result.status;
6209
- remember(result);
6210
- onIgnore?.(result, patch);
6211
- return result;
6212
- }
6213
-
6214
- if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
6215
- const result = {
6216
- status: "ignored-destroyed",
6217
- boundary: normalized.boundary,
6218
- seq: normalized.seq,
6219
- parentScope: normalized.parentScope
6220
- };
6221
- record.ignored += 1;
6222
- record.lastStatus = result.status;
6223
- remember(result);
6224
- onIgnore?.(result, patch);
6225
- return result;
6224
+ const ignored = preflightIgnoredResult(record, normalized);
6225
+ if (ignored) {
6226
+ rememberIgnored(record, ignored, patch);
6227
+ return ignored;
6226
6228
  }
6227
6229
 
6228
6230
  if (Object.hasOwn(normalized, "error")) {
@@ -6244,35 +6246,91 @@ const __boundaryReceiverModule = (() => {
6244
6246
  return result;
6245
6247
  }
6246
6248
 
6247
- if (normalized.signals) {
6248
- if (!signals || typeof signals.set !== "function") {
6249
- throw new Error("Boundary patch includes signals, but no signal registry is available.");
6250
- }
6251
- for (const [path, value] of Object.entries(normalized.signals)) {
6252
- signals.set(path, value);
6253
- }
6249
+ applyStateEffects(normalized);
6250
+
6251
+ if (normalized.reveal) {
6252
+ return await applyRevealPatch(record, normalized, patch);
6254
6253
  }
6255
6254
 
6256
- if (normalized.cache?.browser) {
6257
- if (!cache || typeof cache.restore !== "function") {
6258
- throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
6255
+ return await commitBoundaryPatch(record, normalized, patch, { stateApplied: true });
6256
+ }
6257
+
6258
+ async function applyRevealPatch(record, normalized, patch) {
6259
+ const group = revealGroup(normalized.reveal);
6260
+ const index = normalized.reveal.index;
6261
+ if (group.committed.has(index)) {
6262
+ throw new TypeError(`Reveal group "${group.id}" already committed index ${index}.`);
6263
+ }
6264
+ if (group.pending.has(index)) {
6265
+ throw new TypeError(`Reveal group "${group.id}" already has a pending patch for index ${index}.`);
6266
+ }
6267
+
6268
+ const item = { record, normalized, patch };
6269
+ group.pending.set(index, item);
6270
+ const ready = takeReadyRevealItems(group);
6271
+ if (!ready.includes(item)) {
6272
+ const result = {
6273
+ status: "buffered",
6274
+ boundary: normalized.boundary,
6275
+ seq: normalized.seq,
6276
+ reveal: revealResultMetadata(normalized.reveal)
6277
+ };
6278
+ record.lastStatus = result.status;
6279
+ remember(result);
6280
+ updateRevealTail(group);
6281
+ return result;
6282
+ }
6283
+
6284
+ let currentResult;
6285
+ for (const readyItem of ready) {
6286
+ const result = await commitBoundaryPatch(readyItem.record, readyItem.normalized, readyItem.patch, {
6287
+ stateApplied: true
6288
+ });
6289
+ group.committed.add(readyItem.normalized.reveal.index);
6290
+ if (readyItem === item) {
6291
+ currentResult = result;
6259
6292
  }
6260
- cache.restore(normalized.cache.browser);
6293
+ }
6294
+ updateRevealTail(group);
6295
+ return currentResult;
6296
+ }
6297
+
6298
+ async function commitBoundaryPatch(record, normalized, patch, options = {}) {
6299
+ const ignored = preflightIgnoredResult(record, normalized);
6300
+ if (ignored) {
6301
+ rememberIgnored(record, ignored, patch);
6302
+ return ignored;
6303
+ }
6304
+
6305
+ if (!options.stateApplied) {
6306
+ applyStateEffects(normalized);
6261
6307
  }
6262
6308
 
6309
+ let boundaryElement;
6310
+ let replacementCount = 0;
6263
6311
  if (normalized.html != null) {
6264
- loader.swap(normalized.boundary, normalized.html);
6312
+ boundaryElement = loader.swap(normalized.boundary, normalized.html);
6313
+ }
6314
+ if (normalized.replace) {
6315
+ boundaryElement ??= findBoundaryElement(loader.root, normalized.boundary, attributes);
6316
+ replacementCount = applyReplacements(boundaryElement, normalized.replace);
6317
+ }
6318
+
6319
+ let attrs;
6320
+ if (normalized.attrs) {
6321
+ boundaryElement ??= findBoundaryElement(loader.root, normalized.boundary, attributes);
6322
+ attrs = applyAttributePatches(boundaryElement, normalized.attrs);
6265
6323
  }
6266
6324
 
6267
6325
  await flushScheduler(scheduler, normalized.scope);
6268
6326
 
6269
6327
  if (normalized.redirect) {
6270
- const result = {
6328
+ const result = withPatchMetadata({
6271
6329
  status: "redirected",
6272
6330
  boundary: normalized.boundary,
6273
6331
  seq: normalized.seq,
6274
6332
  redirect: normalized.redirect
6275
- };
6333
+ }, attrs, replacementCount);
6276
6334
  await followRedirect(normalized.redirect, router, loader);
6277
6335
  record.applied += 1;
6278
6336
  record.lastSeq = normalized.seq;
@@ -6282,11 +6340,11 @@ const __boundaryReceiverModule = (() => {
6282
6340
  return result;
6283
6341
  }
6284
6342
 
6285
- const result = {
6343
+ const result = withPatchMetadata({
6286
6344
  status: "applied",
6287
6345
  boundary: normalized.boundary,
6288
6346
  seq: normalized.seq
6289
- };
6347
+ }, attrs, replacementCount);
6290
6348
  record.applied += 1;
6291
6349
  record.lastSeq = normalized.seq;
6292
6350
  record.lastStatus = result.status;
@@ -6295,6 +6353,157 @@ const __boundaryReceiverModule = (() => {
6295
6353
  return result;
6296
6354
  }
6297
6355
 
6356
+ function applyStateEffects(normalized) {
6357
+ if (normalized.signals) {
6358
+ if (!signals || typeof signals.set !== "function") {
6359
+ throw new Error("Boundary patch includes signals, but no signal registry is available.");
6360
+ }
6361
+ for (const [path, value] of Object.entries(normalized.signals)) {
6362
+ signals.set(path, value);
6363
+ }
6364
+ }
6365
+
6366
+ if (normalized.cache?.browser) {
6367
+ if (!cache || typeof cache.restore !== "function") {
6368
+ throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
6369
+ }
6370
+ cache.restore(normalized.cache.browser);
6371
+ }
6372
+ }
6373
+
6374
+ function applyReplacements(boundaryElement, replacements) {
6375
+ let applied = 0;
6376
+ for (const replacement of replacements) {
6377
+ if (replacement.mode === "boundary") {
6378
+ const target = findBoundaryElement(loader.root, replacement.target, attributes);
6379
+ if (!containsOrEquals(boundaryElement, target)) {
6380
+ throw new Error(`Boundary replacement target "${replacement.target}" is outside boundary "${boundaryIdFor(boundaryElement, attributes)}".`);
6381
+ }
6382
+ loader.swap(replacement.target, replacement.html);
6383
+ applied += 1;
6384
+ continue;
6385
+ }
6386
+
6387
+ const target = findUniqueScopedElement(
6388
+ boundaryElement,
6389
+ (element) => element.getAttribute?.(pendingTargetAttribute) === replacement.target,
6390
+ `Pending replacement target "${replacement.target}"`
6391
+ );
6392
+ const fragment = toFragment(replacement.html, ownerDocumentOf(boundaryElement));
6393
+ const inserted = [...fragment.childNodes];
6394
+ target.replaceWith(fragment);
6395
+ for (const node of inserted) {
6396
+ if (node.nodeType === 1 || node.nodeType === 11) {
6397
+ loader.scan?.(node);
6398
+ }
6399
+ }
6400
+ applied += 1;
6401
+ }
6402
+ return applied;
6403
+ }
6404
+
6405
+ function applyAttributePatches(boundaryElement, attrs) {
6406
+ let applied = 0;
6407
+ for (const attr of attrs.items) {
6408
+ const target = attrs.kind === "built"
6409
+ ? resolveBuiltAttrTarget(boundaryElement, attr.target)
6410
+ : resolveNamedAttrTarget(boundaryElement, attr.target);
6411
+ setPatchedAttribute(target, attr.name, attr.value);
6412
+ applied += 1;
6413
+ }
6414
+ return {
6415
+ applied,
6416
+ ignored: 0
6417
+ };
6418
+ }
6419
+
6420
+ function resolveBuiltAttrTarget(boundaryElement, targetIndex) {
6421
+ const targets = scopedElements(boundaryElement)
6422
+ .filter((element) => element.hasAttribute?.(builtBackpatchAttribute));
6423
+ const target = targets[targetIndex];
6424
+ if (!target) {
6425
+ throw new Error(`Built attribute patch target ${targetIndex} was not found in boundary "${boundaryIdFor(boundaryElement, attributes)}".`);
6426
+ }
6427
+ return target;
6428
+ }
6429
+
6430
+ function resolveNamedAttrTarget(boundaryElement, targetName) {
6431
+ return findUniqueScopedElement(
6432
+ boundaryElement,
6433
+ (element) => readAttribute(element, attributes, "async", "patch") === targetName,
6434
+ `Attribute patch target "${targetName}"`
6435
+ );
6436
+ }
6437
+
6438
+ function findUniqueScopedElement(boundaryElement, predicate, label) {
6439
+ const matches = scopedElements(boundaryElement).filter(predicate);
6440
+ if (matches.length === 0) {
6441
+ throw new Error(`${label} was not found.`);
6442
+ }
6443
+ if (matches.length > 1) {
6444
+ throw new Error(`${label} is ambiguous.`);
6445
+ }
6446
+ return matches[0];
6447
+ }
6448
+
6449
+ function scopedElements(boundaryElement) {
6450
+ return elementsIn(boundaryElement)
6451
+ .filter((element) => !isNestedBoundaryElement(element, boundaryElement, attributes));
6452
+ }
6453
+
6454
+ function revealGroup(reveal) {
6455
+ const existing = revealGroups.get(reveal.group);
6456
+ if (existing) {
6457
+ if (existing.count !== reveal.count || existing.order !== reveal.order || existing.tail !== reveal.tail) {
6458
+ throw new TypeError(`Reveal group "${reveal.group}" metadata does not match earlier patches.`);
6459
+ }
6460
+ return existing;
6461
+ }
6462
+
6463
+ const group = {
6464
+ id: reveal.group,
6465
+ count: reveal.count,
6466
+ order: reveal.order,
6467
+ tail: reveal.tail,
6468
+ pending: new Map(),
6469
+ committed: new Set(),
6470
+ nextForward: 0,
6471
+ nextBackward: reveal.count - 1
6472
+ };
6473
+ revealGroups.set(reveal.group, group);
6474
+ return group;
6475
+ }
6476
+
6477
+ function updateRevealTail(group) {
6478
+ if (!group.tail) {
6479
+ return;
6480
+ }
6481
+ const container = findRevealContainer(loader.root, group.id, attributes);
6482
+ if (!container) {
6483
+ return;
6484
+ }
6485
+ const children = directChildBoundaryElements(container, attributes).slice(0, group.count);
6486
+ if (children.length === 0) {
6487
+ return;
6488
+ }
6489
+
6490
+ const pending = [];
6491
+ for (let index = 0; index < children.length; index += 1) {
6492
+ if (!group.committed.has(index)) {
6493
+ pending.push(index);
6494
+ } else {
6495
+ setTailHidden(children[index], false);
6496
+ }
6497
+ }
6498
+
6499
+ const visiblePending = group.tail === "collapsed" && pending.length > 0
6500
+ ? pendingOrderFor(group, pending)[0]
6501
+ : undefined;
6502
+ for (const index of pending) {
6503
+ setTailHidden(children[index], group.tail === "hidden" || index !== visiblePending);
6504
+ }
6505
+ }
6506
+
6298
6507
  function boundaryRecord(boundary) {
6299
6508
  if (!boundaries.has(boundary)) {
6300
6509
  boundaries.set(boundary, {
@@ -6309,6 +6518,34 @@ const __boundaryReceiverModule = (() => {
6309
6518
  return boundaries.get(boundary);
6310
6519
  }
6311
6520
 
6521
+ function preflightIgnoredResult(record, normalized) {
6522
+ if (normalized.seq <= record.lastSeq) {
6523
+ return {
6524
+ status: "ignored-stale",
6525
+ boundary: normalized.boundary,
6526
+ seq: normalized.seq,
6527
+ lastSeq: record.lastSeq
6528
+ };
6529
+ }
6530
+
6531
+ if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
6532
+ return {
6533
+ status: "ignored-destroyed",
6534
+ boundary: normalized.boundary,
6535
+ seq: normalized.seq,
6536
+ parentScope: normalized.parentScope
6537
+ };
6538
+ }
6539
+ return null;
6540
+ }
6541
+
6542
+ function rememberIgnored(record, result, patch) {
6543
+ record.ignored += 1;
6544
+ record.lastStatus = result.status;
6545
+ remember(result);
6546
+ onIgnore?.(result, patch);
6547
+ }
6548
+
6312
6549
  function remember(result) {
6313
6550
  if (recentLimit === 0) {
6314
6551
  return;
@@ -6320,6 +6557,132 @@ const __boundaryReceiverModule = (() => {
6320
6557
  }
6321
6558
  }
6322
6559
 
6560
+ function applyScript(script, options = {}) {
6561
+ if (!script || typeof script !== "object" || typeof script.textContent !== "string") {
6562
+ throw new TypeError("AsyncStream.applyScript(script) requires a JSON script element.");
6563
+ }
6564
+ const attributes = normalizeAttributeConfig(options.attributes ?? options.receiver?.attributes);
6565
+ const patch = parseStreamPatch(script.textContent);
6566
+ const root = options.root ?? script.ownerDocument ?? globalThis.document;
6567
+ const resolved = resolveStreamPatch(patch, { root, attributes });
6568
+ const receiver = resolveReceiver(options);
6569
+ return receiver.apply(resolved);
6570
+ }
6571
+
6572
+ function applyCurrentScript(scriptOrOptions, maybeOptions) {
6573
+ const script = isElementLike(scriptOrOptions)
6574
+ ? scriptOrOptions
6575
+ : globalThis.document?.currentScript;
6576
+ const options = isElementLike(scriptOrOptions) ? maybeOptions ?? {} : scriptOrOptions ?? {};
6577
+ return applyScript(script, options);
6578
+ }
6579
+
6580
+ function resolveReceiver(options = {}) {
6581
+ if (options.receiver && typeof options.receiver.apply === "function") {
6582
+ return options.receiver;
6583
+ }
6584
+ const runtime = options.runtime ?? globalThis.Async?.runtime;
6585
+ const loader = options.loader ?? runtime?.loader;
6586
+ if (!loader) {
6587
+ throw new TypeError("AsyncStream requires receiver, loader, or runtime.loader.");
6588
+ }
6589
+ return createBoundaryReceiver({
6590
+ loader,
6591
+ signals: options.signals ?? runtime?.signals,
6592
+ cache: options.cache ?? runtime?.browser?.cache,
6593
+ scheduler: options.scheduler ?? runtime?.scheduler,
6594
+ router: options.router ?? runtime?.router,
6595
+ attributes: options.attributes ?? runtime?.attributes ?? loader.attributes
6596
+ });
6597
+ }
6598
+
6599
+ function parseStreamPatch(source) {
6600
+ try {
6601
+ return JSON.parse(source);
6602
+ } catch (error) {
6603
+ throw new TypeError(`Async stream patch JSON is invalid: ${error.message}`);
6604
+ }
6605
+ }
6606
+
6607
+ function resolveStreamPatch(patch, { root, attributes }) {
6608
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
6609
+ throw new TypeError("Async stream patch JSON must be an object.");
6610
+ }
6611
+ const resolved = { ...patch };
6612
+ if (patch.replace !== undefined) {
6613
+ resolved.replace = resolveStreamReplacements(patch.replace, root, attributes);
6614
+ }
6615
+ const synthesizedReveal = synthesizeRevealMetadata(resolved, root, attributes);
6616
+ if (patch.reveal !== undefined && synthesizedReveal && !sameRevealMetadata(patch.reveal, synthesizedReveal)) {
6617
+ throw new TypeError("Explicit stream reveal metadata conflicts with DOM reveal metadata.");
6618
+ }
6619
+ if (patch.reveal === undefined && synthesizedReveal) {
6620
+ resolved.reveal = synthesizedReveal;
6621
+ }
6622
+ return resolved;
6623
+ }
6624
+
6625
+ function resolveStreamReplacements(value, root, attributes) {
6626
+ const replacements = Array.isArray(value) ? value : [value];
6627
+ return replacements.map((replacement) => {
6628
+ if (!isPlainObject(replacement)) {
6629
+ throw new TypeError("Stream replacement records must be objects.");
6630
+ }
6631
+ if (replacement.template === undefined || Object.hasOwn(replacement, "html")) {
6632
+ return replacement;
6633
+ }
6634
+ const template = findStreamTemplate(root, replacement.template, attributes);
6635
+ if (!template) {
6636
+ throw new Error(`Stream template "${replacement.template}" was not found.`);
6637
+ }
6638
+ return {
6639
+ ...replacement,
6640
+ html: template.innerHTML
6641
+ };
6642
+ });
6643
+ }
6644
+
6645
+ function findStreamTemplate(root, id, attributes) {
6646
+ if (typeof id !== "string" || id.length === 0) {
6647
+ throw new TypeError("Stream replacement template must be a non-empty string.");
6648
+ }
6649
+ return elementsIn(root)
6650
+ .find((element) => element.tagName === "TEMPLATE" && readAttribute(element, attributes, "async", "stream-template") === id)
6651
+ ?? null;
6652
+ }
6653
+
6654
+ function synthesizeRevealMetadata(patch, root, attributes) {
6655
+ if (typeof patch.boundary !== "string" || patch.boundary.length === 0) {
6656
+ return null;
6657
+ }
6658
+ const boundary = findBoundaryElement(root, patch.boundary, attributes, { required: false });
6659
+ const container = boundary?.parentElement;
6660
+ const group = container ? readAttribute(container, attributes, "async", "reveal") : null;
6661
+ if (!group) {
6662
+ return null;
6663
+ }
6664
+ const children = directChildBoundaryElements(container, attributes);
6665
+ const index = children.indexOf(boundary);
6666
+ if (index === -1) {
6667
+ return null;
6668
+ }
6669
+ return {
6670
+ group,
6671
+ index,
6672
+ count: children.length,
6673
+ order: readAttribute(container, attributes, "async", "reveal-order") || "as-ready",
6674
+ tail: readAttribute(container, attributes, "async", "reveal-tail") || undefined
6675
+ };
6676
+ }
6677
+
6678
+ function sameRevealMetadata(left, right) {
6679
+ return left?.group === right.group &&
6680
+ left?.index === right.index &&
6681
+ left?.count === right.count &&
6682
+ (left?.order ?? "as-ready") === right.order &&
6683
+ left?.tail === right.tail;
6684
+ }
6685
+
6323
6686
  function validatePatch(patch) {
6324
6687
  if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
6325
6688
  throw new TypeError("receiver.apply(patch) requires a boundary patch object.");
@@ -6349,16 +6712,127 @@ const __boundaryReceiverModule = (() => {
6349
6712
  throw new TypeError("Boundary patch scope must be a string.");
6350
6713
  }
6351
6714
 
6715
+ const normalized = { ...patch };
6716
+ if (patch.attrs !== undefined) {
6717
+ normalized.attrs = normalizeAttributePatches(patch.attrs);
6718
+ }
6719
+ if (patch.replace !== undefined) {
6720
+ normalized.replace = normalizeReplacements(patch.replace);
6721
+ }
6722
+ if (patch.reveal !== undefined) {
6723
+ normalized.reveal = normalizeRevealMetadata(patch.reveal);
6724
+ }
6725
+
6352
6726
  const hasHtml = Object.hasOwn(patch, "html") && patch.html != null;
6353
6727
  const hasSignals = patch.signals && Object.keys(patch.signals).length > 0;
6354
6728
  const hasBrowserCache = patch.cache?.browser && Object.keys(patch.cache.browser).length > 0;
6355
6729
  const hasRedirect = Boolean(patch.redirect);
6356
6730
  const hasError = Object.hasOwn(patch, "error");
6357
- if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError) {
6358
- throw new TypeError("Boundary patch must include html, signals, cache.browser, redirect, or error.");
6731
+ const hasAttrs = normalized.attrs?.items.length > 0;
6732
+ const hasReplace = normalized.replace?.length > 0;
6733
+ if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError && !hasAttrs && !hasReplace) {
6734
+ throw new TypeError("Boundary patch must include html, replace, attrs, signals, cache.browser, redirect, or error.");
6359
6735
  }
6360
6736
 
6361
- return patch;
6737
+ return normalized;
6738
+ }
6739
+
6740
+ function normalizeAttributePatches(attrs) {
6741
+ if (!Array.isArray(attrs)) {
6742
+ throw new TypeError("Boundary patch attrs must be an array.");
6743
+ }
6744
+ if (attrs.length === 0) {
6745
+ return { kind: "built", items: [] };
6746
+ }
6747
+ const named = Array.isArray(attrs[0]);
6748
+ if (named) {
6749
+ return {
6750
+ kind: "named",
6751
+ items: attrs.map((tuple) => normalizeNamedAttributePatch(tuple))
6752
+ };
6753
+ }
6754
+ if (attrs.some(Array.isArray)) {
6755
+ throw new TypeError("Boundary patch attrs cannot mix built triples and no-build tuples.");
6756
+ }
6757
+ if (attrs.length % 3 !== 0) {
6758
+ throw new TypeError("Built attribute patch triples must have a length divisible by 3.");
6759
+ }
6760
+ const items = [];
6761
+ for (let index = 0; index < attrs.length; index += 3) {
6762
+ const target = attrs[index];
6763
+ const name = attrs[index + 1];
6764
+ const value = attrs[index + 2];
6765
+ assertBuiltTarget(target);
6766
+ assertAttributeName(name);
6767
+ assertAttributeValue(value);
6768
+ items.push({ target, name, value });
6769
+ }
6770
+ return { kind: "built", items };
6771
+ }
6772
+
6773
+ function normalizeNamedAttributePatch(tuple) {
6774
+ if (!Array.isArray(tuple) || tuple.length !== 3) {
6775
+ throw new TypeError("No-build attribute patches must be [targetName, attrName, value] tuples.");
6776
+ }
6777
+ const [target, name, value] = tuple;
6778
+ if (typeof target !== "string" || target.length === 0) {
6779
+ throw new TypeError("No-build attribute patch target names must be non-empty strings.");
6780
+ }
6781
+ assertAttributeName(name);
6782
+ assertAttributeValue(value);
6783
+ return { target, name, value };
6784
+ }
6785
+
6786
+ function normalizeReplacements(value) {
6787
+ const replacements = Array.isArray(value) ? value : [value];
6788
+ return replacements.map((replacement) => {
6789
+ if (!isPlainObject(replacement)) {
6790
+ throw new TypeError("Boundary patch replace records must be objects.");
6791
+ }
6792
+ if (typeof replacement.target !== "string" || replacement.target.length === 0) {
6793
+ throw new TypeError("Boundary patch replace target must be a non-empty string.");
6794
+ }
6795
+ if (replacement.mode !== undefined && replacement.mode !== "pending" && replacement.mode !== "boundary") {
6796
+ throw new TypeError("Boundary patch replace mode must be \"pending\" or \"boundary\".");
6797
+ }
6798
+ if (!Object.hasOwn(replacement, "html") || replacement.html == null) {
6799
+ throw new TypeError("Boundary patch replace records must include html.");
6800
+ }
6801
+ return {
6802
+ target: replacement.target,
6803
+ html: replacement.html,
6804
+ mode: replacement.mode ?? "pending"
6805
+ };
6806
+ });
6807
+ }
6808
+
6809
+ function normalizeRevealMetadata(reveal) {
6810
+ if (!isPlainObject(reveal)) {
6811
+ throw new TypeError("Boundary patch reveal metadata must be an object.");
6812
+ }
6813
+ const order = reveal.order ?? "as-ready";
6814
+ if (typeof reveal.group !== "string" || reveal.group.length === 0) {
6815
+ throw new TypeError("Reveal group must be a non-empty string.");
6816
+ }
6817
+ if (!Number.isInteger(reveal.count) || reveal.count < 1) {
6818
+ throw new TypeError("Reveal count must be a positive integer.");
6819
+ }
6820
+ if (!Number.isInteger(reveal.index) || reveal.index < 0 || reveal.index >= reveal.count) {
6821
+ throw new TypeError("Reveal index must be an integer from 0 to count - 1.");
6822
+ }
6823
+ if (!revealOrders.has(order)) {
6824
+ throw new TypeError("Reveal order must be as-ready, forwards, backwards, or together.");
6825
+ }
6826
+ if (reveal.tail !== undefined && !revealTails.has(reveal.tail)) {
6827
+ throw new TypeError("Reveal tail must be collapsed or hidden.");
6828
+ }
6829
+ return {
6830
+ group: reveal.group,
6831
+ index: reveal.index,
6832
+ count: reveal.count,
6833
+ order,
6834
+ tail: reveal.tail
6835
+ };
6362
6836
  }
6363
6837
 
6364
6838
  function assertBoundary(boundary) {
@@ -6367,6 +6841,41 @@ const __boundaryReceiverModule = (() => {
6367
6841
  }
6368
6842
  }
6369
6843
 
6844
+ function assertBuiltTarget(target) {
6845
+ if (!Number.isInteger(target) || target < 0) {
6846
+ throw new TypeError("Built attribute patch target indexes must be non-negative integers.");
6847
+ }
6848
+ }
6849
+
6850
+ function assertAttributeName(name) {
6851
+ if (typeof name !== "string" || name.length === 0) {
6852
+ throw new TypeError("Attribute patch names must be non-empty strings.");
6853
+ }
6854
+ const normalized = name.toLowerCase();
6855
+ if (
6856
+ normalized.startsWith("on") ||
6857
+ structuralAttributeNames.has(normalized) ||
6858
+ /[\s<>"'=]/.test(name)
6859
+ ) {
6860
+ throw new TypeError(`Attribute patch name "${name}" is not allowed.`);
6861
+ }
6862
+ }
6863
+
6864
+ function assertAttributeValue(value) {
6865
+ const type = typeof value;
6866
+ if (value != null && type !== "string" && type !== "number" && type !== "boolean") {
6867
+ throw new TypeError("Attribute patch values must be strings, numbers, booleans, null, or undefined.");
6868
+ }
6869
+ }
6870
+
6871
+ function setPatchedAttribute(element, name, value) {
6872
+ if (value === false || value == null) {
6873
+ element.removeAttribute(name);
6874
+ return;
6875
+ }
6876
+ element.setAttribute(name, value === true ? "" : String(value));
6877
+ }
6878
+
6370
6879
  async function flushScheduler(scheduler, scope) {
6371
6880
  if (!scheduler) {
6372
6881
  return;
@@ -6389,6 +6898,90 @@ const __boundaryReceiverModule = (() => {
6389
6898
  location?.assign?.(redirect);
6390
6899
  }
6391
6900
 
6901
+ function takeReadyRevealItems(group) {
6902
+ if (group.order === "as-ready") {
6903
+ return takePendingIndexes(group, [...group.pending.keys()].sort((left, right) => left - right));
6904
+ }
6905
+ if (group.order === "forwards") {
6906
+ const indexes = [];
6907
+ while (group.pending.has(group.nextForward)) {
6908
+ indexes.push(group.nextForward);
6909
+ group.nextForward += 1;
6910
+ }
6911
+ return takePendingIndexes(group, indexes);
6912
+ }
6913
+ if (group.order === "backwards") {
6914
+ const indexes = [];
6915
+ while (group.pending.has(group.nextBackward)) {
6916
+ indexes.push(group.nextBackward);
6917
+ group.nextBackward -= 1;
6918
+ }
6919
+ return takePendingIndexes(group, indexes);
6920
+ }
6921
+ if (group.committed.size + group.pending.size < group.count) {
6922
+ return [];
6923
+ }
6924
+ const indexes = [];
6925
+ for (let index = 0; index < group.count; index += 1) {
6926
+ if (group.pending.has(index)) {
6927
+ indexes.push(index);
6928
+ }
6929
+ }
6930
+ return takePendingIndexes(group, indexes);
6931
+ }
6932
+
6933
+ function takePendingIndexes(group, indexes) {
6934
+ const items = [];
6935
+ for (const index of indexes) {
6936
+ const item = group.pending.get(index);
6937
+ if (item) {
6938
+ group.pending.delete(index);
6939
+ items.push(item);
6940
+ }
6941
+ }
6942
+ return items;
6943
+ }
6944
+
6945
+ function pendingOrderFor(group, pending) {
6946
+ return group.order === "backwards"
6947
+ ? pending.slice().sort((left, right) => right - left)
6948
+ : pending.slice().sort((left, right) => left - right);
6949
+ }
6950
+
6951
+ function inspectRevealGroups(groups) {
6952
+ const inspected = {};
6953
+ for (const [id, group] of groups) {
6954
+ inspected[id] = {
6955
+ count: group.count,
6956
+ order: group.order,
6957
+ tail: group.tail,
6958
+ pending: [...group.pending.keys()].sort((left, right) => left - right),
6959
+ committed: [...group.committed].sort((left, right) => left - right)
6960
+ };
6961
+ }
6962
+ return inspected;
6963
+ }
6964
+
6965
+ function revealResultMetadata(reveal) {
6966
+ return {
6967
+ group: reveal.group,
6968
+ index: reveal.index,
6969
+ count: reveal.count,
6970
+ order: reveal.order,
6971
+ tail: reveal.tail
6972
+ };
6973
+ }
6974
+
6975
+ function withPatchMetadata(result, attrs, replacementCount) {
6976
+ if (attrs) {
6977
+ result.attrs = attrs;
6978
+ }
6979
+ if (replacementCount > 0) {
6980
+ result.replace = { applied: replacementCount };
6981
+ }
6982
+ return result;
6983
+ }
6984
+
6392
6985
  function toStableError(value) {
6393
6986
  if (value instanceof Error) {
6394
6987
  return value;
@@ -6414,13 +7007,118 @@ const __boundaryReceiverModule = (() => {
6414
7007
  if (result.status === "redirected") {
6415
7008
  entry.redirect = result.redirect;
6416
7009
  }
7010
+ if (result.status === "buffered") {
7011
+ entry.reveal = result.reveal;
7012
+ }
7013
+ if (result.attrs) {
7014
+ entry.attrs = { ...result.attrs };
7015
+ }
7016
+ if (result.replace) {
7017
+ entry.replace = { ...result.replace };
7018
+ }
6417
7019
  return entry;
6418
7020
  }
6419
7021
 
7022
+ function findBoundaryElement(root, boundaryId, attributes, options = {}) {
7023
+ const boundary = elementsIn(root)
7024
+ .find((element) => boundaryIdFor(element, attributes) === String(boundaryId));
7025
+ if (!boundary && options.required !== false) {
7026
+ throw new Error(`Boundary "${boundaryId}" was not found.`);
7027
+ }
7028
+ return boundary ?? null;
7029
+ }
7030
+
7031
+ function boundaryIdFor(element, attributes) {
7032
+ if (element?.tagName === "ASYNC-SUSPENSE" && element.hasAttribute?.("for")) {
7033
+ return element.getAttribute("for");
7034
+ }
7035
+ return readAttribute(element, attributes, "async", "boundary");
7036
+ }
7037
+
7038
+ function directChildBoundaryElements(container, attributes) {
7039
+ return [...(container?.children ?? [])]
7040
+ .filter((element) => boundaryIdFor(element, attributes) != null);
7041
+ }
7042
+
7043
+ function findRevealContainer(root, group, attributes) {
7044
+ return elementsIn(root)
7045
+ .find((element) => readAttribute(element, attributes, "async", "reveal") === group)
7046
+ ?? null;
7047
+ }
7048
+
7049
+ function isNestedBoundaryElement(element, boundaryElement, attributes) {
7050
+ if (element === boundaryElement) {
7051
+ return false;
7052
+ }
7053
+ if (boundaryIdFor(element, attributes) != null) {
7054
+ return true;
7055
+ }
7056
+ let parent = element.parentElement;
7057
+ while (parent && parent !== boundaryElement) {
7058
+ if (boundaryIdFor(parent, attributes) != null) {
7059
+ return true;
7060
+ }
7061
+ parent = parent.parentElement;
7062
+ }
7063
+ return false;
7064
+ }
7065
+
7066
+ function setTailHidden(element, hidden) {
7067
+ if (hidden) {
7068
+ element.setAttribute("hidden", "");
7069
+ if ("hidden" in element) {
7070
+ element.hidden = true;
7071
+ }
7072
+ return;
7073
+ }
7074
+ element.removeAttribute("hidden");
7075
+ if ("hidden" in element) {
7076
+ element.hidden = false;
7077
+ }
7078
+ }
7079
+
7080
+ function containsOrEquals(parent, child) {
7081
+ return parent === child || parent.contains?.(child);
7082
+ }
7083
+
7084
+ function ownerDocumentOf(element) {
7085
+ return element.ownerDocument ?? globalThis.document;
7086
+ }
7087
+
7088
+ function toFragment(value, documentRef) {
7089
+ if (value?.nodeType === 11) {
7090
+ return value.cloneNode(true);
7091
+ }
7092
+ if (value?.tagName === "TEMPLATE") {
7093
+ return value.content.cloneNode(true);
7094
+ }
7095
+ if (value?.nodeType) {
7096
+ const fragment = documentRef.createDocumentFragment();
7097
+ fragment.append(value.cloneNode(true));
7098
+ return fragment;
7099
+ }
7100
+ const template = documentRef.createElement("template");
7101
+ template.innerHTML = typeof value === "string" ? value : renderTemplate(value);
7102
+ return template.content.cloneNode(true);
7103
+ }
7104
+
7105
+ function elementsIn(scope) {
7106
+ const elements = [];
7107
+ if (scope?.nodeType === 1) {
7108
+ elements.push(scope);
7109
+ }
7110
+ elements.push(...(scope?.querySelectorAll?.("*") ?? []));
7111
+ return elements;
7112
+ }
7113
+
7114
+ function isElementLike(value) {
7115
+ return Boolean(value?.nodeType === 1 && typeof value.textContent === "string");
7116
+ }
7117
+
6420
7118
  function isPlainObject(value) {
6421
7119
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
6422
7120
  }
6423
- return { createBoundaryReceiver };
7121
+ return { createBoundaryReceiver, AsyncStream };
6424
7122
  })();
6425
7123
 
6426
7124
  const __delayModule = (() => {
@@ -6530,6 +7228,7 @@ const { defineApp: defineApp } = __appModule;
6530
7228
  const { readSnapshot: readSnapshot } = __appModule;
6531
7229
  const { attributeName: attributeName } = __attributesModule;
6532
7230
  const { defineAttributeConfig: defineAttributeConfig } = __attributesModule;
7231
+ const { AsyncStream: AsyncStream } = __boundaryReceiverModule;
6533
7232
  const { createBoundaryReceiver: createBoundaryReceiver } = __boundaryReceiverModule;
6534
7233
  const { createCacheRegistry: createCacheRegistry } = __cacheModule;
6535
7234
  const { defineCache: defineCache } = __cacheModule;
@@ -6562,4 +7261,4 @@ const { createSignalRegistry: createSignalRegistry } = __signalsModule;
6562
7261
  const { effect: effect } = __signalsModule;
6563
7262
  const { signal: signal } = __signalsModule;
6564
7263
 
6565
- 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 };
7264
+ 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, applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult, computed, createSignal, createSignalRegistry, effect, signal };