edytor 5.0.0 → 7.0.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 (137) hide show
  1. package/README.md +3 -3
  2. package/dist/cloudflare/DocumentRoom.d.ts +77 -2
  3. package/dist/cloudflare/DocumentRoom.js +37 -1
  4. package/dist/cloudflare/index.d.ts +1 -1
  5. package/dist/cloudflare/index.js +1 -1
  6. package/dist/cloudflare/room/access.d.ts +6 -1
  7. package/dist/cloudflare/room/access.js +30 -4
  8. package/dist/cloudflare/room/admission.d.ts +11 -0
  9. package/dist/cloudflare/room/admission.js +43 -10
  10. package/dist/cloudflare/room/context.d.ts +73 -76
  11. package/dist/cloudflare/room/context.js +17 -6
  12. package/dist/cloudflare/room/forged.d.ts +1 -1
  13. package/dist/cloudflare/room/forged.js +11 -9
  14. package/dist/cloudflare/room/frames.d.ts +2 -0
  15. package/dist/cloudflare/room/frames.js +2 -0
  16. package/dist/cloudflare/room/presence.js +1 -4
  17. package/dist/cloudflare/room/reseed.d.ts +93 -0
  18. package/dist/cloudflare/room/reseed.js +500 -0
  19. package/dist/cloudflare/room/scheduler.d.ts +1 -1
  20. package/dist/cloudflare/room/scheduler.js +9 -1
  21. package/dist/cloudflare/room/shared.d.ts +10 -0
  22. package/dist/cloudflare/room/shared.js +10 -0
  23. package/dist/cloudflare/room/storage.d.ts +7 -0
  24. package/dist/cloudflare/room/storage.js +33 -1
  25. package/dist/components/Edytor.svelte +18 -3
  26. package/dist/components/Edytor.svelte.d.ts +10 -1
  27. package/dist/components/EdytorRoot.svelte +17 -0
  28. package/dist/components/EdytorRoot.svelte.d.ts +4 -0
  29. package/dist/components/Suggestion.svelte +1 -1
  30. package/dist/crdt/attribution/block.d.ts +3 -0
  31. package/dist/crdt/attribution/block.js +3 -2
  32. package/dist/crdt/doc/history.js +2 -3
  33. package/dist/crdt/doc/ops.d.ts +81 -0
  34. package/dist/crdt/doc/ops.js +1898 -0
  35. package/dist/crdt/doc/suggest.d.ts +6 -2
  36. package/dist/crdt/doc/suggest.js +6 -5
  37. package/dist/crdt/edytor-doc.d.ts +157 -157
  38. package/dist/crdt/edytor-doc.js +15 -43
  39. package/dist/crdt/engine-room.d.ts +4 -0
  40. package/dist/crdt/engine-room.js +6 -0
  41. package/dist/crdt/flow.d.ts +4 -46
  42. package/dist/crdt/flow.js +2 -262
  43. package/dist/crdt/index.d.ts +64 -75
  44. package/dist/crdt/operations.d.ts +1 -1
  45. package/dist/crdt/placement/resolve.d.ts +1 -1
  46. package/dist/crdt/protocol.d.ts +2 -2
  47. package/dist/crdt/protocol.js +2 -2
  48. package/dist/crdt/protocols/auth.d.ts +8 -1
  49. package/dist/crdt/protocols/auth.js +14 -1
  50. package/dist/crdt/protocols/awareness.js +46 -81
  51. package/dist/crdt/providers/index.d.ts +11 -22
  52. package/dist/crdt/providers/index.js +22 -3
  53. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  54. package/dist/crdt/providers/indexeddb.js +10 -16
  55. package/dist/crdt/providers/room.d.ts +26 -0
  56. package/dist/crdt/providers/room.js +41 -12
  57. package/dist/crdt/providers/websocket.d.ts +10 -14
  58. package/dist/crdt/providers/websocket.js +43 -23
  59. package/dist/crdt/rangeDelete.d.ts +2 -47
  60. package/dist/crdt/rangeDelete.js +1 -169
  61. package/dist/crdt/reseed.d.ts +127 -0
  62. package/dist/crdt/reseed.js +533 -0
  63. package/dist/crdt/restore.d.ts +13 -1
  64. package/dist/crdt/restore.js +8 -3
  65. package/dist/crdt/schema.d.ts +6 -0
  66. package/dist/crdt/schema.js +6 -0
  67. package/dist/crdt/structs.d.ts +8 -11
  68. package/dist/crdt/structs.js +25 -44
  69. package/dist/crdt/text/index/build.d.ts +119 -0
  70. package/dist/crdt/text/index/build.js +2801 -0
  71. package/dist/crdt/text/index/shared.d.ts +2 -6
  72. package/dist/crdt/text/index/shared.js +2 -5
  73. package/dist/crdt/text/model.js +2 -3
  74. package/dist/crdt/text/runs.d.ts +9 -12
  75. package/dist/crdt/text/runs.js +2 -165
  76. package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
  77. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  78. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
  79. package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
  80. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  81. package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
  82. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
  83. package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
  84. package/dist/events/domTextOffset.d.ts +2 -0
  85. package/dist/events/domTextOffset.js +8 -0
  86. package/dist/export/forms.d.ts +50 -50
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  90. package/dist/plugins/richtext/richTextOperations.js +26 -32
  91. package/dist/selection/selection.svelte.d.ts +1 -1
  92. package/dist/selection/selection.svelte.js +87 -106
  93. package/dist/selection/selection.utils.js +5 -8
  94. package/dist/session/commands.js +49 -51
  95. package/dist/session/handles.js +3 -7
  96. package/dist/surface/attributes.d.ts +3 -3
  97. package/dist/surface/attributes.js +2 -2
  98. package/dist/surface/observer.svelte.d.ts +7 -7
  99. package/dist/surface/observer.svelte.js +225 -336
  100. package/dist/surface/projector.svelte.js +2 -2
  101. package/dist/utils.d.ts +5 -0
  102. package/dist/utils.js +7 -0
  103. package/package.json +1 -1
  104. package/dist/crdt/doc/capability.d.ts +0 -56
  105. package/dist/crdt/doc/capability.js +0 -207
  106. package/dist/crdt/doc/content.d.ts +0 -16
  107. package/dist/crdt/doc/content.js +0 -77
  108. package/dist/crdt/doc/delete.d.ts +0 -15
  109. package/dist/crdt/doc/delete.js +0 -84
  110. package/dist/crdt/doc/layout.d.ts +0 -9
  111. package/dist/crdt/doc/layout.js +0 -178
  112. package/dist/crdt/doc/meta.d.ts +0 -19
  113. package/dist/crdt/doc/meta.js +0 -198
  114. package/dist/crdt/doc/moves.d.ts +0 -21
  115. package/dist/crdt/doc/moves.js +0 -274
  116. package/dist/crdt/doc/split-merge.d.ts +0 -10
  117. package/dist/crdt/doc/split-merge.js +0 -179
  118. package/dist/crdt/doc/steps.d.ts +0 -65
  119. package/dist/crdt/doc/steps.js +0 -311
  120. package/dist/crdt/text/index/cache.d.ts +0 -16
  121. package/dist/crdt/text/index/cache.js +0 -175
  122. package/dist/crdt/text/index/claims.d.ts +0 -18
  123. package/dist/crdt/text/index/claims.js +0 -203
  124. package/dist/crdt/text/index/fold.d.ts +0 -21
  125. package/dist/crdt/text/index/fold.js +0 -656
  126. package/dist/crdt/text/index/layout.d.ts +0 -22
  127. package/dist/crdt/text/index/layout.js +0 -258
  128. package/dist/crdt/text/index/placement.d.ts +0 -47
  129. package/dist/crdt/text/index/placement.js +0 -738
  130. package/dist/crdt/text/index/records.d.ts +0 -13
  131. package/dist/crdt/text/index/records.js +0 -139
  132. package/dist/crdt/text/index/report.d.ts +0 -18
  133. package/dist/crdt/text/index/report.js +0 -337
  134. package/dist/crdt/text/index/state.d.ts +0 -197
  135. package/dist/crdt/text/index/state.js +0 -116
  136. package/dist/crdt/text/index/streams.d.ts +0 -25
  137. package/dist/crdt/text/index/streams.js +0 -248
