edytor 5.0.0 → 6.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.
- package/README.md +2 -2
- package/dist/cloudflare/room/context.d.ts +62 -74
- package/dist/crdt/doc/history.js +2 -3
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/edytor-doc.d.ts +156 -156
- package/dist/crdt/edytor-doc.js +14 -42
- package/dist/crdt/flow.d.ts +4 -46
- package/dist/crdt/flow.js +2 -262
- package/dist/crdt/index.d.ts +62 -74
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/providers/index.d.ts +10 -22
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +11 -0
- package/dist/crdt/providers/room.js +26 -12
- package/dist/crdt/providers/websocket.d.ts +6 -12
- package/dist/crdt/providers/websocket.js +8 -15
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/text/index/build.d.ts +125 -0
- package/dist/crdt/text/index/build.js +2895 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +2 -5
- package/dist/crdt/text/model.js +2 -3
- package/dist/crdt/text/runs.d.ts +9 -12
- package/dist/crdt/text/runs.js +2 -165
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/selection/selection.utils.js +5 -8
- package/dist/session/handles.js +3 -7
- package/dist/surface/observer.svelte.js +15 -30
- package/dist/surface/projector.svelte.js +2 -2
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +1 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -207
- package/dist/crdt/doc/content.d.ts +0 -16
- package/dist/crdt/doc/content.js +0 -77
- package/dist/crdt/doc/delete.d.ts +0 -15
- package/dist/crdt/doc/delete.js +0 -84
- package/dist/crdt/doc/layout.d.ts +0 -9
- package/dist/crdt/doc/layout.js +0 -178
- package/dist/crdt/doc/meta.d.ts +0 -19
- package/dist/crdt/doc/meta.js +0 -198
- package/dist/crdt/doc/moves.d.ts +0 -21
- package/dist/crdt/doc/moves.js +0 -274
- package/dist/crdt/doc/split-merge.d.ts +0 -10
- package/dist/crdt/doc/split-merge.js +0 -179
- package/dist/crdt/doc/steps.d.ts +0 -65
- package/dist/crdt/doc/steps.js +0 -311
- package/dist/crdt/text/index/cache.d.ts +0 -16
- package/dist/crdt/text/index/cache.js +0 -175
- package/dist/crdt/text/index/claims.d.ts +0 -18
- package/dist/crdt/text/index/claims.js +0 -203
- package/dist/crdt/text/index/fold.d.ts +0 -21
- package/dist/crdt/text/index/fold.js +0 -656
- package/dist/crdt/text/index/layout.d.ts +0 -22
- package/dist/crdt/text/index/layout.js +0 -258
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -738
- package/dist/crdt/text/index/records.d.ts +0 -13
- package/dist/crdt/text/index/records.js +0 -139
- package/dist/crdt/text/index/report.d.ts +0 -18
- package/dist/crdt/text/index/report.js +0 -337
- package/dist/crdt/text/index/state.d.ts +0 -197
- package/dist/crdt/text/index/state.js +0 -116
- package/dist/crdt/text/index/streams.d.ts +0 -25
- package/dist/crdt/text/index/streams.js +0 -248
|
@@ -0,0 +1,2895 @@
|
|
|
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, clientsOf, engineOps, firstItem, itemOf, queuedTransactions, sequenceLength, structAt, walkIdSet } 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 ops = engineOps(Y);
|
|
1786
|
+
const NO_EDITS = Object.freeze({
|
|
1787
|
+
edits: [],
|
|
1788
|
+
scan: false,
|
|
1789
|
+
opaque: false
|
|
1790
|
+
});
|
|
1791
|
+
const holdsBoundary = (s, from, to) => {
|
|
1792
|
+
const arr = s.content?.arr;
|
|
1793
|
+
if (arr === undefined)
|
|
1794
|
+
return false;
|
|
1795
|
+
for (let k = from - s.id.clock; k < to - s.id.clock; k++)
|
|
1796
|
+
if (isRowMark(arr[k]))
|
|
1797
|
+
return true;
|
|
1798
|
+
return false;
|
|
1799
|
+
};
|
|
1800
|
+
/** The struct holding id `client:clock` (unsplit), `null` when the store holds none. */
|
|
1801
|
+
const structOf = (client, clock) => structAt(Y, clientsOf(doc).get(client) ?? [], clock);
|
|
1802
|
+
/**
|
|
1803
|
+
* The edits the id sets `ins`/`del`, the hold log `log` and the move log
|
|
1804
|
+
* `moves` (what the fold has not folded yet) hold: per text, its edits
|
|
1805
|
+
* (a log entry is a unit a hold record hid or showed, fork patch YP20:
|
|
1806
|
+
* `client, clock, ±len`, in order); per sequence, what changed in it
|
|
1807
|
+
* (`seq`): the slots written and removed, the records written and
|
|
1808
|
+
* removed and the units whose mover changed name the blocks to re-place.
|
|
1809
|
+
* Reading the sequences writes the slot map (the slots written and
|
|
1810
|
+
* removed, `putSlot`) and lists the ids whose home to read again
|
|
1811
|
+
* (`rehome`).
|
|
1812
|
+
*/
|
|
1813
|
+
const makeEditIndex = (ins, del, log = [], moves = []) => {
|
|
1814
|
+
let seqs;
|
|
1815
|
+
const rehome = new Set();
|
|
1816
|
+
const seqOf = (node) => {
|
|
1817
|
+
return getOr(seqs, node, () => ({ blocks: new Set(), data: false }));
|
|
1818
|
+
};
|
|
1819
|
+
/** Note unit `c:k` (`value`) of the sequence of `node`: a block slot names its block, a data slot its data. */
|
|
1820
|
+
const noteUnit = (e, owner, value, c, k, inserted) => {
|
|
1821
|
+
if (isBlockSlot(value)) {
|
|
1822
|
+
e.blocks.add(slotOwner(registry, kept, value.b, c));
|
|
1823
|
+
if (inserted === null || (inserted && owner === undefined))
|
|
1824
|
+
return;
|
|
1825
|
+
putSlot(value.b, { c, k }, inserted ? { parent: owner, c, k } : null);
|
|
1826
|
+
rehome.add(value.b);
|
|
1827
|
+
}
|
|
1828
|
+
else if (isDataSlot(value))
|
|
1829
|
+
e.data = true;
|
|
1830
|
+
};
|
|
1831
|
+
const visitSeq = (s, from, to, inserted) => {
|
|
1832
|
+
const parent = s.parent;
|
|
1833
|
+
if (s.parentSub !== null || !isNodeLike(parent) || parent.name === CONTENT_NODE)
|
|
1834
|
+
return;
|
|
1835
|
+
const e = seqOf(parent);
|
|
1836
|
+
const owner = ownerOfNode(registry, kept, parent);
|
|
1837
|
+
const it = s;
|
|
1838
|
+
if (it.content === undefined)
|
|
1839
|
+
return;
|
|
1840
|
+
if (isRecord(it)) {
|
|
1841
|
+
const ids = it.content.ids;
|
|
1842
|
+
for (let i = 0; i < ids.length; i += 3)
|
|
1843
|
+
for (let j = 0; j < ids[i + 2]; j++) {
|
|
1844
|
+
const u = unitOf(doc, { c: ids[i], k: ids[i + 1] + j });
|
|
1845
|
+
if (u !== null)
|
|
1846
|
+
noteUnit(e, owner, u.value, ids[i], ids[i + 1] + j, null);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
else if (it.content.arr !== undefined)
|
|
1850
|
+
for (let k = from; k < to; k++)
|
|
1851
|
+
noteUnit(e, owner, it.content.arr[k - s.id.clock], s.id.client, k, inserted);
|
|
1852
|
+
};
|
|
1853
|
+
const buildSeqs = () => {
|
|
1854
|
+
if (seqs !== undefined)
|
|
1855
|
+
return seqs;
|
|
1856
|
+
seqs = new Map();
|
|
1857
|
+
if (ins === undefined || del === undefined)
|
|
1858
|
+
return seqs;
|
|
1859
|
+
walkIdSet(Y, doc, ins, (s, from, to) => visitSeq(s, from, to, true));
|
|
1860
|
+
walkIdSet(Y, doc, del, (s, from, to) => visitSeq(s, from, to, false));
|
|
1861
|
+
for (let i = 0; i < moves.length; i += 3) {
|
|
1862
|
+
const s = structOf(moves[i], moves[i + 1]);
|
|
1863
|
+
if (s === null)
|
|
1864
|
+
continue;
|
|
1865
|
+
visitSeq(s, moves[i + 1], moves[i + 1] + moves[i + 2], null);
|
|
1866
|
+
// Where the unit shows now: that sequence changed too.
|
|
1867
|
+
const mover = s.mover;
|
|
1868
|
+
if (mover)
|
|
1869
|
+
seqOf(mover.parent);
|
|
1870
|
+
}
|
|
1871
|
+
return seqs;
|
|
1872
|
+
};
|
|
1873
|
+
let built;
|
|
1874
|
+
const build = () => {
|
|
1875
|
+
if (built !== undefined)
|
|
1876
|
+
return built;
|
|
1877
|
+
if (ins === undefined || del === undefined)
|
|
1878
|
+
return (built = null);
|
|
1879
|
+
const out = new Map();
|
|
1880
|
+
const edit = (s, from, to, deleted) => {
|
|
1881
|
+
const parent = s.parent;
|
|
1882
|
+
if (s.parentSub !== null || !isNodeLike(parent))
|
|
1883
|
+
return;
|
|
1884
|
+
const e = getOr(out, parent, () => ({ edits: [], scan: false, opaque: false }));
|
|
1885
|
+
// A format marker moves no unit; its effect reaches past the edit.
|
|
1886
|
+
if (s.countable !== true)
|
|
1887
|
+
return void (e.opaque = true);
|
|
1888
|
+
if (holdsBoundary(s, from, to))
|
|
1889
|
+
return void (e.scan = true);
|
|
1890
|
+
e.edits.push({ item: s, len: to - from, del: deleted });
|
|
1891
|
+
};
|
|
1892
|
+
const visit = (idSet, deleted) => walkIdSet(Y, doc, idSet, (s, from, to) => {
|
|
1893
|
+
// Written and removed (or held) within the fold: nothing moved.
|
|
1894
|
+
if (!(deleted ? ins.has(s.id.client, from) : s.deleted))
|
|
1895
|
+
edit(s, from, to, deleted);
|
|
1896
|
+
});
|
|
1897
|
+
// In order, after the inserts and before the deletes (each gap then
|
|
1898
|
+
// holds what an edit takes); a unit written within the fold shows or
|
|
1899
|
+
// not as its insert says.
|
|
1900
|
+
const held = () => {
|
|
1901
|
+
for (let i = 0; i < log.length; i += 3) {
|
|
1902
|
+
const s = structOf(log[i], log[i + 1]);
|
|
1903
|
+
if (s === null)
|
|
1904
|
+
return false;
|
|
1905
|
+
const n = log[i + 2];
|
|
1906
|
+
if (!ins.has(log[i], log[i + 1]))
|
|
1907
|
+
edit(s, log[i + 1], log[i + 1] + Math.abs(n), n < 0);
|
|
1908
|
+
}
|
|
1909
|
+
return true;
|
|
1910
|
+
};
|
|
1911
|
+
built = visit(ins, false) && held() && visit(del, true) ? out : null;
|
|
1912
|
+
return built;
|
|
1913
|
+
};
|
|
1914
|
+
return {
|
|
1915
|
+
/** `node`'s edits (`null`: unknown — rescan, every consumer recomputes). */
|
|
1916
|
+
text: (node) => {
|
|
1917
|
+
const map = build();
|
|
1918
|
+
return map === null ? null : (map.get(node) ?? NO_EDITS);
|
|
1919
|
+
},
|
|
1920
|
+
/** What changed in `node`'s sequence (`undefined`: nothing). */
|
|
1921
|
+
seq: (node) => buildSeqs().get(node),
|
|
1922
|
+
/** The sequences the fold changed. */
|
|
1923
|
+
seqNodes: () => buildSeqs().keys(),
|
|
1924
|
+
/** The ids whose home to read again (their slots changed). */
|
|
1925
|
+
rehome
|
|
1926
|
+
};
|
|
1927
|
+
};
|
|
1928
|
+
const runs = (b) => {
|
|
1929
|
+
// Read-your-writes: fold the open transaction's pending writes first.
|
|
1930
|
+
syncAll();
|
|
1931
|
+
if (dirty.has(b) || !cache.has(b))
|
|
1932
|
+
computeRuns(b);
|
|
1933
|
+
return cache.get(b)?.runs ?? EMPTY_RUNS;
|
|
1934
|
+
};
|
|
1935
|
+
// ── the fold ─────────────────────────────────────────────────────
|
|
1936
|
+
/** The owner of the sequence `id`'s slot shows in (its display edge's parent), as the engine stands. */
|
|
1937
|
+
const parentOf = (id) => edgeParent(id);
|
|
1938
|
+
/** Note edits of `id`'s own text (`null`: unknown — rescan, every reader re-reads). */
|
|
1939
|
+
const noteText = (ctx, id, e) => {
|
|
1940
|
+
let t = ctx.texts.get(id);
|
|
1941
|
+
if (t === undefined)
|
|
1942
|
+
ctx.texts.set(id, (t = { edits: [], scan: false, opaque: false, seen: new Set() }));
|
|
1943
|
+
if (e === null) {
|
|
1944
|
+
t.scan = t.opaque = true;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (t.seen.has(e))
|
|
1948
|
+
return;
|
|
1949
|
+
t.seen.add(e);
|
|
1950
|
+
t.scan ||= e.scan;
|
|
1951
|
+
t.opaque ||= e.opaque;
|
|
1952
|
+
for (const x of e.edits)
|
|
1953
|
+
t.edits.push(x);
|
|
1954
|
+
};
|
|
1955
|
+
/** Fold one block's changed facets (`edits`: its own text's, one per changed content node). */
|
|
1956
|
+
const foldBlock = (id, facets, edits, ctx, editsOf) => {
|
|
1957
|
+
const kinds = new Set();
|
|
1958
|
+
for (const f of facets)
|
|
1959
|
+
kinds.add(f === ENTRY_FACET ? 'entry' : facetOf(f));
|
|
1960
|
+
// The display parents a shell may flip by (`settleShells`): read while there are shells.
|
|
1961
|
+
const parentBefore = shells.size > 0 ? parentOf(id) : undefined;
|
|
1962
|
+
if (typeof parentBefore === 'string')
|
|
1963
|
+
ctx.parents.add(parentBefore);
|
|
1964
|
+
if (kinds.has('entry') || kinds.has('structure')) {
|
|
1965
|
+
const [claimsBefore, typeBefore] = [blocks.get(id)?.claims, blocks.get(id)?.type];
|
|
1966
|
+
updateBlockRec(id);
|
|
1967
|
+
// A retype that lands with a structure facet is still a retype.
|
|
1968
|
+
if (typeBefore !== undefined && blocks.get(id)?.type !== typeBefore)
|
|
1969
|
+
retyped = true;
|
|
1970
|
+
// A new entry, a nonce or an own text can move streams.
|
|
1971
|
+
if (kinds.has('entry') || facets.has(NONCE) || facets.has(CONTENT_ATTR)) {
|
|
1972
|
+
ctx.table = true;
|
|
1973
|
+
ctx.named.add(id);
|
|
1974
|
+
noteText(ctx, id, null);
|
|
1975
|
+
}
|
|
1976
|
+
// The key's losing incarnations follow its entry and its liveness.
|
|
1977
|
+
for (const v of syncIncarnations(id)) {
|
|
1978
|
+
ctx.table = true;
|
|
1979
|
+
ctx.named.add(v);
|
|
1980
|
+
noteText(ctx, v, null);
|
|
1981
|
+
invalidateBlock(v, ctx);
|
|
1982
|
+
}
|
|
1983
|
+
invalidateBlock(id, ctx, claimsBefore);
|
|
1984
|
+
}
|
|
1985
|
+
else {
|
|
1986
|
+
ensureRec(id);
|
|
1987
|
+
const rec = blocks.get(id);
|
|
1988
|
+
// A data slot in the block's sequence: its arrays' items changed (the seq pass seeded the blocks).
|
|
1989
|
+
if (kinds.has('seq') && rec && editsOf.seq(rec.node)?.data)
|
|
1990
|
+
kinds.add('meta');
|
|
1991
|
+
if (kinds.has('meta') && rec) {
|
|
1992
|
+
const was = rec.type;
|
|
1993
|
+
const facts = tableFacts(id, rec.data);
|
|
1994
|
+
rec.type = typeAttr(rec.node);
|
|
1995
|
+
rec.data = readData(rec.node);
|
|
1996
|
+
if (rec.type !== was) {
|
|
1997
|
+
retyped = true;
|
|
1998
|
+
noteKind(id);
|
|
1999
|
+
}
|
|
2000
|
+
// A table's columns or a cell's column re-place the table's cells (`table.columns`).
|
|
2001
|
+
if (tableFacts(id, rec.data) !== facts)
|
|
2002
|
+
stateSeeds.add(id);
|
|
2003
|
+
// A retype that changes the kind's display shape re-parents
|
|
2004
|
+
// (or re-kinds) its children.
|
|
2005
|
+
if (roles !== null && !sameShape(roles, was, rec.type))
|
|
2006
|
+
stateSeeds.add(id);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
const parentAfter = shells.size > 0 ? parentOf(id) : undefined;
|
|
2010
|
+
if (typeof parentAfter === 'string')
|
|
2011
|
+
ctx.parents.add(parentAfter);
|
|
2012
|
+
if (kinds.has('content'))
|
|
2013
|
+
for (const e of edits)
|
|
2014
|
+
noteText(ctx, id, e);
|
|
2015
|
+
};
|
|
2016
|
+
/**
|
|
2017
|
+
* Move each edited text's row by its edits (or rescan it), re-decide the
|
|
2018
|
+
* delimiters a boundary change names, re-place the rows they cut, and
|
|
2019
|
+
* invalidate the readers: the display owner of each edited stream,
|
|
2020
|
+
* the blocks whose stream changed, every reader of an opaque text.
|
|
2021
|
+
*/
|
|
2022
|
+
const foldTexts = (ctx) => {
|
|
2023
|
+
const homes = new Set();
|
|
2024
|
+
const gapsOf = new Map();
|
|
2025
|
+
const gapsIn = (row, edits) => {
|
|
2026
|
+
const gaps = [];
|
|
2027
|
+
for (const e of edits) {
|
|
2028
|
+
const gap = gapOfItem(row, e.item);
|
|
2029
|
+
if (gap < 0)
|
|
2030
|
+
return null;
|
|
2031
|
+
gaps.push(gap);
|
|
2032
|
+
}
|
|
2033
|
+
return gaps;
|
|
2034
|
+
};
|
|
2035
|
+
for (const [home, t] of ctx.texts) {
|
|
2036
|
+
const text = blocks.get(home)?.content;
|
|
2037
|
+
const row = rows.get(home);
|
|
2038
|
+
let gaps = [];
|
|
2039
|
+
let fresh = t.scan || row === undefined || text === undefined || row.text !== text;
|
|
2040
|
+
if (!fresh) {
|
|
2041
|
+
for (const e of t.edits) {
|
|
2042
|
+
const gap = gapOfItem(row, e.item);
|
|
2043
|
+
if (gap < 0 || !shiftGap(row, gap, e.del ? -e.len : e.len)) {
|
|
2044
|
+
fresh = true;
|
|
2045
|
+
break;
|
|
2046
|
+
}
|
|
2047
|
+
gaps.push(gap);
|
|
2048
|
+
}
|
|
2049
|
+
// The engine's own count of live units: a row that disagrees is stale.
|
|
2050
|
+
if (!fresh && rowLength(row) !== sequenceLength(text))
|
|
2051
|
+
fresh = true;
|
|
2052
|
+
}
|
|
2053
|
+
if (fresh) {
|
|
2054
|
+
const next = scanRow(home);
|
|
2055
|
+
const same = next !== undefined && row !== undefined && next.key === row.key;
|
|
2056
|
+
if (same)
|
|
2057
|
+
[next.cuts, next.heads, next.head] = [row.cuts, row.heads, row.head];
|
|
2058
|
+
setRow(home, next, ctx.named);
|
|
2059
|
+
if (!same)
|
|
2060
|
+
homes.add(home);
|
|
2061
|
+
gaps = next === undefined ? [] : gapsIn(next, t.edits);
|
|
2062
|
+
}
|
|
2063
|
+
gapsOf.set(home, t.opaque ? null : gaps);
|
|
2064
|
+
}
|
|
2065
|
+
refreshDelims(ctx.named, homes);
|
|
2066
|
+
if (homes.size > 0)
|
|
2067
|
+
ctx.table = true;
|
|
2068
|
+
for (const home of homes)
|
|
2069
|
+
placeHome(home, ctx.invalidated);
|
|
2070
|
+
for (const [home, gaps] of gapsOf) {
|
|
2071
|
+
if (gaps === null) {
|
|
2072
|
+
for (const c of textConsumers.get(home) ?? [])
|
|
2073
|
+
ctx.invalidated.add(c);
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
// Only the stream each edit lies in changes its display.
|
|
2077
|
+
const row = rows.get(home);
|
|
2078
|
+
if (row === undefined)
|
|
2079
|
+
continue;
|
|
2080
|
+
for (const gap of gaps) {
|
|
2081
|
+
const b = headOf(row, segmentOfGap(row, gap));
|
|
2082
|
+
const owner = b === null ? DEAD : ownerOf(b);
|
|
2083
|
+
if (typeof owner === 'string')
|
|
2084
|
+
ctx.invalidated.add(owner);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
/** The blocks whose stream lies in a text the fold edited. */
|
|
2089
|
+
const editedStreams = (ctx) => [...ctx.texts.keys()].flatMap((home) => streamsIn(home).map((st) => st.block));
|
|
2090
|
+
/** A live unit in `id`'s stream (boundaries excepted). */
|
|
2091
|
+
const streamHolds = (id) => {
|
|
2092
|
+
const st = streamOf(id);
|
|
2093
|
+
return st !== undefined && st.end - st.start > st.inert.length;
|
|
2094
|
+
};
|
|
2095
|
+
/** A live unit in `id`'s stream or in the stream of a losing incarnation it shows. */
|
|
2096
|
+
const holdsUnit = (id) => streamHolds(id) || (incarnations.get(id)?.some(streamHolds) ?? false);
|
|
2097
|
+
/**
|
|
2098
|
+
* Settle the withdrawn blocks (`hist.undo.withdraw`): a shell is deleted
|
|
2099
|
+
* iff it holds nothing — no live unit in its stream and no child (a
|
|
2100
|
+
* block whose winning candidate names it) that is itself not deleted.
|
|
2101
|
+
* The least fixpoint, computed over every shell whenever an `affected`
|
|
2102
|
+
* one may flip (one whose stream still holds a unit keeps its answer),
|
|
2103
|
+
* so every replica settles the same whatever the fold order. A flip is
|
|
2104
|
+
* a structural change of the shell.
|
|
2105
|
+
*/
|
|
2106
|
+
const settleShells = (affected, ctx) => {
|
|
2107
|
+
if (shells.size === 0)
|
|
2108
|
+
return;
|
|
2109
|
+
let quiet = true;
|
|
2110
|
+
for (const x of affected) {
|
|
2111
|
+
const id = baseIdOf(x);
|
|
2112
|
+
if (!shells.has(id))
|
|
2113
|
+
continue;
|
|
2114
|
+
if (blocks.get(id)?.deleted !== false || !holdsUnit(id))
|
|
2115
|
+
quiet = false;
|
|
2116
|
+
}
|
|
2117
|
+
if (quiet)
|
|
2118
|
+
return;
|
|
2119
|
+
const alive = new Set();
|
|
2120
|
+
const up = [];
|
|
2121
|
+
const hold = (id) => {
|
|
2122
|
+
if (alive.has(id))
|
|
2123
|
+
return;
|
|
2124
|
+
alive.add(id);
|
|
2125
|
+
up.push(id);
|
|
2126
|
+
};
|
|
2127
|
+
for (const id of shells)
|
|
2128
|
+
if (holdsUnit(id))
|
|
2129
|
+
hold(id);
|
|
2130
|
+
for (const [id, rec] of blocks) {
|
|
2131
|
+
if (shells.has(id) || rec.deleted)
|
|
2132
|
+
continue;
|
|
2133
|
+
const p = parentOf(id);
|
|
2134
|
+
if (typeof p === 'string' && shells.has(p))
|
|
2135
|
+
hold(p);
|
|
2136
|
+
}
|
|
2137
|
+
for (let id = up.pop(); id !== undefined; id = up.pop()) {
|
|
2138
|
+
const p = parentOf(id);
|
|
2139
|
+
if (typeof p === 'string' && shells.has(p))
|
|
2140
|
+
hold(p);
|
|
2141
|
+
}
|
|
2142
|
+
for (const id of shells) {
|
|
2143
|
+
const rec = blocks.get(id);
|
|
2144
|
+
if (rec.deleted === !alive.has(id))
|
|
2145
|
+
continue;
|
|
2146
|
+
rec.deleted = !alive.has(id);
|
|
2147
|
+
ownerSeeds.add(id);
|
|
2148
|
+
// Its losing incarnations follow it.
|
|
2149
|
+
for (const v of incarnations.get(id) ?? []) {
|
|
2150
|
+
const inc = blocks.get(v);
|
|
2151
|
+
if (inc === undefined)
|
|
2152
|
+
continue;
|
|
2153
|
+
const deleted = hasDeleteMark(inc.node) || rec.deleted;
|
|
2154
|
+
if (inc.deleted === deleted)
|
|
2155
|
+
continue;
|
|
2156
|
+
inc.deleted = deleted;
|
|
2157
|
+
ownerSeeds.add(v);
|
|
2158
|
+
if (ctx !== null)
|
|
2159
|
+
invalidateBlock(v, ctx);
|
|
2160
|
+
}
|
|
2161
|
+
if (ctx === null)
|
|
2162
|
+
continue;
|
|
2163
|
+
invalidateBlock(id, ctx);
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
const registryNode = registry;
|
|
2167
|
+
/**
|
|
2168
|
+
* The block a changed `(type, parentSub)` pair belongs to, the facet it
|
|
2169
|
+
* entered through (the block attr the chain hangs under; the `content`
|
|
2170
|
+
* attr itself is {@link CONTENT_ATTR}), and whether it is a sequence
|
|
2171
|
+
* edit of the block's own text. `null` outside the registry subtree.
|
|
2172
|
+
*/
|
|
2173
|
+
const locate = (type, sub) => {
|
|
2174
|
+
if (type === registryNode)
|
|
2175
|
+
return typeof sub === 'string' ? [sub, ENTRY_FACET, false] : null;
|
|
2176
|
+
let below = null;
|
|
2177
|
+
for (let cur = type;;) {
|
|
2178
|
+
const it = cur._item;
|
|
2179
|
+
const parent = it?.parent;
|
|
2180
|
+
if (parent === registryNode) {
|
|
2181
|
+
let id = it.parentSub;
|
|
2182
|
+
if (typeof id !== 'string')
|
|
2183
|
+
return null;
|
|
2184
|
+
// A losing incarnation's subtree: its derived id; a seed's shows nothing.
|
|
2185
|
+
if (it.deleted && kept(it)) {
|
|
2186
|
+
const item = itemOf(cur);
|
|
2187
|
+
if (item.id.client < LIVE_WRITERS)
|
|
2188
|
+
return null;
|
|
2189
|
+
id = incarnationId(id, item);
|
|
2190
|
+
}
|
|
2191
|
+
// The block's own sequence (its slots and records: `SEQ_FACET`), or one of its attrs.
|
|
2192
|
+
if (below === null)
|
|
2193
|
+
return [id, sub === CONTENT ? CONTENT_ATTR : sub === null ? SEQ_FACET : sub, false];
|
|
2194
|
+
const facet = below._item?.parentSub ?? CONTENT;
|
|
2195
|
+
return [id, facet, sub === null && below === type && facet === CONTENT];
|
|
2196
|
+
}
|
|
2197
|
+
if (!isNodeLike(parent))
|
|
2198
|
+
return null;
|
|
2199
|
+
below = cur;
|
|
2200
|
+
cur = parent;
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
/** THE fold: one pass over changed `(type, parentSub)` pairs, each block folded once. */
|
|
2204
|
+
const fold = (changed, editsOf) => {
|
|
2205
|
+
const touched = new Map();
|
|
2206
|
+
const edits = new Map();
|
|
2207
|
+
let derived = changed.has(dataRoot);
|
|
2208
|
+
foldStats.folds++;
|
|
2209
|
+
for (const [type, subs] of changed) {
|
|
2210
|
+
foldStats.pairs += subs.size;
|
|
2211
|
+
for (const sub of subs) {
|
|
2212
|
+
const hit = locate(type, sub);
|
|
2213
|
+
if (hit === null)
|
|
2214
|
+
continue;
|
|
2215
|
+
const [id, facet, seq] = hit;
|
|
2216
|
+
const facets = getOr(touched, id, () => new Set());
|
|
2217
|
+
facets.add(facet);
|
|
2218
|
+
if (facet !== CONTENT)
|
|
2219
|
+
continue;
|
|
2220
|
+
// A sequence edit of the block's own text, or a change inside one of its atoms.
|
|
2221
|
+
const list = getOr(edits, id, () => []);
|
|
2222
|
+
list.push(seq ? editsOf.text(type) : null);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
const ctx = {
|
|
2226
|
+
invalidated: new Set(),
|
|
2227
|
+
texts: new Map(),
|
|
2228
|
+
named: new Set(),
|
|
2229
|
+
table: false,
|
|
2230
|
+
parents: new Set()
|
|
2231
|
+
};
|
|
2232
|
+
// The sequences (the root's, the blocks'): their slots index the blocks' homes, their changes seed the placements.
|
|
2233
|
+
for (const node of editsOf.seqNodes()) {
|
|
2234
|
+
const ch = editsOf.seq(node);
|
|
2235
|
+
seqEpoch++;
|
|
2236
|
+
for (const b of ch.blocks)
|
|
2237
|
+
placementSeeds.add(b);
|
|
2238
|
+
if (ch.blocks.size > 0 || ch.data)
|
|
2239
|
+
derived = true;
|
|
2240
|
+
const owner = ownerOfNode(registry, kept, node);
|
|
2241
|
+
if (typeof owner === 'string') {
|
|
2242
|
+
const facets = getOr(touched, owner, () => new Set());
|
|
2243
|
+
facets.add(SEQ_FACET);
|
|
2244
|
+
if (ch.data)
|
|
2245
|
+
facets.add(DATA_SEQ_FACET);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
// A slot's block, and its losing incarnations, re-read their home (which slot is whose).
|
|
2249
|
+
for (const b of editsOf.rehome)
|
|
2250
|
+
for (const id of [b, ...(incarnations.get(b) ?? [])]) {
|
|
2251
|
+
const rec = blocks.get(id);
|
|
2252
|
+
if (rec === undefined)
|
|
2253
|
+
continue;
|
|
2254
|
+
const home = homeFor(id);
|
|
2255
|
+
if (home?.c !== rec.home?.c || home?.k !== rec.home?.k)
|
|
2256
|
+
placementSeeds.add(id);
|
|
2257
|
+
rec.home = home;
|
|
2258
|
+
}
|
|
2259
|
+
for (const [id, facets] of touched) {
|
|
2260
|
+
foldBlock(id, facets, edits.get(id) ?? [], ctx, editsOf);
|
|
2261
|
+
derived ||= [...facets].some((f) => f === ENTRY_FACET || facetOf(f) !== 'ignore');
|
|
2262
|
+
}
|
|
2263
|
+
foldTexts(ctx);
|
|
2264
|
+
if (reclaim.size > 0)
|
|
2265
|
+
applyReclaims(ctx);
|
|
2266
|
+
if (shells.size > 0)
|
|
2267
|
+
settleShells(ctx.table ? shells : [...touched.keys(), ...ctx.parents, ...editedStreams(ctx)], ctx);
|
|
2268
|
+
for (const b of ctx.invalidated)
|
|
2269
|
+
dirty.add(b);
|
|
2270
|
+
if (indexChecks.on)
|
|
2271
|
+
checker().check();
|
|
2272
|
+
if (derived)
|
|
2273
|
+
version++;
|
|
2274
|
+
if (reporting) {
|
|
2275
|
+
for (const id of touched.keys())
|
|
2276
|
+
candidates.add(id);
|
|
2277
|
+
for (const id of ctx.invalidated)
|
|
2278
|
+
candidates.add(id);
|
|
2279
|
+
}
|
|
2280
|
+
for (const f of frames) {
|
|
2281
|
+
for (const [id, facets] of touched) {
|
|
2282
|
+
const into = f.touched.get(id);
|
|
2283
|
+
if (into === undefined)
|
|
2284
|
+
f.touched.set(id, new Set(facets));
|
|
2285
|
+
else
|
|
2286
|
+
for (const x of facets)
|
|
2287
|
+
into.add(x);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return touched;
|
|
2291
|
+
};
|
|
2292
|
+
/** Each transaction's folded part: copies of its id sets, the hold and move logs' folded lengths, and the watermark. */
|
|
2293
|
+
const cursors = new WeakMap();
|
|
2294
|
+
/** The transactions whose commit fold ran. */
|
|
2295
|
+
const committed = new WeakSet();
|
|
2296
|
+
const lengthOf = (set) => {
|
|
2297
|
+
let n = 0;
|
|
2298
|
+
set.clients.forEach((ranges) => {
|
|
2299
|
+
for (const r of ranges.getIds())
|
|
2300
|
+
n += r.len;
|
|
2301
|
+
});
|
|
2302
|
+
return n;
|
|
2303
|
+
};
|
|
2304
|
+
/** How much a transaction wrote so far: its id sets' lengths and its hold and move logs'. */
|
|
2305
|
+
const watermark = (tr) => lengthOf(tr.insertSet) +
|
|
2306
|
+
lengthOf(tr.deleteSet) +
|
|
2307
|
+
(tr.holds?.length ?? 0) +
|
|
2308
|
+
(tr.moves?.length ?? 0);
|
|
2309
|
+
/**
|
|
2310
|
+
* Fold the pending part of `tr` — the structs its insert/delete sets
|
|
2311
|
+
* gained since the last fold — before a read inside it (read-your-
|
|
2312
|
+
* writes; `transaction.changed` does not grow on a second edit to an
|
|
2313
|
+
* already-changed type).
|
|
2314
|
+
*/
|
|
2315
|
+
const syncPending = (tr) => {
|
|
2316
|
+
if (tr?.insertSet === undefined || tr.deleteSet === undefined)
|
|
2317
|
+
return false;
|
|
2318
|
+
let cursor = cursors.get(tr);
|
|
2319
|
+
if (cursor === undefined)
|
|
2320
|
+
cursors.set(tr, (cursor = { ins: ops.idSet(), del: ops.idSet(), holds: 0, moves: 0, mark: 0 }));
|
|
2321
|
+
const mark = watermark(tr);
|
|
2322
|
+
if (mark === cursor.mark)
|
|
2323
|
+
return false;
|
|
2324
|
+
const ins = ops.diff(tr.insertSet, cursor.ins);
|
|
2325
|
+
const del = ops.diff(tr.deleteSet, cursor.del);
|
|
2326
|
+
const log = (tr.holds ?? []).slice(cursor.holds);
|
|
2327
|
+
const moves = (tr.moves ?? []).slice(cursor.moves);
|
|
2328
|
+
ops.insertInto(cursor.ins, ins);
|
|
2329
|
+
ops.insertInto(cursor.del, del);
|
|
2330
|
+
cursor.holds += log.length;
|
|
2331
|
+
cursor.moves += moves.length;
|
|
2332
|
+
cursor.mark = mark;
|
|
2333
|
+
for (const f of frames)
|
|
2334
|
+
f.wrote = true;
|
|
2335
|
+
const changed = new Map();
|
|
2336
|
+
const note = (s) => {
|
|
2337
|
+
foldStats.structs++;
|
|
2338
|
+
if (!isNodeLike(s.parent))
|
|
2339
|
+
return;
|
|
2340
|
+
const subs = getOr(changed, s.parent, () => new Set());
|
|
2341
|
+
subs.add(s.parentSub);
|
|
2342
|
+
};
|
|
2343
|
+
walkIdSet(Y, doc, ins, note);
|
|
2344
|
+
walkIdSet(Y, doc, del, note);
|
|
2345
|
+
for (const l of [log, moves])
|
|
2346
|
+
for (let i = 0; i < l.length; i += 3) {
|
|
2347
|
+
const s = structOf(l[i], l[i + 1]);
|
|
2348
|
+
if (s === null)
|
|
2349
|
+
continue;
|
|
2350
|
+
note(s);
|
|
2351
|
+
// A unit a record moved: where it shows now changed too (a held unit has no mover).
|
|
2352
|
+
const mover = s.mover;
|
|
2353
|
+
if (mover)
|
|
2354
|
+
note(mover);
|
|
2355
|
+
}
|
|
2356
|
+
fold(changed, makeEditIndex(ins, del, log, moves));
|
|
2357
|
+
return true;
|
|
2358
|
+
};
|
|
2359
|
+
const openTx = () => doc._transaction;
|
|
2360
|
+
/**
|
|
2361
|
+
* Fold every transaction whose writes are in the document and whose
|
|
2362
|
+
* commit fold has not run, in their order: the open one, and those a
|
|
2363
|
+
* transaction's cleanup started (an observer's or an `update`
|
|
2364
|
+
* listener's write runs its body at once, its observers later) — a
|
|
2365
|
+
* read in between must not mix their items with an index that never
|
|
2366
|
+
* saw them.
|
|
2367
|
+
*/
|
|
2368
|
+
const syncAll = () => {
|
|
2369
|
+
for (const tr of queuedTransactions(doc))
|
|
2370
|
+
if (!committed.has(tr))
|
|
2371
|
+
syncPending(tr);
|
|
2372
|
+
};
|
|
2373
|
+
/** Some queued transaction holds writes no fold has seen. */
|
|
2374
|
+
const unfolded = () => queuedTransactions(doc).some((tr) => !committed.has(tr) &&
|
|
2375
|
+
tr.insertSet !== undefined &&
|
|
2376
|
+
tr.deleteSet !== undefined &&
|
|
2377
|
+
watermark(tr) !== (cursors.get(tr)?.mark ?? 0));
|
|
2378
|
+
/**
|
|
2379
|
+
* The commit of a transaction that changed the registry or a node under
|
|
2380
|
+
* it: fold `transaction.changed` once (the report publishes from `update`).
|
|
2381
|
+
*/
|
|
2382
|
+
const onCommit = (tr) => {
|
|
2383
|
+
if (!changedUnder(tr, [registry]))
|
|
2384
|
+
return;
|
|
2385
|
+
let [ins, del] = [tr.insertSet, tr.deleteSet];
|
|
2386
|
+
// The writes a read folded already moved the rows: only the rest
|
|
2387
|
+
// are edits now (the facets fold again, idempotent).
|
|
2388
|
+
const cursor = cursors.get(tr);
|
|
2389
|
+
if (cursor !== undefined && ins !== undefined && del !== undefined) {
|
|
2390
|
+
ins = ops.diff(ins, cursor.ins);
|
|
2391
|
+
del = ops.diff(del, cursor.del);
|
|
2392
|
+
}
|
|
2393
|
+
const log = (tr.holds ?? []).slice(cursor?.holds ?? 0);
|
|
2394
|
+
const moves = (tr.moves ?? []).slice(cursor?.moves ?? 0);
|
|
2395
|
+
cursors.delete(tr);
|
|
2396
|
+
committed.add(tr);
|
|
2397
|
+
fold(tr.changed ?? new Map(), makeEditIndex(ins, del, log, moves));
|
|
2398
|
+
};
|
|
2399
|
+
// ══ The change report ═══════════════════════════════════════════
|
|
2400
|
+
// The change report: the fold against the last published index, one per
|
|
2401
|
+
// commit that changed the visible document, and the projection it carries.
|
|
2402
|
+
const docData = () => cloneJsonSafe(readData(dataRoot) ?? {});
|
|
2403
|
+
let dataChanged = false;
|
|
2404
|
+
// ── projection ───────────────────────────────────────────────────
|
|
2405
|
+
/** `id`'s visible content items: its runs, as fresh wrappers over their shared payloads. */
|
|
2406
|
+
const itemsOf = (id) => runs(id).map((r) => ({ ...r }));
|
|
2407
|
+
const projectBlock = (id) => {
|
|
2408
|
+
const rec = blocks.get(id);
|
|
2409
|
+
const projected = {
|
|
2410
|
+
id,
|
|
2411
|
+
type: typeOf(id),
|
|
2412
|
+
data: dataOf(rec),
|
|
2413
|
+
content: itemsOf(id),
|
|
2414
|
+
children: []
|
|
2415
|
+
};
|
|
2416
|
+
for (const k of kidsMap.get(id) ?? [])
|
|
2417
|
+
projected.children.push(projectBlock(k.id));
|
|
2418
|
+
return projected;
|
|
2419
|
+
};
|
|
2420
|
+
// ── the change report: the fold against the last published index ─
|
|
2421
|
+
let published = null;
|
|
2422
|
+
const reportSubs = new Set();
|
|
2423
|
+
/** The reachable tree — nodes carry `prev`'s published payloads where they exist. */
|
|
2424
|
+
const reachable = (prev) => {
|
|
2425
|
+
ensurePlacements();
|
|
2426
|
+
const nodes = new Map();
|
|
2427
|
+
const order = new Map();
|
|
2428
|
+
const stack = [null];
|
|
2429
|
+
for (let parent = stack.pop(); parent !== undefined; parent = stack.pop()) {
|
|
2430
|
+
const ks = kidsMap.get(parent) ?? [];
|
|
2431
|
+
if (ks.length === 0)
|
|
2432
|
+
continue;
|
|
2433
|
+
order.set(parent, Object.freeze(ks.map((k) => k.id)));
|
|
2434
|
+
for (let index = ks.length - 1; index >= 0; index--)
|
|
2435
|
+
stack.push(ks[index].id);
|
|
2436
|
+
ks.forEach(({ id }, index) => {
|
|
2437
|
+
const old = prev?.get(id);
|
|
2438
|
+
const rec = blocks.get(id);
|
|
2439
|
+
nodes.set(id, old === undefined
|
|
2440
|
+
? { parent, index, type: typeOf(id), data: rec.data, runs: runs(id) }
|
|
2441
|
+
: { ...old, parent, index });
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
return { nodes, order, kids: kidsVersion };
|
|
2445
|
+
};
|
|
2446
|
+
/**
|
|
2447
|
+
* The report's structural part from the child lists patched since the
|
|
2448
|
+
* last report, advancing the published tree in place — what
|
|
2449
|
+
* `reachable` would give, without walking the lists nothing changed:
|
|
2450
|
+
* a published block no longer shown leaves (a root when its parent
|
|
2451
|
+
* stays), its still-shown descendants staying; each patched list's
|
|
2452
|
+
* new order is news, a block in it at a new parent or index moved, one
|
|
2453
|
+
* not published before is added with its subtree.
|
|
2454
|
+
*/
|
|
2455
|
+
const advance = (pub, r, covered, reparented) => {
|
|
2456
|
+
const { nodes, order } = pub;
|
|
2457
|
+
const shown = (x) => {
|
|
2458
|
+
for (let c = x; c !== null;) {
|
|
2459
|
+
const slot = slots.get(c);
|
|
2460
|
+
if (slot === undefined)
|
|
2461
|
+
return false;
|
|
2462
|
+
c = slot.parent;
|
|
2463
|
+
}
|
|
2464
|
+
return true;
|
|
2465
|
+
};
|
|
2466
|
+
const lists = [...dirtyLists];
|
|
2467
|
+
// Leaving: a published block of a patched list that no longer shows.
|
|
2468
|
+
const drop = (id) => {
|
|
2469
|
+
const ks = order.get(id);
|
|
2470
|
+
if (ks !== undefined) {
|
|
2471
|
+
order.delete(id);
|
|
2472
|
+
r.order.set(id, EMPTY_IDS);
|
|
2473
|
+
for (const k of ks)
|
|
2474
|
+
if (!shown(k))
|
|
2475
|
+
drop(k);
|
|
2476
|
+
}
|
|
2477
|
+
nodes.delete(id);
|
|
2478
|
+
};
|
|
2479
|
+
for (const id of leftLists) {
|
|
2480
|
+
const o = nodes.get(id);
|
|
2481
|
+
if (o === undefined || shown(id))
|
|
2482
|
+
continue;
|
|
2483
|
+
if (o.parent === null || shown(o.parent))
|
|
2484
|
+
r.removed.add(id);
|
|
2485
|
+
drop(id);
|
|
2486
|
+
}
|
|
2487
|
+
// Each patched list that shows: its order, its moved and new blocks.
|
|
2488
|
+
const fresh = new Map();
|
|
2489
|
+
for (const p of lists) {
|
|
2490
|
+
if (p !== null && !shown(p))
|
|
2491
|
+
continue;
|
|
2492
|
+
const ks = kidsMap.get(p) ?? [];
|
|
2493
|
+
const prev = order.get(p);
|
|
2494
|
+
if (ks.length === 0) {
|
|
2495
|
+
if (prev !== undefined) {
|
|
2496
|
+
order.delete(p);
|
|
2497
|
+
r.order.set(p, EMPTY_IDS);
|
|
2498
|
+
}
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
const ids = ks.map((k) => k.id);
|
|
2502
|
+
if (prev === undefined || !sameIds(prev, ids)) {
|
|
2503
|
+
const frozen = Object.freeze(ids);
|
|
2504
|
+
order.set(p, frozen);
|
|
2505
|
+
r.order.set(p, frozen);
|
|
2506
|
+
}
|
|
2507
|
+
ks.forEach(({ id }, index) => {
|
|
2508
|
+
const n = nodes.get(id);
|
|
2509
|
+
if (n === undefined)
|
|
2510
|
+
fresh.set(id, { parent: p, index });
|
|
2511
|
+
else if (n.parent !== p || n.index !== index) {
|
|
2512
|
+
r.moved.add(id);
|
|
2513
|
+
if (n.parent !== p)
|
|
2514
|
+
reparented.add(id);
|
|
2515
|
+
n.parent = p;
|
|
2516
|
+
n.index = index;
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
// New roots (no new ancestor) bring their subtree.
|
|
2521
|
+
const add = (b, parent, index) => {
|
|
2522
|
+
if (!nodes.has(b.id)) {
|
|
2523
|
+
covered.add(b.id);
|
|
2524
|
+
const rec = blocks.get(b.id);
|
|
2525
|
+
nodes.set(b.id, { parent, index, type: typeOf(b.id), data: rec.data, runs: runs(b.id) });
|
|
2526
|
+
}
|
|
2527
|
+
if (b.children.length > 0 && !order.has(b.id)) {
|
|
2528
|
+
const ids = Object.freeze(b.children.map((c) => c.id));
|
|
2529
|
+
order.set(b.id, ids);
|
|
2530
|
+
r.order.set(b.id, ids);
|
|
2531
|
+
}
|
|
2532
|
+
b.children.forEach((c, i) => add(c, b.id, i));
|
|
2533
|
+
};
|
|
2534
|
+
for (const [id, at] of fresh) {
|
|
2535
|
+
let under = false;
|
|
2536
|
+
for (let c = slots.get(id)?.parent ?? null; c !== null && !under; c = slots.get(c)?.parent ?? null)
|
|
2537
|
+
under = fresh.has(c);
|
|
2538
|
+
if (under || covered.has(id))
|
|
2539
|
+
continue;
|
|
2540
|
+
const b = projectBlock(id);
|
|
2541
|
+
r.added.set(id, b);
|
|
2542
|
+
add(b, at.parent, at.index);
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
/** The blocks whose shown kind followed their slot at the last report. */
|
|
2546
|
+
let followingBefore = [];
|
|
2547
|
+
/** The document data's key at the last report. */
|
|
2548
|
+
let dataKey = '';
|
|
2549
|
+
/** Build the commit's report and advance the published index to it. */
|
|
2550
|
+
const report = () => {
|
|
2551
|
+
const before = published;
|
|
2552
|
+
// Fold every queued transaction first: a follow-up a cleanup
|
|
2553
|
+
// started (a delete-mark repair) has already written, and the
|
|
2554
|
+
// first content read below would fold it mid-report, after the
|
|
2555
|
+
// child lists were read (found by the purge fuzz, rich lane, seeds 651 and 1271).
|
|
2556
|
+
syncAll();
|
|
2557
|
+
ensurePlacements();
|
|
2558
|
+
const r = {
|
|
2559
|
+
added: new Map(),
|
|
2560
|
+
removed: new Set(),
|
|
2561
|
+
moved: new Set(),
|
|
2562
|
+
meta: new Map(),
|
|
2563
|
+
content: new Map(),
|
|
2564
|
+
order: new Map()
|
|
2565
|
+
};
|
|
2566
|
+
// The document's data is news when it differs from what was published.
|
|
2567
|
+
if (dataChanged) {
|
|
2568
|
+
dataChanged = false;
|
|
2569
|
+
const data = docData();
|
|
2570
|
+
const key = keyOf(data);
|
|
2571
|
+
if (key !== dataKey)
|
|
2572
|
+
[r.data, dataKey] = [data, key];
|
|
2573
|
+
}
|
|
2574
|
+
if (kidsVersion === before.kids && candidates.size === 0)
|
|
2575
|
+
return r.data ? r : null;
|
|
2576
|
+
// Added subtrees carry their new descendants. A descendant that was
|
|
2577
|
+
// visible before is reported like any visible block (moved, retyped,
|
|
2578
|
+
// edited against its published baseline), so consumers keep it.
|
|
2579
|
+
const covered = new Set();
|
|
2580
|
+
/** Moved to another display parent: only those can show another kind (an index never does). */
|
|
2581
|
+
const reparented = new Set();
|
|
2582
|
+
const lists = kidsVersion !== before.kids && !allListsDirty;
|
|
2583
|
+
const after = kidsVersion === before.kids || lists ? before : reachable(before.nodes);
|
|
2584
|
+
if (lists)
|
|
2585
|
+
advance(before, r, covered, reparented);
|
|
2586
|
+
else if (after !== before) {
|
|
2587
|
+
for (const [parent, ids] of after.order) {
|
|
2588
|
+
const prev = before.order.get(parent);
|
|
2589
|
+
if (prev === undefined || !sameIds(prev, ids))
|
|
2590
|
+
r.order.set(parent, ids);
|
|
2591
|
+
}
|
|
2592
|
+
for (const parent of before.order.keys()) {
|
|
2593
|
+
if (!after.order.has(parent))
|
|
2594
|
+
r.order.set(parent, EMPTY_IDS);
|
|
2595
|
+
}
|
|
2596
|
+
const register = (b) => {
|
|
2597
|
+
if (!before.nodes.has(b.id))
|
|
2598
|
+
covered.add(b.id);
|
|
2599
|
+
b.children.forEach(register);
|
|
2600
|
+
};
|
|
2601
|
+
for (const [id, n] of after.nodes) {
|
|
2602
|
+
if (!before.nodes.has(id)) {
|
|
2603
|
+
if (covered.has(id))
|
|
2604
|
+
continue;
|
|
2605
|
+
const b = projectBlock(id);
|
|
2606
|
+
r.added.set(id, b);
|
|
2607
|
+
register(b);
|
|
2608
|
+
}
|
|
2609
|
+
else {
|
|
2610
|
+
const o = before.nodes.get(id);
|
|
2611
|
+
if (o.parent !== n.parent || o.index !== n.index)
|
|
2612
|
+
r.moved.add(id);
|
|
2613
|
+
if (o.parent !== n.parent)
|
|
2614
|
+
reparented.add(id);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
// Removed subtree ROOTS: a removed id whose before-parent stays
|
|
2618
|
+
// visible (or is the root). One under a removed parent leaves with
|
|
2619
|
+
// it; one under a surviving child of a removed subtree does not.
|
|
2620
|
+
for (const [id, o] of before.nodes) {
|
|
2621
|
+
if (after.nodes.has(id))
|
|
2622
|
+
continue;
|
|
2623
|
+
if (o.parent === null || after.nodes.has(o.parent))
|
|
2624
|
+
r.removed.add(id);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
const meta = (id, n) => {
|
|
2628
|
+
const rec = blocks.get(id);
|
|
2629
|
+
const type = typeOf(id);
|
|
2630
|
+
r.meta.set(id, { type, data: dataOf(rec) });
|
|
2631
|
+
n.type = type;
|
|
2632
|
+
n.data = rec.data;
|
|
2633
|
+
};
|
|
2634
|
+
for (const id of candidates) {
|
|
2635
|
+
const n = after.nodes.get(id);
|
|
2636
|
+
if (n === undefined || covered.has(id))
|
|
2637
|
+
continue;
|
|
2638
|
+
const rec = blocks.get(id);
|
|
2639
|
+
if (typeOf(id) !== n.type || keyOf(rec.data) !== keyOf(n.data))
|
|
2640
|
+
meta(id, n);
|
|
2641
|
+
const next = runs(id);
|
|
2642
|
+
if (next !== n.runs) {
|
|
2643
|
+
const key = keyOf(next);
|
|
2644
|
+
if ((n.key ??= keyOf(n.runs)) !== key)
|
|
2645
|
+
r.content.set(id, next);
|
|
2646
|
+
n.runs = next;
|
|
2647
|
+
n.key = key;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
// A block that starts or stops displaying a kind other than its
|
|
2651
|
+
// stored one moved, or its kind follows (or followed) its slot:
|
|
2652
|
+
// its kind is news. Without a placement change, only a retype
|
|
2653
|
+
// changes a shown kind — the retyped block's (a candidate) and the
|
|
2654
|
+
// kinds derived from it: a promoted or stray line shows its display
|
|
2655
|
+
// parent's default child.
|
|
2656
|
+
if (after !== before || lists || retyped) {
|
|
2657
|
+
const now = [...following];
|
|
2658
|
+
for (const id of [...reparented, ...now, ...followingBefore]) {
|
|
2659
|
+
const n = after.nodes.get(id);
|
|
2660
|
+
if (n !== undefined && !candidates.has(id) && !covered.has(id) && typeOf(id) !== n.type)
|
|
2661
|
+
meta(id, n);
|
|
2662
|
+
}
|
|
2663
|
+
followingBefore = now;
|
|
2664
|
+
}
|
|
2665
|
+
// A plain block directly in a list shows as its item (`shownItem`):
|
|
2666
|
+
// a block whose shown kind changed re-reads its children's (a worklist:
|
|
2667
|
+
// the map visits the entries added meanwhile).
|
|
2668
|
+
for (const id of r.meta.keys())
|
|
2669
|
+
for (const { id: kid } of kidsMap.get(id) ?? []) {
|
|
2670
|
+
const n = after.nodes.get(kid);
|
|
2671
|
+
if (n !== undefined && !covered.has(kid) && !r.meta.has(kid) && typeOf(kid) !== n.type)
|
|
2672
|
+
meta(kid, n);
|
|
2673
|
+
}
|
|
2674
|
+
retyped = false;
|
|
2675
|
+
candidates.clear();
|
|
2676
|
+
published = after;
|
|
2677
|
+
after.kids = kidsVersion;
|
|
2678
|
+
dirtyLists.clear();
|
|
2679
|
+
leftLists.clear();
|
|
2680
|
+
allListsDirty = false;
|
|
2681
|
+
if (indexChecks.on)
|
|
2682
|
+
checker().checkPublished(published);
|
|
2683
|
+
const empty = r.added.size + r.removed.size + r.moved.size + r.meta.size + r.content.size + r.order.size;
|
|
2684
|
+
return empty === 0 && !r.data ? null : r;
|
|
2685
|
+
};
|
|
2686
|
+
/** Report the commit (or role change) to every subscriber, when it changed the visible document. */
|
|
2687
|
+
const publish = (origin, local) => {
|
|
2688
|
+
const r = report();
|
|
2689
|
+
if (r !== null)
|
|
2690
|
+
callEach('[edytor-doc] change', [...reportSubs], r, origin, local);
|
|
2691
|
+
};
|
|
2692
|
+
const onUpdate = (_u, origin, _d, tr) => {
|
|
2693
|
+
if (tr.changed?.has(dataRoot))
|
|
2694
|
+
dataChanged = true;
|
|
2695
|
+
publish(origin, tr.local === true);
|
|
2696
|
+
};
|
|
2697
|
+
/** Subscribe to the change report; the first subscription takes the baseline. */
|
|
2698
|
+
const onReport = (cb) => {
|
|
2699
|
+
if (reportSubs.size === 0) {
|
|
2700
|
+
syncAll();
|
|
2701
|
+
published = reachable();
|
|
2702
|
+
dataKey = keyOf(docData());
|
|
2703
|
+
candidates.clear();
|
|
2704
|
+
reporting = true;
|
|
2705
|
+
doc.on('update', onUpdate);
|
|
2706
|
+
}
|
|
2707
|
+
reportSubs.add(cb);
|
|
2708
|
+
return () => {
|
|
2709
|
+
if (!reportSubs.delete(cb) || reportSubs.size > 0)
|
|
2710
|
+
return;
|
|
2711
|
+
doc.off('update', onUpdate);
|
|
2712
|
+
published = null;
|
|
2713
|
+
reporting = false;
|
|
2714
|
+
candidates.clear();
|
|
2715
|
+
};
|
|
2716
|
+
};
|
|
2717
|
+
const hasSubscribers = () => reportSubs.size > 0;
|
|
2718
|
+
// ── the self-checks (`indexChecks`), built on their first run: a rebuild
|
|
2719
|
+
// from the replicated state each fact this index maintains must equal
|
|
2720
|
+
let checks;
|
|
2721
|
+
const checker = () => (checks ??= indexChecks.of({
|
|
2722
|
+
doc,
|
|
2723
|
+
registry,
|
|
2724
|
+
blocks,
|
|
2725
|
+
displaysMap,
|
|
2726
|
+
rows,
|
|
2727
|
+
streamIx,
|
|
2728
|
+
get delim() {
|
|
2729
|
+
return delim;
|
|
2730
|
+
},
|
|
2731
|
+
placementsMap,
|
|
2732
|
+
slotsByBlock,
|
|
2733
|
+
edges,
|
|
2734
|
+
rejected,
|
|
2735
|
+
get kids0() {
|
|
2736
|
+
return kids0;
|
|
2737
|
+
},
|
|
2738
|
+
get kidsMap() {
|
|
2739
|
+
return kidsMap;
|
|
2740
|
+
},
|
|
2741
|
+
get slots() {
|
|
2742
|
+
return slots;
|
|
2743
|
+
},
|
|
2744
|
+
get dissolved() {
|
|
2745
|
+
return dissolved;
|
|
2746
|
+
},
|
|
2747
|
+
following,
|
|
2748
|
+
cache,
|
|
2749
|
+
dirty,
|
|
2750
|
+
ownShim,
|
|
2751
|
+
own0,
|
|
2752
|
+
ownerOf,
|
|
2753
|
+
streamOf,
|
|
2754
|
+
sameClaims,
|
|
2755
|
+
dissolve,
|
|
2756
|
+
noteFollowing,
|
|
2757
|
+
ensurePlacements,
|
|
2758
|
+
computeFresh,
|
|
2759
|
+
unfolded,
|
|
2760
|
+
reachable
|
|
2761
|
+
}));
|
|
2762
|
+
// ── the view ──────────────────────────────────────────────────
|
|
2763
|
+
const ctx = {
|
|
2764
|
+
blocks,
|
|
2765
|
+
own: ownShim,
|
|
2766
|
+
get placements() {
|
|
2767
|
+
ensurePlacements();
|
|
2768
|
+
return placementsMap;
|
|
2769
|
+
},
|
|
2770
|
+
get kids() {
|
|
2771
|
+
ensurePlacements();
|
|
2772
|
+
return kidsMap;
|
|
2773
|
+
},
|
|
2774
|
+
get order() {
|
|
2775
|
+
ensurePlacements();
|
|
2776
|
+
return order();
|
|
2777
|
+
},
|
|
2778
|
+
// The publication boundary shares THIS interner, so a payload
|
|
2779
|
+
// emitted by `project()`/`contentItems()` is `===` the runs' one.
|
|
2780
|
+
intern,
|
|
2781
|
+
displays,
|
|
2782
|
+
accepted: (id) => {
|
|
2783
|
+
ensurePlacements();
|
|
2784
|
+
return rejected.has(id) ? null : (edges.get(id) ?? null);
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
// ── initial scan: index every existing block ────────────────────
|
|
2788
|
+
registry.forEachAttr((v, id) => {
|
|
2789
|
+
if (!isNodeLike(v))
|
|
2790
|
+
return;
|
|
2791
|
+
updateBlockRec(id);
|
|
2792
|
+
for (const b of [id, ...syncIncarnations(id)]) {
|
|
2793
|
+
const row = scanRow(b);
|
|
2794
|
+
if (row !== undefined) {
|
|
2795
|
+
rows.set(b, row);
|
|
2796
|
+
homeOfText.set(row.text, b);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
rebuildTable(new Set());
|
|
2801
|
+
applyReclaims(null);
|
|
2802
|
+
settleShells(shells, null);
|
|
2803
|
+
// Every commit folds once, before the document's `afterTransaction` listeners.
|
|
2804
|
+
doc.on('commit', onCommit);
|
|
2805
|
+
/** Derived state lives exactly as long as the doc. */
|
|
2806
|
+
const teardown = () => {
|
|
2807
|
+
doc.off('commit', onCommit);
|
|
2808
|
+
doc.off('destroy', teardown);
|
|
2809
|
+
doc.off('update', onUpdate);
|
|
2810
|
+
indexes.delete(doc);
|
|
2811
|
+
};
|
|
2812
|
+
doc.on('destroy', teardown);
|
|
2813
|
+
// ── public surface ───────────────────────────────────────────────
|
|
2814
|
+
const view = {
|
|
2815
|
+
version: () => {
|
|
2816
|
+
syncAll();
|
|
2817
|
+
return version;
|
|
2818
|
+
},
|
|
2819
|
+
runs,
|
|
2820
|
+
contentItems: (b) => {
|
|
2821
|
+
syncAll();
|
|
2822
|
+
return blocks.get(b)?.deleted === false ? itemsOf(b) : [];
|
|
2823
|
+
},
|
|
2824
|
+
// Inline atoms always carry `data` (`{}` when absent) — the public
|
|
2825
|
+
// `JSONInlineBlock` shape of `edytor.value`.
|
|
2826
|
+
contentJSON: (b) => runs(b).map((r) => r.kind === 'text'
|
|
2827
|
+
? { text: r.text, ...(r.marks !== undefined && { marks: cloneJsonSafe(r.marks) }) }
|
|
2828
|
+
: { id: r.id, type: r.type, data: r.data === undefined ? {} : cloneJsonSafe(r.data) }),
|
|
2829
|
+
view: (tr) => {
|
|
2830
|
+
syncAll();
|
|
2831
|
+
if (tr !== undefined && !committed.has(tr))
|
|
2832
|
+
syncPending(tr);
|
|
2833
|
+
return ctx;
|
|
2834
|
+
},
|
|
2835
|
+
displayType: (id) => {
|
|
2836
|
+
if (roles === null)
|
|
2837
|
+
return undefined;
|
|
2838
|
+
syncAll();
|
|
2839
|
+
ensurePlacements();
|
|
2840
|
+
const shown = blocks.has(id) ? typeOf(id) : undefined;
|
|
2841
|
+
return shown === blocks.get(id)?.type ? undefined : shown;
|
|
2842
|
+
},
|
|
2843
|
+
dissolved: (id) => {
|
|
2844
|
+
syncAll();
|
|
2845
|
+
ensurePlacements();
|
|
2846
|
+
return dissolved.has(id);
|
|
2847
|
+
},
|
|
2848
|
+
project: (root) => {
|
|
2849
|
+
syncAll();
|
|
2850
|
+
ensurePlacements();
|
|
2851
|
+
return root === undefined
|
|
2852
|
+
? (kidsMap.get(null) ?? []).map((k) => projectBlock(k.id))
|
|
2853
|
+
: [projectBlock(root)];
|
|
2854
|
+
},
|
|
2855
|
+
roles: (next) => {
|
|
2856
|
+
roles = next;
|
|
2857
|
+
placementFull = true;
|
|
2858
|
+
version++;
|
|
2859
|
+
// No commit carries a role change: report it now (not inside a
|
|
2860
|
+
// transaction, whose commit reports it), so every view follows.
|
|
2861
|
+
if (hasSubscribers() && !openTx())
|
|
2862
|
+
publish(null, false);
|
|
2863
|
+
},
|
|
2864
|
+
track: () => {
|
|
2865
|
+
syncAll();
|
|
2866
|
+
const f = { touched: new Map(), wrote: false };
|
|
2867
|
+
frames.add(f);
|
|
2868
|
+
return {
|
|
2869
|
+
end: () => {
|
|
2870
|
+
syncAll();
|
|
2871
|
+
frames.delete(f);
|
|
2872
|
+
return f;
|
|
2873
|
+
},
|
|
2874
|
+
quiet: (write) => {
|
|
2875
|
+
syncAll();
|
|
2876
|
+
const g = { touched: new Map(), wrote: false };
|
|
2877
|
+
frames.delete(f);
|
|
2878
|
+
frames.add(g);
|
|
2879
|
+
try {
|
|
2880
|
+
write();
|
|
2881
|
+
syncAll();
|
|
2882
|
+
}
|
|
2883
|
+
finally {
|
|
2884
|
+
frames.delete(g);
|
|
2885
|
+
frames.add(f);
|
|
2886
|
+
f.wrote ||= g.wrote;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
};
|
|
2890
|
+
},
|
|
2891
|
+
onReport,
|
|
2892
|
+
debug
|
|
2893
|
+
};
|
|
2894
|
+
return view;
|
|
2895
|
+
};
|