edytor 0.1.0-next.21 → 0.1.0-next.23
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 +1 -1
- package/dist/cloudflare/DocumentRoom.d.ts +302 -10
- package/dist/cloudflare/DocumentRoom.js +965 -88
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/routeDocumentSocket.js +30 -6
- package/dist/collaboration/awarenessSelection.d.ts +7 -1
- package/dist/collaboration/awarenessSelection.js +7 -1
- package/dist/collaboration/index.d.ts +2 -2
- package/dist/collaboration/index.js +2 -2
- package/dist/collaboration/providers.d.ts +4 -0
- package/dist/collaboration/providers.js +4 -0
- package/dist/components/Edytor.svelte +14 -2
- package/dist/components/Edytor.svelte.d.ts +8 -1
- package/dist/crdt/admission.d.ts +6 -1
- package/dist/crdt/admission.js +9 -2
- package/dist/crdt/data.d.ts +3 -1
- package/dist/crdt/data.js +42 -32
- package/dist/crdt/document.d.ts +31 -8
- package/dist/crdt/document.js +53 -18
- package/dist/crdt/edytor-doc.d.ts +31 -3
- package/dist/crdt/edytor-doc.js +131 -14
- package/dist/crdt/engine.d.ts +6 -0
- package/dist/crdt/engine.js +7 -0
- package/dist/crdt/flow.d.ts +2 -0
- package/dist/crdt/flow.js +2 -2
- package/dist/crdt/index.d.ts +20 -9
- package/dist/crdt/index.js +3 -3
- package/dist/crdt/migration/migrate.js +9 -3
- package/dist/crdt/placement/model.d.ts +9 -1
- package/dist/crdt/placement/model.js +29 -10
- package/dist/crdt/placement/rank.d.ts +20 -0
- package/dist/crdt/placement/rank.js +79 -0
- package/dist/crdt/protocols/envelope.d.ts +20 -1
- package/dist/crdt/protocols/envelope.js +14 -2
- package/dist/crdt/providers/container.d.ts +15 -0
- package/dist/crdt/providers/container.js +17 -3
- package/dist/crdt/providers/index.d.ts +21 -1
- package/dist/crdt/providers/index.js +86 -3
- package/dist/crdt/providers/indexeddb.js +60 -40
- package/dist/crdt/providers/room.d.ts +18 -2
- package/dist/crdt/providers/room.js +29 -5
- package/dist/crdt/providers/websocket.d.ts +9 -1
- package/dist/crdt/providers/websocket.js +21 -6
- package/dist/crdt/storage.d.ts +21 -0
- package/dist/crdt/storage.js +37 -0
- package/dist/crdt/structs.d.ts +17 -0
- package/dist/crdt/structs.js +41 -0
- package/dist/crdt/text/model.d.ts +9 -1
- package/dist/crdt/text/model.js +24 -14
- package/dist/crdt/text/rows.d.ts +94 -0
- package/dist/crdt/text/rows.js +193 -0
- package/dist/crdt/text/runs.d.ts +30 -10
- package/dist/crdt/text/runs.js +1235 -264
- package/dist/edytor.svelte.d.ts +3 -1
- package/dist/edytor.svelte.js +9 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/image/ImageEmpty.svelte +19 -6
- package/dist/plugins/image/ImagePlugin.svelte +9 -3
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +2 -2
- package/dist/plugins/image/image.d.ts +16 -0
- package/dist/plugins/image/image.js +19 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/plugins.d.ts +11 -0
- package/dist/session/commands.d.ts +1 -0
- package/dist/session/commands.js +4 -1
- package/package.json +1 -1
package/dist/crdt/text/runs.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { candidatesOf, childrenIndex, displayIndex, documentOrder, REGISTRY_KEY, resolvePlacements } from '../placement/model.js';
|
|
2
|
-
import { bindText, canonKey, claimGraph, DEAD, deepFreeze, delimiters, displayOf, placeText, protectItems, readClaims, scanText } from './model.js';
|
|
1
|
+
import { candidatesOf, bySlot, childrenIndex, displayIndex, displaySlotOf, documentOrder, REGISTRY_KEY, resolvePlacements } from '../placement/model.js';
|
|
2
|
+
import { bindText, byId, canonKey, claimGraph, cmpStamp, DEAD, deepFreeze, delimiters, displayOf, isBoundary, placeText, protectItems, readClaims, scanText } from './model.js';
|
|
3
|
+
import { gapOfItem, headOf, placeRow, rowLength, rowOf, sameRow, segmentAt, segmentOfCut, segmentOfGap, shiftGap, streamOfSegment } from './rows.js';
|
|
3
4
|
import { AT, CLAIMS, CONTENT, DATA, DATA_LEAF_PREFIX, DOC_DATA_ROOT, hasDeleteMark, hasWithdrawMark, ID, isNodeLike, LAST_CHANGED_ATTR, NONCE, TYPE } from '../schema.js';
|
|
4
|
-
import { walkIdSetStructs } from '../structs.js';
|
|
5
|
+
import { queuedTransactions, walkIdSetRanges, walkIdSetStructs } from '../structs.js';
|
|
5
6
|
import { cloneJsonSafe, sameIds } from '../../utils/json.js';
|
|
6
7
|
import { readData } from '../data.js';
|
|
7
8
|
import { callEach } from '../protocols/observable.js';
|
|
@@ -70,6 +71,15 @@ const sameShape = (roles, a, b) => {
|
|
|
70
71
|
};
|
|
71
72
|
/** One index per engine doc, shared by every binding (the doc's lifetime). */
|
|
72
73
|
const indexes = new WeakMap();
|
|
74
|
+
/**
|
|
75
|
+
* Test lanes turn this on (`globalThis.__EDYTOR_INDEX_CHECKS__`, set before
|
|
76
|
+
* the index loads): after every fold the index checks each fact it keeps
|
|
77
|
+
* incrementally against a rebuild from the replicated state and throws on
|
|
78
|
+
* the first difference. Off in production.
|
|
79
|
+
*/
|
|
80
|
+
export const indexChecks = {
|
|
81
|
+
on: globalThis.__EDYTOR_INDEX_CHECKS__ === true
|
|
82
|
+
};
|
|
73
83
|
export const bindRuns = (Y) => {
|
|
74
84
|
const T = bindText(Y);
|
|
75
85
|
/** The doc's index — built on first use, torn down when the doc is destroyed. */
|
|
@@ -85,38 +95,54 @@ export const bindRuns = (Y) => {
|
|
|
85
95
|
const dataRoot = doc.get(DOC_DATA_ROOT);
|
|
86
96
|
const docData = () => cloneJsonSafe(readData(dataRoot) ?? {});
|
|
87
97
|
let dataChanged = false;
|
|
88
|
-
const
|
|
98
|
+
const NO_EDITS = Object.freeze({
|
|
99
|
+
edits: [],
|
|
100
|
+
scan: false,
|
|
101
|
+
opaque: false
|
|
102
|
+
});
|
|
103
|
+
const holdsBoundary = (s, from, to) => {
|
|
104
|
+
const arr = s.content?.arr;
|
|
105
|
+
if (arr === undefined)
|
|
106
|
+
return false;
|
|
107
|
+
for (let k = from - s.id.clock; k < to - s.id.clock; k++)
|
|
108
|
+
if (isBoundary(arr[k]))
|
|
109
|
+
return true;
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
112
|
+
/** The edits the id sets `ins`/`del` (what the fold has not folded yet) hold, per text. */
|
|
113
|
+
const makeEditIndex = (ins, del) => {
|
|
89
114
|
let built;
|
|
90
115
|
const build = () => {
|
|
91
116
|
if (built !== undefined)
|
|
92
117
|
return built;
|
|
93
|
-
|
|
94
|
-
if (tx?.insertSet === undefined || tx.deleteSet === undefined)
|
|
118
|
+
if (ins === undefined || del === undefined)
|
|
95
119
|
return (built = null);
|
|
96
120
|
const out = new Map();
|
|
97
|
-
const visit = (idSet, deleted) =>
|
|
121
|
+
const visit = (idSet, deleted) => walkIdSetRanges(Y, doc, idSet, (s, from, to) => {
|
|
98
122
|
const parent = s.parent;
|
|
99
123
|
if (s.parentSub !== null || !isNodeLike(parent))
|
|
100
124
|
return;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
out.set(parent, [...(spans ?? []), [abs.index, abs.index + s.length]]);
|
|
114
|
-
}
|
|
125
|
+
let e = out.get(parent);
|
|
126
|
+
if (e === undefined)
|
|
127
|
+
out.set(parent, (e = { edits: [], scan: false, opaque: false }));
|
|
128
|
+
// A format marker moves no unit; its effect reaches past the edit.
|
|
129
|
+
if (s.countable !== true)
|
|
130
|
+
return void (e.opaque = true);
|
|
131
|
+
// Written and removed within the fold: nothing moved.
|
|
132
|
+
if (deleted ? ins.has(s.id.client, from) : s.deleted)
|
|
133
|
+
return;
|
|
134
|
+
if (holdsBoundary(s, from, to))
|
|
135
|
+
return void (e.scan = true);
|
|
136
|
+
e.edits.push({ item: s, len: to - from, del: deleted });
|
|
115
137
|
});
|
|
116
|
-
built = visit(
|
|
138
|
+
built = visit(ins, false) && visit(del, true) ? out : null;
|
|
117
139
|
return built;
|
|
118
140
|
};
|
|
119
|
-
|
|
141
|
+
/** `node`'s edits (`null`: unknown — rescan, every consumer recomputes). */
|
|
142
|
+
return (node) => {
|
|
143
|
+
const map = build();
|
|
144
|
+
return map === null ? null : (map.get(node) ?? NO_EDITS);
|
|
145
|
+
};
|
|
120
146
|
};
|
|
121
147
|
// ── the replicated-state index ──────────────────────────────────
|
|
122
148
|
const blocks = new Map();
|
|
@@ -127,78 +153,349 @@ export const bindRuns = (Y) => {
|
|
|
127
153
|
/** Forward dependencies of cached runs: backing text (by home) / walked block → consumers. */
|
|
128
154
|
const textConsumers = new Map();
|
|
129
155
|
const listConsumers = new Map();
|
|
130
|
-
// ── the claim graph (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
// ── the claim graph (P3: maintained) ─────────────────────────────
|
|
157
|
+
// `top(m)` is the max-stamp claim on `m` held by a live block; each
|
|
158
|
+
// block's owner follows `top` up (`claimGraph`). A structural change
|
|
159
|
+
// of a block (its claims, its delete mark, its record) re-decides the
|
|
160
|
+
// `top` of the blocks it claims (before and after) and the owners of
|
|
161
|
+
// those blocks and of every block whose `top` chain reaches them —
|
|
162
|
+
// never the whole graph.
|
|
163
|
+
const owners = new Map();
|
|
164
|
+
const topOf = new Map();
|
|
165
|
+
/** Claimer → the blocks it tops. */
|
|
166
|
+
const topInv = new Map();
|
|
167
|
+
/** Claimed block → the blocks whose claims list names it (live or not). */
|
|
168
|
+
const claimersOf = new Map();
|
|
169
|
+
/** Owner → the blocks it displays (itself included while it owns itself). */
|
|
170
|
+
const displaysMap = new Map();
|
|
171
|
+
/** Blocks whose claims, delete mark or record changed since the last owner pass. */
|
|
172
|
+
const ownerSeeds = new Set();
|
|
173
|
+
/** Blocks whose owner changed since the last placement pass. */
|
|
174
|
+
const ownerChanged = new Set();
|
|
175
|
+
let ownersBuilt = false;
|
|
176
|
+
const addTo = (index, key, value) => {
|
|
177
|
+
let set = index.get(key);
|
|
178
|
+
if (set === undefined)
|
|
179
|
+
index.set(key, (set = new Set()));
|
|
180
|
+
set.add(value);
|
|
181
|
+
};
|
|
182
|
+
const dropFrom = (index, key, value) => {
|
|
183
|
+
const set = index.get(key);
|
|
184
|
+
set?.delete(value);
|
|
185
|
+
if (set?.size === 0)
|
|
186
|
+
index.delete(key);
|
|
187
|
+
};
|
|
188
|
+
/** The max-stamp claim on `m` held by a live block. */
|
|
189
|
+
const topClaim = (m) => {
|
|
190
|
+
let best;
|
|
191
|
+
for (const holder of claimersOf.get(m) ?? []) {
|
|
192
|
+
const rec = blocks.get(holder);
|
|
193
|
+
if (rec === undefined || rec.deleted)
|
|
194
|
+
continue;
|
|
195
|
+
for (const c of rec.claims)
|
|
196
|
+
if (c.m === m && (best === undefined || cmpStamp(c.stamp, best.stamp) > 0))
|
|
197
|
+
best = { claimer: holder, stamp: c.stamp };
|
|
198
|
+
}
|
|
199
|
+
return best;
|
|
200
|
+
};
|
|
201
|
+
const setTop = (m, t) => {
|
|
202
|
+
const old = topOf.get(m);
|
|
203
|
+
if (old?.claimer === t?.claimer && (old === undefined || cmpStamp(old.stamp, t.stamp) === 0))
|
|
204
|
+
return false;
|
|
205
|
+
if (old !== undefined)
|
|
206
|
+
dropFrom(topInv, old.claimer, m);
|
|
207
|
+
if (t === undefined)
|
|
208
|
+
topOf.delete(m);
|
|
209
|
+
else {
|
|
210
|
+
topOf.set(m, t);
|
|
211
|
+
addTo(topInv, t.claimer, m);
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
/** `owner(b)` along `top` (`claimGraph`'s walk), memoized into `owners`. */
|
|
216
|
+
const walkOwner = (b) => {
|
|
217
|
+
const path = [];
|
|
218
|
+
let cur = b;
|
|
219
|
+
let result;
|
|
220
|
+
for (;;) {
|
|
221
|
+
const known = owners.get(cur);
|
|
222
|
+
if (known !== undefined) {
|
|
223
|
+
result = known;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
const rec = blocks.get(cur);
|
|
227
|
+
if (!rec || rec.deleted) {
|
|
228
|
+
result = DEAD;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const at = path.indexOf(cur);
|
|
232
|
+
if (at >= 0) {
|
|
233
|
+
let best = topOf.get(path[at]);
|
|
234
|
+
for (const member of path.slice(at)) {
|
|
235
|
+
const t = topOf.get(member);
|
|
236
|
+
if (cmpStamp(t.stamp, best.stamp) > 0)
|
|
237
|
+
best = t;
|
|
238
|
+
}
|
|
239
|
+
result = best.claimer;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
const t = topOf.get(cur);
|
|
243
|
+
if (t === undefined) {
|
|
244
|
+
result = cur;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
path.push(cur);
|
|
248
|
+
cur = t.claimer;
|
|
249
|
+
}
|
|
250
|
+
if (blocks.has(cur))
|
|
251
|
+
owners.set(cur, result);
|
|
252
|
+
for (const x of path)
|
|
253
|
+
owners.set(x, result);
|
|
254
|
+
};
|
|
255
|
+
const setDisplay = (b, from, to) => {
|
|
256
|
+
if (typeof from === 'string')
|
|
257
|
+
dropFrom(displaysMap, from, b);
|
|
258
|
+
if (typeof to === 'string')
|
|
259
|
+
addTo(displaysMap, to, b);
|
|
260
|
+
};
|
|
136
261
|
const ensureOwners = () => {
|
|
137
|
-
if (
|
|
262
|
+
if (!ownersBuilt) {
|
|
263
|
+
ownersBuilt = true;
|
|
264
|
+
ownerSeeds.clear();
|
|
265
|
+
const graph = claimGraph(blocks);
|
|
266
|
+
owners.clear();
|
|
267
|
+
for (const [b, o] of graph.owners)
|
|
268
|
+
if (blocks.has(b))
|
|
269
|
+
owners.set(b, o);
|
|
270
|
+
topOf.clear();
|
|
271
|
+
topInv.clear();
|
|
272
|
+
claimersOf.clear();
|
|
273
|
+
for (const [holder, rec] of blocks)
|
|
274
|
+
for (const c of rec.claims)
|
|
275
|
+
addTo(claimersOf, c.m, holder);
|
|
276
|
+
for (const m of claimersOf.keys())
|
|
277
|
+
setTop(m, topClaim(m));
|
|
278
|
+
displaysMap.clear();
|
|
279
|
+
for (const b of blocks.keys())
|
|
280
|
+
setDisplay(b, undefined, owners.get(b));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (ownerSeeds.size === 0)
|
|
138
284
|
return;
|
|
139
|
-
(
|
|
140
|
-
|
|
285
|
+
// The tops a seed's claims (now, and those it dropped: noted in `claimersOf` changes) decide.
|
|
286
|
+
const affected = new Set();
|
|
287
|
+
const stack = [];
|
|
288
|
+
for (const x of ownerSeeds) {
|
|
289
|
+
stack.push(x);
|
|
290
|
+
for (const m of claimTargets.get(x) ?? [])
|
|
291
|
+
if (setTop(m, topClaim(m)))
|
|
292
|
+
stack.push(m);
|
|
293
|
+
for (const c of blocks.get(x)?.claims ?? [])
|
|
294
|
+
if (setTop(c.m, topClaim(c.m)))
|
|
295
|
+
stack.push(c.m);
|
|
296
|
+
}
|
|
297
|
+
ownerSeeds.clear();
|
|
298
|
+
claimTargets.clear();
|
|
299
|
+
for (let x = stack.pop(); x !== undefined; x = stack.pop()) {
|
|
300
|
+
if (affected.has(x))
|
|
301
|
+
continue;
|
|
302
|
+
affected.add(x);
|
|
303
|
+
for (const m of topInv.get(x) ?? [])
|
|
304
|
+
stack.push(m);
|
|
305
|
+
}
|
|
306
|
+
const before = new Map();
|
|
307
|
+
for (const x of affected) {
|
|
308
|
+
before.set(x, owners.get(x));
|
|
309
|
+
owners.delete(x);
|
|
310
|
+
}
|
|
311
|
+
for (const x of affected)
|
|
312
|
+
if (blocks.has(x))
|
|
313
|
+
walkOwner(x);
|
|
314
|
+
for (const [x, was] of before) {
|
|
315
|
+
const now = owners.get(x);
|
|
316
|
+
if (now === was)
|
|
317
|
+
continue;
|
|
318
|
+
setDisplay(x, was, now);
|
|
319
|
+
ownerChanged.add(x);
|
|
320
|
+
}
|
|
141
321
|
};
|
|
322
|
+
/** The claims each seed held before its record changed (their tops are re-decided too). */
|
|
323
|
+
const claimTargets = new Map();
|
|
142
324
|
const ownerOf = (b) => {
|
|
143
325
|
ensureOwners();
|
|
144
326
|
return owners.get(b) ?? DEAD;
|
|
145
327
|
};
|
|
146
|
-
// ── the stream table (R2)
|
|
147
|
-
// One
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
328
|
+
// ── the stream table (R2, P1) ────────────────────────────────────
|
|
329
|
+
// One maintained row per backing text (keyed by its home block): its
|
|
330
|
+
// live boundaries and the units between them (`text/rows.ts`), the
|
|
331
|
+
// delimiting boundary of each block, and each block's segment. An edit
|
|
332
|
+
// that writes or removes no boundary moves its row by its units
|
|
333
|
+
// (`shiftGap`); a row is rescanned only when its boundary set (or its
|
|
334
|
+
// text) changed, and only the delimiters of the blocks its boundaries
|
|
335
|
+
// name are re-decided, so only the rows those delimiters cut re-place.
|
|
152
336
|
const rows = new Map();
|
|
153
337
|
let delim = new Map();
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
/**
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
338
|
+
/** Every live boundary naming a block, by block: key → its row and nonce. */
|
|
339
|
+
const boundsBy = new Map();
|
|
340
|
+
/** Each block's segment: its row and the cut that opens it (`null`: the row's segment 0). */
|
|
341
|
+
const streamIx = new Map();
|
|
342
|
+
const placed = new Map();
|
|
343
|
+
const streamOf = (b) => {
|
|
344
|
+
const at = streamIx.get(b);
|
|
345
|
+
const row = at && rows.get(at.home);
|
|
346
|
+
if (row === undefined)
|
|
347
|
+
return undefined;
|
|
348
|
+
const k = segmentOfCut(row, at.cut);
|
|
349
|
+
return k < 0 ? undefined : streamOfSegment(row, k, b);
|
|
350
|
+
};
|
|
351
|
+
const streamsIn = (home) => {
|
|
165
352
|
const row = rows.get(home);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
353
|
+
const out = [];
|
|
354
|
+
for (let k = 0; row !== undefined && k <= row.cuts.length; k++) {
|
|
355
|
+
const b = headOf(row, k);
|
|
356
|
+
if (b !== null)
|
|
357
|
+
out.push(streamOfSegment(row, k, b));
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
};
|
|
361
|
+
const streamAt = (home, i) => {
|
|
362
|
+
const row = rows.get(home);
|
|
363
|
+
if (row === undefined)
|
|
364
|
+
return undefined;
|
|
365
|
+
const k = segmentAt(row, i);
|
|
366
|
+
const b = headOf(row, k);
|
|
367
|
+
if (b === null)
|
|
368
|
+
return undefined;
|
|
369
|
+
const st = streamOfSegment(row, k, b);
|
|
370
|
+
return st.start <= i && i <= st.end ? st : undefined;
|
|
173
371
|
};
|
|
174
|
-
/**
|
|
175
|
-
const
|
|
372
|
+
/** Record `home`'s new row (or none); the blocks its boundary set change names join `named`. */
|
|
373
|
+
const setRow = (home, row, named) => {
|
|
176
374
|
const old = rows.get(home);
|
|
177
|
-
|
|
178
|
-
if (text === undefined) {
|
|
375
|
+
if (row === undefined)
|
|
179
376
|
rows.delete(home);
|
|
180
|
-
|
|
377
|
+
else
|
|
378
|
+
rows.set(home, row);
|
|
379
|
+
if (old?.key === row?.key)
|
|
380
|
+
return;
|
|
381
|
+
for (const b of old?.bounds ?? []) {
|
|
382
|
+
if (row?.keyIndex.has(b.key))
|
|
383
|
+
continue;
|
|
384
|
+
const by = boundsBy.get(b.s);
|
|
385
|
+
by?.delete(b.key);
|
|
386
|
+
if (by?.size === 0)
|
|
387
|
+
boundsBy.delete(b.s);
|
|
388
|
+
named.add(b.s);
|
|
389
|
+
}
|
|
390
|
+
for (const b of row?.bounds ?? []) {
|
|
391
|
+
if (old?.keyIndex.has(b.key))
|
|
392
|
+
continue;
|
|
393
|
+
let by = boundsBy.get(b.s);
|
|
394
|
+
if (by === undefined)
|
|
395
|
+
boundsBy.set(b.s, (by = new Map()));
|
|
396
|
+
by.set(b.key, { home, n: b.n });
|
|
397
|
+
named.add(b.s);
|
|
181
398
|
}
|
|
182
|
-
const row = scanText(home, text);
|
|
183
|
-
rows.set(home, row);
|
|
184
|
-
return old === undefined || old.text !== text || old.key !== row.key;
|
|
185
399
|
};
|
|
186
|
-
/**
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
400
|
+
/** A fresh row of `home`'s text (none without one). */
|
|
401
|
+
const scanRow = (home) => {
|
|
402
|
+
const text = blocks.get(home)?.content;
|
|
403
|
+
return text === undefined ? undefined : rowOf(scanText(home, text));
|
|
404
|
+
};
|
|
405
|
+
/** Re-decide the delimiting boundary of each block in `named`; the rows a change cuts join `homes`. */
|
|
406
|
+
const refreshDelims = (named, homes) => {
|
|
407
|
+
for (const s of named) {
|
|
408
|
+
const n = blocks.get(s)?.n;
|
|
409
|
+
let best;
|
|
410
|
+
if (blocks.has(s))
|
|
411
|
+
for (const [key, b] of boundsBy.get(s) ?? [])
|
|
412
|
+
if (b.n === n && (best === undefined || byId(key, best) < 0))
|
|
413
|
+
best = key;
|
|
414
|
+
const old = delim.get(s);
|
|
415
|
+
if (old === best)
|
|
196
416
|
continue;
|
|
197
|
-
|
|
198
|
-
|
|
417
|
+
if (best === undefined)
|
|
418
|
+
delim.delete(s);
|
|
419
|
+
else
|
|
420
|
+
delim.set(s, best);
|
|
421
|
+
const was = old === undefined ? undefined : boundsBy.get(s)?.get(old)?.home;
|
|
422
|
+
if (was !== undefined)
|
|
423
|
+
homes.add(was);
|
|
424
|
+
if (best !== undefined)
|
|
425
|
+
homes.add(boundsBy.get(s).get(best).home);
|
|
426
|
+
if (rows.has(s) || placed.has(s))
|
|
427
|
+
homes.add(s);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
/**
|
|
431
|
+
* Place `home`'s row; invalidate the blocks whose segment changed (or
|
|
432
|
+
* appeared, or went). A segment is its two delimiting cuts, so only the
|
|
433
|
+
* segments between the longest common prefix and suffix of the old and
|
|
434
|
+
* new cut lists can differ: the others keep their entries untouched.
|
|
435
|
+
*/
|
|
436
|
+
const placeHome = (home, invalidated) => {
|
|
437
|
+
const row = rows.get(home);
|
|
438
|
+
const before = placed.get(home) ?? { head: null, keys: [], blocks: [] };
|
|
439
|
+
const now = { head: null, keys: [], blocks: [] };
|
|
440
|
+
if (row !== undefined) {
|
|
441
|
+
placeRow(row, delim);
|
|
442
|
+
now.head = row.head;
|
|
443
|
+
for (const j of row.cuts) {
|
|
444
|
+
now.keys.push(row.bounds[j].key);
|
|
445
|
+
now.blocks.push(row.bounds[j].s);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (row === undefined)
|
|
449
|
+
placed.delete(home);
|
|
450
|
+
else
|
|
451
|
+
placed.set(home, now);
|
|
452
|
+
const [a, b] = [before.keys, now.keys];
|
|
453
|
+
let p = 0;
|
|
454
|
+
while (p < a.length && p < b.length && a[p] === b[p])
|
|
455
|
+
p++;
|
|
456
|
+
let q = 0;
|
|
457
|
+
while (q < a.length - p && q < b.length - p && a[a.length - 1 - q] === b[b.length - 1 - q])
|
|
458
|
+
q++;
|
|
459
|
+
// Segment k ends at cut k: it changed iff k lies in [p, n - q] (segment 0 also with its block).
|
|
460
|
+
const from = before.head === now.head ? p : 0;
|
|
461
|
+
const changed = (blk) => {
|
|
462
|
+
if (blk === null)
|
|
463
|
+
return;
|
|
464
|
+
invalidated.add(blk);
|
|
465
|
+
for (const c of listConsumers.get(blk) ?? [])
|
|
199
466
|
invalidated.add(c);
|
|
467
|
+
};
|
|
468
|
+
const blockOf = (x, k) => (k === 0 ? x.head : x.blocks[k - 1]);
|
|
469
|
+
for (let k = from; k <= a.length - q; k++) {
|
|
470
|
+
const blk = blockOf(before, k);
|
|
471
|
+
if (blk !== null && streamIx.get(blk)?.home === home)
|
|
472
|
+
streamIx.delete(blk);
|
|
473
|
+
changed(blk);
|
|
474
|
+
}
|
|
475
|
+
for (let k = from; k <= b.length - q; k++) {
|
|
476
|
+
const blk = blockOf(now, k);
|
|
477
|
+
if (blk === null)
|
|
478
|
+
continue;
|
|
479
|
+
streamIx.set(blk, { home, cut: k === 0 ? null : b[k - 1] });
|
|
480
|
+
changed(blk);
|
|
200
481
|
}
|
|
201
482
|
};
|
|
483
|
+
/** Rebuild the delimiters and every row's segments (the first build). */
|
|
484
|
+
const rebuildTable = (invalidated) => {
|
|
485
|
+
boundsBy.clear();
|
|
486
|
+
for (const [home, row] of rows)
|
|
487
|
+
for (const b of row.bounds) {
|
|
488
|
+
let by = boundsBy.get(b.s);
|
|
489
|
+
if (by === undefined)
|
|
490
|
+
boundsBy.set(b.s, (by = new Map()));
|
|
491
|
+
by.set(b.key, { home, n: b.n });
|
|
492
|
+
}
|
|
493
|
+
delim = new Map();
|
|
494
|
+
const homes = new Set([...rows.keys(), ...placed.keys()]);
|
|
495
|
+
refreshDelims(boundsBy.keys(), homes);
|
|
496
|
+
for (const home of homes)
|
|
497
|
+
placeHome(home, invalidated);
|
|
498
|
+
};
|
|
202
499
|
/** The role table the display reads; `null` → none. */
|
|
203
500
|
let roles = null;
|
|
204
501
|
/** A stored kind changed since the last report: derived kinds may follow it (XW-08). */
|
|
@@ -228,31 +525,43 @@ export const bindRuns = (Y) => {
|
|
|
228
525
|
},
|
|
229
526
|
top: (m) => {
|
|
230
527
|
ensureOwners();
|
|
231
|
-
return
|
|
528
|
+
return topOf.get(m)?.claimer;
|
|
232
529
|
},
|
|
233
|
-
streamOf
|
|
234
|
-
streamsIn
|
|
530
|
+
streamOf,
|
|
531
|
+
streamsIn,
|
|
532
|
+
streamAt,
|
|
235
533
|
display: (b) => displayOf(b, blocks, ownShim)
|
|
236
534
|
};
|
|
237
|
-
/**
|
|
238
|
-
let displaysMap = new Map();
|
|
239
|
-
let displaysAt = -1;
|
|
535
|
+
/** The blocks `owner` displays (the delete step's marks). */
|
|
240
536
|
const displays = (owner) => {
|
|
241
537
|
ensureOwners();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
538
|
+
return [...(displaysMap.get(owner) ?? [])];
|
|
539
|
+
};
|
|
540
|
+
// ── placements and the children index (P3: maintained) ────────────
|
|
541
|
+
// A structural change re-decides only what it can change: the winning
|
|
542
|
+
// placement of the blocks whose candidates (or whose parent's
|
|
543
|
+
// registry entry) changed, and the display slot of every block whose
|
|
544
|
+
// slot walk (`displaySlotOf`) reads one that changed — its stored
|
|
545
|
+
// subtree, through the blocks it displays — patched into the parents'
|
|
546
|
+
// child lists. While every block shows its winning candidate (no
|
|
547
|
+
// cycle rejected, none rehomed), a block's placement is its argmax
|
|
548
|
+
// (`resolvePlacements` accepts it), so only a cycle through a changed
|
|
549
|
+
// display edge needs the global resolution, which then runs whole.
|
|
550
|
+
// The layout rules re-run only on the layouts a change reaches.
|
|
551
|
+
/** Resolved placements. */
|
|
552
|
+
const placementsMap = new Map();
|
|
553
|
+
/** Resolved parent → the blocks placed under it. */
|
|
554
|
+
const kidsOf = new Map();
|
|
555
|
+
/** A block's argmax candidate parent → the blocks naming it (its entry decides theirs). */
|
|
556
|
+
const byArgParent = new Map();
|
|
557
|
+
/** The children index without the layout rules (what they read), and with them. */
|
|
558
|
+
let kids0 = new Map();
|
|
559
|
+
let slots0 = new Map();
|
|
560
|
+
let kidsMap = kids0;
|
|
561
|
+
/** Each visible block's slot (display parent, rank, the island it displays out of). */
|
|
562
|
+
let slots = slots0;
|
|
563
|
+
/** The document holds a layout or an item kind: `kids0` and `kidsMap` differ. */
|
|
564
|
+
let layoutMode = false;
|
|
256
565
|
/** The line kinds the roles declare, and those of the lines islands the document holds. */
|
|
257
566
|
let lineKinds = new Set();
|
|
258
567
|
/**
|
|
@@ -260,106 +569,427 @@ export const bindRuns = (Y) => {
|
|
|
260
569
|
* an island, then in a lined island or of a line kind — which the
|
|
261
570
|
* report re-reads after a retype.
|
|
262
571
|
*/
|
|
263
|
-
|
|
572
|
+
const following = new Set();
|
|
264
573
|
let orderCache = null;
|
|
265
|
-
|
|
266
|
-
let
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
574
|
+
/** Bumps whenever a child list changes (the report compares it). */
|
|
575
|
+
let kidsVersion = 0;
|
|
576
|
+
/** The child lists patched since the last report (`all`: every list was rebuilt). */
|
|
577
|
+
const dirtyLists = new Set();
|
|
578
|
+
/** The blocks that left every list since the last report. */
|
|
579
|
+
const leftLists = new Set();
|
|
580
|
+
let allListsDirty = true;
|
|
581
|
+
/** The next pass rebuilds everything (first build, roles, an irregular state). */
|
|
582
|
+
let placementFull = true;
|
|
583
|
+
/** Some block shows no argmax candidate (a cycle rejected one, or none): passes run whole. */
|
|
584
|
+
let irregular = false;
|
|
585
|
+
/** Blocks whose candidates or record changed, whose display state changed. */
|
|
586
|
+
const placementSeeds = new Set();
|
|
587
|
+
const stateSeeds = new Set();
|
|
588
|
+
/** Per block: whether its kind is a layout, an item, a lines island (with its line kind). */
|
|
589
|
+
const kindsOf = new Map();
|
|
590
|
+
let layoutBlocks = 0;
|
|
591
|
+
let itemBlocks = 0;
|
|
592
|
+
const lineCounts = new Map();
|
|
593
|
+
/** The item kinds the roles declare. */
|
|
594
|
+
const declaredItems = () => {
|
|
595
|
+
const items = new Set();
|
|
596
|
+
for (const type of roles?.layoutKinds() ?? [])
|
|
597
|
+
items.add(roles.layout(type));
|
|
598
|
+
return items;
|
|
599
|
+
};
|
|
600
|
+
let itemKinds = new Set();
|
|
601
|
+
/** Re-count `id`'s kind facts; a fact the roles never declared forces a full pass. */
|
|
602
|
+
const noteKind = (id) => {
|
|
603
|
+
const old = kindsOf.get(id);
|
|
604
|
+
const type = blocks.get(id)?.type;
|
|
605
|
+
const now = type === undefined
|
|
606
|
+
? undefined
|
|
607
|
+
: { layout: itemKind(id) !== undefined, item: itemKinds.has(type), line: lineKind(id) };
|
|
608
|
+
if (old?.layout)
|
|
609
|
+
layoutBlocks--;
|
|
610
|
+
if (old?.item)
|
|
611
|
+
itemBlocks--;
|
|
612
|
+
if (old?.line !== undefined)
|
|
613
|
+
lineCounts.set(old.line, lineCounts.get(old.line) - 1);
|
|
614
|
+
if (now === undefined)
|
|
615
|
+
kindsOf.delete(id);
|
|
616
|
+
else
|
|
617
|
+
kindsOf.set(id, now);
|
|
618
|
+
if (now?.layout)
|
|
619
|
+
layoutBlocks++;
|
|
620
|
+
if (now?.item)
|
|
621
|
+
itemBlocks++;
|
|
622
|
+
if (now?.line !== undefined)
|
|
623
|
+
lineCounts.set(now.line, (lineCounts.get(now.line) ?? 0) + 1);
|
|
624
|
+
const item = itemKind(id);
|
|
625
|
+
if (item !== undefined && !itemKinds.has(item))
|
|
626
|
+
placementFull = true;
|
|
627
|
+
if (now?.line !== undefined && !lineKinds.has(now.line))
|
|
628
|
+
placementFull = true;
|
|
629
|
+
if (layoutMode !== layoutBlocks + itemBlocks > 0)
|
|
630
|
+
placementFull = true;
|
|
631
|
+
};
|
|
632
|
+
/** The argmax candidate's parent (a registry entry or the root), and its index entry. */
|
|
633
|
+
const argParent = (rec) => rec?.cands[0]?.p;
|
|
634
|
+
const noteCands = (id, before) => {
|
|
635
|
+
const after = argParent(blocks.get(id));
|
|
636
|
+
if (before === after)
|
|
637
|
+
return;
|
|
638
|
+
if (typeof before === 'string')
|
|
639
|
+
dropFrom(byArgParent, before, id);
|
|
640
|
+
if (typeof after === 'string')
|
|
641
|
+
addTo(byArgParent, after, id);
|
|
642
|
+
};
|
|
643
|
+
const dissolveVisit = (kids, out, ids, layout) => {
|
|
644
|
+
const isItem = (b) => itemKinds.has(blocks.get(b)?.type ?? '');
|
|
645
|
+
const shown = [];
|
|
646
|
+
for (const id of ids) {
|
|
647
|
+
const own = itemKind(id) !== undefined;
|
|
648
|
+
const sub = dissolveVisit(kids, out, (kids.get(id) ?? []).map((k) => k.id), own);
|
|
649
|
+
if (own) {
|
|
650
|
+
if (sub.length > 1)
|
|
651
|
+
shown.push({ id, kids: [] });
|
|
652
|
+
else {
|
|
300
653
|
out.add(id);
|
|
301
|
-
|
|
654
|
+
for (const k of sub) {
|
|
655
|
+
out.add(k.id);
|
|
656
|
+
shown.push(...k.kids.map((kid) => ({ id: kid, kids: [] })));
|
|
657
|
+
}
|
|
302
658
|
}
|
|
303
|
-
else
|
|
304
|
-
shown.push({ id, kids: sub.map((k) => k.id) });
|
|
305
659
|
}
|
|
306
|
-
|
|
660
|
+
else if (isItem(id) && (!layout || sub.length === 0)) {
|
|
661
|
+
out.add(id);
|
|
662
|
+
shown.push(...sub);
|
|
663
|
+
}
|
|
664
|
+
else
|
|
665
|
+
shown.push({ id, kids: sub.map((k) => k.id) });
|
|
666
|
+
}
|
|
667
|
+
return shown;
|
|
668
|
+
};
|
|
669
|
+
const dissolve = (kids) => {
|
|
670
|
+
const out = new Set();
|
|
671
|
+
dissolveVisit(kids, out, (kids.get(null) ?? []).map((k) => k.id), false);
|
|
672
|
+
return out;
|
|
673
|
+
};
|
|
674
|
+
/** The ownership the layout rules read: none of their decisions applied. */
|
|
675
|
+
const own0 = {
|
|
676
|
+
...ownShim,
|
|
677
|
+
hidden: (b) => ownerOf(b) !== b,
|
|
678
|
+
passes: () => false
|
|
679
|
+
};
|
|
680
|
+
/** `id`'s slot in the index `own` reads (none: hidden, or under no live parent). */
|
|
681
|
+
const slotIn = (own, id) => {
|
|
682
|
+
const pl = placementsMap.get(id);
|
|
683
|
+
if (pl === undefined || !blocks.has(id) || own.hidden(id))
|
|
684
|
+
return undefined;
|
|
685
|
+
const { parent, rank, reset } = displaySlotOf(own, placementsMap, pl, id);
|
|
686
|
+
if (parent === DEAD)
|
|
687
|
+
return undefined;
|
|
688
|
+
return reset === null ? { parent, rank } : { parent, rank, reset };
|
|
689
|
+
};
|
|
690
|
+
/** The index of the first slot of `list` not before `x`. */
|
|
691
|
+
const seek = (list, x) => {
|
|
692
|
+
let lo = 0;
|
|
693
|
+
let hi = list.length;
|
|
694
|
+
while (lo < hi) {
|
|
695
|
+
const mid = (lo + hi) >> 1;
|
|
696
|
+
if (bySlot(list[mid], x) < 0)
|
|
697
|
+
lo = mid + 1;
|
|
698
|
+
else
|
|
699
|
+
hi = mid;
|
|
700
|
+
}
|
|
701
|
+
return lo;
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* Re-decide the slots of `affected` in `kids`/`slotMap` (read through
|
|
705
|
+
* `own`), patching the child lists they leave or join (each list
|
|
706
|
+
* copied once: a list a reader holds never changes under it). Returns
|
|
707
|
+
* the blocks whose slot changed, with their parents before and after.
|
|
708
|
+
*/
|
|
709
|
+
const patch = (kids, slotMap, own, affected) => {
|
|
710
|
+
const changed = new Map();
|
|
711
|
+
const copies = new Map();
|
|
712
|
+
const list = (p) => {
|
|
713
|
+
let l = copies.get(p);
|
|
714
|
+
if (l === undefined)
|
|
715
|
+
copies.set(p, (l = [...(kids.get(p) ?? [])]));
|
|
716
|
+
return l;
|
|
307
717
|
};
|
|
308
|
-
|
|
718
|
+
for (const id of affected) {
|
|
719
|
+
const old = slotMap.get(id);
|
|
720
|
+
const next = slotIn(own, id);
|
|
721
|
+
if (old?.parent === next?.parent &&
|
|
722
|
+
old?.rank === next?.rank &&
|
|
723
|
+
old?.reset === next?.reset &&
|
|
724
|
+
(old === undefined) === (next === undefined))
|
|
725
|
+
continue;
|
|
726
|
+
if (old !== undefined) {
|
|
727
|
+
const l = list(old.parent);
|
|
728
|
+
const at = seek(l, { id, rank: old.rank });
|
|
729
|
+
if (l[at]?.id === id)
|
|
730
|
+
l.splice(at, 1);
|
|
731
|
+
}
|
|
732
|
+
if (next !== undefined) {
|
|
733
|
+
const l = list(next.parent);
|
|
734
|
+
const slot = next.reset === undefined
|
|
735
|
+
? { id, rank: next.rank }
|
|
736
|
+
: { id, rank: next.rank, reset: next.reset };
|
|
737
|
+
l.splice(seek(l, slot), 0, slot);
|
|
738
|
+
slotMap.set(id, next);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
slotMap.delete(id);
|
|
742
|
+
if (kids === kidsMap)
|
|
743
|
+
leftLists.add(id);
|
|
744
|
+
}
|
|
745
|
+
changed.set(id, [old?.parent, next?.parent]);
|
|
746
|
+
}
|
|
747
|
+
for (const [p, l] of copies) {
|
|
748
|
+
if (l.length === 0)
|
|
749
|
+
kids.delete(p);
|
|
750
|
+
else
|
|
751
|
+
kids.set(p, l);
|
|
752
|
+
if (kids === kidsMap)
|
|
753
|
+
dirtyLists.add(p);
|
|
754
|
+
}
|
|
755
|
+
return changed;
|
|
756
|
+
};
|
|
757
|
+
/** `seeds` and every block whose slot walk reads one of them: stored subtrees, through what each displays. */
|
|
758
|
+
const reach = (seeds) => {
|
|
759
|
+
const out = new Set();
|
|
760
|
+
const stack = [...seeds];
|
|
761
|
+
for (let x = stack.pop(); x !== undefined; x = stack.pop()) {
|
|
762
|
+
if (out.has(x))
|
|
763
|
+
continue;
|
|
764
|
+
out.add(x);
|
|
765
|
+
for (const c of kidsOf.get(x) ?? [])
|
|
766
|
+
stack.push(c);
|
|
767
|
+
for (const d of displaysMap.get(x) ?? [])
|
|
768
|
+
if (d !== x)
|
|
769
|
+
for (const c of kidsOf.get(d) ?? [])
|
|
770
|
+
stack.push(c);
|
|
771
|
+
}
|
|
309
772
|
return out;
|
|
310
773
|
};
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
774
|
+
/** Whether `id` shows as its slot's kind (`following`): read from its slot and kinds. */
|
|
775
|
+
const noteFollowing = (id) => {
|
|
776
|
+
const slot = slots.get(id);
|
|
777
|
+
const follows = slot !== undefined &&
|
|
778
|
+
(slot.reset !== undefined ||
|
|
779
|
+
(slot.parent !== null && lineKind(slot.parent) !== undefined) ||
|
|
780
|
+
lineKinds.has(blocks.get(id)?.type ?? ''));
|
|
781
|
+
if (follows)
|
|
782
|
+
following.add(id);
|
|
783
|
+
else
|
|
784
|
+
following.delete(id);
|
|
785
|
+
};
|
|
786
|
+
/** The placement `resolvePlacements` gives `id` while every block shows its argmax. */
|
|
787
|
+
const argmaxOf = (id) => {
|
|
788
|
+
const rec = blocks.get(id);
|
|
789
|
+
if (rec === undefined)
|
|
790
|
+
return undefined;
|
|
791
|
+
const c = rec.cands[0];
|
|
792
|
+
if (c === undefined)
|
|
793
|
+
return null;
|
|
794
|
+
const p = c.p !== null && !blocks.has(c.p) ? null : c.p;
|
|
795
|
+
return { parent: p, rank: c.r };
|
|
796
|
+
};
|
|
797
|
+
/** A display edge walk from `id`'s parent reaches `id` (or loops): the argmax graph has a cycle. */
|
|
798
|
+
const cyclic = (id) => {
|
|
799
|
+
let steps = 0;
|
|
800
|
+
for (let cur = placementsMap.get(id)?.parent ?? null; cur !== null;) {
|
|
801
|
+
const o = ownerOf(cur);
|
|
802
|
+
const at = o === DEAD ? cur : o;
|
|
803
|
+
if (at === id || ++steps > blocks.size)
|
|
804
|
+
return true;
|
|
805
|
+
const cp = placementsMap.get(at);
|
|
806
|
+
if (cp === undefined)
|
|
807
|
+
return false;
|
|
808
|
+
cur = cp.parent;
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
};
|
|
812
|
+
const setPlacement = (id, pl) => {
|
|
813
|
+
const old = placementsMap.get(id);
|
|
814
|
+
if (old !== undefined)
|
|
815
|
+
dropFrom(kidsOf, old.parent, id);
|
|
816
|
+
if (pl === undefined)
|
|
817
|
+
placementsMap.delete(id);
|
|
818
|
+
else {
|
|
819
|
+
placementsMap.set(id, pl);
|
|
820
|
+
addTo(kidsOf, pl.parent, id);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
/** Everything rebuilt from the records (the first build, roles, an irregular state). */
|
|
824
|
+
const rebuildPlacements = () => {
|
|
825
|
+
placementFull = false;
|
|
826
|
+
placementSeeds.clear();
|
|
827
|
+
stateSeeds.clear();
|
|
828
|
+
ownerChanged.clear();
|
|
829
|
+
itemKinds = declaredItems();
|
|
323
830
|
for (const b of blocks.keys()) {
|
|
324
831
|
const item = itemKind(b);
|
|
325
|
-
if (item
|
|
326
|
-
|
|
327
|
-
items.add(item);
|
|
328
|
-
layouts = true;
|
|
329
|
-
}
|
|
330
|
-
if (!layouts)
|
|
331
|
-
for (const rec of blocks.values())
|
|
332
|
-
if (items.has(rec.type))
|
|
333
|
-
layouts = true;
|
|
334
|
-
if (layouts) {
|
|
335
|
-
const out = dissolve(kidsMap, items);
|
|
336
|
-
if (out.size > 0) {
|
|
337
|
-
dissolved = out;
|
|
338
|
-
kidsMap = childrenIndex(placementsMap, ownShim);
|
|
339
|
-
}
|
|
832
|
+
if (item !== undefined)
|
|
833
|
+
itemKinds.add(item);
|
|
340
834
|
}
|
|
341
|
-
slots = new Map();
|
|
342
835
|
lineKinds = new Set(roles?.lineKinds());
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
836
|
+
kindsOf.clear();
|
|
837
|
+
[layoutBlocks, itemBlocks] = [0, 0];
|
|
838
|
+
lineCounts.clear();
|
|
839
|
+
for (const b of blocks.keys())
|
|
840
|
+
noteKind(b);
|
|
841
|
+
for (const line of lineCounts.keys())
|
|
842
|
+
lineKinds.add(line);
|
|
843
|
+
placementFull = false;
|
|
844
|
+
const resolved = resolvePlacements(blocks, ownerOf);
|
|
845
|
+
placementsMap.clear();
|
|
846
|
+
kidsOf.clear();
|
|
847
|
+
irregular = false;
|
|
848
|
+
for (const [id, pl] of resolved) {
|
|
849
|
+
setPlacement(id, pl);
|
|
850
|
+
const arg = argmaxOf(id);
|
|
851
|
+
if (!arg || arg.parent !== pl.parent || arg.rank !== pl.rank)
|
|
852
|
+
irregular = true;
|
|
347
853
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
for (const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
854
|
+
layoutMode = layoutBlocks + itemBlocks > 0;
|
|
855
|
+
dissolved = new Set();
|
|
856
|
+
const index = (own, map) => {
|
|
857
|
+
const at = new Map();
|
|
858
|
+
for (const [parent, list] of map)
|
|
859
|
+
for (const { id, rank, reset } of list)
|
|
860
|
+
at.set(id, reset === undefined ? { parent, rank } : { parent, rank, reset });
|
|
861
|
+
void own;
|
|
862
|
+
return at;
|
|
863
|
+
};
|
|
864
|
+
kids0 = childrenIndex(placementsMap, own0);
|
|
865
|
+
slots0 = index(own0, kids0);
|
|
866
|
+
if (layoutMode) {
|
|
867
|
+
dissolved = dissolve(kids0);
|
|
868
|
+
kidsMap = childrenIndex(placementsMap, ownShim);
|
|
869
|
+
slots = index(ownShim, kidsMap);
|
|
359
870
|
}
|
|
360
|
-
|
|
871
|
+
else
|
|
872
|
+
[kidsMap, slots] = [kids0, slots0];
|
|
873
|
+
following.clear();
|
|
874
|
+
for (const id of slots.keys())
|
|
875
|
+
noteFollowing(id);
|
|
361
876
|
orderCache = null;
|
|
362
|
-
|
|
877
|
+
kidsVersion++;
|
|
878
|
+
allListsDirty = true;
|
|
879
|
+
};
|
|
880
|
+
/**
|
|
881
|
+
* Re-run the layout rules on the layouts `changed` (blocks whose slot
|
|
882
|
+
* in `kids0` changed, with their parents) and `kinds` reach: from each
|
|
883
|
+
* one up through layouts and items, the topmost such ancestor's
|
|
884
|
+
* subtree. Returns the blocks whose dissolved state flipped.
|
|
885
|
+
*/
|
|
886
|
+
const redissolve = (changed, kinds) => {
|
|
887
|
+
const flips = new Set();
|
|
888
|
+
const isLayoutish = (b) => {
|
|
889
|
+
const k = kindsOf.get(b);
|
|
890
|
+
return k !== undefined && (k.layout || k.item);
|
|
891
|
+
};
|
|
892
|
+
const roots = new Set();
|
|
893
|
+
const climb = (b) => {
|
|
894
|
+
let top;
|
|
895
|
+
for (let x = b; typeof x === 'string' && isLayoutish(x); x = slots0.get(x)?.parent)
|
|
896
|
+
top = x;
|
|
897
|
+
if (top !== undefined)
|
|
898
|
+
roots.add(top);
|
|
899
|
+
};
|
|
900
|
+
for (const [id, [from, to]] of changed) {
|
|
901
|
+
climb(id);
|
|
902
|
+
climb(from);
|
|
903
|
+
climb(to);
|
|
904
|
+
}
|
|
905
|
+
for (const id of kinds) {
|
|
906
|
+
// Its kind decides its own rule, its parent's count and its children's (`layout`).
|
|
907
|
+
climb(id);
|
|
908
|
+
climb(slots0.get(id)?.parent);
|
|
909
|
+
for (const k of kids0.get(id) ?? [])
|
|
910
|
+
climb(k.id);
|
|
911
|
+
// Only a layout or an item dissolves: a block that left those kinds shows again.
|
|
912
|
+
if (!isLayoutish(id) && dissolved.delete(id))
|
|
913
|
+
flips.add(id);
|
|
914
|
+
}
|
|
915
|
+
for (const root of roots) {
|
|
916
|
+
if (!slots0.has(root))
|
|
917
|
+
continue;
|
|
918
|
+
const parent = slots0.get(root).parent;
|
|
919
|
+
const nodes = [];
|
|
920
|
+
const stack = [root];
|
|
921
|
+
for (let x = stack.pop(); x !== undefined; x = stack.pop()) {
|
|
922
|
+
nodes.push(x);
|
|
923
|
+
for (const k of kids0.get(x) ?? [])
|
|
924
|
+
stack.push(k.id);
|
|
925
|
+
}
|
|
926
|
+
const out = new Set();
|
|
927
|
+
dissolveVisit(kids0, out, [root], parent !== null && itemKind(parent) !== undefined);
|
|
928
|
+
for (const x of nodes) {
|
|
929
|
+
if (out.has(x) === dissolved.has(x))
|
|
930
|
+
continue;
|
|
931
|
+
if (out.has(x))
|
|
932
|
+
dissolved.add(x);
|
|
933
|
+
else
|
|
934
|
+
dissolved.delete(x);
|
|
935
|
+
flips.add(x);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// A block that left the index (hidden, removed) dissolves no more.
|
|
939
|
+
for (const id of changed.keys())
|
|
940
|
+
if (!slots0.has(id) && dissolved.delete(id))
|
|
941
|
+
flips.add(id);
|
|
942
|
+
return flips;
|
|
943
|
+
};
|
|
944
|
+
const ensurePlacements = () => {
|
|
945
|
+
ensureOwners();
|
|
946
|
+
if (placementFull || irregular) {
|
|
947
|
+
if (placementFull || placementSeeds.size + stateSeeds.size + ownerChanged.size > 0)
|
|
948
|
+
rebuildPlacements();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (placementSeeds.size + stateSeeds.size + ownerChanged.size === 0)
|
|
952
|
+
return;
|
|
953
|
+
const moved = new Set();
|
|
954
|
+
for (const id of placementSeeds) {
|
|
955
|
+
const next = argmaxOf(id);
|
|
956
|
+
if (next === null)
|
|
957
|
+
return rebuildPlacements();
|
|
958
|
+
const old = placementsMap.get(id);
|
|
959
|
+
if (old?.parent === next?.parent && old?.rank === next?.rank)
|
|
960
|
+
continue;
|
|
961
|
+
setPlacement(id, next);
|
|
962
|
+
moved.add(id);
|
|
963
|
+
}
|
|
964
|
+
// A changed display edge closing a cycle needs the global resolution.
|
|
965
|
+
for (const id of moved)
|
|
966
|
+
if (cyclic(id))
|
|
967
|
+
return rebuildPlacements();
|
|
968
|
+
for (const o of ownerChanged)
|
|
969
|
+
for (const c of kidsOf.get(o) ?? [])
|
|
970
|
+
if (cyclic(c))
|
|
971
|
+
return rebuildPlacements();
|
|
972
|
+
const seeds = new Set([...moved, ...stateSeeds, ...ownerChanged]);
|
|
973
|
+
const kinds = [...stateSeeds];
|
|
974
|
+
placementSeeds.clear();
|
|
975
|
+
stateSeeds.clear();
|
|
976
|
+
ownerChanged.clear();
|
|
977
|
+
const affected = reach(seeds);
|
|
978
|
+
let touched = affected;
|
|
979
|
+
const changed = patch(kids0, slots0, own0, affected);
|
|
980
|
+
let any = changed.size > 0;
|
|
981
|
+
if (layoutMode) {
|
|
982
|
+
const flips = redissolve(changed, kinds);
|
|
983
|
+
const again = flips.size > 0 ? reach([...affected, ...flips]) : affected;
|
|
984
|
+
any = patch(kidsMap, slots, ownShim, again).size > 0 || any;
|
|
985
|
+
touched = again;
|
|
986
|
+
}
|
|
987
|
+
for (const id of touched)
|
|
988
|
+
noteFollowing(id);
|
|
989
|
+
if (any) {
|
|
990
|
+
orderCache = null;
|
|
991
|
+
kidsVersion++;
|
|
992
|
+
}
|
|
363
993
|
};
|
|
364
994
|
/**
|
|
365
995
|
* The kind `id` displays as — THE shown-kind rule, read from its slot
|
|
@@ -397,7 +1027,7 @@ export const bindRuns = (Y) => {
|
|
|
397
1027
|
const slot = slots.get(id);
|
|
398
1028
|
if (roles === null || slot === undefined)
|
|
399
1029
|
return stored;
|
|
400
|
-
const { under, reset } = slot;
|
|
1030
|
+
const { parent: under, reset } = slot;
|
|
401
1031
|
const line = under === null || lineKinds.size === 0 ? undefined : lineKind(under);
|
|
402
1032
|
if (line !== undefined)
|
|
403
1033
|
return line;
|
|
@@ -411,7 +1041,7 @@ export const bindRuns = (Y) => {
|
|
|
411
1041
|
// container whose item is the document's default kind (a column) holds no
|
|
412
1042
|
// items of its own: a paragraph under one is no list's item.
|
|
413
1043
|
if (!lined && typeof from === 'string' && roles.container(from))
|
|
414
|
-
for (let u = under; u !== null; u = slots.get(u)?.
|
|
1044
|
+
for (let u = under; u !== null; u = slots.get(u)?.parent ?? null) {
|
|
415
1045
|
const t = typeOf(u);
|
|
416
1046
|
if (roles.container(t) &&
|
|
417
1047
|
roles.defaultChild(t) === stored &&
|
|
@@ -487,15 +1117,48 @@ export const bindRuns = (Y) => {
|
|
|
487
1117
|
for (const c of rec?.claims ?? [])
|
|
488
1118
|
fx.add(c.m);
|
|
489
1119
|
};
|
|
490
|
-
/**
|
|
1120
|
+
/**
|
|
1121
|
+
* Rebuild (or create, or drop) block `id`'s record, and note what the
|
|
1122
|
+
* maintained facts must re-decide: its owner and the tops of the
|
|
1123
|
+
* blocks it claims (P3), its placement — and, when its entry came or
|
|
1124
|
+
* went, the placements of the blocks whose candidate names it.
|
|
1125
|
+
*/
|
|
491
1126
|
const updateBlockRec = (id) => {
|
|
1127
|
+
const old = blocks.get(id);
|
|
492
1128
|
const node = registry.getAttr(id);
|
|
493
1129
|
if (!isNodeLike(node))
|
|
494
1130
|
blocks.delete(id);
|
|
495
1131
|
else
|
|
496
1132
|
blocks.set(id, buildRec(id, node));
|
|
497
|
-
|
|
1133
|
+
const rec = blocks.get(id);
|
|
1134
|
+
noteEffects(id, rec);
|
|
498
1135
|
noteShell(id);
|
|
1136
|
+
const was = new Set(old?.claims.map((c) => c.m));
|
|
1137
|
+
const now = new Set(rec?.claims.map((c) => c.m));
|
|
1138
|
+
for (const m of was)
|
|
1139
|
+
if (!now.has(m)) {
|
|
1140
|
+
dropFrom(claimersOf, m, id);
|
|
1141
|
+
addTo(claimTargets, id, m);
|
|
1142
|
+
}
|
|
1143
|
+
for (const m of now)
|
|
1144
|
+
if (!was.has(m))
|
|
1145
|
+
addTo(claimersOf, m, id);
|
|
1146
|
+
ownerSeeds.add(id);
|
|
1147
|
+
noteCands(id, argParent(old));
|
|
1148
|
+
placementSeeds.add(id);
|
|
1149
|
+
if ((old === undefined) !== (rec === undefined)) {
|
|
1150
|
+
stateSeeds.add(id);
|
|
1151
|
+
for (const c of byArgParent.get(id) ?? [])
|
|
1152
|
+
placementSeeds.add(c);
|
|
1153
|
+
}
|
|
1154
|
+
if (old?.type !== rec?.type) {
|
|
1155
|
+
noteKind(id);
|
|
1156
|
+
if (old === undefined ||
|
|
1157
|
+
rec === undefined ||
|
|
1158
|
+
roles === null ||
|
|
1159
|
+
!sameShape(roles, old.type, rec.type))
|
|
1160
|
+
stateSeeds.add(id);
|
|
1161
|
+
}
|
|
499
1162
|
};
|
|
500
1163
|
/** A withdrawn block without a delete mark: its `deleted` is settled after each fold. */
|
|
501
1164
|
const noteShell = (id) => {
|
|
@@ -638,7 +1301,7 @@ export const bindRuns = (Y) => {
|
|
|
638
1301
|
};
|
|
639
1302
|
const runs = (b) => {
|
|
640
1303
|
// Read-your-writes: fold the open transaction's pending writes first.
|
|
641
|
-
|
|
1304
|
+
syncAll();
|
|
642
1305
|
if (dirty.has(b) || !cache.has(b))
|
|
643
1306
|
computeRuns(b);
|
|
644
1307
|
return cache.get(b)?.runs ?? EMPTY_RUNS;
|
|
@@ -646,7 +1309,6 @@ export const bindRuns = (Y) => {
|
|
|
646
1309
|
const parentOf = (id) => blocks.get(id)?.cands[0]?.p;
|
|
647
1310
|
/** A structural change of `id`: its readers, and the blocks it claims, re-read. */
|
|
648
1311
|
const invalidateBlock = (id, ctx, claimsBefore = []) => {
|
|
649
|
-
ctx.structure = ctx.placement = true;
|
|
650
1312
|
ctx.invalidated.add(id);
|
|
651
1313
|
for (const c of listConsumers.get(id) ?? [])
|
|
652
1314
|
ctx.invalidated.add(c);
|
|
@@ -656,8 +1318,25 @@ export const bindRuns = (Y) => {
|
|
|
656
1318
|
ctx.invalidated.add(c);
|
|
657
1319
|
}
|
|
658
1320
|
};
|
|
659
|
-
/**
|
|
660
|
-
const
|
|
1321
|
+
/** Note edits of `id`'s own text (`null`: unknown — rescan, every reader re-reads). */
|
|
1322
|
+
const noteText = (ctx, id, e) => {
|
|
1323
|
+
let t = ctx.texts.get(id);
|
|
1324
|
+
if (t === undefined)
|
|
1325
|
+
ctx.texts.set(id, (t = { edits: [], scan: false, opaque: false, seen: new Set() }));
|
|
1326
|
+
if (e === null) {
|
|
1327
|
+
t.scan = t.opaque = true;
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (t.seen.has(e))
|
|
1331
|
+
return;
|
|
1332
|
+
t.seen.add(e);
|
|
1333
|
+
t.scan ||= e.scan;
|
|
1334
|
+
t.opaque ||= e.opaque;
|
|
1335
|
+
for (const x of e.edits)
|
|
1336
|
+
t.edits.push(x);
|
|
1337
|
+
};
|
|
1338
|
+
/** Fold one block's changed facets (`edits`: its own text's, one per changed content node). */
|
|
1339
|
+
const foldBlock = (id, facets, edits, ctx) => {
|
|
661
1340
|
const kinds = new Set();
|
|
662
1341
|
for (const f of facets)
|
|
663
1342
|
kinds.add(f === ENTRY_FACET ? 'entry' : facetOf(f));
|
|
@@ -673,73 +1352,119 @@ export const bindRuns = (Y) => {
|
|
|
673
1352
|
// A new entry, a nonce or an own text can move streams (R2).
|
|
674
1353
|
if (kinds.has('entry') || facets.has(NONCE) || facets.has(CONTENT_ATTR)) {
|
|
675
1354
|
ctx.table = true;
|
|
676
|
-
ctx.
|
|
1355
|
+
ctx.named.add(id);
|
|
1356
|
+
noteText(ctx, id, null);
|
|
677
1357
|
}
|
|
678
1358
|
invalidateBlock(id, ctx, claimsBefore);
|
|
679
1359
|
}
|
|
680
1360
|
else {
|
|
681
1361
|
ensureRec(id);
|
|
682
1362
|
const rec = blocks.get(id);
|
|
683
|
-
if (kinds.has('at')) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1363
|
+
if (kinds.has('at') && rec) {
|
|
1364
|
+
const before = argParent(rec);
|
|
1365
|
+
rec.cands = candidatesOf(rec.node);
|
|
1366
|
+
noteCands(id, before);
|
|
1367
|
+
placementSeeds.add(id);
|
|
687
1368
|
}
|
|
688
1369
|
if (kinds.has('meta') && rec) {
|
|
689
1370
|
const was = rec.type;
|
|
690
1371
|
rec.type = typeAttr(rec.node);
|
|
691
1372
|
rec.data = readData(rec.node);
|
|
692
|
-
if (rec.type !== was)
|
|
1373
|
+
if (rec.type !== was) {
|
|
693
1374
|
retyped = true;
|
|
1375
|
+
noteKind(id);
|
|
1376
|
+
}
|
|
694
1377
|
// A retype that changes the kind's display shape re-parents
|
|
695
1378
|
// (or re-kinds) its children.
|
|
696
1379
|
if (roles !== null && !sameShape(roles, was, rec.type))
|
|
697
|
-
|
|
1380
|
+
stateSeeds.add(id);
|
|
698
1381
|
}
|
|
699
1382
|
}
|
|
700
1383
|
const parentAfter = parentOf(id);
|
|
701
1384
|
if (typeof parentAfter === 'string')
|
|
702
1385
|
ctx.parents.add(parentAfter);
|
|
703
|
-
if (kinds.has('content')
|
|
704
|
-
|
|
705
|
-
|
|
1386
|
+
if (kinds.has('content'))
|
|
1387
|
+
for (const e of edits)
|
|
1388
|
+
noteText(ctx, id, e);
|
|
706
1389
|
};
|
|
707
|
-
/**
|
|
1390
|
+
/**
|
|
1391
|
+
* Move each edited text's row by its edits (or rescan it), re-decide the
|
|
1392
|
+
* delimiters a boundary change names, re-place the rows they cut, and
|
|
1393
|
+
* invalidate the readers: the display owner of each edited stream (L7),
|
|
1394
|
+
* the blocks whose stream changed, every reader of an opaque text.
|
|
1395
|
+
*/
|
|
708
1396
|
const foldTexts = (ctx) => {
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1397
|
+
const homes = new Set();
|
|
1398
|
+
const gapsOf = new Map();
|
|
1399
|
+
const gapsIn = (row, edits) => {
|
|
1400
|
+
const gaps = [];
|
|
1401
|
+
for (const e of edits) {
|
|
1402
|
+
const gap = gapOfItem(row, e.item);
|
|
1403
|
+
if (gap < 0)
|
|
1404
|
+
return null;
|
|
1405
|
+
gaps.push(gap);
|
|
1406
|
+
}
|
|
1407
|
+
return gaps;
|
|
1408
|
+
};
|
|
1409
|
+
for (const [home, t] of ctx.texts) {
|
|
1410
|
+
const text = blocks.get(home)?.content;
|
|
1411
|
+
const row = rows.get(home);
|
|
1412
|
+
let gaps = [];
|
|
1413
|
+
let fresh = t.scan || row === undefined || text === undefined || row.text !== text;
|
|
1414
|
+
if (!fresh) {
|
|
1415
|
+
for (const e of t.edits) {
|
|
1416
|
+
const gap = gapOfItem(row, e.item);
|
|
1417
|
+
if (gap < 0 || !shiftGap(row, gap, e.del ? -e.len : e.len)) {
|
|
1418
|
+
fresh = true;
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
gaps.push(gap);
|
|
1422
|
+
}
|
|
1423
|
+
// The engine's own count of live units: a row that disagrees is stale.
|
|
1424
|
+
if (!fresh && rowLength(row) !== text._length)
|
|
1425
|
+
fresh = true;
|
|
1426
|
+
}
|
|
1427
|
+
if (fresh) {
|
|
1428
|
+
const next = scanRow(home);
|
|
1429
|
+
const same = next !== undefined && row !== undefined && next.key === row.key;
|
|
1430
|
+
if (same)
|
|
1431
|
+
[next.cuts, next.head] = [row.cuts, row.head];
|
|
1432
|
+
setRow(home, next, ctx.named);
|
|
1433
|
+
if (!same)
|
|
1434
|
+
homes.add(home);
|
|
1435
|
+
gaps = next === undefined ? [] : gapsIn(next, t.edits);
|
|
1436
|
+
}
|
|
1437
|
+
gapsOf.set(home, t.opaque ? null : gaps);
|
|
1438
|
+
}
|
|
1439
|
+
refreshDelims(ctx.named, homes);
|
|
1440
|
+
if (homes.size > 0)
|
|
714
1441
|
ctx.table = true;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
for (const [home, spans] of ctx.texts) {
|
|
721
|
-
const readers = textConsumers.get(home) ?? [];
|
|
722
|
-
if (spans === null || changed.has(home) || ctx.structure) {
|
|
723
|
-
for (const c of readers)
|
|
1442
|
+
for (const home of homes)
|
|
1443
|
+
placeHome(home, ctx.invalidated);
|
|
1444
|
+
for (const [home, gaps] of gapsOf) {
|
|
1445
|
+
if (gaps === null) {
|
|
1446
|
+
for (const c of textConsumers.get(home) ?? [])
|
|
724
1447
|
ctx.invalidated.add(c);
|
|
725
1448
|
continue;
|
|
726
1449
|
}
|
|
727
|
-
// Only the
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1450
|
+
// Only the stream each edit lies in changes its display (L7).
|
|
1451
|
+
const row = rows.get(home);
|
|
1452
|
+
if (row === undefined)
|
|
1453
|
+
continue;
|
|
1454
|
+
for (const gap of gaps) {
|
|
1455
|
+
const b = headOf(row, segmentOfGap(row, gap));
|
|
1456
|
+
const owner = b === null ? DEAD : ownerOf(b);
|
|
732
1457
|
if (typeof owner === 'string')
|
|
733
1458
|
ctx.invalidated.add(owner);
|
|
734
1459
|
}
|
|
735
1460
|
}
|
|
736
1461
|
};
|
|
737
1462
|
/** The blocks whose stream lies in a text the fold edited. */
|
|
738
|
-
const editedStreams = (ctx) => [...ctx.texts.keys()].flatMap((home) => (
|
|
1463
|
+
const editedStreams = (ctx) => [...ctx.texts.keys()].flatMap((home) => streamsIn(home).map((st) => st.block));
|
|
739
1464
|
/** A live unit in `id`'s stream (boundaries excepted). */
|
|
740
1465
|
const streamHolds = (id) => {
|
|
741
|
-
const
|
|
742
|
-
return
|
|
1466
|
+
const st = streamOf(id);
|
|
1467
|
+
return st !== undefined && st.end - st.start > st.inert.length;
|
|
743
1468
|
};
|
|
744
1469
|
/**
|
|
745
1470
|
* Settle the withdrawn blocks (`hist.undo.withdraw`): a shell is deleted
|
|
@@ -788,9 +1513,9 @@ export const bindRuns = (Y) => {
|
|
|
788
1513
|
if (rec.deleted === !alive.has(id))
|
|
789
1514
|
continue;
|
|
790
1515
|
rec.deleted = !alive.has(id);
|
|
1516
|
+
ownerSeeds.add(id);
|
|
791
1517
|
if (ctx === null)
|
|
792
1518
|
continue;
|
|
793
|
-
structureVersion++;
|
|
794
1519
|
invalidateBlock(id, ctx);
|
|
795
1520
|
}
|
|
796
1521
|
};
|
|
@@ -828,9 +1553,9 @@ export const bindRuns = (Y) => {
|
|
|
828
1553
|
const candidates = new Set();
|
|
829
1554
|
let reporting = false;
|
|
830
1555
|
/** THE fold: one pass over changed `(type, parentSub)` pairs, each block folded once. */
|
|
831
|
-
const fold = (changed,
|
|
1556
|
+
const fold = (changed, editsOf) => {
|
|
832
1557
|
const touched = new Map();
|
|
833
|
-
const
|
|
1558
|
+
const edits = new Map();
|
|
834
1559
|
let derived = changed.has(dataRoot);
|
|
835
1560
|
for (const [type, subs] of changed) {
|
|
836
1561
|
for (const sub of subs) {
|
|
@@ -842,32 +1567,33 @@ export const bindRuns = (Y) => {
|
|
|
842
1567
|
if (facets === undefined)
|
|
843
1568
|
touched.set(id, (facets = new Set()));
|
|
844
1569
|
facets.add(facet);
|
|
845
|
-
if (facet !== CONTENT
|
|
1570
|
+
if (facet !== CONTENT)
|
|
846
1571
|
continue;
|
|
847
|
-
|
|
848
|
-
|
|
1572
|
+
// A sequence edit of the block's own text, or a change inside one of its atoms.
|
|
1573
|
+
let list = edits.get(id);
|
|
1574
|
+
if (list === undefined)
|
|
1575
|
+
edits.set(id, (list = []));
|
|
1576
|
+
list.push(seq ? editsOf(type) : null);
|
|
849
1577
|
}
|
|
850
1578
|
}
|
|
851
1579
|
const ctx = {
|
|
852
1580
|
invalidated: new Set(),
|
|
853
1581
|
texts: new Map(),
|
|
1582
|
+
named: new Set(),
|
|
854
1583
|
table: false,
|
|
855
|
-
structure: false,
|
|
856
|
-
placement: false,
|
|
857
1584
|
parents: new Set()
|
|
858
1585
|
};
|
|
859
1586
|
for (const [id, facets] of touched) {
|
|
860
|
-
foldBlock(id, facets,
|
|
1587
|
+
foldBlock(id, facets, edits.get(id) ?? [], ctx);
|
|
861
1588
|
derived ||= [...facets].some((f) => f === ENTRY_FACET || facetOf(f) !== 'ignore');
|
|
862
1589
|
}
|
|
863
|
-
if (ctx.structure)
|
|
864
|
-
structureVersion++;
|
|
865
1590
|
foldTexts(ctx);
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
placementVersion++;
|
|
1591
|
+
if (shells.size > 0)
|
|
1592
|
+
settle(ctx.table ? shells : [...touched.keys(), ...ctx.parents, ...editedStreams(ctx)], ctx);
|
|
869
1593
|
for (const b of ctx.invalidated)
|
|
870
1594
|
dirty.add(b);
|
|
1595
|
+
if (indexChecks.on)
|
|
1596
|
+
check();
|
|
871
1597
|
if (derived)
|
|
872
1598
|
version++;
|
|
873
1599
|
if (reporting) {
|
|
@@ -888,8 +1614,94 @@ export const bindRuns = (Y) => {
|
|
|
888
1614
|
}
|
|
889
1615
|
return touched;
|
|
890
1616
|
};
|
|
891
|
-
/**
|
|
892
|
-
|
|
1617
|
+
/** {@link indexChecks}: every incrementally maintained fact equals its rebuild. */
|
|
1618
|
+
const check = () => {
|
|
1619
|
+
const fail = (what) => {
|
|
1620
|
+
throw new Error(`[edytor index] ${what} differs from its rebuild`);
|
|
1621
|
+
};
|
|
1622
|
+
const scans = new Map();
|
|
1623
|
+
for (const rec of blocks.values())
|
|
1624
|
+
if (rec.content)
|
|
1625
|
+
scans.set(rec.id, scanText(rec.id, rec.content));
|
|
1626
|
+
for (const home of rows.keys())
|
|
1627
|
+
if (!scans.has(home))
|
|
1628
|
+
fail(`row ${home}`);
|
|
1629
|
+
for (const [home, scan] of scans) {
|
|
1630
|
+
const row = rows.get(home);
|
|
1631
|
+
if (row === undefined || row.text !== scan.text || !sameRow(row, scan))
|
|
1632
|
+
fail(`row ${home}`);
|
|
1633
|
+
}
|
|
1634
|
+
const want = delimiters(blocks, scans.values());
|
|
1635
|
+
if (want.size !== delim.size || [...want].some(([b, k]) => delim.get(b) !== k))
|
|
1636
|
+
fail('delimiters');
|
|
1637
|
+
const streamSig = (st) => st && `${st.home}:${st.start}-${st.end}/${st.inert.join(',')}`;
|
|
1638
|
+
const placed = new Set();
|
|
1639
|
+
for (const [home, scan] of scans)
|
|
1640
|
+
for (const st of placeText(scan, want)) {
|
|
1641
|
+
placed.add(st.block);
|
|
1642
|
+
if (streamSig(streamOf(st.block)) !== streamSig(st))
|
|
1643
|
+
fail(`stream of ${st.block} in ${home}`);
|
|
1644
|
+
}
|
|
1645
|
+
for (const b of streamIx.keys())
|
|
1646
|
+
if (!placed.has(b))
|
|
1647
|
+
fail(`stream of ${b}`);
|
|
1648
|
+
for (const [b, c] of cache) {
|
|
1649
|
+
if (dirty.has(b))
|
|
1650
|
+
continue;
|
|
1651
|
+
const fresh = computeFresh(b).fresh;
|
|
1652
|
+
if (keyOf(fresh) !== keyOf(c.runs))
|
|
1653
|
+
fail(`runs of ${b}`);
|
|
1654
|
+
}
|
|
1655
|
+
// P3: the claim graph, placements, children index and layout rules.
|
|
1656
|
+
ensurePlacements();
|
|
1657
|
+
const graph = claimGraph(blocks);
|
|
1658
|
+
for (const b of blocks.keys()) {
|
|
1659
|
+
if (ownerOf(b) !== (graph.owners.get(b) ?? DEAD))
|
|
1660
|
+
fail(`owner of ${b}`);
|
|
1661
|
+
if (ownShim.top(b) !== graph.top.get(b))
|
|
1662
|
+
fail(`top of ${b}`);
|
|
1663
|
+
}
|
|
1664
|
+
const shown = displayIndex(blocks, ownerOf);
|
|
1665
|
+
for (const [o, list] of shown)
|
|
1666
|
+
if (list.length !== (displaysMap.get(o)?.size ?? 0) ||
|
|
1667
|
+
list.some((b) => !displaysMap.get(o).has(b)))
|
|
1668
|
+
fail(`displays of ${o}`);
|
|
1669
|
+
for (const o of displaysMap.keys())
|
|
1670
|
+
if (!shown.has(o))
|
|
1671
|
+
fail(`displays of ${o}`);
|
|
1672
|
+
const resolved = resolvePlacements(blocks, ownerOf);
|
|
1673
|
+
if (resolved.size !== placementsMap.size)
|
|
1674
|
+
fail('placements');
|
|
1675
|
+
for (const [b, pl] of resolved) {
|
|
1676
|
+
const mine = placementsMap.get(b);
|
|
1677
|
+
if (mine?.parent !== pl.parent || mine.rank !== pl.rank)
|
|
1678
|
+
fail(`placement of ${b}`);
|
|
1679
|
+
}
|
|
1680
|
+
const same = (x, y) => x.size === y.size && [...x].every(([p, l]) => keyOf(l) === keyOf(y.get(p) ?? null));
|
|
1681
|
+
const k0 = childrenIndex(placementsMap, own0);
|
|
1682
|
+
if (!same(k0, kids0))
|
|
1683
|
+
fail('children index (before the layout rules)');
|
|
1684
|
+
const out = dissolve(k0);
|
|
1685
|
+
if (out.size !== dissolved.size || [...out].some((b) => !dissolved.has(b)))
|
|
1686
|
+
fail('dissolved');
|
|
1687
|
+
if (!same(childrenIndex(placementsMap, ownShim), kidsMap))
|
|
1688
|
+
fail('children index');
|
|
1689
|
+
for (const [p, l] of kidsMap)
|
|
1690
|
+
for (const { id, rank, reset } of l) {
|
|
1691
|
+
const slot = slots.get(id);
|
|
1692
|
+
if (slot?.parent !== p || slot.rank !== rank || slot.reset !== reset)
|
|
1693
|
+
fail(`slot of ${id}`);
|
|
1694
|
+
}
|
|
1695
|
+
const follow = new Set(following);
|
|
1696
|
+
for (const id of slots.keys())
|
|
1697
|
+
noteFollowing(id);
|
|
1698
|
+
if (follow.size !== following.size || [...follow].some((b) => !following.has(b)))
|
|
1699
|
+
fail('following');
|
|
1700
|
+
};
|
|
1701
|
+
/** Each transaction's folded part: copies of its id sets, and their lengths (the watermark). */
|
|
1702
|
+
const cursors = new WeakMap();
|
|
1703
|
+
/** The transactions whose commit fold ran. */
|
|
1704
|
+
const committed = new WeakSet();
|
|
893
1705
|
const lengthOf = (set) => {
|
|
894
1706
|
let n = 0;
|
|
895
1707
|
set.clients.forEach((ranges) => {
|
|
@@ -907,9 +1719,9 @@ export const bindRuns = (Y) => {
|
|
|
907
1719
|
const syncPending = (tr) => {
|
|
908
1720
|
if (tr?.insertSet === undefined || tr.deleteSet === undefined)
|
|
909
1721
|
return false;
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1722
|
+
let cursor = cursors.get(tr);
|
|
1723
|
+
if (cursor === undefined)
|
|
1724
|
+
cursors.set(tr, (cursor = { ins: Y.createIdSet(), del: Y.createIdSet(), mark: 0 }));
|
|
913
1725
|
const mark = lengthOf(tr.insertSet) + lengthOf(tr.deleteSet);
|
|
914
1726
|
if (mark === cursor.mark)
|
|
915
1727
|
return false;
|
|
@@ -931,16 +1743,37 @@ export const bindRuns = (Y) => {
|
|
|
931
1743
|
};
|
|
932
1744
|
walkIdSetStructs(Y, doc, ins, note);
|
|
933
1745
|
walkIdSetStructs(Y, doc, del, note);
|
|
934
|
-
fold(changed,
|
|
1746
|
+
fold(changed, makeEditIndex(ins, del));
|
|
935
1747
|
return true;
|
|
936
1748
|
};
|
|
937
1749
|
const openTx = () => doc._transaction;
|
|
1750
|
+
/**
|
|
1751
|
+
* Fold every transaction whose writes are in the document and whose
|
|
1752
|
+
* commit fold has not run, in their order: the open one, and those a
|
|
1753
|
+
* transaction's cleanup started (an observer's or an `update`
|
|
1754
|
+
* listener's write runs its body at once, its observers later) — a
|
|
1755
|
+
* read in between must not mix their items with an index that never
|
|
1756
|
+
* saw them.
|
|
1757
|
+
*/
|
|
1758
|
+
const syncAll = () => {
|
|
1759
|
+
for (const tr of queuedTransactions(doc))
|
|
1760
|
+
if (!committed.has(tr))
|
|
1761
|
+
syncPending(tr);
|
|
1762
|
+
};
|
|
938
1763
|
/** The commit: fold `transaction.changed` once (the report publishes from `update`). */
|
|
939
1764
|
const onCommit = (e) => {
|
|
940
1765
|
const tr = e.transaction;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1766
|
+
let [ins, del] = [tr.insertSet, tr.deleteSet];
|
|
1767
|
+
// The writes a read folded already moved the rows: only the rest
|
|
1768
|
+
// are edits now (the facets fold again, idempotent).
|
|
1769
|
+
const cursor = cursors.get(tr);
|
|
1770
|
+
if (cursor !== undefined && ins !== undefined && del !== undefined) {
|
|
1771
|
+
ins = Y.diffIdSet(ins, cursor.ins);
|
|
1772
|
+
del = Y.diffIdSet(del, cursor.del);
|
|
1773
|
+
}
|
|
1774
|
+
cursors.delete(tr);
|
|
1775
|
+
committed.add(tr);
|
|
1776
|
+
fold(tr.changed ?? new Map(), makeEditIndex(ins, del));
|
|
944
1777
|
};
|
|
945
1778
|
// ── projection ───────────────────────────────────────────────────
|
|
946
1779
|
/** `id`'s visible content items, canonical and frozen (R4). */
|
|
@@ -981,7 +1814,7 @@ export const bindRuns = (Y) => {
|
|
|
981
1814
|
: { ...old, parent, index });
|
|
982
1815
|
});
|
|
983
1816
|
}
|
|
984
|
-
return { nodes, order, kids:
|
|
1817
|
+
return { nodes, order, kids: kidsVersion };
|
|
985
1818
|
};
|
|
986
1819
|
const keyOf = (v) => {
|
|
987
1820
|
try {
|
|
@@ -991,6 +1824,126 @@ export const bindRuns = (Y) => {
|
|
|
991
1824
|
return JSON.stringify(cloneJsonSafe(v ?? null));
|
|
992
1825
|
}
|
|
993
1826
|
};
|
|
1827
|
+
/**
|
|
1828
|
+
* The report's structural part from the child lists patched since the
|
|
1829
|
+
* last report (P3), advancing the published tree in place — what
|
|
1830
|
+
* `reachable` would give, without walking the lists nothing changed:
|
|
1831
|
+
* a published block no longer shown leaves (a root when its parent
|
|
1832
|
+
* stays), its still-shown descendants staying; each patched list's
|
|
1833
|
+
* new order is news, a block in it at a new parent or index moved, one
|
|
1834
|
+
* not published before is added with its subtree.
|
|
1835
|
+
*/
|
|
1836
|
+
const advance = (pub, r, covered, reparented) => {
|
|
1837
|
+
const { nodes, order } = pub;
|
|
1838
|
+
const shown = (x) => {
|
|
1839
|
+
for (let c = x; c !== null;) {
|
|
1840
|
+
const slot = slots.get(c);
|
|
1841
|
+
if (slot === undefined)
|
|
1842
|
+
return false;
|
|
1843
|
+
c = slot.parent;
|
|
1844
|
+
}
|
|
1845
|
+
return true;
|
|
1846
|
+
};
|
|
1847
|
+
const lists = [...dirtyLists];
|
|
1848
|
+
// Leaving: a published block of a patched list that no longer shows.
|
|
1849
|
+
const drop = (id) => {
|
|
1850
|
+
const ks = order.get(id);
|
|
1851
|
+
if (ks !== undefined) {
|
|
1852
|
+
order.delete(id);
|
|
1853
|
+
r.order.set(id, EMPTY_IDS);
|
|
1854
|
+
for (const k of ks)
|
|
1855
|
+
if (!shown(k))
|
|
1856
|
+
drop(k);
|
|
1857
|
+
}
|
|
1858
|
+
nodes.delete(id);
|
|
1859
|
+
};
|
|
1860
|
+
for (const id of leftLists) {
|
|
1861
|
+
const o = nodes.get(id);
|
|
1862
|
+
if (o === undefined || shown(id))
|
|
1863
|
+
continue;
|
|
1864
|
+
if (o.parent === null || shown(o.parent))
|
|
1865
|
+
r.removed.add(id);
|
|
1866
|
+
drop(id);
|
|
1867
|
+
}
|
|
1868
|
+
// Each patched list that shows: its order, its moved and new blocks.
|
|
1869
|
+
const fresh = new Map();
|
|
1870
|
+
for (const p of lists) {
|
|
1871
|
+
if (p !== null && !shown(p))
|
|
1872
|
+
continue;
|
|
1873
|
+
const ks = kidsMap.get(p) ?? [];
|
|
1874
|
+
const prev = order.get(p);
|
|
1875
|
+
if (ks.length === 0) {
|
|
1876
|
+
if (prev !== undefined) {
|
|
1877
|
+
order.delete(p);
|
|
1878
|
+
r.order.set(p, EMPTY_IDS);
|
|
1879
|
+
}
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
const ids = ks.map((k) => k.id);
|
|
1883
|
+
if (prev === undefined || !sameIds(prev, ids)) {
|
|
1884
|
+
const frozen = Object.freeze(ids);
|
|
1885
|
+
order.set(p, frozen);
|
|
1886
|
+
r.order.set(p, frozen);
|
|
1887
|
+
}
|
|
1888
|
+
ks.forEach(({ id }, index) => {
|
|
1889
|
+
const n = nodes.get(id);
|
|
1890
|
+
if (n === undefined)
|
|
1891
|
+
fresh.set(id, { parent: p, index });
|
|
1892
|
+
else if (n.parent !== p || n.index !== index) {
|
|
1893
|
+
r.moved.add(id);
|
|
1894
|
+
if (n.parent !== p)
|
|
1895
|
+
reparented.add(id);
|
|
1896
|
+
n.parent = p;
|
|
1897
|
+
n.index = index;
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
// New roots (no new ancestor) bring their subtree.
|
|
1902
|
+
const add = (b, parent, index) => {
|
|
1903
|
+
if (!nodes.has(b.id)) {
|
|
1904
|
+
covered.add(b.id);
|
|
1905
|
+
const rec = blocks.get(b.id);
|
|
1906
|
+
nodes.set(b.id, { parent, index, type: typeOf(b.id), data: rec.data, runs: runs(b.id) });
|
|
1907
|
+
}
|
|
1908
|
+
if (b.children.length > 0 && !order.has(b.id)) {
|
|
1909
|
+
const ids = Object.freeze(b.children.map((c) => c.id));
|
|
1910
|
+
order.set(b.id, ids);
|
|
1911
|
+
r.order.set(b.id, ids);
|
|
1912
|
+
}
|
|
1913
|
+
b.children.forEach((c, i) => add(c, b.id, i));
|
|
1914
|
+
};
|
|
1915
|
+
for (const [id, at] of fresh) {
|
|
1916
|
+
let under = false;
|
|
1917
|
+
for (let c = slots.get(id)?.parent ?? null; c !== null && !under; c = slots.get(c)?.parent ?? null)
|
|
1918
|
+
under = fresh.has(c);
|
|
1919
|
+
if (under || covered.has(id))
|
|
1920
|
+
continue;
|
|
1921
|
+
const b = projectBlock(id);
|
|
1922
|
+
r.added.set(id, b);
|
|
1923
|
+
add(b, at.parent, at.index);
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
/** {@link indexChecks}: the published tree the report advanced equals a fresh walk. */
|
|
1927
|
+
const checkPublished = () => {
|
|
1928
|
+
const pub = published;
|
|
1929
|
+
// Payloads from the published nodes: the check reads no runs.
|
|
1930
|
+
const want = reachable(pub.nodes);
|
|
1931
|
+
const fail = (what) => {
|
|
1932
|
+
throw new Error(`[edytor index] published ${what} differs from its rebuild`);
|
|
1933
|
+
};
|
|
1934
|
+
if (want.order.size !== pub.order.size)
|
|
1935
|
+
fail('lists');
|
|
1936
|
+
for (const [p, ids] of want.order)
|
|
1937
|
+
if (!sameIds(ids, pub.order.get(p) ?? []))
|
|
1938
|
+
fail(`list of ${p}`);
|
|
1939
|
+
if (want.nodes.size !== pub.nodes.size)
|
|
1940
|
+
fail('blocks');
|
|
1941
|
+
for (const [id, n] of want.nodes) {
|
|
1942
|
+
const m = pub.nodes.get(id);
|
|
1943
|
+
if (m?.parent !== n.parent || m.index !== n.index)
|
|
1944
|
+
fail(`slot of ${id}`);
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
994
1947
|
/** The blocks whose shown kind followed their slot at the last report. */
|
|
995
1948
|
let followingBefore = [];
|
|
996
1949
|
/** The document data's key at the last report. */
|
|
@@ -1015,14 +1968,19 @@ export const bindRuns = (Y) => {
|
|
|
1015
1968
|
if (key !== dataKey)
|
|
1016
1969
|
[r.data, dataKey] = [data, key];
|
|
1017
1970
|
}
|
|
1018
|
-
if (
|
|
1971
|
+
if (kidsVersion === before.kids && candidates.size === 0)
|
|
1019
1972
|
return r.data ? r : null;
|
|
1020
|
-
const after = kidsMap === before.kids ? before : reachable(before.nodes);
|
|
1021
1973
|
// Added subtrees carry their new descendants. A descendant that was
|
|
1022
1974
|
// visible before is reported like any visible block (moved, retyped,
|
|
1023
1975
|
// edited against its published baseline), so consumers keep it (K7).
|
|
1024
1976
|
const covered = new Set();
|
|
1025
|
-
|
|
1977
|
+
/** Moved to another display parent: only those can show another kind (an index never does). */
|
|
1978
|
+
const reparented = new Set();
|
|
1979
|
+
const lists = kidsVersion !== before.kids && !allListsDirty;
|
|
1980
|
+
const after = kidsVersion === before.kids || lists ? before : reachable(before.nodes);
|
|
1981
|
+
if (lists)
|
|
1982
|
+
advance(before, r, covered, reparented);
|
|
1983
|
+
else if (after !== before) {
|
|
1026
1984
|
for (const [parent, ids] of after.order) {
|
|
1027
1985
|
const prev = before.order.get(parent);
|
|
1028
1986
|
if (prev === undefined || !sameIds(prev, ids))
|
|
@@ -1049,6 +2007,8 @@ export const bindRuns = (Y) => {
|
|
|
1049
2007
|
const o = before.nodes.get(id);
|
|
1050
2008
|
if (o.parent !== n.parent || o.index !== n.index)
|
|
1051
2009
|
r.moved.add(id);
|
|
2010
|
+
if (o.parent !== n.parent)
|
|
2011
|
+
reparented.add(id);
|
|
1052
2012
|
}
|
|
1053
2013
|
}
|
|
1054
2014
|
// Removed subtree ROOTS: a removed id whose before-parent stays
|
|
@@ -1090,13 +2050,14 @@ export const bindRuns = (Y) => {
|
|
|
1090
2050
|
// changes a shown kind — the retyped block's (a candidate) and the
|
|
1091
2051
|
// kinds derived from it: a promoted or stray line shows its display
|
|
1092
2052
|
// parent's default child (XW-08).
|
|
1093
|
-
if (after !== before || retyped) {
|
|
1094
|
-
|
|
2053
|
+
if (after !== before || lists || retyped) {
|
|
2054
|
+
const now = [...following];
|
|
2055
|
+
for (const id of [...reparented, ...now, ...followingBefore]) {
|
|
1095
2056
|
const n = after.nodes.get(id);
|
|
1096
2057
|
if (n !== undefined && !candidates.has(id) && !covered.has(id) && typeOf(id) !== n.type)
|
|
1097
2058
|
meta(id, n);
|
|
1098
2059
|
}
|
|
1099
|
-
followingBefore =
|
|
2060
|
+
followingBefore = now;
|
|
1100
2061
|
}
|
|
1101
2062
|
// A plain block directly in a list shows as its item (`itemOf`, AW-04):
|
|
1102
2063
|
// a block whose shown kind changed re-reads its children's (a worklist:
|
|
@@ -1110,6 +2071,12 @@ export const bindRuns = (Y) => {
|
|
|
1110
2071
|
retyped = false;
|
|
1111
2072
|
candidates.clear();
|
|
1112
2073
|
published = after;
|
|
2074
|
+
after.kids = kidsVersion;
|
|
2075
|
+
dirtyLists.clear();
|
|
2076
|
+
leftLists.clear();
|
|
2077
|
+
allListsDirty = false;
|
|
2078
|
+
if (indexChecks.on)
|
|
2079
|
+
checkPublished();
|
|
1113
2080
|
const empty = r.added.size + r.removed.size + r.moved.size + r.meta.size + r.content.size + r.order.size;
|
|
1114
2081
|
return empty === 0 && !r.data ? null : r;
|
|
1115
2082
|
};
|
|
@@ -1129,7 +2096,9 @@ export const bindRuns = (Y) => {
|
|
|
1129
2096
|
if (!isNodeLike(v))
|
|
1130
2097
|
return;
|
|
1131
2098
|
updateBlockRec(id);
|
|
1132
|
-
|
|
2099
|
+
const row = scanRow(id);
|
|
2100
|
+
if (row !== undefined)
|
|
2101
|
+
rows.set(id, row);
|
|
1133
2102
|
});
|
|
1134
2103
|
rebuildTable(new Set());
|
|
1135
2104
|
settle(shells, null);
|
|
@@ -1146,12 +2115,12 @@ export const bindRuns = (Y) => {
|
|
|
1146
2115
|
// ── public surface ───────────────────────────────────────────────
|
|
1147
2116
|
const view = {
|
|
1148
2117
|
version: () => {
|
|
1149
|
-
|
|
2118
|
+
syncAll();
|
|
1150
2119
|
return version;
|
|
1151
2120
|
},
|
|
1152
2121
|
runs,
|
|
1153
2122
|
contentItems: (b) => {
|
|
1154
|
-
|
|
2123
|
+
syncAll();
|
|
1155
2124
|
return blocks.get(b)?.deleted === false ? itemsOf(b) : [];
|
|
1156
2125
|
},
|
|
1157
2126
|
// Inline atoms always carry `data` (`{}` when absent) — the public
|
|
@@ -1160,24 +2129,26 @@ export const bindRuns = (Y) => {
|
|
|
1160
2129
|
? { text: r.text, ...(r.marks !== undefined && { marks: cloneJsonSafe(r.marks) }) }
|
|
1161
2130
|
: { id: r.id, type: r.type, data: r.data === undefined ? {} : cloneJsonSafe(r.data) }),
|
|
1162
2131
|
view: (tr) => {
|
|
1163
|
-
|
|
2132
|
+
syncAll();
|
|
2133
|
+
if (tr !== undefined && !committed.has(tr))
|
|
2134
|
+
syncPending(tr);
|
|
1164
2135
|
return ctx;
|
|
1165
2136
|
},
|
|
1166
2137
|
displayType: (id) => {
|
|
1167
2138
|
if (roles === null)
|
|
1168
2139
|
return undefined;
|
|
1169
|
-
|
|
2140
|
+
syncAll();
|
|
1170
2141
|
ensurePlacements();
|
|
1171
2142
|
const shown = blocks.has(id) ? typeOf(id) : undefined;
|
|
1172
2143
|
return shown === blocks.get(id)?.type ? undefined : shown;
|
|
1173
2144
|
},
|
|
1174
2145
|
dissolved: (id) => {
|
|
1175
|
-
|
|
2146
|
+
syncAll();
|
|
1176
2147
|
ensurePlacements();
|
|
1177
2148
|
return dissolved.has(id);
|
|
1178
2149
|
},
|
|
1179
2150
|
project: (root) => {
|
|
1180
|
-
|
|
2151
|
+
syncAll();
|
|
1181
2152
|
ensurePlacements();
|
|
1182
2153
|
return root === undefined
|
|
1183
2154
|
? (kidsMap.get(null) ?? []).map((k) => projectBlock(k.id))
|
|
@@ -1185,7 +2156,7 @@ export const bindRuns = (Y) => {
|
|
|
1185
2156
|
},
|
|
1186
2157
|
roles: (next) => {
|
|
1187
2158
|
roles = next;
|
|
1188
|
-
|
|
2159
|
+
placementFull = true;
|
|
1189
2160
|
version++;
|
|
1190
2161
|
// No commit carries a role change: report it now (not inside a
|
|
1191
2162
|
// transaction, whose commit reports it), so every view follows.
|
|
@@ -1193,12 +2164,12 @@ export const bindRuns = (Y) => {
|
|
|
1193
2164
|
publish(null, false);
|
|
1194
2165
|
},
|
|
1195
2166
|
track: () => {
|
|
1196
|
-
|
|
2167
|
+
syncAll();
|
|
1197
2168
|
const f = { touched: new Map(), wrote: false };
|
|
1198
2169
|
frames.add(f);
|
|
1199
2170
|
return {
|
|
1200
2171
|
end: () => {
|
|
1201
|
-
|
|
2172
|
+
syncAll();
|
|
1202
2173
|
frames.delete(f);
|
|
1203
2174
|
return f;
|
|
1204
2175
|
}
|
|
@@ -1206,7 +2177,7 @@ export const bindRuns = (Y) => {
|
|
|
1206
2177
|
},
|
|
1207
2178
|
onReport: (cb) => {
|
|
1208
2179
|
if (reportSubs.size === 0) {
|
|
1209
|
-
|
|
2180
|
+
syncAll();
|
|
1210
2181
|
published = reachable();
|
|
1211
2182
|
dataKey = keyOf(docData());
|
|
1212
2183
|
candidates.clear();
|