@@ -0,0 +1,2801 @@
1
+ import { bySlot, childrenIndex, displaySlotOf, documentOrder, REGISTRY_KEY } from '../../placement/model.js';
2
+ import { CONTENT, CONTENT_NODE, DOC_DATA_ROOT, hasDeleteMark, hasWithdrawMark, isNodeLike, NONCE } from '../../schema.js';
3
+ import { changedUnder, engineOps, firstItem, itemOf, queuedTransactions, sequenceLength, walkRange } from '../../structs.js';
4
+ import { gapOfItem, headOf, placeRow, rowLength, rowOf, segmentAt, segmentClaims, segmentOfCut, segmentOfGap, shiftGap, streamOfSegment } from '../rows.js';
5
+ import { addTo, CONTENT_ATTR, DATA_SEQ_FACET, dataOf, dropFrom, EMPTY_IDS, EMPTY_RUNS, ENTRY_FACET, facetOf, indexChecks, keyOf, runEquals, sameShape, SEQ_FACET, typeAttr } from './shared.js';
6
+ import { cloneJsonSafe, sameIds } from '../../../utils/json.js';
7
+ import { byId, canonKey, cmpStamp, DEAD, deepFreeze, displayOf, isRowMark, readSegs, scanText } from '../model.js';
8
+ import { getOr } from '../../../utils.js';
9
+ import { removedKeys } from '../streams.js';
10
+ import { cellKey, denseKey, keyBetween, LAST_CELL, unlistedCellKey } from '../../placement/keys.js';
11
+ import { listedColumns, unlistedRank } from '../../tables.js';
12
+ import { byTail, candidatesOf, collectSlots, cycleThrough, edgeParentOf, homeOf, NO_SLOT, ownerOfNode, positionsOfItem, registryOwners, slotOwner, slotsBefore } from '../../placement/resolve.js';
13
+ import { cmpStamp as cmpSlotStamp, isBlockSlot, isDataSlot, isRecord, sameUnit, unitOf } from '../../placement/slots.js';
14
+ import { Y } from '../../engine.js';
15
+ import { readData } from '../../data.js';
16
+ import { baseIdOf, incarnationId, incarnationNode, incarnationsOf, isIncarnationId, LIVE_WRITERS } from '../../incarnations.js';
17
+ import { callEach } from '../../protocols/observable.js';
18
+ /** One index per engine doc, shared by every binding (the doc's lifetime). */
19
+ const indexes = new WeakMap();
20
+ /** The doc's index — built on first use, torn down when the doc is destroyed. */
21
+ export const indexOf = (doc) => getOr(indexes, doc, () => buildIndex(doc));
22
+ const buildIndex = (doc) => {
23
+ // ══ The state ═══════════════════════════════════════════════════
24
+ // Every fact the parts share. Containers are created once and mutated in
25
+ // place; the fields a part reassigns (`delim`, `roles`, the children
26
+ // indexes, the counters and flags) are `let`. What one part alone keeps
27
+ // stays in that part.
28
+ // The children index starts as the one without the layout rules (no layout yet).
29
+ let kids0 = new Map();
30
+ let slots0 = new Map();
31
+ const registry = doc.get(REGISTRY_KEY);
32
+ /** The document's own data (`crdt/data.ts`): outside the registry, reported apart. */
33
+ const dataRoot = doc.get(DOC_DATA_ROOT);
34
+ // ── the replicated-state index
35
+ const blocks = new Map();
36
+ /** Withdrawn blocks without a delete mark (`hist.undo.withdraw`), see the fold's `settleShells`. */
37
+ const shells = new Set();
38
+ /** Forward dependencies of cached runs: backing text (by home) / walked block → consumers. */
39
+ const textConsumers = new Map();
40
+ const listConsumers = new Map();
41
+ // ── the claim graph
42
+ /** Owner → the blocks it displays (itself included while it owns itself). */
43
+ const displaysMap = new Map();
44
+ /** Blocks whose claims, delete mark or record changed since the last owner pass. */
45
+ const ownerSeeds = new Set();
46
+ /** Blocks whose owner changed since the last placement pass. */
47
+ const ownerChanged = new Set();
48
+ // ── the stream table
49
+ const rows = new Map();
50
+ let delim = new Map();
51
+ /** Each block's segment: its row and the cut that opens it (`null`: the row's segment 0). */
52
+ const streamIx = new Map();
53
+ const placed = new Map();
54
+ /** Each row's text → its home (rows are keyed by home). */
55
+ const homeOfText = new WeakMap();
56
+ /** Blocks whose claims (implicit, then their stream's claim items) the next pass re-reads. */
57
+ const reclaim = new Set();
58
+ // ── roles
59
+ /** The role table the display reads; `null` → none. */
60
+ let roles = null;
61
+ /** A stored kind changed since the last report: derived kinds may follow it. */
62
+ let retyped = false;
63
+ // ── placements, the layout rules and the children index
64
+ /** The live blocks the layout rules do not display (`layout.*`), per placement build. */
65
+ let dissolved = new Set();
66
+ /** Resolved placements. */
67
+ const placementsMap = new Map();
68
+ /** Counts the folds that changed a sequence (a slot or record written or removed): a slot map read before one is stale. */
69
+ let seqEpoch = 0;
70
+ /** Every block slot by the id it names (`{ b }`, fork patch YP22), kept from the folds; a block's home is `homeFor`. */
71
+ const slotsByBlock = new Map();
72
+ /** Each sequence owner's blocks (every block whose display edge it holds), sorted by key. */
73
+ const rawOrder = new Map();
74
+ /** Each block's display edge as accepted, or its strongest when none is (`null`: no slot). */
75
+ const edges = new Map();
76
+ /** The blocks shown below their strongest edge: a cycle dropped a later move, they are back where they stood. */
77
+ const fallen = new Set();
78
+ /** The blocks at the root's tail: every edge closes a cycle, or no slot. */
79
+ const rejected = new Set();
80
+ /** The children index without the layout rules (what they read), and with them. */
81
+ let kidsMap = kids0;
82
+ /** Each visible block's slot (display parent, rank, the island it displays out of). */
83
+ let slots = slots0;
84
+ /**
85
+ * The blocks whose shown kind follows their slot — displayed out of
86
+ * an island, then in a lined island or of a line kind — which the
87
+ * report re-reads after a retype.
88
+ */
89
+ const following = new Set();
90
+ /** Bumps whenever a child list changes (the report compares it). */
91
+ let kidsVersion = 0;
92
+ /** The child lists patched since the last report (`all`: every list was rebuilt). */
93
+ const dirtyLists = new Set();
94
+ /** The blocks that left every list since the last report. */
95
+ const leftLists = new Set();
96
+ let allListsDirty = true;
97
+ /** The next pass rebuilds everything (first build, roles, an irregular state). */
98
+ let placementFull = true;
99
+ /** Blocks whose slot, record or mover changed; blocks whose display state changed. */
100
+ const placementSeeds = new Set();
101
+ const stateSeeds = new Set();
102
+ /**
103
+ * Per block: whether its kind is a layout, an item, a lines island
104
+ * (with its line kind), a table, a table's row or a row's cell.
105
+ */
106
+ const kindsOf = new Map();
107
+ /** The item kinds the roles declare, and those the document holds. */
108
+ let itemKinds = new Set();
109
+ /** The row kinds of the table kinds the roles declare and the document holds, with their cell kinds. */
110
+ let rowKinds = new Map();
111
+ /** The cell kinds of {@link rowKinds}. */
112
+ let cellKinds = new Set();
113
+ // ── records
114
+ /**
115
+ * The losing incarnations each key shows (`id.same.concurrent`):
116
+ * blocks of their own under derived ids, claimed by the key's block
117
+ * before its own claims (`incarnations.ts`).
118
+ */
119
+ const incarnations = new Map();
120
+ // ── the run cache
121
+ const cache = new Map();
122
+ const dirty = new Set();
123
+ /** The index version (`RunView.version`). */
124
+ let version = 0;
125
+ /** The fold counters `debug` exposes read-only. */
126
+ const foldStats = { folds: 0, pairs: 0, structs: 0, placed: 0 };
127
+ // ── the fold and the report
128
+ /** Frames tracking the folds (the write funnel's `track()`), and the report's candidates. */
129
+ const frames = new Set();
130
+ const candidates = new Set();
131
+ let reporting = false;
132
+ // ══ The claim graph ═════════════════════════════════════════════
133
+ // The claim graph (maintained): each block's owner along the max-stamp
134
+ // claims (`top`), the blocks each owner displays, and what a structural
135
+ // change of a block invalidates.
136
+ // ── the claim graph (maintained) ─────────────────────────────
137
+ // `top(m)` is the max-stamp claim on `m` held by a live block; each
138
+ // block's owner follows `top` up (`claimGraph`). A structural change
139
+ // of a block (its claims, its delete mark, its record) re-decides the
140
+ // `top` of the blocks it claims (before and after) and the owners of
141
+ // those blocks and of every block whose `top` chain reaches them —
142
+ // never the whole graph.
143
+ const owners = new Map();
144
+ const topOf = new Map();
145
+ /** Claimer → the blocks it tops. */
146
+ const topInv = new Map();
147
+ /** Claimed block → the blocks whose claims list names it (live or not). */
148
+ const claimersOf = new Map();
149
+ /** The max-stamp claim on `m` held by a live block. */
150
+ const topClaim = (m) => {
151
+ let best;
152
+ for (const holder of claimersOf.get(m) ?? []) {
153
+ const rec = blocks.get(holder);
154
+ if (rec === undefined || rec.deleted)
155
+ continue;
156
+ for (const c of rec.claims)
157
+ if (c.m === m && (best === undefined || cmpStamp(c.stamp, best.stamp) > 0))
158
+ best = { claimer: holder, stamp: c.stamp };
159
+ }
160
+ return best;
161
+ };
162
+ const setTop = (m, t) => {
163
+ const old = topOf.get(m);
164
+ if (old?.claimer === t?.claimer && (old === undefined || cmpStamp(old.stamp, t.stamp) === 0))
165
+ return false;
166
+ if (old !== undefined)
167
+ dropFrom(topInv, old.claimer, m);
168
+ if (t === undefined)
169
+ topOf.delete(m);
170
+ else {
171
+ topOf.set(m, t);
172
+ addTo(topInv, t.claimer, m);
173
+ }
174
+ return true;
175
+ };
176
+ /** `owner(b)` along `top` (`claimGraph`'s walk), memoized into `owners`. */
177
+ const walkOwner = (b) => {
178
+ const path = [];
179
+ let cur = b;
180
+ let result;
181
+ for (;;) {
182
+ const known = owners.get(cur);
183
+ if (known !== undefined) {
184
+ result = known;
185
+ break;
186
+ }
187
+ const rec = blocks.get(cur);
188
+ if (!rec || rec.deleted) {
189
+ result = DEAD;
190
+ break;
191
+ }
192
+ const at = path.indexOf(cur);
193
+ if (at >= 0) {
194
+ let best = topOf.get(path[at]);
195
+ for (const member of path.slice(at)) {
196
+ const t = topOf.get(member);
197
+ if (cmpStamp(t.stamp, best.stamp) > 0)
198
+ best = t;
199
+ }
200
+ result = best.claimer;
201
+ break;
202
+ }
203
+ const t = topOf.get(cur);
204
+ if (t === undefined) {
205
+ result = cur;
206
+ break;
207
+ }
208
+ path.push(cur);
209
+ cur = t.claimer;
210
+ }
211
+ if (blocks.has(cur))
212
+ owners.set(cur, result);
213
+ for (const x of path)
214
+ owners.set(x, result);
215
+ };
216
+ const setDisplay = (b, from, to) => {
217
+ if (typeof from === 'string')
218
+ dropFrom(displaysMap, from, b);
219
+ if (typeof to === 'string')
220
+ addTo(displaysMap, to, b);
221
+ };
222
+ const ensureOwners = () => {
223
+ if (ownerSeeds.size === 0)
224
+ return;
225
+ // The tops a seed's claims (now, and those it dropped: noted in `claimersOf` changes) decide.
226
+ const affected = new Set();
227
+ const stack = [];
228
+ const seeds = [...ownerSeeds];
229
+ for (const x of ownerSeeds) {
230
+ stack.push(x);
231
+ for (const m of claimTargets.get(x) ?? [])
232
+ if (setTop(m, topClaim(m)))
233
+ stack.push(m);
234
+ for (const c of blocks.get(x)?.claims ?? [])
235
+ if (setTop(c.m, topClaim(c.m)))
236
+ stack.push(c.m);
237
+ }
238
+ ownerSeeds.clear();
239
+ claimTargets.clear();
240
+ for (let x = stack.pop(); x !== undefined; x = stack.pop()) {
241
+ if (affected.has(x))
242
+ continue;
243
+ affected.add(x);
244
+ for (const m of topInv.get(x) ?? [])
245
+ stack.push(m);
246
+ }
247
+ const before = new Map();
248
+ for (const x of affected) {
249
+ before.set(x, owners.get(x));
250
+ owners.delete(x);
251
+ }
252
+ for (const x of affected)
253
+ if (blocks.has(x))
254
+ walkOwner(x);
255
+ for (const [x, was] of before) {
256
+ const now = owners.get(x);
257
+ if (now === was)
258
+ continue;
259
+ setDisplay(x, was, now);
260
+ ownerChanged.add(x);
261
+ }
262
+ // A holder whose claims changed re-slots what shows through them: their
263
+ // order among its claims is the key of the blocks shown under it
264
+ // (`claimedRank`) — under its owner when it is itself merged away.
265
+ for (const x of seeds) {
266
+ if (!blocks.has(x))
267
+ continue;
268
+ ownerChanged.add(x);
269
+ const o = owners.get(x);
270
+ if (typeof o === 'string' && o !== x)
271
+ ownerChanged.add(o);
272
+ }
273
+ };
274
+ /** The claims each seed held before its record changed (their tops are re-decided too). */
275
+ const claimTargets = new Map();
276
+ const ownerOf = (b) => {
277
+ ensureOwners();
278
+ return owners.get(b) ?? DEAD;
279
+ };
280
+ /** The live claimer of the max-stamp claim on `m` (`DisplayOwnership.top`). */
281
+ const top = (m) => {
282
+ ensureOwners();
283
+ return topOf.get(m)?.claimer;
284
+ };
285
+ /** The blocks `owner` displays (the delete step's marks). */
286
+ const displays = (owner) => {
287
+ ensureOwners();
288
+ return [...(displaysMap.get(owner) ?? [])];
289
+ };
290
+ /** Union-ever claim targets per holder (never shrinks: a dropped claim still invalidates). */
291
+ const effects = new Map();
292
+ const noteEffects = (id, claims) => {
293
+ const fx = getOr(effects, id, () => new Set());
294
+ for (const c of claims)
295
+ fx.add(c.m);
296
+ };
297
+ /** `id`'s claims went from `before` to `after`: its claim facts follow. */
298
+ const noteClaims = (id, before, after) => {
299
+ noteEffects(id, after);
300
+ const was = new Set(before.map((c) => c.m));
301
+ const now = new Set(after.map((c) => c.m));
302
+ for (const m of was)
303
+ if (!now.has(m)) {
304
+ dropFrom(claimersOf, m, id);
305
+ addTo(claimTargets, id, m);
306
+ }
307
+ for (const m of now)
308
+ if (!was.has(m))
309
+ addTo(claimersOf, m, id);
310
+ ownerSeeds.add(id);
311
+ };
312
+ /** A structural change of `id`: its readers, and the blocks it claims, re-read. */
313
+ const invalidateBlock = (id, ctx, claimsBefore = []) => {
314
+ ctx.invalidated.add(id);
315
+ for (const c of listConsumers.get(id) ?? [])
316
+ ctx.invalidated.add(c);
317
+ for (const m of new Set([...(effects.get(id) ?? []), ...claimsBefore.map((c) => c.m)])) {
318
+ ctx.invalidated.add(m);
319
+ for (const c of listConsumers.get(m) ?? [])
320
+ ctx.invalidated.add(c);
321
+ }
322
+ };
323
+ // ══ The stream table ════════════════════════════════════════════
324
+ // The stream table: one maintained row per backing text, the
325
+ // delimiting boundary of each block, each block's segment, and each
326
+ // block's claims (the claim items of its segment).
327
+ // ── the stream table ────────────────────────────────────
328
+ // One maintained row per backing text (keyed by its home block): its
329
+ // live boundaries and the units between them (`text/rows.ts`), the
330
+ // delimiting boundary of each block, and each block's segment. An edit
331
+ // that writes or removes no boundary moves its row by its units
332
+ // (`shiftGap`); a row is rescanned only when its boundary set (or its
333
+ // text) changed, and only the delimiters of the blocks its boundaries
334
+ // name are re-decided, so only the rows those delimiters cut re-place.
335
+ /** Every live boundary naming a block, by block: key → its row and nonce. */
336
+ const boundsBy = new Map();
337
+ const streamOf = (b) => {
338
+ const at = streamIx.get(b);
339
+ const row = at && rows.get(at.home);
340
+ if (row === undefined)
341
+ return undefined;
342
+ const k = segmentOfCut(row, at.cut);
343
+ return k < 0 ? undefined : streamOfSegment(row, k, b);
344
+ };
345
+ const streamsIn = (home) => {
346
+ const row = rows.get(home);
347
+ const out = [];
348
+ for (let k = 0; row !== undefined && k <= row.cuts.length; k++) {
349
+ const b = headOf(row, k);
350
+ if (b !== null)
351
+ out.push(streamOfSegment(row, k, b));
352
+ }
353
+ return out;
354
+ };
355
+ const streamAt = (home, i) => {
356
+ const row = rows.get(home);
357
+ if (row === undefined)
358
+ return undefined;
359
+ const k = segmentAt(row, i);
360
+ const b = headOf(row, k);
361
+ if (b === null)
362
+ return undefined;
363
+ const st = streamOfSegment(row, k, b);
364
+ return st.start <= i && i <= st.end ? st : undefined;
365
+ };
366
+ /** Record `home`'s new row (or none); the blocks its boundary set change names join `named`. */
367
+ const setRow = (home, row, named) => {
368
+ const old = rows.get(home);
369
+ if (row === undefined)
370
+ rows.delete(home);
371
+ else {
372
+ rows.set(home, row);
373
+ homeOfText.set(row.text, home);
374
+ }
375
+ if (old?.key === row?.key)
376
+ return;
377
+ for (const b of old?.bounds ?? []) {
378
+ if (b.m !== undefined || row?.keyIndex.has(b.key))
379
+ continue;
380
+ const by = boundsBy.get(b.s);
381
+ // A text can change homes (a key's node becomes a losing incarnation,
382
+ // `id.same.concurrent`): drop the entry only while it is still this row's.
383
+ if (by?.get(b.key)?.home === home)
384
+ by.delete(b.key);
385
+ if (by?.size === 0)
386
+ boundsBy.delete(b.s);
387
+ named.add(b.s);
388
+ }
389
+ for (const b of row?.bounds ?? []) {
390
+ if (b.m !== undefined || old?.keyIndex.has(b.key))
391
+ continue;
392
+ const by = getOr(boundsBy, b.s, () => new Map());
393
+ by.set(b.key, { home, n: b.n });
394
+ named.add(b.s);
395
+ }
396
+ };
397
+ /** A fresh row of `home`'s text (none without one). */
398
+ const scanRow = (home) => {
399
+ const text = blocks.get(home)?.content;
400
+ return text === undefined ? undefined : rowOf(scanText(home, text));
401
+ };
402
+ /** A removed block's key (`room.purge.what`): its boundary delimits nobody's stretch. */
403
+ const removed = removedKeys(registry);
404
+ /** A cut's stretch is nobody's when it names no block (a removed one). */
405
+ const nobody = (s) => !blocks.has(s);
406
+ /** Re-decide the delimiting boundary of each block in `named`; the rows a change cuts join `homes`. */
407
+ const refreshDelims = (named, homes) => {
408
+ for (const s of named) {
409
+ const rec = blocks.get(s);
410
+ const gone = rec === undefined && removed(s);
411
+ let best;
412
+ if (rec !== undefined || gone)
413
+ for (const [key, b] of boundsBy.get(s) ?? [])
414
+ if ((gone || b.n === rec.n) && (best === undefined || byId(key, best) < 0))
415
+ best = key;
416
+ const old = delim.get(s);
417
+ if (old === best) {
418
+ // The cut stays, and the block it opens may have come or gone (removed).
419
+ if (best !== undefined)
420
+ homes.add(boundsBy.get(s).get(best).home);
421
+ continue;
422
+ }
423
+ if (best === undefined)
424
+ delim.delete(s);
425
+ else
426
+ delim.set(s, best);
427
+ const was = old === undefined ? undefined : boundsBy.get(s)?.get(old)?.home;
428
+ if (was !== undefined)
429
+ homes.add(was);
430
+ if (best !== undefined)
431
+ homes.add(boundsBy.get(s).get(best).home);
432
+ if (rows.has(s) || placed.has(s))
433
+ homes.add(s);
434
+ }
435
+ };
436
+ /**
437
+ * Place `home`'s row; invalidate the blocks whose segment changed (or
438
+ * appeared, or went). A segment is its two delimiting cuts, so only the
439
+ * segments between the longest common prefix and suffix of the old and
440
+ * new cut lists can differ: the others keep their entries untouched.
441
+ */
442
+ const placeHome = (home, invalidated) => {
443
+ const row = rows.get(home);
444
+ const before = placed.get(home) ?? { head: null, keys: [], blocks: [] };
445
+ const now = { head: null, keys: [], blocks: [] };
446
+ if (row !== undefined) {
447
+ placeRow(row, delim, nobody);
448
+ now.head = row.head;
449
+ row.cuts.forEach((j, k) => {
450
+ now.keys.push(row.bounds[j].key);
451
+ now.blocks.push(row.heads[k]);
452
+ });
453
+ }
454
+ if (row === undefined)
455
+ placed.delete(home);
456
+ else
457
+ placed.set(home, now);
458
+ // Its segments' claim items may have changed, or gone to other blocks.
459
+ for (const x of [before, now]) {
460
+ if (x.head !== null)
461
+ reclaim.add(x.head);
462
+ for (const b of x.blocks)
463
+ if (b !== null)
464
+ reclaim.add(b);
465
+ }
466
+ const [a, b] = [before.keys, now.keys];
467
+ // A cut is the same when its boundary and the block it opens are (a
468
+ // removed block's cut opens nobody's stretch).
469
+ const same = (i, j) => a[i] === b[j] && before.blocks[i] === now.blocks[j];
470
+ let p = 0;
471
+ while (p < a.length && p < b.length && same(p, p))
472
+ p++;
473
+ let q = 0;
474
+ while (q < a.length - p && q < b.length - p && same(a.length - 1 - q, b.length - 1 - q))
475
+ q++;
476
+ // Segment k ends at cut k: it changed iff k lies in [p, n - q] (segment 0 also with its block).
477
+ const from = before.head === now.head ? p : 0;
478
+ const changed = (blk) => {
479
+ if (blk === null)
480
+ return;
481
+ invalidated.add(blk);
482
+ for (const c of listConsumers.get(blk) ?? [])
483
+ invalidated.add(c);
484
+ };
485
+ const blockOf = (x, k) => (k === 0 ? x.head : x.blocks[k - 1]);
486
+ for (let k = from; k <= a.length - q; k++) {
487
+ const blk = blockOf(before, k);
488
+ if (blk !== null && streamIx.get(blk)?.home === home)
489
+ streamIx.delete(blk);
490
+ changed(blk);
491
+ }
492
+ for (let k = from; k <= b.length - q; k++) {
493
+ const blk = blockOf(now, k);
494
+ if (blk === null)
495
+ continue;
496
+ streamIx.set(blk, { home, cut: k === 0 ? null : b[k - 1] });
497
+ changed(blk);
498
+ }
499
+ };
500
+ /** Rebuild the delimiters and every row's segments (the first build). */
501
+ const rebuildTable = (invalidated) => {
502
+ boundsBy.clear();
503
+ for (const [home, row] of rows)
504
+ for (const b of row.bounds) {
505
+ if (b.m !== undefined)
506
+ continue;
507
+ const by = getOr(boundsBy, b.s, () => new Map());
508
+ by.set(b.key, { home, n: b.n });
509
+ }
510
+ delim = new Map();
511
+ const homes = new Set([...rows.keys(), ...placed.keys()]);
512
+ refreshDelims(boundsBy.keys(), homes);
513
+ for (const home of homes)
514
+ placeHome(home, invalidated);
515
+ };
516
+ // ── claims ──────────────────────────────────────────────
517
+ // A block's claims are its implicit ones (`id.same.concurrent`), then
518
+ // the claim items of its segment, in text order (fork patch YP21:
519
+ // YATA placed each one, so a split its writer did not see leaves it in
520
+ // the piece that ends its holder's region, `merge.claim.anchor`). They
521
+ // change with the record (its implicit claims) and with its row's bounds
522
+ // or cuts: re-read after each fold's texts for the blocks queued.
523
+ const sameClaims = (x, y) => x.length === y.length &&
524
+ x.every((c, i) => c.m === y[i].m && cmpStamp(c.stamp, y[i].stamp) === 0);
525
+ /** `b`'s claims now: its implicit ones, then its segment's claim items. */
526
+ const claimsOf = (b) => {
527
+ const at = streamIx.get(b);
528
+ const row = at && rows.get(at.home);
529
+ const k = row === undefined ? -1 : segmentOfCut(row, at.cut);
530
+ const items = k < 0 ? [] : segmentClaims(row, k);
531
+ const implicit = blocks.get(b)?.implicit ?? [];
532
+ return implicit.length === 0 ? items : [...implicit, ...items];
533
+ };
534
+ /** Re-read the claims of the blocks queued; a change is a structural change of the block. */
535
+ const applyReclaims = (ctx) => {
536
+ for (const b of reclaim) {
537
+ const rec = blocks.get(b);
538
+ if (rec === undefined)
539
+ continue;
540
+ const before = rec.claims;
541
+ const after = claimsOf(b);
542
+ if (sameClaims(before, after))
543
+ continue;
544
+ rec.claims = after;
545
+ noteClaims(b, before, after);
546
+ if (ctx !== null)
547
+ invalidateBlock(b, ctx, before);
548
+ }
549
+ reclaim.clear();
550
+ };
551
+ // ══ The layout and table rules ══════════════════════════════════
552
+ // The layout rules (`layout.*`) and the table rules (`table.*`) over a
553
+ // children index, and the role table's answers for a block's stored kind
554
+ // they read.
555
+ /** The role of `b`'s stored kind; `undefined` without roles or a record. */
556
+ const kindOf = (b) => {
557
+ const type = blocks.get(b)?.type;
558
+ return roles === null || type === undefined ? undefined : roles.of(type);
559
+ };
560
+ /** The line kind of `b` when it is an island declared `lines`. */
561
+ const lineKind = (b) => kindOf(b)?.line;
562
+ /** The item kind of `b` when it is a layout. */
563
+ const itemKind = (b) => kindOf(b)?.layout;
564
+ /** The row kind of `b` when it is a table (`table.*`). */
565
+ const rowKind = (b) => kindOf(b)?.table;
566
+ /** `table`'s row kind and that row kind's cell kind, as `[row, cell]`. */
567
+ const rowOfTable = (table) => {
568
+ const row = roles?.of(table).table;
569
+ return row === undefined ? undefined : [row, roles.of(row).child];
570
+ };
571
+ /** The cell kind of `b` when it is a row of a table kind. */
572
+ const cellKind = (b) => rowKinds.get(blocks.get(b)?.type ?? '');
573
+ /**
574
+ * The column ids of table `table` in their order, each with its
575
+ * position (`data.columns[i].id`), or `null` when it is no table or
576
+ * holds no `columns` array: its cells then show in their own order.
577
+ * Read from the record's data, memoized per data object.
578
+ */
579
+ const columnsMemo = new WeakMap();
580
+ const columnsOf = (table) => {
581
+ if (rowKind(table) === undefined)
582
+ return null;
583
+ const data = blocks.get(table)?.data;
584
+ if (data === null || typeof data !== 'object')
585
+ return null;
586
+ let index = columnsMemo.get(data);
587
+ if (index === undefined) {
588
+ const ids = listedColumns(data);
589
+ columnsMemo.set(data, (index = ids && new Map(ids.map((id, i) => [id, i]))));
590
+ }
591
+ return index;
592
+ };
593
+ /** The column a cell names (`data.column`), if any. */
594
+ const columnOf = (cell) => {
595
+ const c = blocks.get(cell)?.data?.column;
596
+ return typeof c === 'string' ? c : undefined;
597
+ };
598
+ /**
599
+ * What of `b`'s data the table rules read, as one key: a table's column
600
+ * ids, a cell's column (`''` for any other block). A change of it
601
+ * re-places the table's cells.
602
+ */
603
+ const tableFacts = (b, data) => {
604
+ const k = kindsOf.get(b);
605
+ if (data === null || typeof data !== 'object')
606
+ return '';
607
+ if (k?.table)
608
+ return JSON.stringify(listedColumns(data));
609
+ if (k?.cell) {
610
+ const c = data.column;
611
+ return typeof c === 'string' ? c : '';
612
+ }
613
+ return '';
614
+ };
615
+ /**
616
+ * The rank cell `id` displays at in row `row` (`table.columns`): its
617
+ * column's position in the table holding the row, so every row shows its
618
+ * cells in the table's column order whatever their placements (a key of
619
+ * the position, ties by block id); a withdrawn cell of a
620
+ * column the table no longer lists after them, its column's place among
621
+ * those fixed by its id (`unlistedRank`); any other cell of no listed
622
+ * column last (it does not display, `table.cell`).
623
+ * `undefined`: no table with columns holds the row — its placement rank.
624
+ */
625
+ const cellRank = (row, id) => {
626
+ const cell = cellKind(row);
627
+ if (cell === undefined || blocks.get(id)?.type !== cell)
628
+ return undefined;
629
+ const table = placementsMap.get(row)?.parent;
630
+ const index = typeof table === 'string' ? columnsOf(table) : null;
631
+ if (index === null)
632
+ return undefined;
633
+ const column = columnOf(id);
634
+ const at = column === undefined ? undefined : index.get(column);
635
+ if (at === undefined && column !== undefined && shells.has(id))
636
+ return unlistedCellKey(unlistedRank(column));
637
+ return at === undefined ? LAST_CELL : cellKey(at);
638
+ };
639
+ /**
640
+ * The table rules (`table.*`), in the same pass: a cell displays only in
641
+ * a row of a table, and, when the table lists its columns, only for a
642
+ * listed column — or, withdrawn by an undo while it holds another
643
+ * writer's text (`hist.undo.withdraw`), for the column the undo took out
644
+ * of the list — and as the first of the row's cells for it
645
+ * (`table.cell`); a row displays only in a table and while it shows a
646
+ * cell (`table.row`); a table displays while it shows a row
647
+ * (`table.empty`). What does not display hands up nothing a table rule
648
+ * shows: a cell's text goes with it.
649
+ */
650
+ const tableVisit = (kids, out, id, ctx, shown) => {
651
+ const k = kindsOf.get(id);
652
+ const childIds = (kids.get(id) ?? []).map((c) => c.id);
653
+ const pass = (sub) => {
654
+ out.add(id);
655
+ shown.push(...sub.filter((x) => !isTableish(x.id)));
656
+ };
657
+ if (k.table) {
658
+ const sub = dissolveVisit(kids, out, childIds, false, { table: id });
659
+ if (sub.some((x) => kindsOf.get(x.id)?.row))
660
+ shown.push({ id, kids: sub.map((x) => x.id) });
661
+ else
662
+ pass(sub);
663
+ }
664
+ else if (k.row) {
665
+ const table = ctx !== null && 'table' in ctx ? ctx.table : null;
666
+ const sub = dissolveVisit(kids, out, childIds, false, table === null ? null : { row: id, columns: columnsOf(table), seen: new Set() });
667
+ if (table !== null && sub.some((x) => kindsOf.get(x.id)?.cell))
668
+ shown.push({ id, kids: sub.map((x) => x.id) });
669
+ else
670
+ pass(sub);
671
+ }
672
+ else {
673
+ const sub = dissolveVisit(kids, out, childIds, false, null);
674
+ const row = ctx !== null && 'row' in ctx ? ctx : null;
675
+ let shows = row !== null;
676
+ if (row !== null && row.columns !== null) {
677
+ const column = columnOf(id);
678
+ shows =
679
+ column !== undefined &&
680
+ (row.columns.has(column) || shells.has(id)) &&
681
+ !row.seen.has(column);
682
+ if (shows)
683
+ row.seen.add(column);
684
+ }
685
+ if (shows)
686
+ shown.push({ id, kids: sub.map((x) => x.id) });
687
+ else
688
+ pass(sub);
689
+ }
690
+ };
691
+ /** `b`'s kind is a table, a table's row or a row's cell. */
692
+ const isTableish = (b) => {
693
+ const k = kindsOf.get(b);
694
+ return k !== undefined && (k.table || k.row || k.cell);
695
+ };
696
+ const dissolveVisit = (kids, out, ids, layout, ctx = null) => {
697
+ const isItem = (b) => itemKinds.has(blocks.get(b)?.type ?? '');
698
+ const shown = [];
699
+ for (const id of ids) {
700
+ if (isTableish(id)) {
701
+ tableVisit(kids, out, id, ctx, shown);
702
+ continue;
703
+ }
704
+ const own = itemKind(id) !== undefined;
705
+ const sub = dissolveVisit(kids, out, (kids.get(id) ?? []).map((k) => k.id), own);
706
+ if (own) {
707
+ if (sub.length > 1)
708
+ shown.push({ id, kids: [] });
709
+ else {
710
+ out.add(id);
711
+ for (const k of sub) {
712
+ out.add(k.id);
713
+ shown.push(...k.kids.map((kid) => ({ id: kid, kids: [] })));
714
+ }
715
+ }
716
+ }
717
+ else if (isItem(id) && (!layout || sub.length === 0)) {
718
+ out.add(id);
719
+ shown.push(...sub);
720
+ }
721
+ else
722
+ shown.push({ id, kids: sub.map((k) => k.id) });
723
+ }
724
+ return shown;
725
+ };
726
+ const dissolve = (kids) => {
727
+ const out = new Set();
728
+ dissolveVisit(kids, out, (kids.get(null) ?? []).map((k) => k.id), false);
729
+ return out;
730
+ };
731
+ /**
732
+ * Re-run the layout rules on the layouts `changed` (blocks whose slot
733
+ * in `kids0` changed, with their parents) and `kinds` reach: from each
734
+ * one up through layouts and items, the topmost such ancestor's
735
+ * subtree. Returns the blocks whose dissolved state flipped.
736
+ */
737
+ const redissolve = (changed, kinds) => {
738
+ const flips = new Set();
739
+ const isLayoutish = (b) => {
740
+ const k = kindsOf.get(b);
741
+ return k !== undefined && (k.layout || k.item || k.table || k.row || k.cell);
742
+ };
743
+ const roots = new Set();
744
+ const climb = (b) => {
745
+ let top;
746
+ for (let x = b; typeof x === 'string' && isLayoutish(x); x = slots0.get(x)?.parent)
747
+ top = x;
748
+ if (top !== undefined)
749
+ roots.add(top);
750
+ };
751
+ for (const [id, [from, to]] of changed) {
752
+ climb(id);
753
+ climb(from);
754
+ climb(to);
755
+ }
756
+ for (const id of kinds) {
757
+ // Its kind decides its own rule, its parent's count and its children's (`layout`).
758
+ climb(id);
759
+ climb(slots0.get(id)?.parent);
760
+ for (const k of kids0.get(id) ?? [])
761
+ climb(k.id);
762
+ // Only a layout, an item or a table's block dissolves: one that left those kinds shows again.
763
+ if (!isLayoutish(id) && dissolved.delete(id))
764
+ flips.add(id);
765
+ }
766
+ for (const root of roots) {
767
+ if (!slots0.has(root))
768
+ continue;
769
+ const parent = slots0.get(root).parent;
770
+ const nodes = [];
771
+ const stack = [root];
772
+ for (let x = stack.pop(); x !== undefined; x = stack.pop()) {
773
+ nodes.push(x);
774
+ for (const k of kids0.get(x) ?? [])
775
+ stack.push(k.id);
776
+ }
777
+ const out = new Set();
778
+ dissolveVisit(kids0, out, [root], parent !== null && itemKind(parent) !== undefined);
779
+ for (const x of nodes) {
780
+ if (out.has(x) === dissolved.has(x))
781
+ continue;
782
+ if (out.has(x))
783
+ dissolved.add(x);
784
+ else
785
+ dissolved.delete(x);
786
+ flips.add(x);
787
+ }
788
+ }
789
+ // A block that left the index (hidden, removed) dissolves no more.
790
+ for (const id of changed.keys())
791
+ if (!slots0.has(id) && dissolved.delete(id))
792
+ flips.add(id);
793
+ return flips;
794
+ };
795
+ // ══ Placements and the children index ═══════════════════════════
796
+ // Placements and the children index (maintained), the display
797
+ // ownership the display reads (`ownShim`), and THE shown-kind rule
798
+ // (`typeOf`).
799
+ //
800
+ // A structural change re-decides only what it can change: the display
801
+ // edge of the blocks whose slot, record or mover changed (fork patch
802
+ // YP22), and the display slot of every block whose slot walk
803
+ // (`displaySlotOf`) reads one that changed — its stored subtree, through
804
+ // the blocks it displays — patched into the parents' child lists. A
805
+ // block's candidate edges are its live move records, strongest first, then
806
+ // its home slot; it shows at the first that closes no cycle through the
807
+ // accepted edges (`settle`): the later move wins, and the block whose edge
808
+ // is the oldest of a cycle falls back to its next candidate, back where it
809
+ // stood (its previous record, else home; the root's tail only when every
810
+ // candidate closes a cycle), so a cycle resolves locally: no global pass.
811
+ // The first build places every block the same way (their unique acyclic
812
+ // acceptance, whatever the order; the tests' greedy rebuild,
813
+ // `src/tests/oracles/placements.ts`, is what the self-checks compare). A
814
+ // placed block's order key is found from its sequence neighbours
815
+ // (`placement/keys.ts`), never by walking the parent's sequence; a rebuild
816
+ // keys every list once, in its sequence's order. The layout rules re-run
817
+ // only on the layouts a change reaches.
818
+ /** Fork patch YP14's predicate: the registry values the engine keeps. */
819
+ const kept = engineOps(Y).isKeptReplaced;
820
+ // ── placements and the children index (maintained) ────────────
821
+ /** The document holds a layout or an item kind: `kids0` and `kidsMap` differ. */
822
+ let layoutMode = false;
823
+ /** The line kinds the roles declare, and those of the lines islands the document holds. */
824
+ let lineKinds = new Set();
825
+ let orderCache = null;
826
+ let layoutBlocks = 0;
827
+ let itemBlocks = 0;
828
+ /** Tables, rows and cells (`table.*`): their rules run in the layout pass. */
829
+ let tableBlocks = 0;
830
+ const lineCounts = new Map();
831
+ const ownShim = {
832
+ ownerOf,
833
+ hidden: (b) => ownerOf(b) !== b || dissolved.has(b),
834
+ childless: (b) => kindOf(b)?.childless === true,
835
+ island: (b) => kindOf(b)?.island === true,
836
+ container: (b) => kindOf(b)?.container === true,
837
+ lined: (b) => lineKind(b) !== undefined,
838
+ passes: (b) => dissolved.has(b),
839
+ sheds: (owner, child) => {
840
+ // A layout shows only its items, a table its rows, a row its cells.
841
+ const only = itemKind(owner) ?? rowKind(owner) ?? cellKind(owner);
842
+ return only !== undefined && blocks.get(child)?.type !== only;
843
+ },
844
+ slotRank: (owner, id) => cellRank(owner, id),
845
+ top,
846
+ claimIndex: (holder, m) => blocks.get(holder)?.claims.findIndex((c) => c.m === m) ?? -1,
847
+ streamOf,
848
+ streamsIn,
849
+ streamAt,
850
+ display: (b) => displayOf(b, blocks, ownShim)
851
+ };
852
+ /** Re-count `id`'s kind facts; a fact the roles never declared forces a full pass. */
853
+ const noteKind = (id) => {
854
+ const old = kindsOf.get(id);
855
+ const type = blocks.get(id)?.type;
856
+ const now = type === undefined
857
+ ? undefined
858
+ : {
859
+ layout: itemKind(id) !== undefined,
860
+ item: itemKinds.has(type),
861
+ line: lineKind(id),
862
+ table: rowKind(id) !== undefined,
863
+ row: rowKinds.has(type),
864
+ cell: cellKinds.has(type)
865
+ };
866
+ const tableish = (k) => k?.table || k?.row || k?.cell ? 1 : 0;
867
+ tableBlocks += tableish(now) - tableish(old);
868
+ if (old?.layout)
869
+ layoutBlocks--;
870
+ if (old?.item)
871
+ itemBlocks--;
872
+ if (old?.line !== undefined)
873
+ lineCounts.set(old.line, lineCounts.get(old.line) - 1);
874
+ if (now === undefined)
875
+ kindsOf.delete(id);
876
+ else
877
+ kindsOf.set(id, now);
878
+ if (now?.layout)
879
+ layoutBlocks++;
880
+ if (now?.item)
881
+ itemBlocks++;
882
+ if (now?.line !== undefined)
883
+ lineCounts.set(now.line, (lineCounts.get(now.line) ?? 0) + 1);
884
+ const item = itemKind(id);
885
+ if (item !== undefined && !itemKinds.has(item))
886
+ placementFull = true;
887
+ if (now?.line !== undefined && !lineKinds.has(now.line))
888
+ placementFull = true;
889
+ // A table of a kind whose row the index does not know yet: its rows and cells change kind facts.
890
+ const rows = type === undefined ? undefined : rowOfTable(type);
891
+ if (rows !== undefined && rowKinds.get(rows[0]) !== rows[1])
892
+ placementFull = true;
893
+ if (layoutMode !== layoutBlocks + itemBlocks + tableBlocks > 0)
894
+ placementFull = true;
895
+ };
896
+ /** The ownership the layout rules read: none of their decisions applied. */
897
+ const own0 = {
898
+ ...ownShim,
899
+ hidden: (b) => ownerOf(b) !== b,
900
+ passes: () => false
901
+ };
902
+ /** `id`'s slot in the index `own` reads (none: hidden, or under no live parent). */
903
+ const slotIn = (own, id) => {
904
+ const pl = placementsMap.get(id);
905
+ if (pl === undefined || !blocks.has(id) || own.hidden(id))
906
+ return undefined;
907
+ const { parent, rank, reset } = displaySlotOf(own, placementsMap, pl, id);
908
+ if (parent === DEAD)
909
+ return undefined;
910
+ return reset === null ? { parent, rank } : { parent, rank, reset };
911
+ };
912
+ /** The index of the first slot of `list` not before `x`. */
913
+ const seek = (list, x) => {
914
+ let lo = 0;
915
+ let hi = list.length;
916
+ while (lo < hi) {
917
+ const mid = (lo + hi) >> 1;
918
+ if (bySlot(list[mid], x) < 0)
919
+ lo = mid + 1;
920
+ else
921
+ hi = mid;
922
+ }
923
+ return lo;
924
+ };
925
+ /**
926
+ * Re-decide the slots of `affected` in `kids`/`slotMap` (read through
927
+ * `own`), patching the child lists they leave or join (each list
928
+ * copied once: a list a reader holds never changes under it). Returns
929
+ * the blocks whose slot changed, with their parents before and after.
930
+ */
931
+ const patch = (kids, slotMap, own, affected) => {
932
+ const changed = new Map();
933
+ const copies = new Map();
934
+ const list = (p) => {
935
+ return getOr(copies, p, () => [...(kids.get(p) ?? [])]);
936
+ };
937
+ for (const id of affected) {
938
+ const old = slotMap.get(id);
939
+ const next = slotIn(own, id);
940
+ if (old?.parent === next?.parent &&
941
+ old?.rank === next?.rank &&
942
+ old?.reset === next?.reset &&
943
+ (old === undefined) === (next === undefined))
944
+ continue;
945
+ if (old !== undefined) {
946
+ const l = list(old.parent);
947
+ const at = seek(l, { id, rank: old.rank });
948
+ if (l[at]?.id === id)
949
+ l.splice(at, 1);
950
+ }
951
+ if (next !== undefined) {
952
+ const l = list(next.parent);
953
+ const slot = next.reset === undefined
954
+ ? { id, rank: next.rank }
955
+ : { id, rank: next.rank, reset: next.reset };
956
+ l.splice(seek(l, slot), 0, slot);
957
+ slotMap.set(id, next);
958
+ }
959
+ else {
960
+ slotMap.delete(id);
961
+ if (kids === kidsMap)
962
+ leftLists.add(id);
963
+ }
964
+ changed.set(id, [old?.parent, next?.parent]);
965
+ }
966
+ for (const [p, l] of copies) {
967
+ if (l.length === 0)
968
+ kids.delete(p);
969
+ else
970
+ kids.set(p, l);
971
+ if (kids === kidsMap)
972
+ dirtyLists.add(p);
973
+ }
974
+ return changed;
975
+ };
976
+ /** `seeds` and every block whose slot walk reads one of them: stored subtrees, through what each displays. */
977
+ const reach = (seeds) => {
978
+ const out = new Set();
979
+ const stack = [...seeds];
980
+ for (let x = stack.pop(); x !== undefined; x = stack.pop()) {
981
+ if (out.has(x))
982
+ continue;
983
+ out.add(x);
984
+ for (const c of rawOrder.get(x) ?? [])
985
+ stack.push(c.id);
986
+ for (const d of displaysMap.get(x) ?? [])
987
+ if (d !== x)
988
+ for (const c of rawOrder.get(d) ?? [])
989
+ stack.push(c.id);
990
+ }
991
+ return out;
992
+ };
993
+ /** Whether `id` shows as its slot's kind (`following`): read from its slot and kinds. */
994
+ const noteFollowing = (id) => {
995
+ const slot = slots.get(id);
996
+ const follows = slot !== undefined &&
997
+ (slot.reset !== undefined ||
998
+ (slot.parent !== null && lineKind(slot.parent) !== undefined) ||
999
+ lineKinds.has(blocks.get(id)?.type ?? ''));
1000
+ if (follows)
1001
+ following.add(id);
1002
+ else
1003
+ following.delete(id);
1004
+ };
1005
+ // ── edges, keys and acceptance ─────────────────────────────────
1006
+ /**
1007
+ * Whether the slot map has been read from the sequences: once, before the
1008
+ * first rebuild (a shell settled on the first fold asks for a home), then
1009
+ * the fold keeps it and every rebuild fills it again.
1010
+ */
1011
+ let slotsRead = false;
1012
+ /** What the first read collected, for a rebuild while no sequence changed since (one scan). */
1013
+ let collected = null;
1014
+ let collectedAt = -1;
1015
+ const readSlots = () => {
1016
+ slotsRead = true;
1017
+ slotsByBlock.clear();
1018
+ // Every registry node, its kept incarnations too (the records may not all be built yet).
1019
+ collected = collectSlots(doc, registryOwners(registry, kept));
1020
+ collectedAt = seqEpoch;
1021
+ for (const [b, refs] of collected.slots)
1022
+ slotsByBlock.set(b, refs);
1023
+ };
1024
+ /** Put `ref` (`null`: none) where the slot map holds unit `u` of the slots naming id `b`. */
1025
+ const putSlot = (b, u, ref) => {
1026
+ const list = (slotsByBlock.get(b) ?? []).filter((x) => !sameUnit(x, u));
1027
+ if (ref !== null)
1028
+ list.push(ref);
1029
+ if (list.length === 0)
1030
+ slotsByBlock.delete(b);
1031
+ else
1032
+ slotsByBlock.set(b, list);
1033
+ };
1034
+ /** `id`'s home slot among the slots naming its registry id (`null`: none). */
1035
+ const homeFor = (id) => {
1036
+ if (!slotsRead)
1037
+ readSlots();
1038
+ return homeOf(registry, kept, slotsByBlock, id);
1039
+ };
1040
+ /**
1041
+ * Re-own the home slots of `node`'s sequence after the registry value it
1042
+ * stands for changed (a concurrent creation of one id won or lost, its
1043
+ * losing incarnation shown or not): each slot's parent is the node's owner
1044
+ * now, and its block re-placed; a node that is no block of the document
1045
+ * any more holds no slot (as a rebuild reads it).
1046
+ */
1047
+ const reslot = (node) => {
1048
+ if (!slotsRead)
1049
+ return; // the first read collects them as they are
1050
+ const owner = ownerOfNode(registry, kept, node);
1051
+ for (let it = firstItem(node); it !== null; it = it.right)
1052
+ for (const { value, unit, via } of positionsOfItem(doc, it)) {
1053
+ if (via !== null)
1054
+ continue; // a record's unit is another sequence's slot
1055
+ const b = value.b;
1056
+ putSlot(b, unit, owner === undefined ? null : { parent: owner, ...unit });
1057
+ for (const id of new Set([b, slotOwner(registry, kept, b, unit.c)])) {
1058
+ const rec = blocks.get(id);
1059
+ if (rec !== undefined)
1060
+ rec.home = homeOf(registry, kept, slotsByBlock, id);
1061
+ placementSeeds.add(id);
1062
+ }
1063
+ }
1064
+ };
1065
+ /** The slot items a rebuild collected (its homes' items, read once), while it places every block. */
1066
+ let known = null;
1067
+ /** `id`'s candidate edges, read from the engine: its live records, strongest first, then home (`[]`: no slot, or gone). */
1068
+ const candidatesFor = (id) => {
1069
+ const home = blocks.get(id)?.home ?? homeFor(id);
1070
+ return home === null ? [] : candidatesOf(doc, registry, kept, home, known?.get(home));
1071
+ };
1072
+ /** The owner of the sequence `id`'s slot shows in as the engine stands (its mover's, else home's; `null`: the root). */
1073
+ const edgeParent = (id) => {
1074
+ const home = blocks.get(id)?.home ?? homeFor(id);
1075
+ return home === null ? undefined : edgeParentOf(doc, registry, kept, home);
1076
+ };
1077
+ /** The raw children of `parent` in order (every block whose edge parent it is), kept sorted by key. */
1078
+ const rawList = (parent) => {
1079
+ return getOr(rawOrder, parent, () => []);
1080
+ };
1081
+ /** Counts the placement writes: a key's walk that saw one happen under it walks again. */
1082
+ let placedEpoch = 0;
1083
+ /** Whether a placement is keyed as it is decided: not while a rebuild places every block (it keys them after, in one pass). */
1084
+ let keyed = true;
1085
+ const setPlacement = (id, pl) => {
1086
+ foldStats.placed++;
1087
+ placedEpoch++;
1088
+ const old = placementsMap.get(id);
1089
+ if (keyed && old !== undefined) {
1090
+ const l = rawList(old.parent);
1091
+ const at = seek(l, { id, rank: old.rank });
1092
+ if (l[at]?.id === id)
1093
+ l.splice(at, 1);
1094
+ if (l.length === 0)
1095
+ rawOrder.delete(old.parent);
1096
+ }
1097
+ if (pl === undefined)
1098
+ placementsMap.delete(id);
1099
+ else {
1100
+ placementsMap.set(id, pl);
1101
+ if (!keyed)
1102
+ return;
1103
+ const l = rawList(pl.parent);
1104
+ const slot = { id, rank: pl.rank };
1105
+ l.splice(seek(l, slot), 0, slot);
1106
+ }
1107
+ };
1108
+ /**
1109
+ * The key `id` takes under `edge.parent`: between the key of the nearest
1110
+ * block shown where it stands before `id`'s place in the sequence (placed
1111
+ * first when it is a pending seed; walked past when it shows elsewhere)
1112
+ * and the next key of that list. Placing a pending neighbour can drop a
1113
+ * block the walk already passed back into that stretch (a cycle's
1114
+ * fallback, back where it stood): a walk under which a placement changed
1115
+ * walks again, so the key it returns reads one state.
1116
+ */
1117
+ const keyFor = (id, edge, place) => {
1118
+ for (;;) {
1119
+ const before = placedEpoch;
1120
+ const lrank = leftKey(id, edge, place);
1121
+ if (placedEpoch !== before)
1122
+ continue;
1123
+ const list = rawOrder.get(edge.parent) ?? [];
1124
+ const i = lrank === undefined ? 0 : seek(list, { id: '', rank: lrank }) + 1;
1125
+ return keyBetween(lrank, list[i]?.rank);
1126
+ }
1127
+ };
1128
+ /** The key of the nearest block shown before `id`'s place at `edge`, under its parent (`undefined`: none). */
1129
+ const leftKey = (id, edge, place) => {
1130
+ const home = blocks.get(id)?.home ?? homeFor(id);
1131
+ for (const e of slotsBefore(doc, edge, home)) {
1132
+ const b = slotOwner(registry, kept, e.value.b, e.unit.c);
1133
+ // `id` itself (its place before this one) keys nothing.
1134
+ if (b === id)
1135
+ continue;
1136
+ // A slot that is not its block's home (a second creation of one id) places nothing.
1137
+ const h = blocks.get(b)?.home ?? homeFor(b);
1138
+ if (h === null || !sameUnit(h, e.unit))
1139
+ continue;
1140
+ place(b);
1141
+ const pl = placementsMap.get(b);
1142
+ const shows = edges.get(b);
1143
+ if (pl !== undefined &&
1144
+ pl.parent === edge.parent &&
1145
+ !rejected.has(b) &&
1146
+ shows != null &&
1147
+ shows.via === e.via)
1148
+ return pl.rank;
1149
+ }
1150
+ return undefined;
1151
+ };
1152
+ /** The key of `id` at the root's tail, among the rejected by stamp then id, after the last top-level block. */
1153
+ const tailKey = (id, stamp) => {
1154
+ const tail = [...rejected]
1155
+ .filter((x) => x !== id)
1156
+ .map((x) => ({ id: x, stamp: edges.get(x)?.stamp ?? NO_SLOT }))
1157
+ .sort(byTail);
1158
+ const list = rawOrder.get(null) ?? [];
1159
+ const me = { id, stamp };
1160
+ let at = 0;
1161
+ while (at < tail.length && byTail(tail[at], me) < 0)
1162
+ at++;
1163
+ const prev = at > 0
1164
+ ? placementsMap.get(tail[at - 1].id)?.rank
1165
+ : list[list.length - tail.length - 1]?.rank;
1166
+ const next = at < tail.length ? placementsMap.get(tail[at].id)?.rank : undefined;
1167
+ return keyBetween(prev, next);
1168
+ };
1169
+ /** Show `id` at the root's tail: every edge closes a cycle, or none. */
1170
+ const reject = (id, edge) => {
1171
+ edges.set(id, edge);
1172
+ if (placementsMap.has(id))
1173
+ setPlacement(id, undefined);
1174
+ fallen.delete(id);
1175
+ rejected.add(id);
1176
+ const rank = keyed ? tailKey(id, edge?.stamp ?? NO_SLOT) : '';
1177
+ setPlacement(id, { parent: null, rank });
1178
+ };
1179
+ /** The same display edge: the same sequence, the same record, the same stamp. */
1180
+ const sameEdge = (a, b) => a.parent === b.parent && a.via === b.via && cmpSlotStamp(a.stamp, b.stamp) === 0;
1181
+ /** Whether `edge` (of `id`) is a place `id` may take now: a live parent, and no cycle it would be the oldest edge of. */
1182
+ const admits = (id, edge) => {
1183
+ const parent = edge.parent;
1184
+ if (parent === undefined || (parent !== null && !blocks.has(parent)))
1185
+ return { ok: false, drop: null };
1186
+ const cycle = cycleThrough(placementsMap, ownerOf, id, parent);
1187
+ if (cycle === null)
1188
+ return { ok: true, drop: null };
1189
+ let [min, minStamp] = [id, edge.stamp];
1190
+ for (const m of cycle) {
1191
+ const e = m === id ? edge : edges.get(m);
1192
+ if (m !== id && e && cmpSlotStamp(e.stamp, minStamp) < 0)
1193
+ [min, minStamp] = [m, e.stamp];
1194
+ }
1195
+ return min === id ? { ok: false, drop: null } : { ok: true, drop: min };
1196
+ };
1197
+ /**
1198
+ * Place `id` at its strongest candidate edge from `from` on that closes no
1199
+ * cycle it would be the oldest edge of (the later move wins). When the
1200
+ * edge closes a cycle through an older edge, that edge's block is dropped
1201
+ * to its next candidate (back where it stood before that move), and so on
1202
+ * down its records to home; a block every candidate of which fails shows
1203
+ * at the root's tail. `place` places a pending neighbour first. Returns
1204
+ * the blocks whose placement changed.
1205
+ */
1206
+ const settle = (id, place, from = 0) => {
1207
+ const changed = [id];
1208
+ const cands = candidatesFor(id);
1209
+ for (let n = from; n < cands.length; n++) {
1210
+ const edge = cands[n];
1211
+ // The key first: its walk places the pending neighbours, so the cycle
1212
+ // check reads them placed (a neighbour placed after it could close a
1213
+ // cycle through `id` that no check saw).
1214
+ const rank = keyed ? keyFor(id, edge, place) : '';
1215
+ const { ok, drop } = admits(id, edge);
1216
+ if (!ok)
1217
+ continue;
1218
+ edges.set(id, edge);
1219
+ rejected.delete(id);
1220
+ if (n > 0)
1221
+ fallen.add(id);
1222
+ else
1223
+ fallen.delete(id);
1224
+ if (placementsMap.has(id))
1225
+ setPlacement(id, undefined);
1226
+ setPlacement(id, { parent: edge.parent, rank });
1227
+ if (drop !== null) {
1228
+ const now = edges.get(drop);
1229
+ const at = now ? candidatesFor(drop).findIndex((c) => sameEdge(c, now)) : -1;
1230
+ changed.push(...settle(drop, () => { }, at + 1));
1231
+ }
1232
+ return changed;
1233
+ }
1234
+ reject(id, cands[0] ?? null);
1235
+ return changed;
1236
+ };
1237
+ /** Whether a candidate stronger than `id`'s accepted edge would take it now. */
1238
+ const canRise = (id) => {
1239
+ const cands = candidatesFor(id);
1240
+ const now = edges.get(id);
1241
+ const at = rejected.has(id) || !now ? cands.length : cands.findIndex((c) => sameEdge(c, now));
1242
+ for (let n = 0; n < (at < 0 ? cands.length : at); n++)
1243
+ if (admits(id, cands[n]).ok)
1244
+ return true;
1245
+ return false;
1246
+ };
1247
+ /**
1248
+ * Try every block below its strongest edge again (a changed edge may have
1249
+ * broken its cycle, or made another the one to drop): those that rise join
1250
+ * `moved`.
1251
+ */
1252
+ const retryFallen = (moved) => {
1253
+ for (let round = 0; round < fallen.size + rejected.size + 2; round++) {
1254
+ let changed = false;
1255
+ const below = [...rejected, ...fallen]
1256
+ .map((x) => ({ id: x, stamp: candidatesFor(x)[0]?.stamp ?? NO_SLOT }))
1257
+ .sort((a, b) => -byTail(a, b));
1258
+ for (const { id } of below) {
1259
+ if (!canRise(id))
1260
+ continue;
1261
+ for (const x of settle(id, () => { }))
1262
+ moved.add(x);
1263
+ changed = true;
1264
+ }
1265
+ if (!changed)
1266
+ break;
1267
+ }
1268
+ };
1269
+ /**
1270
+ * Everything rebuilt from the sequences (the first build, roles): every
1271
+ * block placed as an edit places one (`settle`, then `retryFallen`) —
1272
+ * the one acyclic acceptance, whatever the order the blocks come in —
1273
+ * then keyed in one pass over each sequence's places (dense keys in its
1274
+ * order), the root's tail after the last top-level block.
1275
+ */
1276
+ const rebuildPlacements = () => {
1277
+ placementFull = false;
1278
+ placementSeeds.clear();
1279
+ stateSeeds.clear();
1280
+ ownerChanged.clear();
1281
+ const declared = roles?.declared();
1282
+ itemKinds = new Set(declared?.items);
1283
+ for (const b of blocks.keys()) {
1284
+ const item = itemKind(b);
1285
+ if (item !== undefined)
1286
+ itemKinds.add(item);
1287
+ }
1288
+ lineKinds = new Set(declared?.lines);
1289
+ rowKinds = new Map(declared?.rows);
1290
+ for (const b of blocks.keys()) {
1291
+ const type = blocks.get(b)?.type;
1292
+ const rows = type === undefined ? undefined : rowOfTable(type);
1293
+ if (rows !== undefined && rowKind(b) !== undefined)
1294
+ rowKinds.set(rows[0], rows[1]);
1295
+ }
1296
+ cellKinds = new Set(rowKinds.values());
1297
+ kindsOf.clear();
1298
+ [layoutBlocks, itemBlocks, tableBlocks] = [0, 0, 0];
1299
+ lineCounts.clear();
1300
+ for (const b of blocks.keys())
1301
+ noteKind(b);
1302
+ for (const line of lineCounts.keys())
1303
+ lineKinds.add(line);
1304
+ placementFull = false;
1305
+ // The first read's slots while no sequence changed since (one scan).
1306
+ const { slots: found, items, places } = collected !== null && collectedAt === seqEpoch
1307
+ ? collected
1308
+ : collectSlots(doc, blocks.values());
1309
+ collected = null;
1310
+ slotsByBlock.clear();
1311
+ for (const [b, refs] of found)
1312
+ slotsByBlock.set(b, refs);
1313
+ slotsRead = true;
1314
+ placementsMap.clear();
1315
+ edges.clear();
1316
+ rawOrder.clear();
1317
+ fallen.clear();
1318
+ rejected.clear();
1319
+ [keyed, known] = [false, items];
1320
+ for (const id of blocks.keys())
1321
+ settle(id, () => { });
1322
+ if (fallen.size + rejected.size > 0)
1323
+ retryFallen(new Set());
1324
+ [keyed, known] = [true, null];
1325
+ // The lists in key order: a block where its edge shows it, in its sequence's order.
1326
+ for (const [owner, list] of places)
1327
+ for (const e of list) {
1328
+ const b = slotOwner(registry, kept, e.value.b, e.unit.c);
1329
+ const pl = placementsMap.get(b);
1330
+ const h = blocks.get(b)?.home;
1331
+ if (pl?.parent !== owner || rejected.has(b) || !h || !sameUnit(h, e.unit))
1332
+ continue;
1333
+ if (edges.get(b)?.via !== e.via)
1334
+ continue;
1335
+ const l = rawList(owner);
1336
+ pl.rank = denseKey(l.length);
1337
+ l.push({ id: b, rank: pl.rank });
1338
+ }
1339
+ // The root's tail, keyed as an edit keys a rejected block, in its order.
1340
+ const tail = [...rejected].map((x) => ({ id: x, stamp: edges.get(x)?.stamp ?? NO_SLOT }));
1341
+ rejected.clear();
1342
+ for (const { id } of tail.sort(byTail))
1343
+ reject(id, edges.get(id) ?? null);
1344
+ layoutMode = layoutBlocks + itemBlocks + tableBlocks > 0;
1345
+ dissolved = new Set();
1346
+ const index = (map) => {
1347
+ const at = new Map();
1348
+ for (const [parent, list] of map)
1349
+ for (const { id, rank, reset } of list)
1350
+ at.set(id, reset === undefined ? { parent, rank } : { parent, rank, reset });
1351
+ return at;
1352
+ };
1353
+ kids0 = childrenIndex(placementsMap, own0);
1354
+ slots0 = index(kids0);
1355
+ if (layoutMode) {
1356
+ dissolved = dissolve(kids0);
1357
+ kidsMap = childrenIndex(placementsMap, ownShim);
1358
+ slots = index(kidsMap);
1359
+ }
1360
+ else
1361
+ [kidsMap, slots] = [kids0, slots0];
1362
+ following.clear();
1363
+ for (const id of slots.keys())
1364
+ noteFollowing(id);
1365
+ orderCache = null;
1366
+ kidsVersion++;
1367
+ allListsDirty = true;
1368
+ };
1369
+ const ensurePlacements = () => {
1370
+ ensureOwners();
1371
+ if (placementFull)
1372
+ return rebuildPlacements();
1373
+ if (placementSeeds.size + stateSeeds.size + ownerChanged.size === 0)
1374
+ return;
1375
+ const moved = new Set();
1376
+ const seeds = new Set(placementSeeds);
1377
+ const done = new Set();
1378
+ /** Place one seed (its left neighbour first, when it is a seed too). */
1379
+ const place = (id) => {
1380
+ if (done.has(id) || !seeds.has(id))
1381
+ return;
1382
+ done.add(id);
1383
+ if (!blocks.has(id)) {
1384
+ if (placementsMap.has(id)) {
1385
+ setPlacement(id, undefined);
1386
+ edges.delete(id);
1387
+ fallen.delete(id);
1388
+ rejected.delete(id);
1389
+ moved.add(id);
1390
+ }
1391
+ return;
1392
+ }
1393
+ const top = candidatesFor(id)[0] ?? null;
1394
+ const old = edges.get(id);
1395
+ // The same edge, placed at its strongest: its key still orders it (its neighbours' keys are theirs).
1396
+ if (top !== null &&
1397
+ old &&
1398
+ !rejected.has(id) &&
1399
+ !fallen.has(id) &&
1400
+ placementsMap.has(id) &&
1401
+ sameEdge(top, old))
1402
+ return;
1403
+ for (const x of settle(id, place))
1404
+ moved.add(x);
1405
+ };
1406
+ for (const id of [...seeds])
1407
+ place(id);
1408
+ // A changed claim owner can close a cycle through a display edge that did not change.
1409
+ for (const o of ownerChanged)
1410
+ for (const c of [...(rawOrder.get(o) ?? [])]) {
1411
+ const edge = edges.get(c.id);
1412
+ if (!edge || rejected.has(c.id) || edge.parent === undefined)
1413
+ continue;
1414
+ if (cycleThrough(placementsMap, ownerOf, c.id, edge.parent) !== null)
1415
+ for (const x of settle(c.id, () => { }))
1416
+ moved.add(x);
1417
+ }
1418
+ if (rejected.size + fallen.size > 0 && (moved.size > 0 || ownerChanged.size > 0))
1419
+ retryFallen(moved);
1420
+ const seeds2 = new Set([...moved, ...stateSeeds, ...ownerChanged]);
1421
+ const kinds = [...stateSeeds];
1422
+ placementSeeds.clear();
1423
+ stateSeeds.clear();
1424
+ ownerChanged.clear();
1425
+ const affected = reach(seeds2);
1426
+ let touched = affected;
1427
+ const changed = patch(kids0, slots0, own0, affected);
1428
+ let any = changed.size > 0;
1429
+ if (layoutMode) {
1430
+ const flips = redissolve(changed, kinds);
1431
+ const again = flips.size > 0 ? reach([...affected, ...flips]) : affected;
1432
+ any = patch(kidsMap, slots, ownShim, again).size > 0 || any;
1433
+ touched = again;
1434
+ }
1435
+ for (const id of touched)
1436
+ noteFollowing(id);
1437
+ if (any) {
1438
+ orderCache = null;
1439
+ kidsVersion++;
1440
+ }
1441
+ };
1442
+ /**
1443
+ * A plain block (the document's default kind) directly in a container
1444
+ * whose item is a kind of its own that renders content (a list) shows
1445
+ * as that item — the read-time side of the facade's `fitted`: a race
1446
+ * (a peer's lift of an item another peer's outdent moves, a concurrent
1447
+ * undo of a retype) or an explicit write never shows a bare paragraph
1448
+ * in a list. Anywhere else it shows `plain`.
1449
+ */
1450
+ const shownItem = (under, plain) => {
1451
+ const parent = typeOf(under);
1452
+ const { container, child } = roles.of(parent);
1453
+ return container && roles.of(child).rendersContent ? child : plain;
1454
+ };
1455
+ /**
1456
+ * The kind `id` displays as — THE shown-kind rule, read from its slot
1457
+ * and the role table. A block directly in an island declared `lines`
1458
+ * shows its line kind. A block of a line kind anywhere else shows its
1459
+ * display parent's default child — an undo can put a line a peer
1460
+ * retyped back in its island, or leave one a peer moved away outside
1461
+ * it — and so does a block displayed out of an island
1462
+ * or a container (a list) that still has its default child
1463
+ * kind, unless an outer container shows it as one of its
1464
+ * items (a nested list's item). A block that renders
1465
+ * content never shows as a kind that does not: a line kind
1466
+ * then shows the document's default kind. A block stored as the
1467
+ * document's default kind directly in a list shows as its item
1468
+ * (`shownItem`). Any other shows its stored kind (a retype shows). Read from the stored kinds
1469
+ * at call time: a retype rebuilds no placement.
1470
+ */
1471
+ const typeOf = (id) => {
1472
+ const stored = blocks.get(id)?.type ?? 'unknown';
1473
+ const slot = slots.get(id);
1474
+ if (roles === null || slot === undefined)
1475
+ return stored;
1476
+ const { parent: under, reset } = slot;
1477
+ const line = under === null || lineKinds.size === 0 ? undefined : lineKind(under);
1478
+ if (line !== undefined)
1479
+ return line;
1480
+ const lined = lineKinds.has(stored);
1481
+ const from = reset === undefined ? undefined : (blocks.get(reset)?.type ?? null);
1482
+ const plain = roles.of(null).child;
1483
+ if (!lined && (from === undefined || stored !== roles.of(from).child))
1484
+ return stored === plain && under !== null ? shownItem(under, stored) : stored;
1485
+ // Out of a removed list, inside an outer list of its kind: still an item. A
1486
+ // container whose item is the document's default kind (a column) holds no
1487
+ // items of its own: a paragraph under one is no list's item.
1488
+ if (!lined && typeof from === 'string' && roles.of(from).container)
1489
+ for (let u = under; u !== null; u = slots.get(u)?.parent ?? null) {
1490
+ const t = roles.of(typeOf(u));
1491
+ if (t.container && t.child === stored && stored !== plain)
1492
+ return stored;
1493
+ }
1494
+ const kind = roles.of(under === null ? null : typeOf(under)).child;
1495
+ if (roles.of(kind).rendersContent || !roles.of(stored).rendersContent)
1496
+ return kind;
1497
+ // Never a line kind outside its island, even where the slot's kind shows nothing.
1498
+ return lined ? plain : stored;
1499
+ };
1500
+ /** The document order, cached until a child list changes. */
1501
+ const order = () => (orderCache ??= documentOrder(kidsMap));
1502
+ // ══ Block records ═══════════════════════════════════════════════
1503
+ // Block records: each registry entry's record (and the losing
1504
+ // incarnations a key shows), rebuilt on a structural change, noting what
1505
+ // the maintained facts must re-decide.
1506
+ // ── records ──────────────────────────────────────────────────────
1507
+ /** The node of `id`: its registry value, or the losing incarnation a derived id names. */
1508
+ const nodeAt = (id) => {
1509
+ const v = registry.getAttr(id);
1510
+ if (isNodeLike(v) || !isIncarnationId(id))
1511
+ return v;
1512
+ return incarnationNode(registry, id, kept) ?? undefined;
1513
+ };
1514
+ const buildRec = (id, node) => {
1515
+ const content = node.getAttr(CONTENT);
1516
+ const derived = isIncarnationId(id);
1517
+ // A losing incarnation lives and dies with its key's block: hidden by its
1518
+ // own delete mark or while that block is deleted (a withdrawn one included).
1519
+ const keyDeleted = derived ? (blocks.get(baseIdOf(id))?.deleted ?? true) : false;
1520
+ // The implicit claims stamp below every written one: an explicit claim
1521
+ // (a split moving them to its tail block) outranks them.
1522
+ const implicit = derived
1523
+ ? []
1524
+ : incarnationsOf(registry, id, kept).map((x, i) => ({
1525
+ m: x.id,
1526
+ stamp: { c: -1, k: -1 - i }
1527
+ }));
1528
+ return {
1529
+ id,
1530
+ node,
1531
+ type: typeAttr(node),
1532
+ data: readData(node),
1533
+ n: node.getAttr(NONCE),
1534
+ deleted: hasDeleteMark(node) || keyDeleted,
1535
+ content: isNodeLike(content) ? content : undefined,
1536
+ implicit,
1537
+ // Read from its stream once the record is in (`claimsOf`), again after the fold's texts.
1538
+ claims: [],
1539
+ home: homeFor(id)
1540
+ };
1541
+ };
1542
+ /**
1543
+ * Rebuild (or create, or drop) block `id`'s record, and note what the
1544
+ * maintained facts must re-decide: its owner and the tops of the
1545
+ * blocks it claims, its placement — and, when its entry came or
1546
+ * went, the placements of the blocks whose display edge it holds.
1547
+ */
1548
+ const updateBlockRec = (id) => {
1549
+ const old = blocks.get(id);
1550
+ const node = nodeAt(id);
1551
+ if (!isNodeLike(node))
1552
+ blocks.delete(id);
1553
+ else {
1554
+ const rec = buildRec(id, node);
1555
+ blocks.set(id, rec);
1556
+ rec.claims = claimsOf(id);
1557
+ reclaim.add(id);
1558
+ }
1559
+ const rec = blocks.get(id);
1560
+ // Another node now stands for `id` (or none): its sequence's slots change owner.
1561
+ if (old?.node !== rec?.node) {
1562
+ if (old !== undefined)
1563
+ reslot(old.node);
1564
+ if (rec !== undefined)
1565
+ reslot(rec.node);
1566
+ }
1567
+ noteShell(id);
1568
+ noteClaims(id, old?.claims ?? [], rec?.claims ?? []);
1569
+ placementSeeds.add(id);
1570
+ if ((old === undefined) !== (rec === undefined)) {
1571
+ stateSeeds.add(id);
1572
+ for (const c of rawOrder.get(id) ?? [])
1573
+ placementSeeds.add(c.id);
1574
+ }
1575
+ if (old?.type !== rec?.type) {
1576
+ noteKind(id);
1577
+ if (old === undefined ||
1578
+ rec === undefined ||
1579
+ roles === null ||
1580
+ !sameShape(roles, old.type, rec.type))
1581
+ stateSeeds.add(id);
1582
+ }
1583
+ };
1584
+ /** A withdrawn block without a delete mark: its `deleted` is settled after each fold. */
1585
+ const noteShell = (id) => {
1586
+ const rec = blocks.get(id);
1587
+ const was = shells.has(id);
1588
+ // A losing incarnation is never withdrawn on its own: its key's block decides.
1589
+ if (isIncarnationId(id))
1590
+ shells.delete(id);
1591
+ else if (rec !== undefined && !rec.deleted && hasWithdrawMark(rec.node))
1592
+ shells.add(id);
1593
+ else
1594
+ shells.delete(id);
1595
+ // A withdrawn cell shows in a column its table no longer lists (`table.cell`): re-place it.
1596
+ if (was !== shells.has(id) && rec !== undefined && cellKinds.has(rec.type))
1597
+ stateSeeds.add(id);
1598
+ };
1599
+ const ensureRec = (id) => {
1600
+ if (!blocks.has(id))
1601
+ updateBlockRec(id);
1602
+ };
1603
+ /**
1604
+ * Re-read the losing incarnations of key `id` after its record
1605
+ * changed: each one shown now or before gets its record rebuilt (its
1606
+ * liveness is the key's); returns them, for the fold to rescan.
1607
+ */
1608
+ const syncIncarnations = (id) => {
1609
+ if (isIncarnationId(id))
1610
+ return [];
1611
+ const before = incarnations.get(id) ?? [];
1612
+ const now = incarnationsOf(registry, id, kept).map((x) => x.id);
1613
+ if (now.length === 0)
1614
+ incarnations.delete(id);
1615
+ else
1616
+ incarnations.set(id, now);
1617
+ const all = [...new Set([...before, ...now])];
1618
+ for (const v of all)
1619
+ updateBlockRec(v);
1620
+ return all;
1621
+ };
1622
+ // ══ The run cache ═══════════════════════════════════════════════
1623
+ // The run cache: each block's visible runs as a frozen snapshot, recomputed
1624
+ // when a fold invalidates it, sharing unchanged runs and interned payloads.
1625
+ const rangeStats = { items: 0, markers: 0 };
1626
+ // ── run cache ────────────────────────────────────────────────────
1627
+ const internMap = new Map();
1628
+ const debug = {
1629
+ recomputes: 0,
1630
+ recomputed: new Set(),
1631
+ get itemsWalked() {
1632
+ return rangeStats.items;
1633
+ },
1634
+ get markersWalked() {
1635
+ return rangeStats.markers;
1636
+ },
1637
+ get frames() {
1638
+ return frames.size;
1639
+ },
1640
+ get folds() {
1641
+ return foldStats.folds;
1642
+ },
1643
+ get foldedPairs() {
1644
+ return foldStats.pairs;
1645
+ },
1646
+ get foldedStructs() {
1647
+ return foldStats.structs;
1648
+ },
1649
+ get placements() {
1650
+ return foldStats.placed;
1651
+ },
1652
+ reset() {
1653
+ debug.recomputes = 0;
1654
+ debug.recomputed.clear();
1655
+ foldStats.folds = 0;
1656
+ foldStats.pairs = 0;
1657
+ foldStats.structs = 0;
1658
+ foldStats.placed = 0;
1659
+ rangeStats.items = 0;
1660
+ rangeStats.markers = 0;
1661
+ }
1662
+ };
1663
+ // ── interning / freezing ─────────────────────────────────────────
1664
+ // `cloneJsonSafe` is total even against hostile replicated payloads,
1665
+ // and interning the normalized form keys it by its own canonical shape.
1666
+ // The shared instance is built FROM that key (sorted keys): it must not
1667
+ // remember the key order its first reader happened to fold, or
1668
+ // `toJSON` would depend on when the view was read.
1669
+ const intern = (v) => {
1670
+ const key = canonKey(cloneJsonSafe(v));
1671
+ let f = internMap.get(key);
1672
+ if (f === undefined)
1673
+ internMap.set(key, (f = deepFreeze(JSON.parse(key))));
1674
+ return f;
1675
+ };
1676
+ // ── dep bookkeeping ──────────────────────────────────────────────
1677
+ const drop = (index, keys, b) => {
1678
+ for (const k of keys) {
1679
+ const set = index.get(k);
1680
+ set?.delete(b);
1681
+ if (set?.size === 0)
1682
+ index.delete(k);
1683
+ }
1684
+ };
1685
+ const add = (index, keys, b) => {
1686
+ for (const k of keys) {
1687
+ const set = getOr(index, k, () => new Set());
1688
+ set.add(b);
1689
+ }
1690
+ };
1691
+ const applyDeps = (b, deps) => {
1692
+ const old = cache.get(b)?.deps;
1693
+ if (old) {
1694
+ drop(textConsumers, old.texts, b);
1695
+ drop(listConsumers, old.lists, b);
1696
+ }
1697
+ add(textConsumers, deps.texts, b);
1698
+ add(listConsumers, deps.lists, b);
1699
+ };
1700
+ // ── snapshot construction ────────────────────────────────────────
1701
+ const freezeFresh = (r) => Object.freeze(r);
1702
+ /** The normalized fresh runs of `b` (interned payloads, equal marks merged) and their deps. */
1703
+ const computeFresh = (b) => {
1704
+ const deps = { texts: new Set(), lists: new Set([b]) };
1705
+ const segs = displayOf(b, blocks, ownShim, (x, home) => {
1706
+ deps.lists.add(x);
1707
+ if (home !== undefined)
1708
+ deps.texts.add(home);
1709
+ // Every claim on a walked list is read, followed or not: a claim
1710
+ // skipped for a dead target or a higher claimer becomes effective
1711
+ // when that target revives or its winning claim goes away.
1712
+ for (const c of blocks.get(x)?.claims ?? [])
1713
+ deps.lists.add(c.m);
1714
+ }) ?? [];
1715
+ const fresh = [];
1716
+ for (const item of readSegs(segs, rangeStats)) {
1717
+ if (item.kind === 'text') {
1718
+ const last = fresh[fresh.length - 1];
1719
+ const marks = item.marks === undefined ? undefined : intern(item.marks);
1720
+ if (last && last.kind === 'text' && last.marks === marks) {
1721
+ last.text += item.text;
1722
+ }
1723
+ else {
1724
+ fresh.push({
1725
+ kind: 'text',
1726
+ text: item.text,
1727
+ ...(marks === undefined ? {} : { marks })
1728
+ });
1729
+ }
1730
+ }
1731
+ else {
1732
+ fresh.push({
1733
+ kind: 'inline',
1734
+ id: item.id,
1735
+ type: item.type,
1736
+ ...(item.data === undefined ? {} : { data: intern(item.data) })
1737
+ });
1738
+ }
1739
+ }
1740
+ return { fresh, deps };
1741
+ };
1742
+ /**
1743
+ * Structural sharing: reuse unchanged run objects from `old` — maximal
1744
+ * equal prefix + equal suffix; `old` itself when every run is identical.
1745
+ */
1746
+ const reconcile = (old, fresh) => {
1747
+ if (!old)
1748
+ return Object.freeze(fresh.map(freezeFresh));
1749
+ let s = 0;
1750
+ const n = Math.min(old.length, fresh.length);
1751
+ while (s < n && runEquals(old[s], fresh[s]))
1752
+ s++;
1753
+ if (s === old.length && s === fresh.length)
1754
+ return old;
1755
+ let eo = old.length - 1;
1756
+ let ef = fresh.length - 1;
1757
+ while (eo >= s && ef >= s && runEquals(old[eo], fresh[ef])) {
1758
+ eo--;
1759
+ ef--;
1760
+ }
1761
+ const out = new Array(fresh.length);
1762
+ for (let i = 0; i < s; i++)
1763
+ out[i] = old[i];
1764
+ for (let i = s; i <= ef; i++)
1765
+ out[i] = freezeFresh(fresh[i]);
1766
+ for (let i = eo + 1; i < old.length; i++)
1767
+ out[fresh.length - (old.length - i)] = old[i];
1768
+ return Object.freeze(out);
1769
+ };
1770
+ // ── recompute ────────────────────────────────────────────────────
1771
+ const computeRuns = (b) => {
1772
+ const old = cache.get(b);
1773
+ const { fresh, deps } = computeFresh(b);
1774
+ const runs = reconcile(old?.runs, fresh);
1775
+ applyDeps(b, deps);
1776
+ cache.set(b, { runs, deps });
1777
+ dirty.delete(b);
1778
+ debug.recomputes++;
1779
+ debug.recomputed.add(b);
1780
+ };
1781
+ // ══ The fold ════════════════════════════════════════════════════
1782
+ // THE fold: one pass over a transaction's changed `(type, parentSub)` pairs
1783
+ // into the index, once at commit and, for read-your-writes, over the
1784
+ // pending part of every transaction whose commit fold has not run.
1785
+ const NO_EDITS = Object.freeze({
1786
+ edits: [],
1787
+ scan: false,
1788
+ opaque: false
1789
+ });
1790
+ const holdsBoundary = (s, from, to) => {
1791
+ const arr = s.content?.arr;
1792
+ if (arr === undefined)
1793
+ return false;
1794
+ for (let k = from - s.id.clock; k < to - s.id.clock; k++)
1795
+ if (isRowMark(arr[k]))
1796
+ return true;
1797
+ return false;
1798
+ };
1799
+ /**
1800
+ * What a transaction's log past a read's cursor did (fork patch YP33:
1801
+ * per run of items `kind, client, clock, len`, in order), in one pass: per text
1802
+ * (`texts`), its edits (the units each item showed or hid, in order); per
1803
+ * sequence (`seqs`), what changed in it: the slots written and removed, the
1804
+ * records written and removed and the units whose mover changed name
1805
+ * the blocks to re-place; with `pairs` (a read's fold), the `(type, key)`
1806
+ * pairs it changed, and the structs it named (`structs`). Reading the sequences writes the
1807
+ * slot map (the slots written and removed, `putSlot`) and lists the ids
1808
+ * whose home to read again (`rehome`).
1809
+ */
1810
+ const makeEditIndex = (log, [from, seen], pairs) => {
1811
+ const texts = new Map();
1812
+ const seqs = new Map();
1813
+ const rehome = new Set();
1814
+ let structs = 0;
1815
+ const seqOf = (node) => getOr(seqs, node, () => ({ blocks: new Set(), data: false }));
1816
+ const note = (s) => {
1817
+ if (pairs && isNodeLike(s.parent))
1818
+ getOr(pairs, s.parent, () => new Set()).add(s.parentSub);
1819
+ };
1820
+ /** Note unit `c:k` (`value`) of a sequence: a block slot names its block, a data slot its data. */
1821
+ const noteUnit = (e, owner, value, c, k, inserted) => {
1822
+ if (isBlockSlot(value)) {
1823
+ e.blocks.add(slotOwner(registry, kept, value.b, c));
1824
+ if (inserted === null || (inserted && owner === undefined))
1825
+ return;
1826
+ putSlot(value.b, { c, k }, inserted ? { parent: owner, c, k } : null);
1827
+ rehome.add(value.b);
1828
+ }
1829
+ else if (isDataSlot(value))
1830
+ e.data = true;
1831
+ };
1832
+ /** Walk `n` units of entry `i` from clock `k` (`n`'s sign is the entry's). */
1833
+ const run = (i, k, n) => {
1834
+ const kind = log[i];
1835
+ walkRange(Y, doc, log[i + 1], k, Math.abs(n), (s, a, b) => {
1836
+ structs++;
1837
+ note(s);
1838
+ // A unit a record moved: where it shows now changed too.
1839
+ const mover = s.mover;
1840
+ if (mover) {
1841
+ note(mover);
1842
+ seqOf(mover.parent);
1843
+ }
1844
+ const parent = s.parent;
1845
+ const it = s;
1846
+ if (s.parentSub !== null || !isNodeLike(parent) || it.content === undefined)
1847
+ return;
1848
+ if (parent.name === CONTENT_NODE) {
1849
+ if (kind > 1)
1850
+ return;
1851
+ const e = getOr(texts, parent, () => ({ edits: [], scan: false, opaque: false }));
1852
+ // A format marker moves no unit; its effect reaches past the edit.
1853
+ if (s.countable !== true)
1854
+ e.opaque = true;
1855
+ else if (holdsBoundary(s, a, b))
1856
+ e.scan = true;
1857
+ else
1858
+ e.edits.push({ item: s, n: n < 0 ? a - b : b - a });
1859
+ return;
1860
+ }
1861
+ const e = seqOf(parent);
1862
+ const owner = ownerOfNode(registry, kept, parent);
1863
+ const inserted = kind > 1 ? null : n > 0;
1864
+ if (isRecord(it)) {
1865
+ const ids = it.content.ids;
1866
+ for (let i = 0; i < ids.length; i += 3)
1867
+ for (let j = 0; j < ids[i + 2]; j++) {
1868
+ const u = unitOf(doc, { c: ids[i], k: ids[i + 1] + j });
1869
+ if (u !== null)
1870
+ noteUnit(e, owner, u.value, ids[i], ids[i + 1] + j, null);
1871
+ }
1872
+ }
1873
+ else if (it.content.arr !== undefined)
1874
+ for (let k = a; k < b; k++)
1875
+ noteUnit(e, owner, it.content.arr[k - s.id.clock], s.id.client, k, inserted);
1876
+ });
1877
+ };
1878
+ // The units the engine added to the entry a read folded last (`seen`: its length then), then the rest.
1879
+ if (from > 0 && log[from - 1] !== seen)
1880
+ run(from - 4, log[from - 2] + Math.abs(seen), log[from - 1] - seen);
1881
+ for (let i = from; i < log.length; i += 4)
1882
+ run(i, log[i + 2], log[i + 3]);
1883
+ return { texts, seqs, structs, rehome };
1884
+ };
1885
+ const runs = (b) => {
1886
+ // Read-your-writes: fold the open transaction's pending writes first.
1887
+ syncAll();
1888
+ if (dirty.has(b) || !cache.has(b))
1889
+ computeRuns(b);
1890
+ return cache.get(b)?.runs ?? EMPTY_RUNS;
1891
+ };
1892
+ // ── the fold ─────────────────────────────────────────────────────
1893
+ /** The owner of the sequence `id`'s slot shows in (its display edge's parent), as the engine stands. */
1894
+ const parentOf = (id) => edgeParent(id);
1895
+ /** Note edits of `id`'s own text (`null`: unknown — rescan, every reader re-reads). */
1896
+ const noteText = (ctx, id, e) => {
1897
+ let t = ctx.texts.get(id);
1898
+ if (t === undefined)
1899
+ ctx.texts.set(id, (t = { edits: [], scan: false, opaque: false, seen: new Set() }));
1900
+ if (e === null) {
1901
+ t.scan = t.opaque = true;
1902
+ return;
1903
+ }
1904
+ if (t.seen.has(e))
1905
+ return;
1906
+ t.seen.add(e);
1907
+ t.scan ||= e.scan;
1908
+ t.opaque ||= e.opaque;
1909
+ for (const x of e.edits)
1910
+ t.edits.push(x);
1911
+ };
1912
+ /** Fold one block's changed facets (`edits`: its own text's, one per changed content node). */
1913
+ const foldBlock = (id, facets, edits, ctx, editsOf) => {
1914
+ const kinds = new Set();
1915
+ for (const f of facets)
1916
+ kinds.add(f === ENTRY_FACET ? 'entry' : facetOf(f));
1917
+ // The display parents a shell may flip by (`settleShells`): read while there are shells.
1918
+ const parentBefore = shells.size > 0 ? parentOf(id) : undefined;
1919
+ if (typeof parentBefore === 'string')
1920
+ ctx.parents.add(parentBefore);
1921
+ if (kinds.has('entry') || kinds.has('structure')) {
1922
+ const [claimsBefore, typeBefore] = [blocks.get(id)?.claims, blocks.get(id)?.type];
1923
+ updateBlockRec(id);
1924
+ // A retype that lands with a structure facet is still a retype.
1925
+ if (typeBefore !== undefined && blocks.get(id)?.type !== typeBefore)
1926
+ retyped = true;
1927
+ // A new entry, a nonce or an own text can move streams.
1928
+ if (kinds.has('entry') || facets.has(NONCE) || facets.has(CONTENT_ATTR)) {
1929
+ ctx.table = true;
1930
+ ctx.named.add(id);
1931
+ noteText(ctx, id, null);
1932
+ }
1933
+ // The key's losing incarnations follow its entry and its liveness.
1934
+ for (const v of syncIncarnations(id)) {
1935
+ ctx.table = true;
1936
+ ctx.named.add(v);
1937
+ noteText(ctx, v, null);
1938
+ invalidateBlock(v, ctx);
1939
+ }
1940
+ invalidateBlock(id, ctx, claimsBefore);
1941
+ }
1942
+ else {
1943
+ ensureRec(id);
1944
+ const rec = blocks.get(id);
1945
+ // A data slot in the block's sequence: its arrays' items changed (the seq pass seeded the blocks).
1946
+ if (kinds.has('seq') && rec && editsOf.seqs.get(rec.node)?.data)
1947
+ kinds.add('meta');
1948
+ if (kinds.has('meta') && rec) {
1949
+ const was = rec.type;
1950
+ const facts = tableFacts(id, rec.data);
1951
+ rec.type = typeAttr(rec.node);
1952
+ rec.data = readData(rec.node);
1953
+ if (rec.type !== was) {
1954
+ retyped = true;
1955
+ noteKind(id);
1956
+ }
1957
+ // A table's columns or a cell's column re-place the table's cells (`table.columns`).
1958
+ if (tableFacts(id, rec.data) !== facts)
1959
+ stateSeeds.add(id);
1960
+ // A retype that changes the kind's display shape re-parents
1961
+ // (or re-kinds) its children.
1962
+ if (roles !== null && !sameShape(roles, was, rec.type))
1963
+ stateSeeds.add(id);
1964
+ }
1965
+ }
1966
+ const parentAfter = shells.size > 0 ? parentOf(id) : undefined;
1967
+ if (typeof parentAfter === 'string')
1968
+ ctx.parents.add(parentAfter);
1969
+ if (kinds.has('content'))
1970
+ for (const e of edits)
1971
+ noteText(ctx, id, e);
1972
+ };
1973
+ /**
1974
+ * Move each edited text's row by its edits (or rescan it), re-decide the
1975
+ * delimiters a boundary change names, re-place the rows they cut, and
1976
+ * invalidate the readers: the display owner of each edited stream,
1977
+ * the blocks whose stream changed, every reader of an opaque text.
1978
+ */
1979
+ const foldTexts = (ctx) => {
1980
+ const homes = new Set();
1981
+ const gapsOf = new Map();
1982
+ const gapsIn = (row, edits) => {
1983
+ const gaps = [];
1984
+ for (const e of edits) {
1985
+ const gap = gapOfItem(row, e.item);
1986
+ if (gap < 0)
1987
+ return null;
1988
+ gaps.push(gap);
1989
+ }
1990
+ return gaps;
1991
+ };
1992
+ for (const [home, t] of ctx.texts) {
1993
+ const text = blocks.get(home)?.content;
1994
+ const row = rows.get(home);
1995
+ let gaps = [];
1996
+ let fresh = t.scan || row === undefined || text === undefined || row.text !== text;
1997
+ if (!fresh) {
1998
+ for (const e of t.edits) {
1999
+ const gap = gapOfItem(row, e.item);
2000
+ if (gap < 0 || !shiftGap(row, gap, e.n)) {
2001
+ fresh = true;
2002
+ break;
2003
+ }
2004
+ gaps.push(gap);
2005
+ }
2006
+ // The engine's own count of live units: a row that disagrees is stale.
2007
+ if (!fresh && rowLength(row) !== sequenceLength(text))
2008
+ fresh = true;
2009
+ }
2010
+ if (fresh) {
2011
+ const next = scanRow(home);
2012
+ const same = next !== undefined && row !== undefined && next.key === row.key;
2013
+ if (same)
2014
+ [next.cuts, next.heads, next.head] = [row.cuts, row.heads, row.head];
2015
+ setRow(home, next, ctx.named);
2016
+ if (!same)
2017
+ homes.add(home);
2018
+ gaps = next === undefined ? [] : gapsIn(next, t.edits);
2019
+ }
2020
+ gapsOf.set(home, t.opaque ? null : gaps);
2021
+ }
2022
+ refreshDelims(ctx.named, homes);
2023
+ if (homes.size > 0)
2024
+ ctx.table = true;
2025
+ for (const home of homes)
2026
+ placeHome(home, ctx.invalidated);
2027
+ for (const [home, gaps] of gapsOf) {
2028
+ if (gaps === null) {
2029
+ for (const c of textConsumers.get(home) ?? [])
2030
+ ctx.invalidated.add(c);
2031
+ continue;
2032
+ }
2033
+ // Only the stream each edit lies in changes its display.
2034
+ const row = rows.get(home);
2035
+ if (row === undefined)
2036
+ continue;
2037
+ for (const gap of gaps) {
2038
+ const b = headOf(row, segmentOfGap(row, gap));
2039
+ const owner = b === null ? DEAD : ownerOf(b);
2040
+ if (typeof owner === 'string')
2041
+ ctx.invalidated.add(owner);
2042
+ }
2043
+ }
2044
+ };
2045
+ /** The blocks whose stream lies in a text the fold edited. */
2046
+ const editedStreams = (ctx) => [...ctx.texts.keys()].flatMap((home) => streamsIn(home).map((st) => st.block));
2047
+ /** A live unit in `id`'s stream (boundaries excepted). */
2048
+ const streamHolds = (id) => {
2049
+ const st = streamOf(id);
2050
+ return st !== undefined && st.end - st.start > st.inert.length;
2051
+ };
2052
+ /** A live unit in `id`'s stream or in the stream of a losing incarnation it shows. */
2053
+ const holdsUnit = (id) => streamHolds(id) || (incarnations.get(id)?.some(streamHolds) ?? false);
2054
+ /**
2055
+ * Settle the withdrawn blocks (`hist.undo.withdraw`): a shell is deleted
2056
+ * iff it holds nothing — no live unit in its stream and no child (a
2057
+ * block whose winning candidate names it) that is itself not deleted.
2058
+ * The least fixpoint, computed over every shell whenever an `affected`
2059
+ * one may flip (one whose stream still holds a unit keeps its answer),
2060
+ * so every replica settles the same whatever the fold order. A flip is
2061
+ * a structural change of the shell.
2062
+ */
2063
+ const settleShells = (affected, ctx) => {
2064
+ if (shells.size === 0)
2065
+ return;
2066
+ let quiet = true;
2067
+ for (const x of affected) {
2068
+ const id = baseIdOf(x);
2069
+ if (!shells.has(id))
2070
+ continue;
2071
+ if (blocks.get(id)?.deleted !== false || !holdsUnit(id))
2072
+ quiet = false;
2073
+ }
2074
+ if (quiet)
2075
+ return;
2076
+ const alive = new Set();
2077
+ const up = [];
2078
+ const hold = (id) => {
2079
+ if (alive.has(id))
2080
+ return;
2081
+ alive.add(id);
2082
+ up.push(id);
2083
+ };
2084
+ for (const id of shells)
2085
+ if (holdsUnit(id))
2086
+ hold(id);
2087
+ for (const [id, rec] of blocks) {
2088
+ if (shells.has(id) || rec.deleted)
2089
+ continue;
2090
+ const p = parentOf(id);
2091
+ if (typeof p === 'string' && shells.has(p))
2092
+ hold(p);
2093
+ }
2094
+ for (let id = up.pop(); id !== undefined; id = up.pop()) {
2095
+ const p = parentOf(id);
2096
+ if (typeof p === 'string' && shells.has(p))
2097
+ hold(p);
2098
+ }
2099
+ for (const id of shells) {
2100
+ const rec = blocks.get(id);
2101
+ if (rec.deleted === !alive.has(id))
2102
+ continue;
2103
+ rec.deleted = !alive.has(id);
2104
+ ownerSeeds.add(id);
2105
+ // Its losing incarnations follow it.
2106
+ for (const v of incarnations.get(id) ?? []) {
2107
+ const inc = blocks.get(v);
2108
+ if (inc === undefined)
2109
+ continue;
2110
+ const deleted = hasDeleteMark(inc.node) || rec.deleted;
2111
+ if (inc.deleted === deleted)
2112
+ continue;
2113
+ inc.deleted = deleted;
2114
+ ownerSeeds.add(v);
2115
+ if (ctx !== null)
2116
+ invalidateBlock(v, ctx);
2117
+ }
2118
+ if (ctx === null)
2119
+ continue;
2120
+ invalidateBlock(id, ctx);
2121
+ }
2122
+ };
2123
+ const registryNode = registry;
2124
+ /**
2125
+ * The block a changed `(type, parentSub)` pair belongs to, the facet it
2126
+ * entered through (the block attr the chain hangs under; the `content`
2127
+ * attr itself is {@link CONTENT_ATTR}), and whether it is a sequence
2128
+ * edit of the block's own text. `null` outside the registry subtree.
2129
+ */
2130
+ const locate = (type, sub) => {
2131
+ if (type === registryNode)
2132
+ return typeof sub === 'string' ? [sub, ENTRY_FACET, false] : null;
2133
+ let below = null;
2134
+ for (let cur = type;;) {
2135
+ const it = cur._item;
2136
+ const parent = it?.parent;
2137
+ if (parent === registryNode) {
2138
+ let id = it.parentSub;
2139
+ if (typeof id !== 'string')
2140
+ return null;
2141
+ // A losing incarnation's subtree: its derived id; a seed's shows nothing.
2142
+ if (it.deleted && kept(it)) {
2143
+ const item = itemOf(cur);
2144
+ if (item.id.client < LIVE_WRITERS)
2145
+ return null;
2146
+ id = incarnationId(id, item);
2147
+ }
2148
+ // The block's own sequence (its slots and records: `SEQ_FACET`), or one of its attrs.
2149
+ if (below === null)
2150
+ return [id, sub === CONTENT ? CONTENT_ATTR : sub === null ? SEQ_FACET : sub, false];
2151
+ const facet = below._item?.parentSub ?? CONTENT;
2152
+ return [id, facet, sub === null && below === type && facet === CONTENT];
2153
+ }
2154
+ if (!isNodeLike(parent))
2155
+ return null;
2156
+ below = cur;
2157
+ cur = parent;
2158
+ }
2159
+ };
2160
+ /** THE fold: one pass over changed `(type, parentSub)` pairs, each block folded once. */
2161
+ const fold = (changed, editsOf) => {
2162
+ const touched = new Map();
2163
+ const edits = new Map();
2164
+ let derived = changed.has(dataRoot);
2165
+ foldStats.folds++;
2166
+ for (const [type, subs] of changed) {
2167
+ foldStats.pairs += subs.size;
2168
+ for (const sub of subs) {
2169
+ const hit = locate(type, sub);
2170
+ if (hit === null)
2171
+ continue;
2172
+ const [id, facet, seq] = hit;
2173
+ const facets = getOr(touched, id, () => new Set());
2174
+ facets.add(facet);
2175
+ if (facet !== CONTENT)
2176
+ continue;
2177
+ // A sequence edit of the block's own text, or a change inside one of its atoms.
2178
+ const list = getOr(edits, id, () => []);
2179
+ list.push(seq ? (editsOf.texts.get(type) ?? NO_EDITS) : null);
2180
+ }
2181
+ }
2182
+ const ctx = {
2183
+ invalidated: new Set(),
2184
+ texts: new Map(),
2185
+ named: new Set(),
2186
+ table: false,
2187
+ parents: new Set()
2188
+ };
2189
+ // The sequences (the root's, the blocks'): their slots index the blocks' homes, their changes seed the placements.
2190
+ for (const [node, ch] of editsOf.seqs) {
2191
+ seqEpoch++;
2192
+ for (const b of ch.blocks)
2193
+ placementSeeds.add(b);
2194
+ if (ch.blocks.size > 0 || ch.data)
2195
+ derived = true;
2196
+ const owner = ownerOfNode(registry, kept, node);
2197
+ if (typeof owner === 'string') {
2198
+ const facets = getOr(touched, owner, () => new Set());
2199
+ facets.add(SEQ_FACET);
2200
+ if (ch.data)
2201
+ facets.add(DATA_SEQ_FACET);
2202
+ }
2203
+ }
2204
+ // A slot's block, and its losing incarnations, re-read their home (which slot is whose).
2205
+ for (const b of editsOf.rehome)
2206
+ for (const id of [b, ...(incarnations.get(b) ?? [])]) {
2207
+ const rec = blocks.get(id);
2208
+ if (rec === undefined)
2209
+ continue;
2210
+ const home = homeFor(id);
2211
+ if (home?.c !== rec.home?.c || home?.k !== rec.home?.k)
2212
+ placementSeeds.add(id);
2213
+ rec.home = home;
2214
+ }
2215
+ for (const [id, facets] of touched) {
2216
+ foldBlock(id, facets, edits.get(id) ?? [], ctx, editsOf);
2217
+ derived ||= [...facets].some((f) => f === ENTRY_FACET || facetOf(f) !== 'ignore');
2218
+ }
2219
+ foldTexts(ctx);
2220
+ if (reclaim.size > 0)
2221
+ applyReclaims(ctx);
2222
+ if (shells.size > 0)
2223
+ settleShells(ctx.table ? shells : [...touched.keys(), ...ctx.parents, ...editedStreams(ctx)], ctx);
2224
+ for (const b of ctx.invalidated)
2225
+ dirty.add(b);
2226
+ if (indexChecks.on)
2227
+ checker().check();
2228
+ if (derived)
2229
+ version++;
2230
+ if (reporting) {
2231
+ for (const id of touched.keys())
2232
+ candidates.add(id);
2233
+ for (const id of ctx.invalidated)
2234
+ candidates.add(id);
2235
+ }
2236
+ for (const f of frames) {
2237
+ for (const [id, facets] of touched) {
2238
+ const into = f.touched.get(id);
2239
+ if (into === undefined)
2240
+ f.touched.set(id, new Set(facets));
2241
+ else
2242
+ for (const x of facets)
2243
+ into.add(x);
2244
+ }
2245
+ }
2246
+ return touched;
2247
+ };
2248
+ /** Each transaction's folded part (`Cursor`). */
2249
+ const cursors = new WeakMap();
2250
+ const NONE = [0];
2251
+ /** Whether `log` holds entries or units past `cursor`. */
2252
+ const past = (log, [from, seen]) => log.length !== from || log[from - 1] !== seen;
2253
+ /** The transactions whose commit fold ran. */
2254
+ const committed = new WeakSet();
2255
+ /**
2256
+ * Fold the pending part of `tr` — the entries its log gained since the
2257
+ * last fold — before a read inside it (read-your-writes;
2258
+ * `transaction.changed` does not grow on a second edit to an
2259
+ * already-changed type).
2260
+ */
2261
+ const syncPending = (tr) => {
2262
+ const log = tr?.log;
2263
+ const cursor = cursors.get(tr) ?? NONE;
2264
+ if (log === undefined || !past(log, cursor))
2265
+ return false;
2266
+ cursors.set(tr, [log.length, log[log.length - 1]]);
2267
+ for (const f of frames)
2268
+ f.wrote = true;
2269
+ const pairs = new Map();
2270
+ const edits = makeEditIndex(log, cursor, pairs);
2271
+ foldStats.structs += edits.structs;
2272
+ fold(pairs, edits);
2273
+ return true;
2274
+ };
2275
+ const openTx = () => doc._transaction;
2276
+ /**
2277
+ * Fold every transaction whose writes are in the document and whose
2278
+ * commit fold has not run, in their order: the open one, and those a
2279
+ * transaction's cleanup started (an observer's or an `update`
2280
+ * listener's write runs its body at once, its observers later) — a
2281
+ * read in between must not mix their items with an index that never
2282
+ * saw them.
2283
+ */
2284
+ const syncAll = () => {
2285
+ for (const tr of queuedTransactions(doc))
2286
+ if (!committed.has(tr))
2287
+ syncPending(tr);
2288
+ };
2289
+ /** Some queued transaction holds writes no fold has seen. */
2290
+ const unfolded = () => queuedTransactions(doc).some((tr) => !committed.has(tr) && tr.log !== undefined && past(tr.log, cursors.get(tr) ?? NONE));
2291
+ /**
2292
+ * The commit of a transaction that changed the registry or a node under
2293
+ * it: fold `transaction.changed` once (the report publishes from `update`).
2294
+ */
2295
+ const onCommit = (tr) => {
2296
+ if (!changedUnder(tr, [registry]))
2297
+ return;
2298
+ // The writes a read folded already moved the rows: only the rest
2299
+ // are edits now (the facets fold again, idempotent).
2300
+ const cursor = cursors.get(tr) ?? NONE;
2301
+ cursors.delete(tr);
2302
+ committed.add(tr);
2303
+ fold(tr.changed ?? new Map(), makeEditIndex(tr.log ?? [], cursor));
2304
+ };
2305
+ // ══ The change report ═══════════════════════════════════════════
2306
+ // The change report: the fold against the last published index, one per
2307
+ // commit that changed the visible document, and the projection it carries.
2308
+ const docData = () => cloneJsonSafe(readData(dataRoot) ?? {});
2309
+ let dataChanged = false;
2310
+ // ── projection ───────────────────────────────────────────────────
2311
+ /** `id`'s visible content items: its runs, as fresh wrappers over their shared payloads. */
2312
+ const itemsOf = (id) => runs(id).map((r) => ({ ...r }));
2313
+ const projectBlock = (id) => {
2314
+ const rec = blocks.get(id);
2315
+ const projected = {
2316
+ id,
2317
+ type: typeOf(id),
2318
+ data: dataOf(rec),
2319
+ content: itemsOf(id),
2320
+ children: []
2321
+ };
2322
+ for (const k of kidsMap.get(id) ?? [])
2323
+ projected.children.push(projectBlock(k.id));
2324
+ return projected;
2325
+ };
2326
+ // ── the change report: the fold against the last published index ─
2327
+ let published = null;
2328
+ const reportSubs = new Set();
2329
+ /** The reachable tree — nodes carry `prev`'s published payloads where they exist. */
2330
+ const reachable = (prev) => {
2331
+ ensurePlacements();
2332
+ const nodes = new Map();
2333
+ const order = new Map();
2334
+ const stack = [null];
2335
+ for (let parent = stack.pop(); parent !== undefined; parent = stack.pop()) {
2336
+ const ks = kidsMap.get(parent) ?? [];
2337
+ if (ks.length === 0)
2338
+ continue;
2339
+ order.set(parent, Object.freeze(ks.map((k) => k.id)));
2340
+ for (let index = ks.length - 1; index >= 0; index--)
2341
+ stack.push(ks[index].id);
2342
+ ks.forEach(({ id }, index) => {
2343
+ const old = prev?.get(id);
2344
+ const rec = blocks.get(id);
2345
+ nodes.set(id, old === undefined
2346
+ ? { parent, index, type: typeOf(id), data: rec.data, runs: runs(id) }
2347
+ : { ...old, parent, index });
2348
+ });
2349
+ }
2350
+ return { nodes, order, kids: kidsVersion };
2351
+ };
2352
+ /**
2353
+ * The report's structural part from the child lists patched since the
2354
+ * last report, advancing the published tree in place — what
2355
+ * `reachable` would give, without walking the lists nothing changed:
2356
+ * a published block no longer shown leaves (a root when its parent
2357
+ * stays), its still-shown descendants staying; each patched list's
2358
+ * new order is news, a block in it at a new parent or index moved, one
2359
+ * not published before is added with its subtree.
2360
+ */
2361
+ const advance = (pub, r, covered, reparented) => {
2362
+ const { nodes, order } = pub;
2363
+ const shown = (x) => {
2364
+ for (let c = x; c !== null;) {
2365
+ const slot = slots.get(c);
2366
+ if (slot === undefined)
2367
+ return false;
2368
+ c = slot.parent;
2369
+ }
2370
+ return true;
2371
+ };
2372
+ const lists = [...dirtyLists];
2373
+ // Leaving: a published block of a patched list that no longer shows.
2374
+ const drop = (id) => {
2375
+ const ks = order.get(id);
2376
+ if (ks !== undefined) {
2377
+ order.delete(id);
2378
+ r.order.set(id, EMPTY_IDS);
2379
+ for (const k of ks)
2380
+ if (!shown(k))
2381
+ drop(k);
2382
+ }
2383
+ nodes.delete(id);
2384
+ };
2385
+ for (const id of leftLists) {
2386
+ const o = nodes.get(id);
2387
+ if (o === undefined || shown(id))
2388
+ continue;
2389
+ if (o.parent === null || shown(o.parent))
2390
+ r.removed.add(id);
2391
+ drop(id);
2392
+ }
2393
+ // Each patched list that shows: its order, its moved and new blocks.
2394
+ const fresh = new Map();
2395
+ for (const p of lists) {
2396
+ if (p !== null && !shown(p))
2397
+ continue;
2398
+ const ks = kidsMap.get(p) ?? [];
2399
+ const prev = order.get(p);
2400
+ if (ks.length === 0) {
2401
+ if (prev !== undefined) {
2402
+ order.delete(p);
2403
+ r.order.set(p, EMPTY_IDS);
2404
+ }
2405
+ continue;
2406
+ }
2407
+ const ids = ks.map((k) => k.id);
2408
+ if (prev === undefined || !sameIds(prev, ids)) {
2409
+ const frozen = Object.freeze(ids);
2410
+ order.set(p, frozen);
2411
+ r.order.set(p, frozen);
2412
+ }
2413
+ ks.forEach(({ id }, index) => {
2414
+ const n = nodes.get(id);
2415
+ if (n === undefined)
2416
+ fresh.set(id, { parent: p, index });
2417
+ else if (n.parent !== p || n.index !== index) {
2418
+ r.moved.add(id);
2419
+ if (n.parent !== p)
2420
+ reparented.add(id);
2421
+ n.parent = p;
2422
+ n.index = index;
2423
+ }
2424
+ });
2425
+ }
2426
+ // New roots (no new ancestor) bring their subtree.
2427
+ const add = (b, parent, index) => {
2428
+ if (!nodes.has(b.id)) {
2429
+ covered.add(b.id);
2430
+ const rec = blocks.get(b.id);
2431
+ nodes.set(b.id, { parent, index, type: typeOf(b.id), data: rec.data, runs: runs(b.id) });
2432
+ }
2433
+ if (b.children.length > 0 && !order.has(b.id)) {
2434
+ const ids = Object.freeze(b.children.map((c) => c.id));
2435
+ order.set(b.id, ids);
2436
+ r.order.set(b.id, ids);
2437
+ }
2438
+ b.children.forEach((c, i) => add(c, b.id, i));
2439
+ };
2440
+ for (const [id, at] of fresh) {
2441
+ let under = false;
2442
+ for (let c = slots.get(id)?.parent ?? null; c !== null && !under; c = slots.get(c)?.parent ?? null)
2443
+ under = fresh.has(c);
2444
+ if (under || covered.has(id))
2445
+ continue;
2446
+ const b = projectBlock(id);
2447
+ r.added.set(id, b);
2448
+ add(b, at.parent, at.index);
2449
+ }
2450
+ };
2451
+ /** The blocks whose shown kind followed their slot at the last report. */
2452
+ let followingBefore = [];
2453
+ /** The document data's key at the last report. */
2454
+ let dataKey = '';
2455
+ /** Build the commit's report and advance the published index to it. */
2456
+ const report = () => {
2457
+ const before = published;
2458
+ // Fold every queued transaction first: a follow-up a cleanup
2459
+ // started (a delete-mark repair) has already written, and the
2460
+ // first content read below would fold it mid-report, after the
2461
+ // child lists were read (found by the purge fuzz, rich lane, seeds 651 and 1271).
2462
+ syncAll();
2463
+ ensurePlacements();
2464
+ const r = {
2465
+ added: new Map(),
2466
+ removed: new Set(),
2467
+ moved: new Set(),
2468
+ meta: new Map(),
2469
+ content: new Map(),
2470
+ order: new Map()
2471
+ };
2472
+ // The document's data is news when it differs from what was published.
2473
+ if (dataChanged) {
2474
+ dataChanged = false;
2475
+ const data = docData();
2476
+ const key = keyOf(data);
2477
+ if (key !== dataKey)
2478
+ [r.data, dataKey] = [data, key];
2479
+ }
2480
+ if (kidsVersion === before.kids && candidates.size === 0)
2481
+ return r.data ? r : null;
2482
+ // Added subtrees carry their new descendants. A descendant that was
2483
+ // visible before is reported like any visible block (moved, retyped,
2484
+ // edited against its published baseline), so consumers keep it.
2485
+ const covered = new Set();
2486
+ /** Moved to another display parent: only those can show another kind (an index never does). */
2487
+ const reparented = new Set();
2488
+ const lists = kidsVersion !== before.kids && !allListsDirty;
2489
+ const after = kidsVersion === before.kids || lists ? before : reachable(before.nodes);
2490
+ if (lists)
2491
+ advance(before, r, covered, reparented);
2492
+ else if (after !== before) {
2493
+ for (const [parent, ids] of after.order) {
2494
+ const prev = before.order.get(parent);
2495
+ if (prev === undefined || !sameIds(prev, ids))
2496
+ r.order.set(parent, ids);
2497
+ }
2498
+ for (const parent of before.order.keys()) {
2499
+ if (!after.order.has(parent))
2500
+ r.order.set(parent, EMPTY_IDS);
2501
+ }
2502
+ const register = (b) => {
2503
+ if (!before.nodes.has(b.id))
2504
+ covered.add(b.id);
2505
+ b.children.forEach(register);
2506
+ };
2507
+ for (const [id, n] of after.nodes) {
2508
+ if (!before.nodes.has(id)) {
2509
+ if (covered.has(id))
2510
+ continue;
2511
+ const b = projectBlock(id);
2512
+ r.added.set(id, b);
2513
+ register(b);
2514
+ }
2515
+ else {
2516
+ const o = before.nodes.get(id);
2517
+ if (o.parent !== n.parent || o.index !== n.index)
2518
+ r.moved.add(id);
2519
+ if (o.parent !== n.parent)
2520
+ reparented.add(id);
2521
+ }
2522
+ }
2523
+ // Removed subtree ROOTS: a removed id whose before-parent stays
2524
+ // visible (or is the root). One under a removed parent leaves with
2525
+ // it; one under a surviving child of a removed subtree does not.
2526
+ for (const [id, o] of before.nodes) {
2527
+ if (after.nodes.has(id))
2528
+ continue;
2529
+ if (o.parent === null || after.nodes.has(o.parent))
2530
+ r.removed.add(id);
2531
+ }
2532
+ }
2533
+ const meta = (id, n) => {
2534
+ const rec = blocks.get(id);
2535
+ const type = typeOf(id);
2536
+ r.meta.set(id, { type, data: dataOf(rec) });
2537
+ n.type = type;
2538
+ n.data = rec.data;
2539
+ };
2540
+ for (const id of candidates) {
2541
+ const n = after.nodes.get(id);
2542
+ if (n === undefined || covered.has(id))
2543
+ continue;
2544
+ const rec = blocks.get(id);
2545
+ if (typeOf(id) !== n.type || keyOf(rec.data) !== keyOf(n.data))
2546
+ meta(id, n);
2547
+ const next = runs(id);
2548
+ if (next !== n.runs) {
2549
+ const key = keyOf(next);
2550
+ if ((n.key ??= keyOf(n.runs)) !== key)
2551
+ r.content.set(id, next);
2552
+ n.runs = next;
2553
+ n.key = key;
2554
+ }
2555
+ }
2556
+ // A block that starts or stops displaying a kind other than its
2557
+ // stored one moved, or its kind follows (or followed) its slot:
2558
+ // its kind is news. Without a placement change, only a retype
2559
+ // changes a shown kind — the retyped block's (a candidate) and the
2560
+ // kinds derived from it: a promoted or stray line shows its display
2561
+ // parent's default child.
2562
+ if (after !== before || lists || retyped) {
2563
+ const now = [...following];
2564
+ for (const id of [...reparented, ...now, ...followingBefore]) {
2565
+ const n = after.nodes.get(id);
2566
+ if (n !== undefined && !candidates.has(id) && !covered.has(id) && typeOf(id) !== n.type)
2567
+ meta(id, n);
2568
+ }
2569
+ followingBefore = now;
2570
+ }
2571
+ // A plain block directly in a list shows as its item (`shownItem`):
2572
+ // a block whose shown kind changed re-reads its children's (a worklist:
2573
+ // the map visits the entries added meanwhile).
2574
+ for (const id of r.meta.keys())
2575
+ for (const { id: kid } of kidsMap.get(id) ?? []) {
2576
+ const n = after.nodes.get(kid);
2577
+ if (n !== undefined && !covered.has(kid) && !r.meta.has(kid) && typeOf(kid) !== n.type)
2578
+ meta(kid, n);
2579
+ }
2580
+ retyped = false;
2581
+ candidates.clear();
2582
+ published = after;
2583
+ after.kids = kidsVersion;
2584
+ dirtyLists.clear();
2585
+ leftLists.clear();
2586
+ allListsDirty = false;
2587
+ if (indexChecks.on)
2588
+ checker().checkPublished(published);
2589
+ const empty = r.added.size + r.removed.size + r.moved.size + r.meta.size + r.content.size + r.order.size;
2590
+ return empty === 0 && !r.data ? null : r;
2591
+ };
2592
+ /** Report the commit (or role change) to every subscriber, when it changed the visible document. */
2593
+ const publish = (origin, local) => {
2594
+ const r = report();
2595
+ if (r !== null)
2596
+ callEach('[edytor-doc] change', [...reportSubs], r, origin, local);
2597
+ };
2598
+ const onUpdate = (_u, origin, _d, tr) => {
2599
+ if (tr.changed?.has(dataRoot))
2600
+ dataChanged = true;
2601
+ publish(origin, tr.local === true);
2602
+ };
2603
+ /** Subscribe to the change report; the first subscription takes the baseline. */
2604
+ const onReport = (cb) => {
2605
+ if (reportSubs.size === 0) {
2606
+ syncAll();
2607
+ published = reachable();
2608
+ dataKey = keyOf(docData());
2609
+ candidates.clear();
2610
+ reporting = true;
2611
+ doc.on('update', onUpdate);
2612
+ }
2613
+ reportSubs.add(cb);
2614
+ return () => {
2615
+ if (!reportSubs.delete(cb) || reportSubs.size > 0)
2616
+ return;
2617
+ doc.off('update', onUpdate);
2618
+ published = null;
2619
+ reporting = false;
2620
+ candidates.clear();
2621
+ };
2622
+ };
2623
+ const hasSubscribers = () => reportSubs.size > 0;
2624
+ // ── the self-checks (`indexChecks`), built on their first run: a rebuild
2625
+ // from the replicated state each fact this index maintains must equal
2626
+ let checks;
2627
+ const checker = () => (checks ??= indexChecks.of({
2628
+ doc,
2629
+ registry,
2630
+ blocks,
2631
+ displaysMap,
2632
+ rows,
2633
+ streamIx,
2634
+ get delim() {
2635
+ return delim;
2636
+ },
2637
+ placementsMap,
2638
+ slotsByBlock,
2639
+ edges,
2640
+ rejected,
2641
+ get kids0() {
2642
+ return kids0;
2643
+ },
2644
+ get kidsMap() {
2645
+ return kidsMap;
2646
+ },
2647
+ get slots() {
2648
+ return slots;
2649
+ },
2650
+ get dissolved() {
2651
+ return dissolved;
2652
+ },
2653
+ following,
2654
+ cache,
2655
+ dirty,
2656
+ ownShim,
2657
+ own0,
2658
+ ownerOf,
2659
+ streamOf,
2660
+ sameClaims,
2661
+ dissolve,
2662
+ noteFollowing,
2663
+ ensurePlacements,
2664
+ computeFresh,
2665
+ unfolded,
2666
+ reachable
2667
+ }));
2668
+ // ── the view ──────────────────────────────────────────────────
2669
+ const ctx = {
2670
+ blocks,
2671
+ own: ownShim,
2672
+ get placements() {
2673
+ ensurePlacements();
2674
+ return placementsMap;
2675
+ },
2676
+ get kids() {
2677
+ ensurePlacements();
2678
+ return kidsMap;
2679
+ },
2680
+ get order() {
2681
+ ensurePlacements();
2682
+ return order();
2683
+ },
2684
+ // The publication boundary shares THIS interner, so a payload
2685
+ // emitted by `project()`/`contentItems()` is `===` the runs' one.
2686
+ intern,
2687
+ displays,
2688
+ accepted: (id) => {
2689
+ ensurePlacements();
2690
+ return rejected.has(id) ? null : (edges.get(id) ?? null);
2691
+ }
2692
+ };
2693
+ // ── initial scan: index every existing block ────────────────────
2694
+ registry.forEachAttr((v, id) => {
2695
+ if (!isNodeLike(v))
2696
+ return;
2697
+ updateBlockRec(id);
2698
+ for (const b of [id, ...syncIncarnations(id)]) {
2699
+ const row = scanRow(b);
2700
+ if (row !== undefined) {
2701
+ rows.set(b, row);
2702
+ homeOfText.set(row.text, b);
2703
+ }
2704
+ }
2705
+ });
2706
+ rebuildTable(new Set());
2707
+ applyReclaims(null);
2708
+ settleShells(shells, null);
2709
+ // Every commit folds once, before the document's `afterTransaction` listeners.
2710
+ doc.on('commit', onCommit);
2711
+ /** Derived state lives exactly as long as the doc. */
2712
+ const teardown = () => {
2713
+ doc.off('commit', onCommit);
2714
+ doc.off('destroy', teardown);
2715
+ doc.off('update', onUpdate);
2716
+ indexes.delete(doc);
2717
+ };
2718
+ doc.on('destroy', teardown);
2719
+ // ── public surface ───────────────────────────────────────────────
2720
+ const view = {
2721
+ version: () => {
2722
+ syncAll();
2723
+ return version;
2724
+ },
2725
+ runs,
2726
+ contentItems: (b) => {
2727
+ syncAll();
2728
+ return blocks.get(b)?.deleted === false ? itemsOf(b) : [];
2729
+ },
2730
+ // Inline atoms always carry `data` (`{}` when absent) — the public
2731
+ // `JSONInlineBlock` shape of `edytor.value`.
2732
+ contentJSON: (b) => runs(b).map((r) => r.kind === 'text'
2733
+ ? { text: r.text, ...(r.marks !== undefined && { marks: cloneJsonSafe(r.marks) }) }
2734
+ : { id: r.id, type: r.type, data: r.data === undefined ? {} : cloneJsonSafe(r.data) }),
2735
+ view: (tr) => {
2736
+ syncAll();
2737
+ if (tr !== undefined && !committed.has(tr))
2738
+ syncPending(tr);
2739
+ return ctx;
2740
+ },
2741
+ displayType: (id) => {
2742
+ if (roles === null)
2743
+ return undefined;
2744
+ syncAll();
2745
+ ensurePlacements();
2746
+ const shown = blocks.has(id) ? typeOf(id) : undefined;
2747
+ return shown === blocks.get(id)?.type ? undefined : shown;
2748
+ },
2749
+ dissolved: (id) => {
2750
+ syncAll();
2751
+ ensurePlacements();
2752
+ return dissolved.has(id);
2753
+ },
2754
+ project: (root) => {
2755
+ syncAll();
2756
+ ensurePlacements();
2757
+ return root === undefined
2758
+ ? (kidsMap.get(null) ?? []).map((k) => projectBlock(k.id))
2759
+ : [projectBlock(root)];
2760
+ },
2761
+ roles: (next) => {
2762
+ roles = next;
2763
+ placementFull = true;
2764
+ version++;
2765
+ // No commit carries a role change: report it now (not inside a
2766
+ // transaction, whose commit reports it), so every view follows.
2767
+ if (hasSubscribers() && !openTx())
2768
+ publish(null, false);
2769
+ },
2770
+ track: () => {
2771
+ syncAll();
2772
+ const f = { touched: new Map(), wrote: false };
2773
+ frames.add(f);
2774
+ return {
2775
+ end: () => {
2776
+ syncAll();
2777
+ frames.delete(f);
2778
+ return f;
2779
+ },
2780
+ quiet: (write) => {
2781
+ syncAll();
2782
+ const g = { touched: new Map(), wrote: false };
2783
+ frames.delete(f);
2784
+ frames.add(g);
2785
+ try {
2786
+ write();
2787
+ syncAll();
2788
+ }
2789
+ finally {
2790
+ frames.delete(g);
2791
+ frames.add(f);
2792
+ f.wrote ||= g.wrote;
2793
+ }
2794
+ }
2795
+ };
2796
+ },
2797
+ onReport,
2798
+ debug
2799
+ };
2800
+ return view;
2801
+ };