@stream-mdx/react 0.4.0 → 0.5.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +14 -4
  2. package/README.md +65 -15
  3. package/dist/components/bottom-stick-scroll-area.cjs +344 -0
  4. package/dist/components/bottom-stick-scroll-area.d.cts +36 -0
  5. package/dist/components/bottom-stick-scroll-area.d.ts +36 -0
  6. package/dist/components/bottom-stick-scroll-area.mjs +320 -0
  7. package/dist/components/index.cjs +503 -172
  8. package/dist/components/index.d.cts +3 -1
  9. package/dist/components/index.d.ts +3 -1
  10. package/dist/components/index.mjs +350 -22
  11. package/dist/{index-D2MYusAM.d.cts → index-BVhp_pcF.d.ts} +3 -0
  12. package/dist/{index-D2MYusAM.d.ts → index-EaeouYp2.d.cts} +3 -0
  13. package/dist/index.cjs +1472 -486
  14. package/dist/index.d.cts +6 -4
  15. package/dist/index.d.ts +6 -4
  16. package/dist/index.mjs +1256 -272
  17. package/dist/mdx-client.cjs +1 -1
  18. package/dist/mdx-client.mjs +1 -1
  19. package/dist/mdx-coordinator.cjs +28 -7
  20. package/dist/mdx-coordinator.d.cts +2 -2
  21. package/dist/mdx-coordinator.d.ts +2 -2
  22. package/dist/mdx-coordinator.mjs +28 -7
  23. package/dist/renderer/node-views.cjs +271 -26
  24. package/dist/renderer/node-views.d.cts +3 -1
  25. package/dist/renderer/node-views.d.ts +3 -1
  26. package/dist/renderer/node-views.mjs +272 -27
  27. package/dist/renderer/patch-batching.cjs +2 -0
  28. package/dist/renderer/patch-batching.d.cts +1 -1
  29. package/dist/renderer/patch-batching.d.ts +1 -1
  30. package/dist/renderer/patch-batching.mjs +2 -1
  31. package/dist/renderer/patch-commit-scheduler.cjs +82 -30
  32. package/dist/renderer/patch-commit-scheduler.d.cts +15 -3
  33. package/dist/renderer/patch-commit-scheduler.d.ts +15 -3
  34. package/dist/renderer/patch-commit-scheduler.mjs +84 -30
  35. package/dist/renderer/store.cjs +382 -36
  36. package/dist/renderer/store.d.cts +3 -0
  37. package/dist/renderer/store.d.ts +3 -0
  38. package/dist/renderer/store.mjs +382 -36
  39. package/dist/renderer.cjs +758 -114
  40. package/dist/renderer.d.cts +3 -1
  41. package/dist/renderer.d.ts +3 -1
  42. package/dist/renderer.mjs +760 -116
  43. package/dist/server.cjs +669 -71
  44. package/dist/server.d.cts +5 -0
  45. package/dist/server.d.ts +5 -0
  46. package/dist/server.mjs +670 -72
  47. package/dist/{streaming-markdown-DOc2uLiP.d.ts → streaming-markdown-CEI9eDZm.d.ts} +3 -1
  48. package/dist/{streaming-markdown-D-JcByd9.d.cts → streaming-markdown-DRisClXb.d.cts} +3 -1
  49. package/dist/streaming-markdown.cjs +788 -121
  50. package/dist/streaming-markdown.d.cts +4 -2
  51. package/dist/streaming-markdown.d.ts +4 -2
  52. package/dist/streaming-markdown.mjs +790 -123
  53. package/dist/utils/inline-html.d.cts +3 -1
  54. package/dist/utils/inline-html.d.ts +3 -1
  55. package/package.json +14 -3
  56. package/shadcn/README.md +28 -0
  57. package/shadcn/bottom-stick-scroll-area.tsx +358 -0
@@ -10,6 +10,7 @@ import {
10
10
  sanitizeHTML,
11
11
  PATCH_ROOT_ID
12
12
  } from "@stream-mdx/core";
13
+ import { getPatchKind } from "@stream-mdx/core/perf/patch-batching";
13
14
 
14
15
  // src/renderer/list-utils.ts
15
16
  function updateNodeDepth(node, depth) {
@@ -114,12 +115,28 @@ function snapshotToBlock(node) {
114
115
  }
115
116
  return null;
116
117
  }
117
- var nodeSnapshotCache = /* @__PURE__ */ new Map();
118
- var childrenSnapshotCache = /* @__PURE__ */ new Map();
118
+ var nodeSnapshotCacheByMap = /* @__PURE__ */ new WeakMap();
119
+ var childrenSnapshotCacheByMap = /* @__PURE__ */ new WeakMap();
119
120
  var NODE_SNAPSHOT_CACHE_LIMIT = 5e4;
120
121
  var CHILDREN_SNAPSHOT_CACHE_LIMIT = 5e4;
121
122
  var NODE_SNAPSHOT_CACHE_BUFFER = Math.max(200, Math.floor(NODE_SNAPSHOT_CACHE_LIMIT * 0.1));
122
123
  var CHILDREN_SNAPSHOT_CACHE_BUFFER = Math.max(200, Math.floor(CHILDREN_SNAPSHOT_CACHE_LIMIT * 0.1));
