@stream-mdx/react 0.1.1 → 0.3.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 (45) hide show
  1. package/dist/components/index.cjs +497 -163
  2. package/dist/components/index.d.cts +1 -1
  3. package/dist/components/index.d.ts +1 -1
  4. package/dist/components/index.mjs +496 -163
  5. package/dist/{index-Bt1opGCs.d.cts → index-D7px9jug.d.cts} +27 -2
  6. package/dist/{index-Bt1opGCs.d.ts → index-D7px9jug.d.ts} +27 -2
  7. package/dist/index.cjs +3767 -2043
  8. package/dist/index.d.cts +43 -5
  9. package/dist/index.d.ts +43 -5
  10. package/dist/index.mjs +3991 -2265
  11. package/dist/mdx-client.cjs +60 -18
  12. package/dist/mdx-client.d.cts +11 -0
  13. package/dist/mdx-client.d.ts +11 -0
  14. package/dist/mdx-client.mjs +60 -18
  15. package/dist/mdx-coordinator.cjs +60 -18
  16. package/dist/mdx-coordinator.mjs +60 -18
  17. package/dist/renderer/node-views.cjs +466 -133
  18. package/dist/renderer/node-views.d.cts +1 -1
  19. package/dist/renderer/node-views.d.ts +1 -1
  20. package/dist/renderer/node-views.mjs +409 -68
  21. package/dist/renderer/patch-commit-scheduler.cjs +68 -7
  22. package/dist/renderer/patch-commit-scheduler.d.cts +6 -5
  23. package/dist/renderer/patch-commit-scheduler.d.ts +6 -5
  24. package/dist/renderer/patch-commit-scheduler.mjs +68 -7
  25. package/dist/renderer/store.cjs +481 -56
  26. package/dist/renderer/store.d.cts +2 -1
  27. package/dist/renderer/store.d.ts +2 -1
  28. package/dist/renderer/store.mjs +479 -47
  29. package/dist/renderer/virtualized-code.cjs +8 -2
  30. package/dist/renderer/virtualized-code.d.cts +4 -0
  31. package/dist/renderer/virtualized-code.d.ts +4 -0
  32. package/dist/renderer/virtualized-code.mjs +8 -2
  33. package/dist/renderer.cjs +3199 -2208
  34. package/dist/renderer.d.cts +4 -2
  35. package/dist/renderer.d.ts +4 -2
  36. package/dist/renderer.mjs +2956 -1957
  37. package/dist/streaming-markdown-DSC4L0xR.d.cts +157 -0
  38. package/dist/streaming-markdown-Dp1IDgMT.d.ts +157 -0
  39. package/dist/streaming-markdown.cjs +3950 -2248
  40. package/dist/streaming-markdown.d.cts +6 -95
  41. package/dist/streaming-markdown.d.ts +6 -95
  42. package/dist/streaming-markdown.mjs +3962 -2252
  43. package/dist/utils/inline-html.d.cts +1 -1
  44. package/dist/utils/inline-html.d.ts +1 -1
  45. package/package.json +3 -3
@@ -1,10 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  // src/renderer/store.ts
4
- import { cloneBlock, createBlockSnapshot } from "@stream-mdx/core";
5
- import { extractCodeWrapperAttributes } from "@stream-mdx/core";
6
- import { sanitizeCodeHTML, sanitizeHTML } from "@stream-mdx/core";
7
- import { PATCH_ROOT_ID } from "@stream-mdx/core";
4
+ import {
5
+ cloneBlock,
6
+ createBlockSnapshot,
7
+ extractCodeWrapperAttributes,
8
+ getDefaultCodeWrapperAttributes,
9
+ sanitizeCodeHTML,
10
+ sanitizeHTML,
11
+ PATCH_ROOT_ID
12
+ } from "@stream-mdx/core";
8
13
 
9
14
  // src/renderer/list-utils.ts
10
15
  function updateNodeDepth(node, depth) {
@@ -29,6 +34,22 @@ function normalizeAllListDepths(map, touched, rootId = "__root__") {
29
34
  }
30
35
  }
31
36
  }
37
+ function normalizeListDepthsForIds(map, touched, listIds, rootId = "__root__") {
38
+ if (listIds.size === 0) {
39
+ normalizeAllListDepths(map, touched, rootId);
40
+ return;
41
+ }
42
+ const visited = /* @__PURE__ */ new Set();
43
+ for (const listId of listIds) {
44
+ const base = map.get(listId);
45
+ if (!base) continue;
46
+ const listNode = base.type === "list" ? base : findNearestListAncestor(map, base);
47
+ if (!listNode || visited.has(listNode.id)) continue;
48
+ visited.add(listNode.id);
49
+ const depth = computeListDepth(map, listNode, rootId);
50
+ normalizeListDepthRecursive(map, listNode, depth, touched);
51
+ }
52
+ }
32
53
  function normalizeListDepthRecursive(map, listNode, depth, touched) {
33
54
  if (!listNode) return;
34
55
  if (updateNodeDepth(listNode, depth)) {
@@ -53,6 +74,28 @@ function normalizeListDepthRecursive(map, listNode, depth, touched) {
53
74
  }
54
75
  }
55
76
  }
