edytor 3.0.0 → 4.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/DocumentRoom.d.ts +13 -9
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/room/context.d.ts +27 -26
- package/dist/cloudflare/room/context.js +2 -2
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +44 -19
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/crdt/attribution/attribution.d.ts +31 -12
- package/dist/crdt/attribution/attribution.js +56 -18
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.js +29 -24
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +25 -9
- package/dist/crdt/doc/history.js +150 -40
- package/dist/crdt/doc/layout.js +11 -22
- package/dist/crdt/doc/meta.js +12 -17
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.js +2 -2
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +52 -122
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +16 -5
- package/dist/crdt/document.js +21 -4
- package/dist/crdt/edytor-doc.d.ts +62 -64
- package/dist/crdt/edytor-doc.js +18 -13
- package/dist/crdt/engine-api.d.ts +33 -1
- package/dist/crdt/engine.d.ts +0 -1
- package/dist/crdt/engine.js +1 -4
- package/dist/crdt/flow.d.ts +1 -1
- package/dist/crdt/flow.js +25 -27
- package/dist/crdt/index.d.ts +26 -25
- package/dist/crdt/index.js +1 -1
- package/dist/crdt/placement/display.d.ts +5 -30
- package/dist/crdt/placement/display.js +39 -144
- package/dist/crdt/placement/keys.d.ts +58 -0
- package/dist/crdt/placement/keys.js +90 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +187 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.js +5 -3
- package/dist/crdt/purge.d.ts +17 -8
- package/dist/crdt/purge.js +236 -126
- package/dist/crdt/restore.d.ts +2 -2
- package/dist/crdt/restore.js +18 -34
- package/dist/crdt/schema.d.ts +22 -21
- package/dist/crdt/schema.js +29 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +15 -15
- package/dist/crdt/structs.js +8 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +12 -0
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.js +5 -8
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +404 -138
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +12 -3
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +77 -62
- package/dist/crdt/text/model.js +84 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +17 -12
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
- package/dist/edytor.svelte.js +4 -1
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +25 -25
- package/dist/surface/observer.svelte.js +2 -2
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +59 -8
- package/package.json +2 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
package/dist/crdt/purge.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import { view as modelView } from './placement/model.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { deleteIndex } from './text/delete-index.js';
|
|
1
|
+
import { recordsOf, registryOf, view as modelView } from './placement/model.js';
|
|
2
|
+
import { removeUnit, unitOf } from './placement/slots.js';
|
|
3
|
+
import { deleteClaims, purgeStreams, scanText } from './text/model.js';
|
|
4
|
+
import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
|
|
6
5
|
import { baseIdOf, isIncarnationId } from './incarnations.js';
|
|
7
|
-
import { attrItems, firstItem } from './structs.js';
|
|
6
|
+
import { attrItems, dropSearchMarkers, engineOps, firstItem, holdsIds } from './structs.js';
|
|
8
7
|
const mapOf = (node) => attrItems(node);
|
|
8
|
+
/**
|
|
9
|
+
* The ended histories a document holds (`hist.release.end`): per writer,
|
|
10
|
+
* the clock its latest live release record ended at (what it wrote below
|
|
11
|
+
* it is released).
|
|
12
|
+
*/
|
|
13
|
+
export const releasesOf = (doc) => {
|
|
14
|
+
const out = new Map();
|
|
15
|
+
for (let it = firstItem(doc.get(RELEASE_ROOT)); it; it = it.right) {
|
|
16
|
+
if (it.deleted)
|
|
17
|
+
continue;
|
|
18
|
+
const { client, clock } = it.id;
|
|
19
|
+
out.set(client, Math.max(out.get(client) ?? 0, clock + it.length - 1));
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
9
23
|
/** The horizon a document holds (`null`: never purged). */
|
|
10
24
|
export const readHorizon = (doc) => {
|
|
11
25
|
const h = doc.get(HORIZON_ROOT).getAttr(HORIZON_ATTR);
|
|
@@ -14,73 +28,76 @@ export const readHorizon = (doc) => {
|
|
|
14
28
|
: null;
|
|
15
29
|
};
|
|
16
30
|
export const bindPurge = (Y) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
31
|
+
const ops = engineOps(Y);
|
|
32
|
+
/** The live hold records of `doc`, one per held range. */
|
|
33
|
+
const holdsOf = (doc) => {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (let it = firstItem(doc.get(TEXT_DELETES_ROOT)); it; it = it.right) {
|
|
36
|
+
const ids = it.content.ids;
|
|
37
|
+
if (it.deleted || ids === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
for (let j = 0; j < ids.length; j += 3)
|
|
40
|
+
out.push({
|
|
41
|
+
client: it.id.client,
|
|
42
|
+
clock: it.id.clock + j / 3,
|
|
43
|
+
c: ids[j],
|
|
44
|
+
k: ids[j + 1],
|
|
45
|
+
n: ids[j + 2]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
19
50
|
/**
|
|
20
|
-
* Whether every live
|
|
21
|
-
*
|
|
22
|
-
*
|
|
51
|
+
* Whether every live record holding an item of `spans` was written where
|
|
52
|
+
* `old` says (a withdrawn block's content was deleted before the
|
|
53
|
+
* horizon). Items no record holds count as old.
|
|
23
54
|
*/
|
|
24
|
-
const heldBefore = (
|
|
25
|
-
|
|
26
|
-
return spans
|
|
27
|
-
.flatMap((sp) => rootSpans(s, sp))
|
|
28
|
-
.every((r) => holders(s, r).every((mark) => old(mark.client, mark.clock)));
|
|
29
|
-
};
|
|
55
|
+
const heldBefore = (holds, spans, old) => holds.every((h) => old(h.client, h.clock) ||
|
|
56
|
+
!spans.some((sp) => sp.c === h.c && sp.k < h.k + h.n && h.k < sp.k + sp.n));
|
|
30
57
|
/**
|
|
31
|
-
* The text's part (`room.purge.what`), inside the purge
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
58
|
+
* The text's part (`room.purge.what`), inside the purge transaction:
|
|
59
|
+
* every hold record `old` names is deleted with what it holds (a real
|
|
60
|
+
* delete: the engine collects it everywhere; a newer record holding the
|
|
61
|
+
* same text has nothing to show again, the older delete is past the
|
|
62
|
+
* horizon). A record whose text the room lacks (lost, coming back with
|
|
63
|
+
* its author) is kept: it holds that text when it returns.
|
|
36
64
|
*/
|
|
37
65
|
const purgeText = (doc, old) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (tr === null)
|
|
66
|
+
const tr = doc._transaction;
|
|
67
|
+
if (tr == null)
|
|
41
68
|
throw new Error('purge: outside a transaction');
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const last = spans[spans.length - 1];
|
|
52
|
-
if (last !== undefined && last.c === r.client && last.k + last.n === r.clock)
|
|
53
|
-
last.n++;
|
|
54
|
-
else
|
|
55
|
-
spans.push({ c: r.client, k: r.clock, n: 1 });
|
|
56
|
-
}
|
|
57
|
-
dropMarks(s, dropped);
|
|
58
|
-
const marks = dropped.length;
|
|
59
|
-
D.remove(tr, spans);
|
|
60
|
-
const doomed = [];
|
|
61
|
-
attrItems(s.restored).forEach((it, key) => {
|
|
62
|
-
if (it.deleted || !old(it.id.client, it.id.clock))
|
|
63
|
-
return;
|
|
64
|
-
const [record, bytes] = recordsIn(it, null)[0] ?? [];
|
|
65
|
-
if (bytes === undefined)
|
|
69
|
+
const doomed = holdsOf(doc).filter((h) => old(h.client, h.clock) && holdsIds(doc, h.c, h.k, h.n));
|
|
70
|
+
const ids = (spans) => {
|
|
71
|
+
const set = ops.idSet();
|
|
72
|
+
for (const sp of spans)
|
|
73
|
+
set.add(sp.c, sp.k, sp.n);
|
|
74
|
+
return set;
|
|
75
|
+
};
|
|
76
|
+
ops.iterate(tr, ids(doomed), (it) => {
|
|
77
|
+
if (it.removed !== false)
|
|
66
78
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
holders(s, { c: e.rc, k: e.rk, n: e.n }).length === 0);
|
|
70
|
-
if (dead)
|
|
71
|
-
doomed.push({ key, copies });
|
|
79
|
+
it.delete(tr);
|
|
80
|
+
dropSearchMarkers(it.parent);
|
|
72
81
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
// The records are read first and deleted after: deleting one releases
|
|
83
|
+
// its holds, which can split items of the store the walk is reading
|
|
84
|
+
// (its writer's own text), and the walk would then visit the structs
|
|
85
|
+
// written before the record (a delete mark beside it) as if they were it.
|
|
86
|
+
const records = [];
|
|
87
|
+
ops.iterate(tr, ids(doomed.map((h) => ({ c: h.client, k: h.clock, n: 1 }))), (it) => {
|
|
88
|
+
records.push(it);
|
|
89
|
+
});
|
|
90
|
+
for (const it of records)
|
|
91
|
+
it.delete(tr);
|
|
92
|
+
return doomed.length;
|
|
79
93
|
};
|
|
80
94
|
/**
|
|
81
95
|
* Purge from `doc` (through `facade`, the facade over it) what was
|
|
82
|
-
* deleted before `horizon`, and
|
|
83
|
-
*
|
|
96
|
+
* deleted before `horizon`, and what the sessions whose history ended
|
|
97
|
+
* deleted (`hist.release.end`), and write the horizon record. With no
|
|
98
|
+
* horizon (`null`: the room's tick found none due), only the ended
|
|
99
|
+
* sessions' deletes go, and the horizon record is left as it is. Runs
|
|
100
|
+
* inside the caller's transaction.
|
|
84
101
|
*/
|
|
85
102
|
const purge = (doc, facade, horizon) => {
|
|
86
103
|
const report = {
|
|
@@ -88,13 +105,18 @@ export const bindPurge = (Y) => {
|
|
|
88
105
|
emptied: 0,
|
|
89
106
|
marks: 0,
|
|
90
107
|
records: 0,
|
|
91
|
-
candidates: 0,
|
|
92
108
|
claims: 0,
|
|
93
|
-
merged: 0
|
|
109
|
+
merged: 0,
|
|
110
|
+
released: 0
|
|
94
111
|
};
|
|
95
|
-
const vector =
|
|
112
|
+
const vector = horizon === null
|
|
113
|
+
? new Map()
|
|
114
|
+
: Y.decodeStateVector(horizon.sv);
|
|
96
115
|
const old = (client, clock) => clock < (vector.get(client) ?? 0);
|
|
97
|
-
|
|
116
|
+
// A delete of an ended session is past the horizon (`hist.release.end`).
|
|
117
|
+
const releases = releasesOf(doc);
|
|
118
|
+
const freed = (client, clock) => old(client, clock) || clock < (releases.get(client) ?? 0);
|
|
119
|
+
const oldItem = (item) => item !== undefined && !item.deleted && freed(item.id.client, item.id.clock);
|
|
98
120
|
const view = modelView(doc);
|
|
99
121
|
const { blocks, own, placements } = view;
|
|
100
122
|
const marks = (node, prefix) => [...node.attrKeys()]
|
|
@@ -120,6 +142,35 @@ export const bindPurge = (Y) => {
|
|
|
120
142
|
}
|
|
121
143
|
return out;
|
|
122
144
|
};
|
|
145
|
+
/** The paired mark items of `s`'s region whose start and end both lie in it. */
|
|
146
|
+
const regionMarks = (s) => {
|
|
147
|
+
const opened = new Map();
|
|
148
|
+
const out = [];
|
|
149
|
+
let at = 0;
|
|
150
|
+
let inside = s.start === 0;
|
|
151
|
+
for (let it = firstItem(s.text); it !== null; it = it.right) {
|
|
152
|
+
const live = !it.deleted && it.countable !== false;
|
|
153
|
+
if (live && at >= s.end)
|
|
154
|
+
break;
|
|
155
|
+
const { key, value } = it.content;
|
|
156
|
+
if (inside && !it.deleted && typeof key === 'string' && Array.isArray(value)) {
|
|
157
|
+
const role = key.charCodeAt(0);
|
|
158
|
+
if (role === 1)
|
|
159
|
+
opened.set(`${value[2]}:${value[3]}`, it);
|
|
160
|
+
else if (role === 2) {
|
|
161
|
+
const start = opened.get(`${value[0]}:${value[1]}`);
|
|
162
|
+
if (start !== undefined)
|
|
163
|
+
out.push(start, it);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (live) {
|
|
167
|
+
at += it.length;
|
|
168
|
+
if (at === s.start)
|
|
169
|
+
inside = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
};
|
|
123
174
|
const dead = new Set();
|
|
124
175
|
const shells = [];
|
|
125
176
|
for (const [id, rec] of blocks) {
|
|
@@ -136,7 +187,8 @@ export const bindPurge = (Y) => {
|
|
|
136
187
|
shells.push(id);
|
|
137
188
|
}
|
|
138
189
|
// A withdrawn block is hidden since it last held something: what its
|
|
139
|
-
// stream held was deleted, and its children left
|
|
190
|
+
// stream held was deleted, and its children left (the move that took
|
|
191
|
+
// them is old), before the horizon.
|
|
140
192
|
const children = new Map();
|
|
141
193
|
const add = (map, key, id) => {
|
|
142
194
|
const list = map.get(key);
|
|
@@ -145,23 +197,27 @@ export const bindPurge = (Y) => {
|
|
|
145
197
|
else
|
|
146
198
|
list.push(id);
|
|
147
199
|
};
|
|
148
|
-
for (const [id, rec] of blocks)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
for (const [id, rec] of blocks) {
|
|
201
|
+
const p = rec.home?.parent;
|
|
202
|
+
if (typeof p === 'string')
|
|
203
|
+
add(children, p, id);
|
|
204
|
+
}
|
|
205
|
+
/** The block's display edge is old: it shows at its home slot (written with the block), or at an old record. */
|
|
206
|
+
const edgeOld = (id) => {
|
|
207
|
+
const home = blocks.get(id)?.home;
|
|
208
|
+
const r = home ? (unitOf(doc, home)?.item.mover ?? null) : null;
|
|
209
|
+
return r === null ? true : old(r.id.client, r.id.clock);
|
|
210
|
+
};
|
|
211
|
+
const holds = shells.length > 0 ? holdsOf(doc) : [];
|
|
153
212
|
for (let grew = true; grew;) {
|
|
154
213
|
grew = false;
|
|
155
214
|
for (const id of shells) {
|
|
156
215
|
if (dead.has(id))
|
|
157
216
|
continue;
|
|
158
217
|
const s = own.streamOf(id);
|
|
159
|
-
if (s !== undefined && !heldBefore(
|
|
218
|
+
if (s !== undefined && !heldBefore(holds, regionSpans(s), freed))
|
|
160
219
|
continue;
|
|
161
|
-
const left = (children.get(id) ?? []).every((child) =>
|
|
162
|
-
const rec = blocks.get(child);
|
|
163
|
-
return rec.cands[0]?.p === id ? dead.has(child) : winnerOld(rec);
|
|
164
|
-
});
|
|
220
|
+
const left = (children.get(id) ?? []).every((child) => placements.get(child)?.parent === id ? dead.has(child) : edgeOld(child));
|
|
165
221
|
if (!left)
|
|
166
222
|
continue;
|
|
167
223
|
dead.add(id);
|
|
@@ -174,8 +230,11 @@ export const bindPurge = (Y) => {
|
|
|
174
230
|
dead.add(id);
|
|
175
231
|
// ── Which of them go whole ────────────────────────────────────────
|
|
176
232
|
// A text family: the blocks with a stream in one backing text, and the
|
|
177
|
-
// block owning it.
|
|
178
|
-
//
|
|
233
|
+
// block owning it. The block owning a text goes only with every block
|
|
234
|
+
// streaming in it (its text goes with its node). A block streaming in
|
|
235
|
+
// another's text, owning none, goes alone when one boundary names it:
|
|
236
|
+
// that boundary stays and delimits the stream the purge empties, which
|
|
237
|
+
// nobody shows (a removed key's, `text/streams.ts` `removedKeys`).
|
|
179
238
|
const parent = new Map();
|
|
180
239
|
const root = (id) => {
|
|
181
240
|
let r = id;
|
|
@@ -205,32 +264,71 @@ export const bindPurge = (Y) => {
|
|
|
205
264
|
if (family.every((id) => dead.has(id)))
|
|
206
265
|
for (const id of family)
|
|
207
266
|
removable.add(id);
|
|
208
|
-
//
|
|
267
|
+
// The live boundaries naming each block, and the keys with losing incarnations.
|
|
268
|
+
const naming = new Map();
|
|
269
|
+
for (const rec of blocks.values())
|
|
270
|
+
if (rec.content)
|
|
271
|
+
for (const b of scanText(rec.id, rec.content).bounds)
|
|
272
|
+
if (b.m === undefined)
|
|
273
|
+
naming.set(b.s, (naming.get(b.s) ?? 0) + 1);
|
|
274
|
+
const incarnated = new Set();
|
|
275
|
+
for (const id of blocks.keys())
|
|
276
|
+
if (isIncarnationId(id))
|
|
277
|
+
incarnated.add(baseIdOf(id));
|
|
278
|
+
const lone = new Set();
|
|
279
|
+
for (const id of dead) {
|
|
280
|
+
if (removable.has(id) || isIncarnationId(id) || incarnated.has(id))
|
|
281
|
+
continue;
|
|
282
|
+
const s = own.streamOf(id);
|
|
283
|
+
if (s === undefined || s.home === id || blocks.get(id).content !== undefined)
|
|
284
|
+
continue;
|
|
285
|
+
if (naming.get(id) !== 1)
|
|
286
|
+
continue;
|
|
287
|
+
lone.add(id);
|
|
288
|
+
removable.add(id);
|
|
289
|
+
}
|
|
290
|
+
// Nothing that stays may be placed under a block that goes, or keep its
|
|
291
|
+
// home slot in the sequence of one (the slot goes with the node).
|
|
209
292
|
for (let shrank = true; shrank;) {
|
|
210
293
|
shrank = false;
|
|
211
|
-
for (const [id,
|
|
212
|
-
if (removable.has(id)
|
|
294
|
+
for (const [id, rec] of blocks) {
|
|
295
|
+
if (removable.has(id))
|
|
213
296
|
continue;
|
|
214
|
-
for (const
|
|
215
|
-
removable.
|
|
216
|
-
|
|
297
|
+
for (const p of [placements.get(id)?.parent, rec.home?.parent]) {
|
|
298
|
+
if (typeof p !== 'string' || !removable.has(p))
|
|
299
|
+
continue;
|
|
300
|
+
for (const member of lone.has(p) ? [p] : (families.get(root(p)) ?? [])) {
|
|
301
|
+
removable.delete(member);
|
|
302
|
+
lone.delete(member);
|
|
303
|
+
}
|
|
304
|
+
shrank = true;
|
|
305
|
+
}
|
|
217
306
|
}
|
|
218
307
|
}
|
|
219
308
|
// ── The writes ────────────────────────────────────────────────────
|
|
220
309
|
// A deleted block that stays: its stream's text (boundaries kept), its
|
|
221
|
-
// data leaves and its claims
|
|
310
|
+
// data leaves and its claims; a block that goes alone: its stream's
|
|
311
|
+
// text and its claims (the rest goes with its node).
|
|
222
312
|
const emptied = [...dead].filter((id) => !removable.has(id));
|
|
223
|
-
|
|
313
|
+
const purged = [...emptied, ...lone].flatMap((id) => {
|
|
224
314
|
const s = own.streamOf(id);
|
|
225
|
-
return s === undefined
|
|
226
|
-
})
|
|
315
|
+
return s === undefined ? [] : [s];
|
|
316
|
+
});
|
|
317
|
+
// Their marks format nothing any reader shows: each pair whose start and
|
|
318
|
+
// end lie in one of these streams goes (collected, it keeps its class).
|
|
319
|
+
const tr = doc._transaction;
|
|
320
|
+
for (const s of purged)
|
|
321
|
+
for (const item of regionMarks(s))
|
|
322
|
+
item.delete(tr);
|
|
323
|
+
purgeStreams(purged.filter((s) => s.end - s.start > s.inert.length));
|
|
324
|
+
for (const id of lone)
|
|
325
|
+
deleteClaims(doc, blocks.get(id).claims);
|
|
227
326
|
for (const id of emptied) {
|
|
228
327
|
const rec = blocks.get(id);
|
|
229
328
|
for (const key of [...rec.node.attrKeys()])
|
|
230
329
|
if (key === DATA || key.startsWith(DATA_LEAF_PREFIX))
|
|
231
330
|
rec.node.deleteAttr(key);
|
|
232
|
-
|
|
233
|
-
rec.claimsNode.delete(0, rec.claimsNode.length);
|
|
331
|
+
deleteClaims(doc, rec.claims);
|
|
234
332
|
report.emptied++;
|
|
235
333
|
}
|
|
236
334
|
// The blocks that go whole: their entries, attribution records and the claims naming them.
|
|
@@ -238,18 +336,17 @@ export const bindPurge = (Y) => {
|
|
|
238
336
|
const attribution = doc.get(BLOCK_ATTR_ROOT);
|
|
239
337
|
for (const [id, rec] of blocks) {
|
|
240
338
|
// An emptied block's claims are gone already (above).
|
|
241
|
-
if (removable.has(id) || dead.has(id)
|
|
339
|
+
if (removable.has(id) || dead.has(id))
|
|
242
340
|
continue;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.filter((c) => removable.has(c.m))
|
|
246
|
-
.map((c) => c.seqIndex)
|
|
247
|
-
.filter((at) => at >= 0);
|
|
248
|
-
for (const at of named.sort((a, b) => b - a))
|
|
249
|
-
rec.claimsNode.delete(at, 1);
|
|
341
|
+
const named = rec.claims.filter((c) => c.stamp.c >= 0 && removable.has(c.m));
|
|
342
|
+
deleteClaims(doc, named);
|
|
250
343
|
report.claims += named.length;
|
|
251
344
|
}
|
|
252
345
|
for (const id of removable) {
|
|
346
|
+
// Its home slot goes with it: nothing shows it any more.
|
|
347
|
+
const home = blocks.get(id)?.home;
|
|
348
|
+
if (home)
|
|
349
|
+
removeUnit(doc, home);
|
|
253
350
|
if (isIncarnationId(id)) {
|
|
254
351
|
// A losing incarnation is no registry value: its subtree goes.
|
|
255
352
|
// Its node is a deleted value, which the node API writes nothing to:
|
|
@@ -264,8 +361,8 @@ export const bindPurge = (Y) => {
|
|
|
264
361
|
continue;
|
|
265
362
|
}
|
|
266
363
|
registry.deleteAttr(id);
|
|
267
|
-
if (attribution.getAttr(
|
|
268
|
-
attribution.deleteAttr(
|
|
364
|
+
if (attribution.getAttr(id) !== undefined)
|
|
365
|
+
attribution.deleteAttr(id);
|
|
269
366
|
report.removed++;
|
|
270
367
|
}
|
|
271
368
|
// Blocks merged away before the horizon (`room.purge.merged`): live,
|
|
@@ -273,10 +370,10 @@ export const bindPurge = (Y) => {
|
|
|
273
370
|
// own attribution records go (no history reaches a step below the
|
|
274
371
|
// horizon to split them out again); their registry entries stay.
|
|
275
372
|
const claimsOn = new Map();
|
|
276
|
-
for (const
|
|
373
|
+
for (const rec of blocks.values()) {
|
|
277
374
|
if (rec.deleted)
|
|
278
375
|
continue;
|
|
279
|
-
for (const c of
|
|
376
|
+
for (const c of rec.claims) {
|
|
280
377
|
const list = claimsOn.get(c.m);
|
|
281
378
|
if (list === undefined)
|
|
282
379
|
claimsOn.set(c.m, [c]);
|
|
@@ -284,40 +381,53 @@ export const bindPurge = (Y) => {
|
|
|
284
381
|
list.push(c);
|
|
285
382
|
}
|
|
286
383
|
}
|
|
287
|
-
const claimedOld = (id) => (claimsOn.get(id) ?? []).every((c) => c.
|
|
384
|
+
const claimedOld = (id) => (claimsOn.get(id) ?? []).every((c) => c.stamp.c >= 0 && old(c.stamp.c, c.stamp.k));
|
|
288
385
|
for (const [id, rec] of blocks) {
|
|
289
386
|
if (rec.deleted || removable.has(id) || dead.has(id) || isIncarnationId(id))
|
|
290
387
|
continue;
|
|
291
388
|
const owner = own.ownerOf(id);
|
|
292
389
|
if (owner === id || typeof owner !== 'string' || !claimedOld(id))
|
|
293
390
|
continue;
|
|
294
|
-
if (attribution.getAttr(
|
|
391
|
+
if (attribution.getAttr(id) === undefined)
|
|
295
392
|
continue;
|
|
296
|
-
attribution.deleteAttr(
|
|
393
|
+
attribution.deleteAttr(id);
|
|
297
394
|
report.merged++;
|
|
298
395
|
}
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
396
|
+
// Old move records no block shows at (lost to a newer move, or their blocks
|
|
397
|
+
// removed): no undo below the horizon can make them movers again.
|
|
398
|
+
const sequences = [registryOf(doc)];
|
|
399
|
+
for (const [id, rec] of blocks)
|
|
400
|
+
if (!removable.has(id))
|
|
401
|
+
sequences.push(rec.node);
|
|
402
|
+
// A record a block shows at stays, though it is no unit's mover (a cycle
|
|
403
|
+
// sent the block back to it): it is read before any record goes.
|
|
404
|
+
const shownNow = modelView(doc);
|
|
405
|
+
const listed = sequences.map((node) => [node, recordsOf(doc, node, shownNow)]);
|
|
406
|
+
doc.transact((tr) => {
|
|
407
|
+
for (const [node, records] of listed)
|
|
408
|
+
for (const r of records) {
|
|
409
|
+
if (r.wins || !old(r.item.id.client, r.item.id.clock))
|
|
410
|
+
continue;
|
|
411
|
+
ops.remove(tr, r.item);
|
|
412
|
+
dropSearchMarkers(node);
|
|
413
|
+
report.records++;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
// Text hold records past the horizon, with what they hold.
|
|
417
|
+
report.marks = purgeText(doc, freed);
|
|
418
|
+
// Release records the horizon covers: what they released is past it.
|
|
419
|
+
{
|
|
420
|
+
const tr = doc._transaction;
|
|
421
|
+
for (let it = firstItem(doc.get(RELEASE_ROOT)); it; it = it.right) {
|
|
422
|
+
if (it.deleted || !old(it.id.client, it.id.clock + it.length - 1))
|
|
423
|
+
continue;
|
|
424
|
+
it.delete(tr);
|
|
425
|
+
report.released++;
|
|
426
|
+
}
|
|
314
427
|
}
|
|
315
|
-
// Text delete marks and restoration records past the horizon.
|
|
316
|
-
const text = purgeText(doc, old);
|
|
317
|
-
report.marks = text.marks;
|
|
318
|
-
report.records = text.records;
|
|
319
428
|
// The horizon record, last.
|
|
320
|
-
|
|
429
|
+
if (horizon !== null)
|
|
430
|
+
doc.get(HORIZON_ROOT).setAttr(HORIZON_ATTR, { at: horizon.at, sv: horizon.sv });
|
|
321
431
|
return report;
|
|
322
432
|
};
|
|
323
433
|
return { purge };
|
package/dist/crdt/restore.d.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* 2. its type and data, per leaf, where they differ;
|
|
15
15
|
* 3. its place: under its snapshot parent, in the snapshot's order (the
|
|
16
16
|
* longest run of that parent's children already in order stays; each
|
|
17
|
-
* other block
|
|
18
|
-
*
|
|
17
|
+
* other block is moved after the one before it); a snapshot block the
|
|
18
|
+
* registry no longer holds is created there;
|
|
19
19
|
* 4. its content, where it differs: its own claims removed, then its
|
|
20
20
|
* stream's text replaced through the facade's per-stream delete and
|
|
21
21
|
* insert, the common prefix and suffix kept;
|
package/dist/crdt/restore.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { blockNodeOf,
|
|
1
|
+
import { blockNodeOf, insertSpecsAt, moveBlocksTo, view as modelView } from './placement/model.js';
|
|
2
2
|
import { DEL_PREFIX, TYPE, WITHDRAW_PREFIX } from './schema.js';
|
|
3
|
-
import { common, patchWrites,
|
|
4
|
-
import { canonKey } from './text/model.js';
|
|
3
|
+
import { common, patchWrites, writeData } from './data.js';
|
|
4
|
+
import { canonKey, deleteClaims } from './text/model.js';
|
|
5
5
|
import { randOf } from './rand.js';
|
|
6
6
|
import { toBlockSpec } from '../utils/json.js';
|
|
7
7
|
/** `items` one unit each (a character with its marks, or an atom), keyed for comparison. */
|
|
@@ -117,15 +117,8 @@ export const restoreDocument = (doc, facade, json) => {
|
|
|
117
117
|
const { blocks } = modelView(doc);
|
|
118
118
|
for (const rec of blocks.values()) {
|
|
119
119
|
// A deleted holder's claims are void (and stay so): only the others.
|
|
120
|
-
if (rec.deleted
|
|
121
|
-
|
|
122
|
-
// The claims its list stores (an anchor may show one in another block).
|
|
123
|
-
const named = (rec.listClaims ?? rec.claims)
|
|
124
|
-
.filter((claim) => all.has(claim.m))
|
|
125
|
-
.map((c) => c.seqIndex)
|
|
126
|
-
.filter((at) => at >= 0);
|
|
127
|
-
for (const at of named.sort((a, b) => b - a))
|
|
128
|
-
rec.claimsNode.delete(at, 1);
|
|
120
|
+
if (!rec.deleted)
|
|
121
|
+
deleteClaims(doc, rec.claims.filter((claim) => all.has(claim.m)));
|
|
129
122
|
}
|
|
130
123
|
}
|
|
131
124
|
// 2 · Type and data, per leaf, where they differ.
|
|
@@ -135,7 +128,7 @@ export const restoreDocument = (doc, facade, json) => {
|
|
|
135
128
|
continue;
|
|
136
129
|
if (n.getAttr(TYPE) !== spec.type)
|
|
137
130
|
n.setAttr(TYPE, spec.type);
|
|
138
|
-
|
|
131
|
+
writeData(n, patchWrites(n, [{ path: [], value: spec.data ?? {} }], doc.clientID, randOf(doc)) ?? []);
|
|
139
132
|
}
|
|
140
133
|
// 3 · Places, parent by parent in snapshot order (the root first).
|
|
141
134
|
const parents = [[null, specs]];
|
|
@@ -145,36 +138,28 @@ export const restoreDocument = (doc, facade, json) => {
|
|
|
145
138
|
if (list.length === 0)
|
|
146
139
|
continue;
|
|
147
140
|
const slots = facade.childSlots(parent);
|
|
148
|
-
const index = new Map(slots.map((slot, i) => [slot.id, i]));
|
|
149
141
|
// What a longest common run of the two orders keeps stays where it is.
|
|
150
142
|
const kept = new Set(common(slots.map((slot) => slot.id), list.map((spec) => spec.id)).map(([, j]) => j));
|
|
151
|
-
let prev;
|
|
143
|
+
let prev = null;
|
|
152
144
|
list.forEach((spec, i) => {
|
|
153
145
|
if (kept.has(i)) {
|
|
154
|
-
prev =
|
|
146
|
+
prev = spec.id;
|
|
155
147
|
return;
|
|
156
148
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
const bounds = [
|
|
164
|
-
...(prev === undefined ? [] : [{ rank: prev }]),
|
|
165
|
-
...(next === undefined ? [] : [{ rank: next }])
|
|
166
|
-
];
|
|
167
|
-
const [rank] = ranksAt(bounds, prev === undefined ? 0 : 1, 1, doc.clientID, randOf(doc));
|
|
149
|
+
const v = modelView(doc);
|
|
150
|
+
// Right after the block before it in the snapshot (the list as it stands, the block itself left out).
|
|
151
|
+
const shown = (v.kids.get(parent) ?? []).map((k) => k.id).filter((id) => id !== spec.id);
|
|
152
|
+
const at = prev === null ? 0 : shown.indexOf(prev) + 1;
|
|
168
153
|
const n = node(spec.id);
|
|
169
154
|
if (n === null) {
|
|
170
|
-
|
|
155
|
+
insertSpecsAt(doc, v, parent, at, [{ ...spec, children: [] }]);
|
|
171
156
|
report.created++;
|
|
172
157
|
}
|
|
173
158
|
else {
|
|
174
|
-
|
|
159
|
+
moveBlocksTo(doc, v, [spec.id], parent, at);
|
|
175
160
|
report.moved++;
|
|
176
161
|
}
|
|
177
|
-
prev =
|
|
162
|
+
prev = spec.id;
|
|
178
163
|
});
|
|
179
164
|
}
|
|
180
165
|
// 4 · Content, where it differs: own claims removed, the stream rewritten.
|
|
@@ -183,10 +168,9 @@ export const restoreDocument = (doc, facade, json) => {
|
|
|
183
168
|
let have = unitsOf(facade.contentItems(id));
|
|
184
169
|
if (have.length === want.length && have.every((u, i) => u.key === want[i].key))
|
|
185
170
|
continue;
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
claims.delete(0, claims.length);
|
|
171
|
+
const claims = modelView(doc).blocks.get(id)?.claims ?? [];
|
|
172
|
+
if (claims.some((c) => c.stamp.c >= 0)) {
|
|
173
|
+
deleteClaims(doc, claims);
|
|
190
174
|
have = unitsOf(facade.contentItems(id));
|
|
191
175
|
}
|
|
192
176
|
rewriteContent(facade, id, have, want);
|