124
+ function getNodeSnapshotCacheForMap(map) {
125
+ let cache = nodeSnapshotCacheByMap.get(map);
126
+ if (!cache) {
127
+ cache = /* @__PURE__ */ new Map();
128
+ nodeSnapshotCacheByMap.set(map, cache);
129
+ }
130
+ return cache;
131
+ }
132
+ function getChildrenSnapshotCacheForMap(map) {
133
+ let cache = childrenSnapshotCacheByMap.get(map);
134
+ if (!cache) {
135
+ cache = /* @__PURE__ */ new Map();
136
+ childrenSnapshotCacheByMap.set(map, cache);
137
+ }
138
+ return cache;
139
+ }
123
140
  function pruneCache(cache, max, buffer) {
124
141
  if (cache.size <= max + buffer) return;
125
142
  let remaining = cache.size - max;
@@ -129,6 +146,14 @@ function pruneCache(cache, max, buffer) {
129
146
  if (remaining <= 0) break;
130
147
  }
131
148
  }
149
+ function deleteSnapshotCacheEntry(map, id) {
150
+ getNodeSnapshotCacheForMap(map).delete(id);
151
+ getChildrenSnapshotCacheForMap(map).delete(id);
152
+ }
153
+ function clearSnapshotCaches(map) {
154
+ getNodeSnapshotCacheForMap(map).clear();
155
+ getChildrenSnapshotCacheForMap(map).clear();
156
+ }
132
157
  var EMPTY_CHILDREN = Object.freeze([]);
133
158
  var EMPTY_NODE_SNAPSHOT = Object.freeze({ version: -1, node: void 0 });
134
159
  var EMPTY_CHILDREN_SNAPSHOT = Object.freeze({
@@ -296,9 +321,57 @@ function createRootRecord() {
296
321
  parentId: null,
297
322
  children: [],
298
323
  props: {},
299
- version: 0
324
+ version: 0,
325
+ blockEpoch: 0
300
326
  };
301
327
  }
328
+ function readNumericEpoch(value) {
329
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
330
+ return void 0;
331
+ }
332
+ return value;
333
+ }
334
+ function readSnapshotBlockEpoch(snapshot) {
335
+ return readNumericEpoch(snapshot.meta?.blockEpoch);
336
+ }
337
+ function readPatchBlockEpoch(patch) {
338
+ if (patch.op === "setHTML") {
339
+ return readNumericEpoch(patch.patchMeta?.blockEpoch);
340
+ }
341
+ return readNumericEpoch(patch.meta?.blockEpoch);
342
+ }
343
+ function readPatchParseEpoch(patch) {
344
+ if (patch.op === "setHTML") {
345
+ return readNumericEpoch(patch.patchMeta?.parseEpoch);
346
+ }
347
+ return readNumericEpoch(patch.meta?.parseEpoch);
348
+ }
349
+ function readPatchTx(patch) {
350
+ if (patch.op === "setHTML") {
351
+ return readNumericEpoch(patch.patchMeta?.tx);
352
+ }
353
+ return readNumericEpoch(patch.meta?.tx);
354
+ }
355
+ function assignSubtreeBlockEpoch(map, rootId, blockEpoch, touched) {
356
+ const root = map.get(rootId);
357
+ if (!root) return;
358
+ const stack = [root];
359
+ while (stack.length > 0) {
360
+ const node = stack.pop();
361
+ if (!node) continue;
362
+ if (node.blockEpoch !== blockEpoch) {
363
+ node.blockEpoch = blockEpoch;
364
+ node.version += 1;
365
+ touched?.add(node.id);
366
+ }
367
+ for (const childId of node.children) {
368
+ const child = map.get(childId);
369
+ if (child) {
370
+ stack.push(child);
371
+ }
372
+ }
373
+ }
374
+ }
302
375
  function ensureArray(value) {
303
376
  return Array.isArray(value) ? value : [];
304
377
  }
@@ -358,7 +431,7 @@ function escapeHtml(value) {
358
431
  }