77
+ function findNearestListAncestor(map, node) {
78
+ let current = node;
79
+ while (current) {
80
+ if (current.type === "list") return current;
81
+ if (!current.parentId) return void 0;
82
+ current = map.get(current.parentId);
83
+ }
84
+ return void 0;
85
+ }
86
+ function computeListDepth(map, listNode, rootId) {
87
+ let depth = 0;
88
+ let current = listNode;
89
+ while (current && current.parentId && current.parentId !== rootId) {
90
+ const parent = map.get(current.parentId);
91
+ if (!parent) break;
92
+ if (parent.type === "list") {
93
+ depth += 1;
94
+ }
95
+ current = parent;
96
+ }
97
+ return depth;
98
+ }
56
99
 
57
100
  // src/renderer/patch-coalescing.ts
58
101
  import {
@@ -73,12 +116,35 @@ function snapshotToBlock(node) {
73
116
  }
74
117
  var nodeSnapshotCache = /* @__PURE__ */ new Map();
75
118
  var childrenSnapshotCache = /* @__PURE__ */ new Map();
119
+ var NODE_SNAPSHOT_CACHE_LIMIT = 5e4;
120
+ var CHILDREN_SNAPSHOT_CACHE_LIMIT = 5e4;
121
+ var NODE_SNAPSHOT_CACHE_BUFFER = Math.max(200, Math.floor(NODE_SNAPSHOT_CACHE_LIMIT * 0.1));
122
+ var CHILDREN_SNAPSHOT_CACHE_BUFFER = Math.max(200, Math.floor(CHILDREN_SNAPSHOT_CACHE_LIMIT * 0.1));
123
+ function pruneCache(cache, max, buffer) {
124
+ if (cache.size <= max + buffer) return;
125
+ let remaining = cache.size - max;
126
+ for (const key of cache.keys()) {
127
+ cache.delete(key);
128
+ remaining -= 1;
129
+ if (remaining <= 0) break;
130
+ }
131
+ }
76
132
  var EMPTY_CHILDREN = Object.freeze([]);
77
133
  var EMPTY_NODE_SNAPSHOT = Object.freeze({ version: -1, node: void 0 });
78
134
  var EMPTY_CHILDREN_SNAPSHOT = Object.freeze({
79
135
  version: -1,
80
136
  children: EMPTY_CHILDREN
81
137
  });
138
+ function isListPatchDebugEnabled() {
139
+ if (typeof globalThis !== "undefined") {
140
+ const flag = globalThis.__STREAMING_LIST_DEBUG__;
141
+ if (flag === true) return true;
142
+ }
143
+ if (typeof process !== "undefined" && process.env.NEXT_PUBLIC_STREAMING_LIST_DEBUG === "true") {
144
+ return true;
145
+ }
146
+ return false;
147
+ }
82
148
  var CODE_BLOCK_DEBUG_ENABLED = (() => {
83
149
  try {
84
150
  if (typeof process !== "undefined" && process.env && process.env.NEXT_PUBLIC_STREAMING_DEBUG_CODELINES === "1") {
@@ -94,6 +160,128 @@ var CODE_BLOCK_DEBUG_ENABLED = (() => {
94
160
  }
95
161
  return false;
96
162
  })() || false;
163
+ var CODE_BLOCK_VALIDATION_ENABLED = typeof process !== "undefined" && process.env ? process.env.NODE_ENV !== "production" : true;
164
+ var PATCH_PERF_HISTORY_LIMIT = 120;
165
+ var getPerfNow = (() => {
166
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
167
+ return () => performance.now();
168
+ }
169
+ return () => Date.now();
170
+ })();
171
+ function isPatchPerfDebugEnabled() {
172
+ try {
173
+ if (typeof process !== "undefined" && process.env) {
174
+ const value = process.env.NEXT_PUBLIC_STREAMING_DEBUG_PATCH_PERF;
175
+ if (value === "1" || value === "true") {
176
+ return true;
177
+ }
178
+ }
179
+ } catch {
180
+ }
181
+ try {
182
+ const debug = globalThis.__STREAMING_DEBUG__;
183
+ if (debug?.patchPerf) {
184
+ return true;
185
+ }
186
+ } catch {
187
+ }
188
+ return false;
189
+ }
190
+ function recordPatchPerf(summary) {
191
+ try {
192
+ if (typeof globalThis === "undefined") return;
193
+ const root = globalThis;
194
+ const history = root.__STREAM_MDX_PATCH_STATS__ ?? [];
195
+ history.push(summary);
196
+ if (history.length > PATCH_PERF_HISTORY_LIMIT) {
197
+ history.splice(0, history.length - PATCH_PERF_HISTORY_LIMIT);
198
+ }
199
+ root.__STREAM_MDX_PATCH_STATS__ = history;
200
+ root.__STREAM_MDX_PATCH_STATS_LAST__ = summary;
201
+ const totals = root.__STREAM_MDX_PATCH_STATS_TOTALS__ ?? {
202
+ calls: 0,
203
+ totalMs: 0,
204
+ applyMs: 0,
205
+ coalesceMs: 0,
206
+ listNormalizeMs: 0,
207
+ patches: 0,
208
+ coalescedPatches: 0,
209
+ ops: {},
210
+ setPropsByType: {},
211
+ setPropsBySize: {}
212
+ };
213
+ totals.calls += 1;
214
+ totals.totalMs += summary.totalMs;
215
+ totals.applyMs += summary.applyMs;
216
+ totals.coalesceMs += summary.coalesceMs;
217
+ totals.listNormalizeMs += summary.listNormalizeMs;
218
+ totals.patches += summary.patches;
219
+ totals.coalescedPatches += summary.coalescedPatches;
220
+ for (const [op, stats] of Object.entries(summary.ops)) {
221
+ const entry = totals.ops[op] ?? { count: 0, durationMs: 0 };
222
+ entry.count += stats.count;
223
+ entry.durationMs += stats.durationMs;
224
+ totals.ops[op] = entry;
225
+ }
226
+ if (summary.setPropsByType) {
227
+ for (const [key, stats] of Object.entries(summary.setPropsByType)) {
228
+ const entry = totals.setPropsByType[key] ?? { count: 0, durationMs: 0 };
229
+ entry.count += stats.count;
230
+ entry.durationMs += stats.durationMs;
231
+ totals.setPropsByType[key] = entry;
232
+ }
233
+ }
234
+ if (summary.setPropsBySize) {
235
+ for (const [key, stats] of Object.entries(summary.setPropsBySize)) {
236
+ const entry = totals.setPropsBySize[key] ?? { count: 0, durationMs: 0 };
237
+ entry.count += stats.count;
238
+ entry.durationMs += stats.durationMs;
239
+ totals.setPropsBySize[key] = entry;
240
+ }
241
+ }
242
+ root.__STREAM_MDX_PATCH_STATS_TOTALS__ = totals;
243
+ } catch {
244
+ }
245
+ }
246
+ function estimateBlockPayloadSize(block) {
247
+ if (!block || !block.payload) return 0;
248
+ let size = 0;
249
+ const payload = block.payload;
250
+ if (typeof payload.raw === "string") size += payload.raw.length;
251
+ if (typeof payload.highlightedHtml === "string") size += payload.highlightedHtml.length;
252
+ if (typeof payload.sanitizedHtml === "string") size += payload.sanitizedHtml.length;
253
+ if (typeof payload.compiledMdxModule?.code === "string") size += payload.compiledMdxModule.code.length;
254
+ if (Array.isArray(payload.inline)) size += payload.inline.length * 8;
255
+ return size;
256
+ }
257
+ function estimatePropsSize(props, block) {
258
+ let size = 0;
259
+ if (props) {
260
+ for (const [key, value] of Object.entries(props)) {
261
+ if (key === "block") continue;
262
+ if (typeof value === "string") {
263
+ size += value.length;
264
+ } else if (typeof value === "number") {
265
+ size += 8;
266
+ } else if (typeof value === "boolean") {
267
+ size += 4;
268
+ } else if (Array.isArray(value)) {
269
+ size += value.length * 4;
270
+ }
271
+ }
272
+ }
273
+ if (block) {
274
+ size += estimateBlockPayloadSize(block);
275
+ }
276
+ return size;
277
+ }
278
+ function bucketBySize(size) {
279
+ if (size < 1024) return "<1k";
280
+ if (size < 10 * 1024) return "1-10k";
281
+ if (size < 50 * 1024) return "10-50k";
282
+ if (size < 200 * 1024) return "50-200k";
283
+ return ">=200k";
284
+ }
97
285
  function debugCodeBlock(event, payload) {
98
286
  if (!CODE_BLOCK_DEBUG_ENABLED) return;
99
287
  try {
@@ -170,7 +358,7 @@ function escapeHtml(value) {
170
358
  }
171
359
  function stripOuterLineSpan(html) {
172
360
  if (!html) return null;
173
- const openTagMatch = html.match(/<span[^>]*class="[^"]*\bline\b[^"]*"[^>]*>/i);
361
+ const openTagMatch = html.match(/<span[^>]*>/i);
174
362
  if (!openTagMatch) {
175
363
  return null;
176
364
  }
@@ -287,8 +475,12 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
287
475
  touched.add(node.id);
288
476
  mutated = true;
289
477
  }
290
- const normalizedProps = normalizeCodeLineProps({ index: idx, text: node.props?.text, html: node.props?.html }, node.props);
291
- if (node.props?.index !== normalizedProps.index || node.props?.text !== normalizedProps.text || node.props?.html !== normalizedProps.html) {
478
+ const incoming = { index: idx, text: node.props?.text, html: node.props?.html };
479
+ if (Object.prototype.hasOwnProperty.call(node.props ?? {}, "tokens")) {
480
+ incoming.tokens = node.props.tokens;
481
+ }
482
+ const normalizedProps = normalizeCodeLineProps(incoming, node.props);
483
+ if (normalizedProps !== node.props) {
292
484
  node.props = normalizedProps;
293
485
  node.version++;
294
486
  touched.add(node.id);
@@ -334,43 +526,68 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
334
526
  children: parent.children.slice()
335
527
  });
336
528
  }
337
- const validation = validateCodeBlockChildren(nodes, parent);
338
- if (!validation.ok) {
339
- debugCodeBlock("code-block-validation-failed", {
340
- parentId: parent.id,
341
- issues: validation.issues
342
- });
343
- if (!rebuildCodeBlockFromSnapshot(nodes, parent, touched, validation.issues)) {
344
- console.warn("[renderer-store] unable to fully normalize code block", {
529
+ if (CODE_BLOCK_VALIDATION_ENABLED) {
530
+ const validation = validateCodeBlockChildren(nodes, parent);
531
+ if (!validation.ok) {
532
+ debugCodeBlock("code-block-validation-failed", {
345
533
  parentId: parent.id,
346
534
  issues: validation.issues
347
535
  });
536
+ if (!rebuildCodeBlockFromSnapshot(nodes, parent, touched, validation.issues)) {
537
+ console.warn("[renderer-store] unable to fully normalize code block", {
538
+ parentId: parent.id,
539
+ issues: validation.issues
540
+ });
541
+ }
348
542
  }
349
543
  }
350
544
  }
351
545
  function normalizeCodeLineProps(incoming, previous) {
352
- const index = typeof incoming.index === "number" ? incoming.index : typeof previous?.index === "number" ? previous?.index : 0;
546
+ const previousIndex = typeof previous?.index === "number" ? previous?.index : 0;
547
+ const index = typeof incoming.index === "number" ? incoming.index : previousIndex;
353
548
  const previousText = typeof previous?.text === "string" ? previous?.text : "";
354
549
  const previousHtml = typeof previous?.html === "string" ? previous?.html : null;
550
+ const previousTokens = Object.prototype.hasOwnProperty.call(previous ?? {}, "tokens") ? previous?.tokens : void 0;
355
551
  const hasIncomingText = typeof incoming.text === "string";
552
+ const hasIncomingHtml = typeof incoming.html === "string";
553
+ const hasIncomingTokens = Object.prototype.hasOwnProperty.call(incoming, "tokens");
554
+ const incomingTokens = hasIncomingTokens ? incoming.tokens : void 0;
356
555
  const rawText = hasIncomingText ? incoming.text : previousText;
556
+ if (previous && !hasIncomingHtml && !hasIncomingTokens && index === previousIndex && (!hasIncomingText || rawText === previousText)) {
557
+ return previous;
558
+ }
559
+ if (previous && hasIncomingHtml && !hasIncomingTokens && incoming.html === previousHtml && index === previousIndex && (!hasIncomingText || rawText === previousText)) {
560
+ return previous;
561
+ }
357
562
  let highlight = null;
358
- if (typeof incoming.html === "string") {
563
+ if (hasIncomingHtml) {
359
564
  highlight = incoming.html;
360
565
  } else if (!hasIncomingText || rawText === previousText) {
361
566
  highlight = previousHtml;
362
567
  }
363
- return createCodeLineProps(index, rawText, highlight);
568
+ let tokens = void 0;
569
+ if (hasIncomingTokens) {
570
+ tokens = incomingTokens ?? null;
571
+ } else if (!hasIncomingText || rawText === previousText) {
572
+ tokens = previousTokens;
573
+ } else {
574
+ tokens = null;
575
+ }
576
+ return createCodeLineProps(index, rawText, highlight, tokens);
364
577
  }
365
- function createCodeLineProps(index, text, highlight) {
578
+ function createCodeLineProps(index, text, highlight, tokens) {
366
579
  const safeText = typeof text === "string" ? text : "";
367
580
  const sanitized = sanitizeLineInnerHtml(highlight, safeText);
368
581
  const html = sanitized && sanitized.length >= safeText.length ? sanitized : escapeHtml(safeText);
369
- return {
582
+ const props = {
370
583
  index,
371
584
  text: safeText,
372
585
  html
373
586
  };
587
+ if (tokens !== void 0) {
588
+ props.tokens = tokens;
589
+ }
590
+ return props;
374
591
  }
375
592
  function clampIndex(index, length) {
376
593
  if (!Number.isFinite(index)) return 0;
@@ -412,8 +629,13 @@ function mergeInlineSegmentProps(previous, incoming) {
412
629
  function applyCodeBlockMetadata(record) {
413
630
  const block = record.block;
414
631
  const highlightedHtml = block?.payload.highlightedHtml ?? "";
415
- const { preAttrs, codeAttrs } = extractCodeWrapperAttributes(highlightedHtml);
416
632
  const lang = typeof block?.payload.meta?.lang === "string" ? block?.payload.meta?.lang : record.props?.lang;
633
+ let { preAttrs, codeAttrs } = extractCodeWrapperAttributes(highlightedHtml);
634
+ if (!preAttrs || !codeAttrs) {
635
+ const defaults = getDefaultCodeWrapperAttributes(typeof lang === "string" ? lang : void 0);
636
+ preAttrs = preAttrs ?? defaults.preAttrs;
637
+ codeAttrs = codeAttrs ?? defaults.codeAttrs;
638
+ }
417
639
  record.props = {
418
640
  ...record.props ?? {},
419
641
  lang,
@@ -426,7 +648,8 @@ function normalizeNodeProps(snapshot) {
426
648
  const index = typeof snapshot.props?.index === "number" ? snapshot.props?.index : 0;
427
649
  const text = typeof snapshot.props?.text === "string" ? snapshot.props?.text : "";
428
650
  const html = typeof snapshot.props?.html === "string" ? snapshot.props?.html : null;
429
- return createCodeLineProps(index, text, html);
651
+ const tokens = Object.prototype.hasOwnProperty.call(snapshot.props ?? {}, "tokens") ? snapshot.props?.tokens : void 0;
652
+ return createCodeLineProps(index, text, html, tokens);
430
653
  }
431
654
  if (isInlineSegmentType(snapshot.type)) {
432
655
  const incoming = snapshot.props ? { ...snapshot.props } : {};
@@ -694,6 +917,13 @@ function createRendererStore(initialBlocks = []) {
694
917
  lastCoalescingMetrics = options?.metrics ?? null;
695
918
  return touched;
696
919
  }
920
+ const perfEnabled = isPatchPerfDebugEnabled();
921
+ const perfStart = perfEnabled ? getPerfNow() : 0;
922
+ const opStats = perfEnabled ? {} : {};
923
+ const setPropsByType = perfEnabled ? {} : {};
924
+ const setPropsBySize = perfEnabled ? {} : {};
925
+ let applyMs = 0;
926
+ let listNormalizeMs = 0;
697
927
  let coalescedPatches = patches;
698
928
  let metrics = options?.metrics ?? null;
699
929
  if (!options?.coalesced) {
@@ -708,32 +938,111 @@ function createRendererStore(initialBlocks = []) {
708
938
  }
709
939
  let mutatedBlocks = false;
710
940
  let listDepthDirty = false;
941
+ const listDepthDirtyNodes = /* @__PURE__ */ new Set();
942
+ const debugListPatches = isListPatchDebugEnabled();
943
+ const isListRelated = (type) => type === "list" || type === "list-item";
944
+ const logListPatch = (label, details) => {
945
+ if (!debugListPatches) return;
946
+ try {
947
+ console.debug(`[list-patch] ${label}`, details);
948
+ } catch {
949
+ }
950
+ };
951
+ const findNearestListNode = (nodeId) => {
952
+ if (!nodeId) return void 0;
953
+ let current = nodes.get(nodeId);
954
+ while (current) {
955
+ if (current.type === "list") return current;
956
+ if (!current.parentId) return void 0;
957
+ current = nodes.get(current.parentId);
958
+ }
959
+ return void 0;
960
+ };
961
+ const markListDepthDirty = (nodeId) => {
962
+ const listNode = findNearestListNode(nodeId);
963
+ if (listNode) {
964
+ listDepthDirtyNodes.add(listNode.id);
965
+ } else {
966
+ listDepthDirty = true;
967
+ }
968
+ };
711
969
  const applyPropsUpdate = (at, props) => {
970
+ const perfStartLocal = perfEnabled ? getPerfNow() : 0;
712
971
  const targetId = at.nodeId ?? at.blockId;
713
972
  if (!targetId) return;
714
973
  const target = nodes.get(targetId);
715
974
  if (!target) return;
716
- const nextProps = props ? { ...props } : {};
717
- const incomingBlock = nextProps.block;
718
- if (incomingBlock !== void 0) {
719
- nextProps.block = void 0;
975
+ const parentType = target.parentId ? nodes.get(target.parentId)?.type : void 0;
976
+ if (debugListPatches && (isListRelated(target.type) || isListRelated(parentType))) {
977
+ logListPatch("setProps", {
978
+ id: target.id,
979
+ type: target.type,
980
+ parentType,
981
+ keys: props ? Object.keys(props) : []
982
+ });
720
983
  }
721
984
  let propsChanged = false;
722
985
  if (target.type === "code-line") {
723
- const normalized = normalizeCodeLineProps(nextProps, target.props);
724
- if (!shallowEqualRecords(target.props, normalized)) {
986
+ const incoming = {
987
+ index: props?.index,
988
+ text: props?.text,
989
+ html: props?.html
990
+ };
991
+ if (Object.prototype.hasOwnProperty.call(props ?? {}, "tokens")) {
992
+ incoming.tokens = props.tokens;
993
+ }
994
+ const normalized = normalizeCodeLineProps(incoming, target.props);
995
+ if (normalized !== target.props) {
725
996
  target.props = normalized;
726
997
  propsChanged = true;
727
998
  }
999
+ if (propsChanged) {
1000
+ target.version++;
1001
+ touched.add(target.id);
1002
+ const parent = target.parentId ? nodes.get(target.parentId) : void 0;
1003
+ if (parent) {
1004
+ parent.version++;
1005
+ touched.add(parent.id);
1006
+ if (parent.type === "code") {
1007
+ normalizeCodeBlockChildren(nodes, parent, touched);
1008
+ }
1009
+ }
1010
+ }
1011
+ if (perfEnabled) {
1012
+ const duration = getPerfNow() - perfStartLocal;
1013
+ const typeKey = target.type || "unknown";
1014
+ const sizeKey = bucketBySize(estimatePropsSize(props, void 0));
1015
+ const typeEntry = setPropsByType[typeKey] ?? { count: 0, durationMs: 0 };
1016
+ typeEntry.count += 1;
1017
+ typeEntry.durationMs += duration;
1018
+ setPropsByType[typeKey] = typeEntry;
1019
+ const sizeEntry = setPropsBySize[sizeKey] ?? { count: 0, durationMs: 0 };
1020
+ sizeEntry.count += 1;
1021
+ sizeEntry.durationMs += duration;
1022
+ setPropsBySize[sizeKey] = sizeEntry;
1023
+ }
1024
+ return;
728
1025
  } else if (isInlineSegmentType(target.type)) {
1026
+ const nextProps = props ? { ...props } : {};
1027
+ if ("block" in nextProps) {
1028
+ delete nextProps.block;
1029
+ }
729
1030
  const mergedSegments = mergeInlineSegmentProps(target.props, nextProps);
730
1031
  if (!shallowEqualRecords(target.props, mergedSegments)) {
731
1032
  target.props = mergedSegments;
732
1033
  propsChanged = true;
733
1034
  }
734
1035
  } else {
1036
+ const nextProps = props ? { ...props } : {};
1037
+ if ("block" in nextProps) {
1038
+ delete nextProps.block;
1039
+ }
735
1040
  const base = target.props ? { ...target.props } : {};
736
1041
  let localChanged = false;
1042
+ if (Object.prototype.hasOwnProperty.call(base, "block")) {
1043
+ delete base.block;
1044
+ localChanged = true;
1045
+ }
737
1046
  for (const [key, value] of Object.entries(nextProps)) {
738
1047
  if (value === void 0) {
739
1048
  if (Object.prototype.hasOwnProperty.call(base, key)) {
@@ -751,6 +1060,7 @@ function createRendererStore(initialBlocks = []) {
751
1060
  }
752
1061
  }
753
1062
  let blockChanged = false;
1063
+ const incomingBlock = props?.block;
754
1064
  if (incomingBlock && typeof incomingBlock === "object") {
755
1065
  target.block = cloneBlock(incomingBlock);
756
1066
  if (target.type === "code") {
@@ -780,13 +1090,26 @@ function createRendererStore(initialBlocks = []) {
780
1090
  } else if (parentForNormalization) {
781
1091
  normalizeCodeBlockChildren(nodes, parentForNormalization, touched);
782
1092
  }
783
- const parentType = target.parentId ? nodes.get(target.parentId)?.type : void 0;
784
- if (target.type === "list" || target.type === "list-item" || parentType === "list" || parentType === "list-item") {
785
- listDepthDirty = true;
1093
+ const parentType2 = target.parentId ? nodes.get(target.parentId)?.type : void 0;
1094
+ if (isListRelated(target.type) || isListRelated(parentType2)) {
1095
+ markListDepthDirty(target.id);
786
1096
  }
787
1097
  }
1098
+ if (perfEnabled) {
1099
+ const duration = getPerfNow() - perfStartLocal;
1100
+ const typeKey = target.type || "unknown";
1101
+ const sizeKey = bucketBySize(estimatePropsSize(props, incomingBlock && typeof incomingBlock === "object" ? incomingBlock : void 0));
1102
+ const typeEntry = setPropsByType[typeKey] ?? { count: 0, durationMs: 0 };
1103
+ typeEntry.count += 1;
1104
+ typeEntry.durationMs += duration;
1105
+ setPropsByType[typeKey] = typeEntry;
1106
+ const sizeEntry = setPropsBySize[sizeKey] ?? { count: 0, durationMs: 0 };
1107
+ sizeEntry.count += 1;
1108
+ sizeEntry.durationMs += duration;
1109
+ setPropsBySize[sizeKey] = sizeEntry;
1110
+ }
788
1111
  };
789
- for (const patch of coalescedPatches) {
1112
+ const applyPatch = (patch) => {
790
1113
  switch (patch.op) {
791
1114
  case "insertChild": {
792
1115
  const parent = resolveParent(nodes, patch.at);
@@ -796,8 +1119,20 @@ function createRendererStore(initialBlocks = []) {
796
1119
  touched.add(id);
797
1120
  }
798
1121
  mutatedBlocks = true;
799
- if (inserted.record.type === "list" || inserted.record.type === "list-item" || parent && (parent.type === "list" || parent.type === "list-item")) {
800
- listDepthDirty = true;
1122
+ if (isListRelated(inserted.record.type) || isListRelated(parent?.type)) {
1123
+ markListDepthDirty(inserted.record.id);
1124
+ if (parent) {
1125
+ markListDepthDirty(parent.id);
1126
+ }
1127
+ }
1128
+ if (debugListPatches && (isListRelated(inserted.record.type) || isListRelated(parent?.type))) {
1129
+ logListPatch("insertChild", {
1130
+ id: inserted.record.id,
1131
+ type: inserted.record.type,
1132
+ parentId: parent?.id,
1133
+ parentType: parent?.type,
1134
+ index: patch.index
1135
+ });
801
1136
  }
802
1137
  }
803
1138
  break;
@@ -814,8 +1149,17 @@ function createRendererStore(initialBlocks = []) {
814
1149
  touched.add(parent.id);
815
1150
  ensureUniqueChildren(parent, touched);
816
1151
  mutatedBlocks = true;
817
- if (parent.type === "list" || parent.type === "list-item" || removedNode?.type === "list" || removedNode?.type === "list-item") {
818
- listDepthDirty = true;
1152
+ if (isListRelated(parent.type) || isListRelated(removedNode?.type)) {
1153
+ markListDepthDirty(parent.id);
1154
+ }
1155
+ if (debugListPatches && (isListRelated(parent.type) || isListRelated(removedNode?.type))) {
1156
+ logListPatch("deleteChild", {
1157
+ id: childId,
1158
+ type: removedNode?.type,
1159
+ parentId: parent.id,
1160
+ parentType: parent.type,
1161
+ index: patch.index
1162
+ });
819
1163
  }
820
1164
  }
821
1165
  break;
@@ -826,8 +1170,15 @@ function createRendererStore(initialBlocks = []) {
826
1170
  replaceSnapshotAt(nodes, parent, patch.index, patch.node, touched);
827
1171
  touched.add(parent.id);
828
1172
  mutatedBlocks = true;
829
- if (parent.type === "list" || parent.type === "list-item") {
830
- listDepthDirty = true;
1173
+ if (isListRelated(parent.type)) {
1174
+ markListDepthDirty(parent.id);
1175
+ }
1176
+ if (debugListPatches && isListRelated(parent.type)) {
1177
+ logListPatch("replaceChild", {
1178
+ parentId: parent.id,
1179
+ parentType: parent.type,
1180
+ index: patch.index
1181
+ });
831
1182
  }
832
1183
  break;
833
1184
  }
@@ -853,6 +1204,15 @@ function createRendererStore(initialBlocks = []) {
853
1204
  touched.add(target.id);
854
1205
  mutatedBlocks = true;
855
1206
  }
1207
+ if (isListRelated(target.type)) {
1208
+ markListDepthDirty(target.id);
1209
+ }
1210
+ if (debugListPatches && isListRelated(target.type)) {
1211
+ logListPatch("finalize", {
1212
+ id: target.id,
1213
+ type: target.type
1214
+ });
1215
+ }
856
1216
  break;
857
1217
  }
858
1218
  case "reorder": {
@@ -870,8 +1230,17 @@ function createRendererStore(initialBlocks = []) {
870
1230
  parent.children.splice(to, 0, ...moved);
871
1231
  parent.version++;
872
1232
  touched.add(parent.id);
873
- if (parent.type === "list" || parent.type === "list-item") {
874
- listDepthDirty = true;
1233
+ if (isListRelated(parent.type)) {
1234
+ markListDepthDirty(parent.id);
1235
+ }
1236
+ if (debugListPatches && isListRelated(parent.type)) {
1237
+ logListPatch("reorder", {
1238
+ parentId: parent.id,
1239
+ parentType: parent.type,
1240
+ from: patch.from,
1241
+ to: patch.to,
1242
+ count: patch.count
1243
+ });
875
1244
  }
876
1245
  break;
877
1246
  }
@@ -892,7 +1261,14 @@ function createRendererStore(initialBlocks = []) {
892
1261
  const lineId = `${parent.id}::line:${absoluteIndex}`;
893
1262
  insertedIds.push(lineId);
894
1263
  insertedSet.add(lineId);
895
- const nextLineProps = createCodeLineProps(absoluteIndex, patch.lines[offset] ?? "", patch.highlight?.[offset] ?? null);
1264
+ const hasTokens = Object.prototype.hasOwnProperty.call(patch, "tokens");
1265
+ const nextTokens = hasTokens ? patch.tokens?.[offset] ?? null : void 0;
1266
+ const nextLineProps = createCodeLineProps(
1267
+ absoluteIndex,
1268
+ patch.lines[offset] ?? "",
1269
+ patch.highlight?.[offset] ?? null,
1270
+ nextTokens
1271
+ );
896
1272
  const existing = nodes.get(lineId);
897
1273
  if (existing && existing.type === "code-line") {
898
1274
  const existingIndex = parent.children.indexOf(lineId);
@@ -945,8 +1321,12 @@ function createRendererStore(initialBlocks = []) {
945
1321
  const childId = parent.children[idx];
946
1322
  const child = nodes.get(childId);
947
1323
  if (child && child.type === "code-line") {
948
- const normalized = normalizeCodeLineProps({ index: idx, text: child.props?.text, html: child.props?.html }, child.props);
949
- if (!shallowEqualRecords(child.props, normalized)) {
1324
+ const incoming = { index: idx, text: child.props?.text, html: child.props?.html };
1325
+ if (Object.prototype.hasOwnProperty.call(child.props ?? {}, "tokens")) {
1326
+ incoming.tokens = child.props.tokens;
1327
+ }
1328
+ const normalized = normalizeCodeLineProps(incoming, child.props);
1329
+ if (normalized !== child.props) {
950
1330
  child.props = normalized;
951
1331
  child.version++;
952
1332
  touched.add(child.id);
@@ -989,17 +1369,66 @@ function createRendererStore(initialBlocks = []) {
989
1369
  default:
990
1370
  break;
991
1371
  }
1372
+ };
1373
+ if (perfEnabled) {
1374
+ const loopStart = getPerfNow();
1375
+ for (const patch of coalescedPatches) {
1376
+ const opStart = getPerfNow();
1377
+ applyPatch(patch);
1378
+ const opDuration = getPerfNow() - opStart;
1379
+ const entry = opStats[patch.op] ?? { count: 0, durationMs: 0 };
1380
+ entry.count += 1;
1381
+ entry.durationMs += opDuration;
1382
+ opStats[patch.op] = entry;
1383
+ }
1384
+ applyMs = getPerfNow() - loopStart;
1385
+ } else {
1386
+ for (const patch of coalescedPatches) {
1387
+ applyPatch(patch);
1388
+ }
992
1389
  }
993
1390
  if (mutatedBlocks) {
994
1391
  blocksDirty = true;
995
1392
  }
996
1393
  if (listDepthDirty) {
997
- normalizeAllListDepths(nodes, touched, PATCH_ROOT_ID);
1394
+ if (perfEnabled) {
1395
+ const start = getPerfNow();
1396
+ normalizeAllListDepths(nodes, touched, PATCH_ROOT_ID);
1397
+ listNormalizeMs = getPerfNow() - start;
1398
+ } else {
1399
+ normalizeAllListDepths(nodes, touched, PATCH_ROOT_ID);
1400
+ }
1401
+ } else if (listDepthDirtyNodes.size > 0) {
1402
+ if (perfEnabled) {
1403
+ const start = getPerfNow();
1404
+ normalizeListDepthsForIds(nodes, touched, listDepthDirtyNodes, PATCH_ROOT_ID);
1405
+ listNormalizeMs = getPerfNow() - start;
1406
+ } else {
1407
+ normalizeListDepthsForIds(nodes, touched, listDepthDirtyNodes, PATCH_ROOT_ID);
1408
+ }
998
1409
  }
999
1410
  if (touched.size > 0 || mutatedBlocks) {
1000
1411
  version++;
1001
1412
  scheduleNotify();
1002
1413
  }
1414
+ if (perfEnabled) {
1415
+ const totalMs = getPerfNow() - perfStart;
1416
+ const coalesceMs = typeof metrics?.durationMs === "number" ? metrics.durationMs : 0;
1417
+ recordPatchPerf({
1418
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1419
+ totalMs,
1420
+ applyMs,
1421
+ coalesceMs,
1422
+ listNormalizeMs,
1423
+ patches: patches.length,
1424
+ coalescedPatches: coalescedPatches.length,
1425
+ touchedCount: touched.size,
1426
+ mutatedBlocks,
1427
+ ops: opStats,
1428
+ setPropsByType,
1429
+ setPropsBySize
1430
+ });
1431
+ }
1003
1432
  return touched;
1004
1433
  },
1005
1434
  getBlocks() {
@@ -1032,6 +1461,7 @@ function createRendererStore(initialBlocks = []) {
1032
1461
  }
1033
1462
  const snapshot = { version: record.version, node: record };
1034
1463
  nodeSnapshotCache.set(id, snapshot);
1464
+ pruneCache(nodeSnapshotCache, NODE_SNAPSHOT_CACHE_LIMIT, NODE_SNAPSHOT_CACHE_BUFFER);
1035
1465
  return snapshot;
1036
1466
  },
1037
1467
  getChildren(id) {
@@ -1045,11 +1475,12 @@ function createRendererStore(initialBlocks = []) {
1045
1475
  return EMPTY_CHILDREN_SNAPSHOT;
1046
1476
  }
1047
1477
  const cached = childrenSnapshotCache.get(id);
1048
- if (cached && cached.version === record.version && cached.children === record.children) {
1478
+ if (cached && cached.version === record.version) {
1049
1479
  return cached;
1050
1480
  }
1051
- const snapshot = { version: record.version, children: record.children };
1481
+ const snapshot = { version: record.version, children: record.children.slice() };
1052
1482
  childrenSnapshotCache.set(id, snapshot);
1483
+ pruneCache(childrenSnapshotCache, CHILDREN_SNAPSHOT_CACHE_LIMIT, CHILDREN_SNAPSHOT_CACHE_BUFFER);
1053
1484
  return snapshot;
1054
1485
  },
1055
1486
  getLastCoalescingMetrics() {
@@ -1065,5 +1496,6 @@ function createRendererStore(initialBlocks = []) {
1065
1496
  return store;
1066
1497
  }
1067
1498
  export {
1068
- createRendererStore
1499
+ createRendererStore,
1500
+ isListPatchDebugEnabled
1069
1501
  };