359
432
  function stripOuterLineSpan(html) {
360
433
  if (!html) return null;
361
- const openTagMatch = html.match(/<span[^>]*>/i);
434
+ const openTagMatch = html.match(/<span[^>]*class="[^"]*\bline\b[^"]*"[^>]*>/i);
362
435
  if (!openTagMatch) {
363
436
  return null;
364
437
  }
@@ -407,8 +480,7 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
407
480
  if (node.type !== "code-line") {
408
481
  anomalies.push(`type-mismatch:${node.id}:${node.type}`);
409
482
  nodes.delete(node.id);
410
- nodeSnapshotCache.delete(node.id);
411
- childrenSnapshotCache.delete(node.id);
483
+ deleteSnapshotCacheEntry(nodes, node.id);
412
484
  touched.add(node.id);
413
485
  continue;
414
486
  }
@@ -452,16 +524,14 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
452
524
  const existing = nodes.get(desiredId);
453
525
  if (existing && existing !== node) {
454
526
  nodes.delete(desiredId);
455
- nodeSnapshotCache.delete(desiredId);
456
- childrenSnapshotCache.delete(desiredId);
527
+ deleteSnapshotCacheEntry(nodes, desiredId);
457
528
  touched.add(desiredId);
458
529
  mutated = true;
459
530
  }
460
531
  if (node.id !== desiredId) {
461
532
  const previousId = node.id;
462
533
  nodes.delete(node.id);
463
- nodeSnapshotCache.delete(previousId);
464
- childrenSnapshotCache.delete(previousId);
534
+ deleteSnapshotCacheEntry(nodes, previousId);
465
535
  node.id = desiredId;
466
536
  nodes.set(desiredId, node);
467
537
  touched.add(desiredId);
@@ -492,8 +562,7 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
492
562
  for (const [id, record] of nodes) {
493
563
  if (record.parentId === parent.id && record.type === "code-line" && !allowed.has(id)) {
494
564
  nodes.delete(id);
495
- nodeSnapshotCache.delete(id);
496
- childrenSnapshotCache.delete(id);
565
+ deleteSnapshotCacheEntry(nodes, id);
497
566
  touched.add(id);
498
567
  mutated = true;
499
568
  }
@@ -577,8 +646,10 @@ function normalizeCodeLineProps(incoming, previous) {
577
646
  }
578
647
  function createCodeLineProps(index, text, highlight, tokens) {
579
648
  const safeText = typeof text === "string" ? text : "";
580
- const sanitized = sanitizeLineInnerHtml(highlight, safeText);
581
- const html = sanitized && sanitized.length >= safeText.length ? sanitized : escapeHtml(safeText);
649
+ const hasIncomingHighlight = typeof highlight === "string" && highlight.trim().length > 0;
650
+ const sanitized = hasIncomingHighlight ? sanitizeLineInnerHtml(highlight, safeText) : null;
651
+ const hasRenderableTokens = tokens !== void 0 && tokens !== null;
652
+ const html = sanitized && sanitized.length >= safeText.length ? sanitized : hasRenderableTokens ? null : escapeHtml(safeText);
582
653
  const props = {
583
654
  index,
584
655
  text: safeText,
@@ -689,6 +760,18 @@ function validateCodeBlockChildren(nodes, parent) {
689
760
  issues.push({ kind: "dangling-node", id });
690
761
  }
691
762
  }
763
+ if (parent.block?.isFinalized) {
764
+ const snapshot = createBlockSnapshot(cloneBlock(parent.block));
765
+ const expectedChildren = ensureArray(snapshot.children).filter((child) => child.type === "code-line");
766
+ if (expectedChildren.length !== parent.children.length) {
767
+ issues.push({
768
+ kind: "line-count-mismatch",
769
+ id: parent.id,
770
+ expected: expectedChildren.length,
771
+ actual: parent.children.length
772
+ });
773
+ }
774
+ }
692
775
  return { ok: issues.length === 0, issues };
693
776
  }
694
777
  function rebuildCodeBlockFromSnapshot(nodes, parent, touched, issues) {
@@ -705,7 +788,7 @@ function rebuildCodeBlockFromSnapshot(nodes, parent, touched, issues) {
705
788
  }
706
789
  const snapshot = createBlockSnapshot(cloneBlock(parent.block));
707
790
  removeSubtree(nodes, parent.id, touched);
708
- const inserted = insertSnapshotAt(nodes, ancestor, index, snapshot);
791
+ const inserted = insertSnapshotAt(nodes, ancestor, index, snapshot, touched, parent.blockEpoch);
709
792
  if (!inserted) {
710
793
  return false;
711
794
  }
@@ -728,7 +811,55 @@ function arraysEqual(a, b) {
728
811
  }
729
812
  return true;
730
813
  }
731
- function buildNodeFromSnapshot(map, snapshot, parentId) {
814
+ function expectedCodeLineId(parentId, index) {
815
+ return `${parentId}::line:${index}`;
816
+ }
817
+ function collectStoreInvariantViolations(nodes) {
818
+ const violations = [];
819
+ for (const [id, node] of nodes) {
820
+ if (node.type === "__root__") continue;
821
+ if (node.children.length > 1) {
822
+ const seen = /* @__PURE__ */ new Set();
823
+ for (const childId of node.children) {
824
+ if (seen.has(childId)) {
825
+ violations.push(`duplicate child id:${id}:${childId}`);
826
+ } else {
827
+ seen.add(childId);
828
+ }
829
+ }
830
+ }
831
+ if (node.type === "list") {
832
+ const parent = node.parentId ? nodes.get(node.parentId) : void 0;
833
+ if (parent?.type === "list-item" && node.children.length === 0) {
834
+ violations.push(`empty nested list:${id}`);
835
+ }
836
+ }
837
+ if (node.type === "code") {
838
+ for (let index = 0; index < node.children.length; index += 1) {
839
+ const childId = node.children[index];
840
+ const child = nodes.get(childId);
841
+ if (!child) {
842
+ violations.push(`missing code-line node:${node.id}:${childId}`);
843
+ continue;
844
+ }
845
+ if (child.type !== "code-line") {
846
+ violations.push(`non-code child in code block:${node.id}:${child.id}:${child.type}`);
847
+ continue;
848
+ }
849
+ const expectedId = expectedCodeLineId(node.id, index);
850
+ if (child.id !== expectedId) {
851
+ violations.push(`code-line id mismatch:${node.id}:${child.id}:${expectedId}`);
852
+ }
853
+ const indexProp = typeof child.props?.index === "number" ? child.props.index : null;
854
+ if (indexProp !== index) {
855
+ violations.push(`code-line index mismatch:${node.id}:${child.id}:${indexProp}:${index}`);
856
+ }
857
+ }
858
+ }
859
+ }
860
+ return violations;
861
+ }
862
+ function buildNodeFromSnapshot(map, snapshot, parentId, blockEpoch) {
732
863
  const record = {
733
864
  id: snapshot.id,
734
865
  type: snapshot.type,
@@ -738,6 +869,7 @@ function buildNodeFromSnapshot(map, snapshot, parentId) {
738
869
  meta: snapshot.meta ? { ...snapshot.meta } : void 0,
739
870
  range: snapshot.range ? { ...snapshot.range } : void 0,
740
871
  version: 0,
872
+ blockEpoch,
741
873
  block: snapshotToBlock(snapshot) ?? void 0
742
874
  };
743
875
  if (record.type === "code") {
@@ -746,7 +878,7 @@ function buildNodeFromSnapshot(map, snapshot, parentId) {
746
878
  map.set(record.id, record);
747
879
  const childSnapshots = ensureArray(snapshot.children);
748
880
  for (const child of childSnapshots) {
749
- const childRecord = buildNodeFromSnapshot(map, child, record.id);
881
+ const childRecord = buildNodeFromSnapshot(map, child, record.id, blockEpoch);
750
882
  record.children.push(childRecord.id);
751
883
  }
752
884
  return record;
@@ -774,8 +906,7 @@ function removeSubtree(map, id, touched) {
774
906
  }
775
907
  }
776
908
  map.delete(node.id);
777
- nodeSnapshotCache.delete(node.id);
778
- childrenSnapshotCache.delete(node.id);
909
+ deleteSnapshotCacheEntry(map, node.id);
779
910
  touched.add(node.id);
780
911
  for (const childId of node.children) {
781
912
  const child = map.get(childId);
@@ -807,13 +938,13 @@ function resolveParent(map, path) {
807
938
  }
808
939
  return map.get(path.blockId);
809
940
  }
810
- function insertSnapshotAt(map, parent, index, snapshot, touched) {
941
+ function insertSnapshotAt(map, parent, index, snapshot, touched, blockEpoch) {
811
942
  if (!parent) return null;
812
943
  const existingIndex = parent.children.indexOf(snapshot.id);
813
944
  if (existingIndex !== -1) {
814
945
  removeSubtree(map, parent.children[existingIndex], touched ?? /* @__PURE__ */ new Set());
815
946
  }
816
- const record = buildNodeFromSnapshot(map, snapshot, parent.id);
947
+ const record = buildNodeFromSnapshot(map, snapshot, parent.id, blockEpoch ?? readSnapshotBlockEpoch(snapshot) ?? parent.blockEpoch);
817
948
  const insertedIds = [];
818
949
  const queue = [record];
819
950
  while (queue.length > 0) {
@@ -831,13 +962,13 @@ function insertSnapshotAt(map, parent, index, snapshot, touched) {
831
962
  ensureUniqueChildren(parent, touched);
832
963
  return { record, insertedIds };
833
964
  }
834
- function replaceSnapshotAt(map, parent, index, snapshot, touched) {
965
+ function replaceSnapshotAt(map, parent, index, snapshot, touched, blockEpoch) {
835
966
  if (!parent) return;
836
967
  if (index < 0 || index >= parent.children.length) return;
837
968
  const targetId = parent.children[index];
838
969
  parent.children.splice(index, 1);
839
970
  removeSubtree(map, targetId, touched);
840
- const inserted = insertSnapshotAt(map, parent, index, snapshot, touched);
971
+ const inserted = insertSnapshotAt(map, parent, index, snapshot, touched, blockEpoch);
841
972
  if (inserted) {
842
973
  for (const id of inserted.insertedIds) {
843
974
  touched.add(id);
@@ -857,6 +988,24 @@ function createRendererStore(initialBlocks = []) {
857
988
  const listeners = /* @__PURE__ */ new Set();
858
989
  let notifyScheduled = false;
859
990
  let lastCoalescingMetrics = null;
991
+ let invariantViolations = [];
992
+ let transientDiagnostics = [];
993
+ let nextBlockEpoch = 1;
994
+ const debugCounters = {
995
+ appendLineGuardRejected: 0,
996
+ staleEpochRejected: 0,
997
+ storeInvariantViolationCount: 0
998
+ };
999
+ const allocateBlockEpoch = (preferred) => {
1000
+ const resolved = readNumericEpoch(preferred);
1001
+ if (resolved !== void 0) {
1002
+ nextBlockEpoch = Math.max(nextBlockEpoch, resolved + 1);
1003
+ return resolved;
1004
+ }
1005
+ const epoch = nextBlockEpoch;
1006
+ nextBlockEpoch += 1;
1007
+ return epoch;
1008
+ };
860
1009
  const getRaf = () => {
861
1010
  if (typeof globalThis !== "undefined" && typeof globalThis.requestAnimationFrame === "function") {
862
1011
  return globalThis.requestAnimationFrame;
@@ -888,9 +1037,12 @@ function createRendererStore(initialBlocks = []) {
888
1037
  }
889
1038
  }
890
1039
  }
1040
+ function appendDiagnostic(message) {
1041
+ transientDiagnostics = [...transientDiagnostics, message];
1042
+ invariantViolations = [...invariantViolations, message];
1043
+ }
891
1044
  function rebuildFromBlocks(blocks) {
892
- nodeSnapshotCache.clear();
893
- childrenSnapshotCache.clear();
1045
+ clearSnapshotCaches(nodes);
894
1046
  nodes = /* @__PURE__ */ new Map();
895
1047
  const root = createRootRecord();
896
1048
  nodes.set(root.id, root);
@@ -899,9 +1051,12 @@ function createRendererStore(initialBlocks = []) {
899
1051
  for (let i = 0; i < blocks.length; i++) {
900
1052
  const block = cloneBlock(blocks[i]);
901
1053
  const snapshot = createBlockSnapshot(block);
902
- insertSnapshotAt(nodes, root, root.children.length, snapshot);
1054
+ insertSnapshotAt(nodes, root, root.children.length, snapshot, void 0, allocateBlockEpoch(readSnapshotBlockEpoch(snapshot)));
903
1055
  }
904
1056
  normalizeAllListDepths(nodes, /* @__PURE__ */ new Set(), PATCH_ROOT_ID);
1057
+ transientDiagnostics = [];
1058
+ invariantViolations = collectStoreInvariantViolations(nodes);
1059
+ debugCounters.storeInvariantViolationCount += invariantViolations.length;
905
1060
  version++;
906
1061
  blocksDirty = true;
907
1062
  scheduleNotify();
@@ -966,11 +1121,57 @@ function createRendererStore(initialBlocks = []) {
966
1121
  listDepthDirty = true;
967
1122
  }
968
1123
  };
969
- const applyPropsUpdate = (at, props) => {
1124
+ const semanticTxEpochs = /* @__PURE__ */ new Map();
1125
+ const resolveBlockEpochTarget = (patch, at) => {
1126
+ if ((patch.op === "insertChild" || patch.op === "replaceChild") && at.blockId === PATCH_ROOT_ID) {
1127
+ const nodeId = patch.node?.id;
1128
+ return nodeId ? nodes.get(nodeId) : void 0;
1129
+ }
1130
+ return nodes.get(at.blockId) ?? (at.nodeId ? nodes.get(at.nodeId) : void 0);
1131
+ };
1132
+ const rejectStaleEpochPatch = (patch, at) => {
1133
+ const expectedBlockEpoch = readPatchBlockEpoch(patch);
1134
+ const patchTx = readPatchTx(patch);
1135
+ if (expectedBlockEpoch === void 0) {
1136
+ return false;
1137
+ }
1138
+ if (getPatchKind(patch) !== "semantic" || !at) {
1139
+ return false;
1140
+ }
1141
+ const target = resolveBlockEpochTarget(patch, at);
1142
+ if (!target || target.blockEpoch === expectedBlockEpoch) {
1143
+ return false;
1144
+ }
1145
+ const txEpoch = at.blockId && patchTx !== void 0 ? semanticTxEpochs.get(at.blockId) : void 0;
1146
+ if (txEpoch && txEpoch.tx === patchTx && txEpoch.blockEpoch === target.blockEpoch) {
1147
+ return false;
1148
+ }
1149
+ debugCounters.staleEpochRejected += 1;
1150
+ appendDiagnostic(
1151
+ `stale epoch rejected:${patch.op}:${target.id}:expected=${expectedBlockEpoch}:actual=${target.blockEpoch}`
1152
+ );
1153
+ return true;
1154
+ };
1155
+ const advanceBlockEpoch = (blockId, touchedSet, preferredEpoch, tx) => {
1156
+ if (!blockId || blockId === PATCH_ROOT_ID) {
1157
+ return null;
1158
+ }
1159
+ const blockRoot = nodes.get(blockId);
1160
+ if (!blockRoot) {
1161
+ return null;
1162
+ }
1163
+ const nextEpoch = allocateBlockEpoch(preferredEpoch);
1164
+ assignSubtreeBlockEpoch(nodes, blockRoot.id, nextEpoch, touchedSet);
1165
+ if (tx !== void 0) {
1166
+ semanticTxEpochs.set(blockRoot.id, { tx, blockEpoch: nextEpoch });
1167
+ }
1168
+ return nextEpoch;
1169
+ };
1170
+ const applyPropsUpdate = (at, props, preferredEpoch, tx, semanticUpdate = false) => {
970
1171
  const perfStartLocal = perfEnabled ? getPerfNow() : 0;
971
1172
  const targetId = at.nodeId ?? at.blockId;
972
1173
  if (!targetId) return;
973
- const target = nodes.get(targetId);
1174
+ let target = nodes.get(targetId);
974
1175
  if (!target) return;
975
1176
  const parentType = target.parentId ? nodes.get(target.parentId)?.type : void 0;
976
1177
  if (debugListPatches && (isListRelated(target.type) || isListRelated(parentType))) {
@@ -997,6 +1198,9 @@ function createRendererStore(initialBlocks = []) {
997
1198
  propsChanged = true;
998
1199
  }
999
1200
  if (propsChanged) {
1201
+ if (semanticUpdate) {
1202
+ advanceBlockEpoch(at.blockId, touched, preferredEpoch, tx);
1203
+ }
1000
1204
  target.version++;
1001
1205
  touched.add(target.id);
1002
1206
  const parent = target.parentId ? nodes.get(target.parentId) : void 0;
@@ -1060,16 +1264,56 @@ function createRendererStore(initialBlocks = []) {
1060
1264
  }
1061
1265
  }
1062
1266
  let blockChanged = false;
1267
+ let typeChanged = false;
1268
+ let rebuiltFromFinalizedBlock = false;
1269
+ let blockEpochAdvanced = false;
1063
1270
  const incomingBlock = props?.block;
1064
1271
  if (incomingBlock && typeof incomingBlock === "object") {
1065
1272
  target.block = cloneBlock(incomingBlock);
1273
+ const previousType = target.type;
1274
+ const incomingType = typeof target.block.type === "string" ? target.block.type : target.type;
1275
+ if (incomingType !== target.type) {
1276
+ target.type = incomingType;
1277
+ typeChanged = true;
1278
+ if (isListRelated(previousType) || isListRelated(target.type)) {
1279
+ markListDepthDirty(target.id);
1280
+ }
1281
+ }
1066
1282
  if (target.type === "code") {
1067
1283
  applyCodeBlockMetadata(target);
1068
1284
  }
1285
+ if (semanticUpdate) {
1286
+ advanceBlockEpoch(at.blockId, touched, preferredEpoch, tx);
1287
+ blockEpochAdvanced = true;
1288
+ }
1289
+ const shouldRebuildFinalizedTree = !at.nodeId && target.id === at.blockId && Boolean(target.block?.isFinalized) && target.type !== "code" && Boolean(target.parentId);
1290
+ if (shouldRebuildFinalizedTree) {
1291
+ const parent = target.parentId ? nodes.get(target.parentId) : void 0;
1292
+ const index = parent ? parent.children.indexOf(target.id) : -1;
1293
+ if (parent && index !== -1) {
1294
+ const snapshot = createBlockSnapshot(cloneBlock(target.block));
1295
+ replaceSnapshotAt(nodes, parent, index, snapshot, touched, target.blockEpoch);
1296
+ mutatedBlocks = true;
1297
+ blockChanged = true;
1298
+ rebuiltFromFinalizedBlock = true;
1299
+ const rebuilt = nodes.get(target.id);
1300
+ if (isListRelated(previousType) || isListRelated(rebuilt?.type)) {
1301
+ markListDepthDirty(target.id);
1302
+ }
1303
+ target = rebuilt ?? target;
1304
+ }
1305
+ }
1069
1306
  mutatedBlocks = true;
1070
1307
  blockChanged = true;
1071
1308
  }
1072
- if (propsChanged || blockChanged) {
1309
+ if (rebuiltFromFinalizedBlock) {
1310
+ return;
1311
+ }
1312
+ if (propsChanged || blockChanged || typeChanged) {
1313
+ if (semanticUpdate && !blockEpochAdvanced) {
1314
+ advanceBlockEpoch(at.blockId, touched, preferredEpoch, tx);
1315
+ blockEpochAdvanced = true;
1316
+ }
1073
1317
  target.version++;
1074
1318
  touched.add(target.id);
1075
1319
  let parentForNormalization;
@@ -1112,13 +1356,22 @@ function createRendererStore(initialBlocks = []) {
1112
1356
  const applyPatch = (patch) => {
1113
1357
  switch (patch.op) {
1114
1358
  case "insertChild": {
1359
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1115
1360
  const parent = resolveParent(nodes, patch.at);
1116
- const inserted = insertSnapshotAt(nodes, parent, patch.index, patch.node, touched);
1361
+ const inserted = insertSnapshotAt(
1362
+ nodes,
1363
+ parent,
1364
+ patch.index,
1365
+ patch.node,
1366
+ touched,
1367
+ allocateBlockEpoch(readPatchParseEpoch(patch) ?? readSnapshotBlockEpoch(patch.node))
1368
+ );
1117
1369
  if (inserted) {
1118
1370
  for (const id of inserted.insertedIds) {
1119
1371
  touched.add(id);
1120
1372
  }
1121
1373
  mutatedBlocks = true;
1374
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
1122
1375
  if (isListRelated(inserted.record.type) || isListRelated(parent?.type)) {
1123
1376
  markListDepthDirty(inserted.record.id);
1124
1377
  if (parent) {
@@ -1138,6 +1391,7 @@ function createRendererStore(initialBlocks = []) {
1138
1391
  break;
1139
1392
  }
1140
1393
  case "deleteChild": {
1394
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1141
1395
  const parent = resolveParent(nodes, patch.at);
1142
1396
  if (!parent) break;
1143
1397
  if (patch.index < 0 || patch.index >= parent.children.length) break;
@@ -1149,6 +1403,7 @@ function createRendererStore(initialBlocks = []) {
1149
1403
  touched.add(parent.id);
1150
1404
  ensureUniqueChildren(parent, touched);
1151
1405
  mutatedBlocks = true;
1406
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
1152
1407
  if (isListRelated(parent.type) || isListRelated(removedNode?.type)) {
1153
1408
  markListDepthDirty(parent.id);
1154
1409
  }
@@ -1165,11 +1420,20 @@ function createRendererStore(initialBlocks = []) {
1165
1420
  break;
1166
1421
  }
1167
1422
  case "replaceChild": {
1423
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1168
1424
  const parent = resolveParent(nodes, patch.at);
1169
1425
  if (!parent) break;
1170
- replaceSnapshotAt(nodes, parent, patch.index, patch.node, touched);
1426
+ replaceSnapshotAt(
1427
+ nodes,
1428
+ parent,
1429
+ patch.index,
1430
+ patch.node,
1431
+ touched,
1432
+ allocateBlockEpoch(readPatchParseEpoch(patch) ?? readSnapshotBlockEpoch(patch.node))
1433
+ );
1171
1434
  touched.add(parent.id);
1172
1435
  mutatedBlocks = true;
1436
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
1173
1437
  if (isListRelated(parent.type)) {
1174
1438
  markListDepthDirty(parent.id);
1175
1439
  }
@@ -1183,18 +1447,33 @@ function createRendererStore(initialBlocks = []) {
1183
1447
  break;
1184
1448
  }
1185
1449
  case "setProps": {
1186
- applyPropsUpdate(patch.at, patch.props);
1450
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1451
+ applyPropsUpdate(patch.at, patch.props, readPatchParseEpoch(patch), readPatchTx(patch), getPatchKind(patch) === "semantic");
1187
1452
  break;
1188
1453
  }
1189
1454
  case "setPropsBatch": {
1190
1455
  const entries = Array.isArray(patch.entries) ? patch.entries : [];
1191
1456
  for (const entry of entries) {
1192
1457
  if (!entry || !entry.at) continue;
1193
- applyPropsUpdate(entry.at, entry.props);
1458
+ const entryPatch = {
1459
+ op: "setProps",
1460
+ at: entry.at,
1461
+ props: entry.props,
1462
+ meta: entry.meta ?? patch.meta
1463
+ };
1464
+ if (rejectStaleEpochPatch(entryPatch, entry.at)) continue;
1465
+ applyPropsUpdate(
1466
+ entry.at,
1467
+ entry.props,
1468
+ readPatchParseEpoch(entryPatch),
1469
+ readPatchTx(entryPatch),
1470
+ getPatchKind(entryPatch) === "semantic"
1471
+ );
1194
1472
  }
1195
1473
  break;
1196
1474
  }
1197
1475
  case "finalize": {
1476
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1198
1477
  const target = nodes.get(patch.at.nodeId ?? patch.at.blockId);
1199
1478
  if (!target || !target.block) break;
1200
1479
  if (!target.block.isFinalized) {
@@ -1203,6 +1482,17 @@ function createRendererStore(initialBlocks = []) {
1203
1482
  target.version++;
1204
1483
  touched.add(target.id);
1205
1484
  mutatedBlocks = true;
1485
+ if (!patch.at.nodeId && target.id === patch.at.blockId && target.type !== "code" && target.parentId) {
1486
+ const parent = nodes.get(target.parentId);
1487
+ const index = parent ? parent.children.indexOf(target.id) : -1;
1488
+ if (parent && index !== -1) {
1489
+ const snapshot = createBlockSnapshot(cloneBlock(target.block));
1490
+ replaceSnapshotAt(nodes, parent, index, snapshot, touched, target.blockEpoch);
1491
+ if (isListRelated(target.type) || isListRelated(snapshot.type)) {
1492
+ markListDepthDirty(target.id);
1493
+ }
1494
+ }
1495
+ }
1206
1496
  }
1207
1497
  if (isListRelated(target.type)) {
1208
1498
  markListDepthDirty(target.id);
@@ -1216,6 +1506,7 @@ function createRendererStore(initialBlocks = []) {
1216
1506
  break;
1217
1507
  }
1218
1508
  case "reorder": {
1509
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1219
1510
  const parent = resolveParent(nodes, patch.at);
1220
1511
  if (!parent) break;
1221
1512
  const length = parent.children.length;
@@ -1230,6 +1521,9 @@ function createRendererStore(initialBlocks = []) {
1230
1521
  parent.children.splice(to, 0, ...moved);
1231
1522
  parent.version++;
1232
1523
  touched.add(parent.id);
1524
+ if (getPatchKind(patch) === "semantic") {
1525
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
1526
+ }
1233
1527
  if (isListRelated(parent.type)) {
1234
1528
  markListDepthDirty(parent.id);
1235
1529
  }
@@ -1245,15 +1539,31 @@ function createRendererStore(initialBlocks = []) {
1245
1539
  break;
1246
1540
  }
1247
1541
  case "appendLines": {
1542
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1248
1543
  const parent = resolveParent(nodes, patch.at);
1249
1544
  if (!parent) break;
1250
- const startIndex = Math.max(0, Math.min(patch.startIndex, parent.children.length));
1545
+ if (parent.type !== "code") {
1546
+ debugCounters.appendLineGuardRejected += 1;
1547
+ appendDiagnostic(`appendLines target not code:${parent.id}:${parent.type}`);
1548
+ break;
1549
+ }
1550
+ const requestedStartIndex = Math.max(0, patch.startIndex);
1551
+ const startIndex = Math.max(0, Math.min(requestedStartIndex, parent.children.length));
1251
1552
  const lineCount = patch.lines?.length ?? 0;
1252
1553
  if (lineCount === 0) break;
1554
+ const prefixMatches = parent.children.every((childId, index) => childId === expectedCodeLineId(parent.id, index));
1555
+ const isStrictTailAppend = requestedStartIndex === parent.children.length;
1556
+ if (!isStrictTailAppend || !prefixMatches) {
1557
+ debugCounters.appendLineGuardRejected += 1;
1558
+ appendDiagnostic(
1559
+ `appendLines guard rejected:${parent.id}:requestedStart=${requestedStartIndex}:childCount=${parent.children.length}:prefixMatches=${prefixMatches}`
1560
+ );
1561
+ break;
1562
+ }
1253
1563
  const originalChildren = parent.children.slice();
1254
1564
  const insertedIds = [];
1255
1565
  const insertedSet = /* @__PURE__ */ new Set();
1256
- const isTrailingAppend = startIndex >= originalChildren.length;
1566
+ const isTrailingAppend = true;
1257
1567
  let parentChildrenChanged = false;
1258
1568
  let codeLinesMutated = false;
1259
1569
  for (let offset = 0; offset < lineCount; offset++) {
@@ -1295,7 +1605,8 @@ function createRendererStore(initialBlocks = []) {
1295
1605
  parentId: parent.id,
1296
1606
  children: [],
1297
1607
  props: nextLineProps,
1298
- version: 0
1608
+ version: 0,
1609
+ blockEpoch: parent.blockEpoch
1299
1610
  };
1300
1611
  nodes.set(lineId, record);
1301
1612
  touched.add(lineId);
@@ -1345,6 +1656,7 @@ function createRendererStore(initialBlocks = []) {
1345
1656
  break;
1346
1657
  }
1347
1658
  case "setHTML": {
1659
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
1348
1660
  const target = nodes.get(patch.at.nodeId ?? patch.at.blockId);
1349
1661
  if (!target) break;
1350
1662
  const sanitized = patch.sanitized ? patch.html : sanitizeHTML(patch.html);
@@ -1387,6 +1699,30 @@ function createRendererStore(initialBlocks = []) {
1387
1699
  applyPatch(patch);
1388
1700
  }
1389
1701
  }
1702
+ const finalizedTableRoots = /* @__PURE__ */ new Set();
1703
+ for (const touchedId of touched) {
1704
+ let cursor = nodes.get(touchedId);
1705
+ while (cursor && cursor.id !== PATCH_ROOT_ID) {
1706
+ if (cursor.type === "table" && cursor.block?.isFinalized && cursor.parentId) {
1707
+ finalizedTableRoots.add(cursor.id);
1708
+ break;
1709
+ }
1710
+ cursor = cursor.parentId ? nodes.get(cursor.parentId) : void 0;
1711
+ }
1712
+ }
1713
+ if (finalizedTableRoots.size > 0) {
1714
+ for (const tableId of finalizedTableRoots) {
1715
+ const tableNode = nodes.get(tableId);
1716
+ if (!tableNode || !tableNode.block || !tableNode.parentId) continue;
1717
+ const parent = nodes.get(tableNode.parentId);
1718
+ if (!parent) continue;
1719
+ const index = parent.children.indexOf(tableNode.id);
1720
+ if (index === -1) continue;
1721
+ const snapshot = createBlockSnapshot(cloneBlock(tableNode.block));
1722
+ replaceSnapshotAt(nodes, parent, index, snapshot, touched, tableNode.blockEpoch);
1723
+ mutatedBlocks = true;
1724
+ }
1725
+ }
1390
1726
  if (mutatedBlocks) {
1391
1727
  blocksDirty = true;
1392
1728
  }
@@ -1407,6 +1743,8 @@ function createRendererStore(initialBlocks = []) {
1407
1743
  normalizeListDepthsForIds(nodes, touched, listDepthDirtyNodes, PATCH_ROOT_ID);
1408
1744
  }
1409
1745
  }
1746
+ invariantViolations = [...collectStoreInvariantViolations(nodes), ...transientDiagnostics];
1747
+ debugCounters.storeInvariantViolationCount += invariantViolations.length;
1410
1748
  if (touched.size > 0 || mutatedBlocks) {
1411
1749
  version++;
1412
1750
  scheduleNotify();
@@ -1455,6 +1793,7 @@ function createRendererStore(initialBlocks = []) {
1455
1793
  if (!record) {
1456
1794
  return EMPTY_NODE_SNAPSHOT;
1457
1795
  }
1796
+ const nodeSnapshotCache = getNodeSnapshotCacheForMap(nodes);
1458
1797
  const cached = nodeSnapshotCache.get(id);
1459
1798
  if (cached && cached.version === record.version && cached.node === record) {
1460
1799
  return cached;
@@ -1474,6 +1813,7 @@ function createRendererStore(initialBlocks = []) {
1474
1813
  if (!record) {
1475
1814
  return EMPTY_CHILDREN_SNAPSHOT;
1476
1815
  }
1816
+ const childrenSnapshotCache = getChildrenSnapshotCacheForMap(nodes);
1477
1817
  const cached = childrenSnapshotCache.get(id);
1478
1818
  if (cached && cached.version === record.version) {
1479
1819
  return cached;
@@ -1483,6 +1823,12 @@ function createRendererStore(initialBlocks = []) {
1483
1823
  pruneCache(childrenSnapshotCache, CHILDREN_SNAPSHOT_CACHE_LIMIT, CHILDREN_SNAPSHOT_CACHE_BUFFER);
1484
1824
  return snapshot;
1485
1825
  },
1826
+ getInvariantViolations() {
1827
+ return invariantViolations.slice();
1828
+ },
1829
+ getDebugCounters() {
1830
+ return { ...debugCounters };
1831
+ },
1486
1832
  getLastCoalescingMetrics() {
1487
1833
  return lastCoalescingMetrics;
1488
1834
  },