edytor 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +23 -12
  6. package/dist/cloudflare/DocumentRoom.js +10 -6
  7. package/dist/cloudflare/index.d.ts +1 -1
  8. package/dist/cloudflare/room/access.d.ts +8 -3
  9. package/dist/cloudflare/room/access.js +44 -20
  10. package/dist/cloudflare/room/admission.js +28 -5
  11. package/dist/cloudflare/room/comments.js +28 -1
  12. package/dist/cloudflare/room/context.d.ts +139 -129
  13. package/dist/cloudflare/room/context.js +11 -4
  14. package/dist/cloudflare/room/forged.d.ts +14 -0
  15. package/dist/cloudflare/room/forged.js +30 -2
  16. package/dist/cloudflare/room/frames.d.ts +4 -0
  17. package/dist/cloudflare/room/frames.js +5 -1
  18. package/dist/cloudflare/room/purge.d.ts +6 -3
  19. package/dist/cloudflare/room/purge.js +45 -19
  20. package/dist/cloudflare/room/replicas.d.ts +4 -2
  21. package/dist/cloudflare/room/replicas.js +8 -3
  22. package/dist/cloudflare/room/suggest.d.ts +7 -0
  23. package/dist/cloudflare/room/suggest.js +143 -0
  24. package/dist/cloudflare/room/updates.d.ts +3 -4
  25. package/dist/cloudflare/room/updates.js +6 -8
  26. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  27. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  28. package/dist/components/Content.svelte +9 -1
  29. package/dist/crdt/anchors.js +3 -2
  30. package/dist/crdt/attribution/attribution.d.ts +34 -12
  31. package/dist/crdt/attribution/attribution.js +59 -21
  32. package/dist/crdt/attribution/block.d.ts +3 -3
  33. package/dist/crdt/attribution/block.js +7 -8
  34. package/dist/crdt/attribution/index.d.ts +1 -1
  35. package/dist/crdt/attribution/index.js +1 -1
  36. package/dist/crdt/data.d.ts +62 -31
  37. package/dist/crdt/data.js +211 -103
  38. package/dist/crdt/doc/capability.js +9 -13
  39. package/dist/crdt/doc/delete.d.ts +1 -1
  40. package/dist/crdt/doc/delete.js +13 -30
  41. package/dist/crdt/doc/funnel.d.ts +1 -1
  42. package/dist/crdt/doc/funnel.js +34 -26
  43. package/dist/crdt/doc/gate.d.ts +3 -3
  44. package/dist/crdt/doc/history.d.ts +31 -9
  45. package/dist/crdt/doc/history.js +204 -73
  46. package/dist/crdt/doc/layout.js +16 -27
  47. package/dist/crdt/doc/meta.js +17 -23
  48. package/dist/crdt/doc/moves.d.ts +1 -1
  49. package/dist/crdt/doc/moves.js +52 -32
  50. package/dist/crdt/doc/reads.d.ts +2 -2
  51. package/dist/crdt/doc/reads.js +20 -30
  52. package/dist/crdt/doc/seed.js +2 -2
  53. package/dist/crdt/doc/split-merge.js +12 -15
  54. package/dist/crdt/doc/steps.d.ts +25 -13
  55. package/dist/crdt/doc/steps.js +55 -125
  56. package/dist/crdt/doc/suggest.d.ts +167 -0
  57. package/dist/crdt/doc/suggest.js +579 -0
  58. package/dist/crdt/doc/types.d.ts +21 -9
  59. package/dist/crdt/document.d.ts +26 -6
  60. package/dist/crdt/document.js +40 -15
  61. package/dist/crdt/edytor-doc.d.ts +410 -390
  62. package/dist/crdt/edytor-doc.js +143 -95
  63. package/dist/crdt/engine-api.d.ts +39 -1
  64. package/dist/crdt/engine.d.ts +2 -2
  65. package/dist/crdt/engine.js +4 -5
  66. package/dist/crdt/flow.d.ts +2 -2
  67. package/dist/crdt/flow.js +29 -37
  68. package/dist/crdt/index.d.ts +134 -127
  69. package/dist/crdt/index.js +4 -1
  70. package/dist/crdt/operations.d.ts +12 -3
  71. package/dist/crdt/parts.d.ts +43 -0
  72. package/dist/crdt/parts.js +44 -0
  73. package/dist/crdt/placement/display.d.ts +6 -31
  74. package/dist/crdt/placement/display.js +41 -146
  75. package/dist/crdt/placement/keys.d.ts +63 -0
  76. package/dist/crdt/placement/keys.js +95 -0
  77. package/dist/crdt/placement/model.d.ts +119 -118
  78. package/dist/crdt/placement/model.js +242 -142
  79. package/dist/crdt/placement/resolve.d.ts +123 -35
  80. package/dist/crdt/placement/resolve.js +188 -153
  81. package/dist/crdt/placement/slots.d.ts +134 -0
  82. package/dist/crdt/placement/slots.js +152 -0
  83. package/dist/crdt/protocol.d.ts +1 -1
  84. package/dist/crdt/protocol.js +1 -1
  85. package/dist/crdt/protocols/auth.d.ts +11 -1
  86. package/dist/crdt/protocols/auth.js +17 -1
  87. package/dist/crdt/protocols/comments.d.ts +16 -1
  88. package/dist/crdt/protocols/comments.js +9 -1
  89. package/dist/crdt/providers/index.d.ts +6 -4
  90. package/dist/crdt/providers/index.js +3 -1
  91. package/dist/crdt/providers/room.d.ts +16 -2
  92. package/dist/crdt/providers/room.js +64 -17
  93. package/dist/crdt/providers/websocket.d.ts +6 -4
  94. package/dist/crdt/providers/websocket.js +14 -7
  95. package/dist/crdt/purge.d.ts +24 -9
  96. package/dist/crdt/purge.js +266 -126
  97. package/dist/crdt/restore.d.ts +4 -3
  98. package/dist/crdt/restore.js +27 -34
  99. package/dist/crdt/schema.d.ts +30 -21
  100. package/dist/crdt/schema.js +39 -23
  101. package/dist/crdt/storage.d.ts +1 -1
  102. package/dist/crdt/storage.js +1 -1
  103. package/dist/crdt/structs.d.ts +17 -15
  104. package/dist/crdt/structs.js +10 -22
  105. package/dist/crdt/text/index/cache.js +4 -0
  106. package/dist/crdt/text/index/claims.js +13 -23
  107. package/dist/crdt/text/index/fold.d.ts +2 -3
  108. package/dist/crdt/text/index/fold.js +194 -44
  109. package/dist/crdt/text/index/layout.d.ts +2 -4
  110. package/dist/crdt/text/index/layout.js +14 -30
  111. package/dist/crdt/text/index/placement.d.ts +26 -18
  112. package/dist/crdt/text/index/placement.js +420 -157
  113. package/dist/crdt/text/index/records.d.ts +3 -3
  114. package/dist/crdt/text/index/records.js +24 -31
  115. package/dist/crdt/text/index/shared.d.ts +11 -3
  116. package/dist/crdt/text/index/shared.js +29 -17
  117. package/dist/crdt/text/index/state.d.ts +21 -17
  118. package/dist/crdt/text/index/state.js +15 -17
  119. package/dist/crdt/text/index/streams.d.ts +10 -5
  120. package/dist/crdt/text/index/streams.js +78 -27
  121. package/dist/crdt/text/items.d.ts +10 -3
  122. package/dist/crdt/text/items.js +7 -0
  123. package/dist/crdt/text/model.d.ts +87 -62
  124. package/dist/crdt/text/model.js +91 -169
  125. package/dist/crdt/text/ranges.d.ts +1 -1
  126. package/dist/crdt/text/ranges.js +3 -3
  127. package/dist/crdt/text/rows.d.ts +13 -7
  128. package/dist/crdt/text/rows.js +24 -7
  129. package/dist/crdt/text/runs.d.ts +51 -42
  130. package/dist/crdt/text/runs.js +24 -7
  131. package/dist/crdt/text/streams.d.ts +26 -13
  132. package/dist/crdt/text/streams.js +41 -14
  133. package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
  134. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  135. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  136. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  137. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
  138. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  139. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  140. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  141. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  142. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  143. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  144. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  145. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
  146. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  147. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  148. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  149. package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
  150. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  151. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  152. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  153. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  154. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  155. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  156. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  157. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  158. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  159. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  160. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  161. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  162. package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
  163. package/dist/edytor.svelte.d.ts +1 -1
  164. package/dist/edytor.svelte.js +103 -75
  165. package/dist/events/beforeInputCommands.d.ts +2 -2
  166. package/dist/events/beforeInputCommands.js +2 -2
  167. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  168. package/dist/events/nativeInteractiveControl.js +41 -1
  169. package/dist/events/onBeforeInput.js +3 -3
  170. package/dist/events/onFocus.js +9 -11
  171. package/dist/events/onPageHide.d.ts +3 -0
  172. package/dist/events/onPageHide.js +16 -0
  173. package/dist/export/forms.d.ts +50 -50
  174. package/dist/index.d.ts +4 -2
  175. package/dist/index.js +1 -1
  176. package/dist/labels.d.ts +8 -0
  177. package/dist/labels.js +6 -1
  178. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  179. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  180. package/dist/plugins/find/search.js +2 -2
  181. package/dist/plugins/index.d.ts +1 -0
  182. package/dist/plugins/index.js +1 -0
  183. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  184. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  185. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  186. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  187. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  188. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  189. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  190. package/dist/selection/domSelection.d.ts +2 -0
  191. package/dist/selection/domSelection.js +6 -2
  192. package/dist/selection/pointer.svelte.js +75 -48
  193. package/dist/selection/replaceSelection.js +2 -6
  194. package/dist/selection/selection.svelte.d.ts +3 -3
  195. package/dist/selection/selection.svelte.js +39 -32
  196. package/dist/selection/selection.utils.d.ts +14 -0
  197. package/dist/selection/selection.utils.js +28 -3
  198. package/dist/session/commands.js +21 -11
  199. package/dist/session/composition.svelte.d.ts +5 -3
  200. package/dist/session/composition.svelte.js +21 -17
  201. package/dist/session/handles.d.ts +4 -17
  202. package/dist/session/handles.js +11 -37
  203. package/dist/session/navigation.js +6 -9
  204. package/dist/session/selection.d.ts +11 -5
  205. package/dist/session/selection.js +14 -48
  206. package/dist/session/suggestions.svelte.d.ts +38 -9
  207. package/dist/session/suggestions.svelte.js +37 -18
  208. package/dist/session/virtual.d.ts +1 -1
  209. package/dist/surface/attributes.d.ts +5 -0
  210. package/dist/surface/attributes.js +2 -2
  211. package/dist/surface/cells.d.ts +2 -26
  212. package/dist/surface/cells.js +2 -33
  213. package/dist/surface/observer.svelte.d.ts +6 -1
  214. package/dist/surface/observer.svelte.js +130 -83
  215. package/dist/surface/pin.svelte.d.ts +9 -9
  216. package/dist/surface/pin.svelte.js +15 -22
  217. package/dist/surface/projector.svelte.d.ts +15 -3
  218. package/dist/surface/projector.svelte.js +67 -22
  219. package/dist/text/text.svelte.js +14 -19
  220. package/dist/themes/notion.css +29 -0
  221. package/package.json +4 -2
  222. package/dist/crdt/placement/rank.d.ts +0 -87
  223. package/dist/crdt/placement/rank.js +0 -332
  224. package/dist/crdt/placement/source.d.ts +0 -43
  225. package/dist/crdt/placement/source.js +0 -66
  226. package/dist/crdt/text/delete-index.d.ts +0 -129
  227. package/dist/crdt/text/delete-index.js +0 -365
  228. package/dist/crdt/text/deletes.d.ts +0 -76
  229. package/dist/crdt/text/deletes.js +0 -362
  230. package/dist/crdt/text/index/anchored.d.ts +0 -17
  231. package/dist/crdt/text/index/anchored.js +0 -197
@@ -1,11 +1,28 @@
1
- import { view as modelView } from './placement/model.js';
2
- import { purgeStreams } from './text/model.js';
3
- import { AT, BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, isNodeLike, REC_PREFIX, REGISTRY_KEY, WITHDRAW_PREFIX } from './schema.js';
4
- import { bindDeletes } from './text/deletes.js';
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, SUGGEST_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 { suggestionsOf } from './doc/suggest.js';
7
+ import { attrItems, dropSearchMarkers, engineOps, firstItem, holdsIds } from './structs.js';
8
8
  const mapOf = (node) => attrItems(node);
9
+ /** The clock a suggestion's record opened at (its writer's clocks it owns start there). */
10
+ const clockOf = (rec) => rec.start ?? 0;
11
+ /**
12
+ * The ended histories a document holds (`hist.release.end`): per writer,
13
+ * the clock its latest live release record ended at (what it wrote below
14
+ * it is released).
15
+ */
16
+ export const releasesOf = (doc) => {
17
+ const out = new Map();
18
+ for (let it = firstItem(doc.get(RELEASE_ROOT)); it; it = it.right) {
19
+ if (it.deleted)
20
+ continue;
21
+ const { client, clock } = it.id;
22
+ out.set(client, Math.max(out.get(client) ?? 0, clock + it.length - 1));
23
+ }
24
+ return out;
25
+ };
9
26
  /** The horizon a document holds (`null`: never purged). */
10
27
  export const readHorizon = (doc) => {
11
28
  const h = doc.get(HORIZON_ROOT).getAttr(HORIZON_ATTR);
@@ -14,73 +31,76 @@ export const readHorizon = (doc) => {
14
31
  : null;
15
32
  };
16
33
  export const bindPurge = (Y) => {
17
- const D = bindDeletes(Y);
18
- const { rootSpans, holders, dropMarks, recordsIn, copiesOf, liveRoots, forget, collect } = deleteIndex(Y);
34
+ const ops = engineOps(Y);
35
+ /** The live hold records of `doc`, one per held range. */
36
+ const holdsOf = (doc) => {
37
+ const out = [];
38
+ for (let it = firstItem(doc.get(TEXT_DELETES_ROOT)); it; it = it.right) {
39
+ const ids = it.content.ids;
40
+ if (it.deleted || ids === undefined)
41
+ continue;
42
+ for (let j = 0; j < ids.length; j += 3)
43
+ out.push({
44
+ client: it.id.client,
45
+ clock: it.id.clock + j / 3,
46
+ c: ids[j],
47
+ k: ids[j + 1],
48
+ n: ids[j + 2]
49
+ });
50
+ }
51
+ return out;
52
+ };
19
53
  /**
20
- * Whether every live mark holding a character of `spans` was written
21
- * where `old` says (a withdrawn block's content was deleted
22
- * before the horizon). Characters no mark holds count as old.
54
+ * Whether every live record holding an item of `spans` was written where
55
+ * `old` says (a withdrawn block's content was deleted before the
56
+ * horizon). Items no record holds count as old.
23
57
  */
24
- const heldBefore = (doc, spans, old) => {
25
- const s = D.state(doc);
26
- return spans
27
- .flatMap((sp) => rootSpans(s, sp))
28
- .every((r) => holders(s, r).every((mark) => old(mark.client, mark.clock)));
29
- };
58
+ const heldBefore = (holds, spans, old) => holds.every((h) => old(h.client, h.clock) ||
59
+ !spans.some((sp) => sp.c === h.c && sp.k < h.k + h.n && h.k < sp.k + sp.n));
30
60
  /**
31
- * The text's part (`room.purge.what`), inside the purge
32
- * transaction: delete every text delete mark record `old` names, and
33
- * every restoration record `old` names whose copies are all deleted
34
- * and that no remaining mark holds; its copies are forgotten (the
35
- * `gcFilter` no longer keeps them) and collected. Writes no mark.
61
+ * The text's part (`room.purge.what`), inside the purge transaction:
62
+ * every hold record `old` names is deleted with what it holds (a real
63
+ * delete: the engine collects it everywhere; a newer record holding the
64
+ * same text has nothing to show again, the older delete is past the
65
+ * horizon). A record whose text the room lacks (lost, coming back with
66
+ * its author) is kept: it holds that text when it returns.
36
67
  */
37
68
  const purgeText = (doc, old) => {
38
- const s = D.state(doc);
39
- const tr = s.doc._transaction;
40
- if (tr === null)
69
+ const tr = doc._transaction;
70
+ if (tr == null)
41
71
  throw new Error('purge: outside a transaction');
42
- const spans = [];
43
- const dropped = [];
44
- for (let it = firstItem(s.marks); it; it = it.right)
45
- if (!it.deleted)
46
- for (let j = 0; j < it.length; j++) {
47
- const r = { client: it.id.client, clock: it.id.clock + j };
48
- if (!old(r.client, r.clock))
49
- continue;
50
- dropped.push(r);
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)
72
+ const doomed = holdsOf(doc).filter((h) => old(h.client, h.clock) && holdsIds(doc, h.c, h.k, h.n));
73
+ const ids = (spans) => {
74
+ const set = ops.idSet();
75
+ for (const sp of spans)
76
+ set.add(sp.c, sp.k, sp.n);
77
+ return set;
78
+ };
79
+ ops.iterate(tr, ids(doomed), (it) => {
80
+ if (it.removed !== false)
66
81
  return;
67
- const copies = copiesOf(record, bytes);
68
- const dead = copies.every((e) => liveRoots(s, { c: e.c, k: e.k, n: e.n, rk: e.k }).length === 0 &&
69
- holders(s, { c: e.rc, k: e.rk, n: e.n }).length === 0);
70
- if (dead)
71
- doomed.push({ key, copies });
82
+ it.delete(tr);
83
+ dropSearchMarkers(it.parent);
72
84
  });
73
- for (const { key, copies } of doomed) {
74
- s.restored.deleteAttr(key);
75
- forget(s, copies);
76
- collect(s, tr, copies);
77
- }
78
- return { marks, records: doomed.length };
85
+ // The records are read first and deleted after: deleting one releases
86
+ // its holds, which can split items of the store the walk is reading
87
+ // (its writer's own text), and the walk would then visit the structs
88
+ // written before the record (a delete mark beside it) as if they were it.
89
+ const records = [];
90
+ ops.iterate(tr, ids(doomed.map((h) => ({ c: h.client, k: h.clock, n: 1 }))), (it) => {
91
+ records.push(it);
92
+ });
93
+ for (const it of records)
94
+ it.delete(tr);
95
+ return doomed.length;
79
96
  };
80
97
  /**
81
98
  * Purge from `doc` (through `facade`, the facade over it) what was
82
- * deleted before `horizon`, and write the horizon record. Runs inside the
83
- * caller's transaction.
99
+ * deleted before `horizon`, and what the sessions whose history ended
100
+ * deleted (`hist.release.end`), and write the horizon record. With no
101
+ * horizon (`null`: the room's tick found none due), only the ended
102
+ * sessions' deletes go, and the horizon record is left as it is. Runs
103
+ * inside the caller's transaction.
84
104
  */
85
105
  const purge = (doc, facade, horizon) => {
86
106
  const report = {
@@ -88,13 +108,19 @@ export const bindPurge = (Y) => {
88
108
  emptied: 0,
89
109
  marks: 0,
90
110
  records: 0,
91
- candidates: 0,
92
111
  claims: 0,
93
- merged: 0
112
+ merged: 0,
113
+ released: 0,
114
+ suggestions: 0
94
115
  };
95
- const vector = Y.decodeStateVector(horizon.sv);
116
+ const vector = horizon === null
117
+ ? new Map()
118
+ : Y.decodeStateVector(horizon.sv);
96
119
  const old = (client, clock) => clock < (vector.get(client) ?? 0);
97
- const oldItem = (item) => item !== undefined && !item.deleted && old(item.id.client, item.id.clock);
120
+ // A delete of an ended session is past the horizon (`hist.release.end`).
121
+ const releases = releasesOf(doc);
122
+ const freed = (client, clock) => old(client, clock) || clock < (releases.get(client) ?? 0);
123
+ const oldItem = (item) => item !== undefined && !item.deleted && freed(item.id.client, item.id.clock);
98
124
  const view = modelView(doc);
99
125
  const { blocks, own, placements } = view;
100
126
  const marks = (node, prefix) => [...node.attrKeys()]
@@ -120,6 +146,35 @@ export const bindPurge = (Y) => {
120
146
  }
121
147
  return out;
122
148
  };
149
+ /** The paired mark items of `s`'s region whose start and end both lie in it. */
150
+ const regionMarks = (s) => {
151
+ const opened = new Map();
152
+ const out = [];
153
+ let at = 0;
154
+ let inside = s.start === 0;
155
+ for (let it = firstItem(s.text); it !== null; it = it.right) {
156
+ const live = !it.deleted && it.countable !== false;
157
+ if (live && at >= s.end)
158
+ break;
159
+ const { key, value } = it.content;
160
+ if (inside && !it.deleted && typeof key === 'string' && Array.isArray(value)) {
161
+ const role = key.charCodeAt(0);
162
+ if (role === 1)
163
+ opened.set(`${value[2]}:${value[3]}`, it);
164
+ else if (role === 2) {
165
+ const start = opened.get(`${value[0]}:${value[1]}`);
166
+ if (start !== undefined)
167
+ out.push(start, it);
168
+ }
169
+ }
170
+ if (live) {
171
+ at += it.length;
172
+ if (at === s.start)
173
+ inside = true;
174
+ }
175
+ }
176
+ return out;
177
+ };
123
178
  const dead = new Set();
124
179
  const shells = [];
125
180
  for (const [id, rec] of blocks) {
@@ -136,7 +191,8 @@ export const bindPurge = (Y) => {
136
191
  shells.push(id);
137
192
  }
138
193
  // A withdrawn block is hidden since it last held something: what its
139
- // stream held was deleted, and its children left, before the horizon.
194
+ // stream held was deleted, and its children left (the move that took
195
+ // them is old), before the horizon.
140
196
  const children = new Map();
141
197
  const add = (map, key, id) => {
142
198
  const list = map.get(key);
@@ -145,23 +201,27 @@ export const bindPurge = (Y) => {
145
201
  else
146
202
  list.push(id);
147
203
  };
148
- for (const [id, rec] of blocks)
149
- for (const c of rec.cands)
150
- if (typeof c.p === 'string')
151
- add(children, c.p, id);
152
- const winnerOld = (rec) => oldItem(mapOf(rec.node.getAttr(AT))?.get(rec.cands[0]?.key));
204
+ for (const [id, rec] of blocks) {
205
+ const p = rec.home?.parent;
206
+ if (typeof p === 'string')
207
+ add(children, p, id);
208
+ }
209
+ /** The block's display edge is old: it shows at its home slot (written with the block), or at an old record. */
210
+ const edgeOld = (id) => {
211
+ const home = blocks.get(id)?.home;
212
+ const r = home ? (unitOf(doc, home)?.item.mover ?? null) : null;
213
+ return r === null ? true : old(r.id.client, r.id.clock);
214
+ };
215
+ const holds = shells.length > 0 ? holdsOf(doc) : [];
153
216
  for (let grew = true; grew;) {
154
217
  grew = false;
155
218
  for (const id of shells) {
156
219
  if (dead.has(id))
157
220
  continue;
158
221
  const s = own.streamOf(id);
159
- if (s !== undefined && !heldBefore(doc, regionSpans(s), old))
222
+ if (s !== undefined && !heldBefore(holds, regionSpans(s), freed))
160
223
  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
- });
224
+ const left = (children.get(id) ?? []).every((child) => placements.get(child)?.parent === id ? dead.has(child) : edgeOld(child));
165
225
  if (!left)
166
226
  continue;
167
227
  dead.add(id);
@@ -174,8 +234,11 @@ export const bindPurge = (Y) => {
174
234
  dead.add(id);
175
235
  // ── Which of them go whole ────────────────────────────────────────
176
236
  // A text family: the blocks with a stream in one backing text, and the
177
- // block owning it. It goes whole or not at all: a stream whose
178
- // delimiting block went would join the stream before it.
237
+ // block owning it. The block owning a text goes only with every block
238
+ // streaming in it (its text goes with its node). A block streaming in
239
+ // another's text, owning none, goes alone when one boundary names it:
240
+ // that boundary stays and delimits the stream the purge empties, which
241
+ // nobody shows (a removed key's, `text/streams.ts` `removedKeys`).
179
242
  const parent = new Map();
180
243
  const root = (id) => {
181
244
  let r = id;
@@ -205,32 +268,71 @@ export const bindPurge = (Y) => {
205
268
  if (family.every((id) => dead.has(id)))
206
269
  for (const id of family)
207
270
  removable.add(id);
208
- // Nothing that stays may be placed under a block that goes.
271
+ // The live boundaries naming each block, and the keys with losing incarnations.
272
+ const naming = new Map();
273
+ for (const rec of blocks.values())
274
+ if (rec.content)
275
+ for (const b of scanText(rec.id, rec.content).bounds)
276
+ if (b.m === undefined)
277
+ naming.set(b.s, (naming.get(b.s) ?? 0) + 1);
278
+ const incarnated = new Set();
279
+ for (const id of blocks.keys())
280
+ if (isIncarnationId(id))
281
+ incarnated.add(baseIdOf(id));
282
+ const lone = new Set();
283
+ for (const id of dead) {
284
+ if (removable.has(id) || isIncarnationId(id) || incarnated.has(id))
285
+ continue;
286
+ const s = own.streamOf(id);
287
+ if (s === undefined || s.home === id || blocks.get(id).content !== undefined)
288
+ continue;
289
+ if (naming.get(id) !== 1)
290
+ continue;
291
+ lone.add(id);
292
+ removable.add(id);
293
+ }
294
+ // Nothing that stays may be placed under a block that goes, or keep its
295
+ // home slot in the sequence of one (the slot goes with the node).
209
296
  for (let shrank = true; shrank;) {
210
297
  shrank = false;
211
- for (const [id, pl] of placements) {
212
- if (removable.has(id) || typeof pl.parent !== 'string' || !removable.has(pl.parent))
298
+ for (const [id, rec] of blocks) {
299
+ if (removable.has(id))
213
300
  continue;
214
- for (const member of families.get(root(pl.parent)) ?? [])
215
- removable.delete(member);
216
- shrank = true;
301
+ for (const p of [placements.get(id)?.parent, rec.home?.parent]) {
302
+ if (typeof p !== 'string' || !removable.has(p))
303
+ continue;
304
+ for (const member of lone.has(p) ? [p] : (families.get(root(p)) ?? [])) {
305
+ removable.delete(member);
306
+ lone.delete(member);
307
+ }
308
+ shrank = true;
309
+ }
217
310
  }
218
311
  }
219
312
  // ── The writes ────────────────────────────────────────────────────
220
313
  // A deleted block that stays: its stream's text (boundaries kept), its
221
- // data leaves and its claims.
314
+ // data leaves and its claims; a block that goes alone: its stream's
315
+ // text and its claims (the rest goes with its node).
222
316
  const emptied = [...dead].filter((id) => !removable.has(id));
223
- purgeStreams(emptied.flatMap((id) => {
317
+ const purged = [...emptied, ...lone].flatMap((id) => {
224
318
  const s = own.streamOf(id);
225
- return s === undefined || s.end - s.start <= s.inert.length ? [] : [s];
226
- }));
319
+ return s === undefined ? [] : [s];
320
+ });
321
+ // Their marks format nothing any reader shows: each pair whose start and
322
+ // end lie in one of these streams goes (collected, it keeps its class).
323
+ const tr = doc._transaction;
324
+ for (const s of purged)
325
+ for (const item of regionMarks(s))
326
+ item.delete(tr);
327
+ purgeStreams(purged.filter((s) => s.end - s.start > s.inert.length));
328
+ for (const id of lone)
329
+ deleteClaims(doc, blocks.get(id).claims);
227
330
  for (const id of emptied) {
228
331
  const rec = blocks.get(id);
229
332
  for (const key of [...rec.node.attrKeys()])
230
333
  if (key === DATA || key.startsWith(DATA_LEAF_PREFIX))
231
334
  rec.node.deleteAttr(key);
232
- if (rec.claimsNode !== undefined && rec.claimsNode.length > 0)
233
- rec.claimsNode.delete(0, rec.claimsNode.length);
335
+ deleteClaims(doc, rec.claims);
234
336
  report.emptied++;
235
337
  }
236
338
  // The blocks that go whole: their entries, attribution records and the claims naming them.
@@ -238,18 +340,17 @@ export const bindPurge = (Y) => {
238
340
  const attribution = doc.get(BLOCK_ATTR_ROOT);
239
341
  for (const [id, rec] of blocks) {
240
342
  // An emptied block's claims are gone already (above).
241
- if (removable.has(id) || dead.has(id) || rec.claimsNode === undefined)
343
+ if (removable.has(id) || dead.has(id))
242
344
  continue;
243
- // The claims its list stores (an anchor may show one in another block).
244
- const named = (rec.listClaims ?? rec.claims)
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);
345
+ const named = rec.claims.filter((c) => c.stamp.c >= 0 && removable.has(c.m));
346
+ deleteClaims(doc, named);
250
347
  report.claims += named.length;
251
348
  }
252
349
  for (const id of removable) {
350
+ // Its home slot goes with it: nothing shows it any more.
351
+ const home = blocks.get(id)?.home;
352
+ if (home)
353
+ removeUnit(doc, home);
253
354
  if (isIncarnationId(id)) {
254
355
  // A losing incarnation is no registry value: its subtree goes.
255
356
  // Its node is a deleted value, which the node API writes nothing to:
@@ -264,8 +365,8 @@ export const bindPurge = (Y) => {
264
365
  continue;
265
366
  }
266
367
  registry.deleteAttr(id);
267
- if (attribution.getAttr(`${REC_PREFIX}${id}`) !== undefined)
268
- attribution.deleteAttr(`${REC_PREFIX}${id}`);
368
+ if (attribution.getAttr(id) !== undefined)
369
+ attribution.deleteAttr(id);
269
370
  report.removed++;
270
371
  }
271
372
  // Blocks merged away before the horizon (`room.purge.merged`): live,
@@ -273,10 +374,10 @@ export const bindPurge = (Y) => {
273
374
  // own attribution records go (no history reaches a step below the
274
375
  // horizon to split them out again); their registry entries stay.
275
376
  const claimsOn = new Map();
276
- for (const [holder, rec] of blocks) {
377
+ for (const rec of blocks.values()) {
277
378
  if (rec.deleted)
278
379
  continue;
279
- for (const c of own.claimsOf?.(holder) ?? rec.claims) {
380
+ for (const c of rec.claims) {
280
381
  const list = claimsOn.get(c.m);
281
382
  if (list === undefined)
282
383
  claimsOn.set(c.m, [c]);
@@ -284,40 +385,79 @@ export const bindPurge = (Y) => {
284
385
  list.push(c);
285
386
  }
286
387
  }
287
- const claimedOld = (id) => (claimsOn.get(id) ?? []).every((c) => c.seqIndex >= 0 && old(c.stamp.c, c.stamp.k));
388
+ const claimedOld = (id) => (claimsOn.get(id) ?? []).every((c) => c.stamp.c >= 0 && old(c.stamp.c, c.stamp.k));
288
389
  for (const [id, rec] of blocks) {
289
390
  if (rec.deleted || removable.has(id) || dead.has(id) || isIncarnationId(id))
290
391
  continue;
291
392
  const owner = own.ownerOf(id);
292
393
  if (owner === id || typeof owner !== 'string' || !claimedOld(id))
293
394
  continue;
294
- if (attribution.getAttr(`${REC_PREFIX}${id}`) === undefined)
395
+ if (attribution.getAttr(id) === undefined)
295
396
  continue;
296
- attribution.deleteAttr(`${REC_PREFIX}${id}`);
397
+ attribution.deleteAttr(id);
297
398
  report.merged++;
298
399
  }
299
- // Runner-up candidates of an old winner that stands.
300
- for (const [id, rec] of blocks) {
301
- if (removable.has(id) || rec.cands.length < 2)
302
- continue;
303
- const winner = rec.cands[0];
304
- const at = rec.node.getAttr(AT);
305
- if (!isNodeLike(at) || !oldItem(mapOf(at).get(winner.key)))
306
- continue;
307
- const pl = placements.get(id);
308
- const p = winner.p !== null && !blocks.has(winner.p) ? null : winner.p;
309
- if (pl === undefined || pl.parent !== p || pl.rank !== winner.r)
310
- continue;
311
- for (const c of rec.cands.slice(1))
312
- at.deleteAttr(c.key);
313
- report.candidates += rec.cands.length - 1;
400
+ // Old move records no block shows at (lost to a newer move, or their blocks
401
+ // removed): no undo below the horizon can make them movers again.
402
+ const sequences = [registryOf(doc)];
403
+ for (const [id, rec] of blocks)
404
+ if (!removable.has(id))
405
+ sequences.push(rec.node);
406
+ // A record a block shows at stays, though it is no unit's mover (a cycle
407
+ // sent the block back to it): it is read before any record goes.
408
+ const shownNow = modelView(doc);
409
+ const listed = sequences.map((node) => [node, recordsOf(doc, node, shownNow)]);
410
+ doc.transact((tr) => {
411
+ for (const [node, records] of listed)
412
+ for (const r of records) {
413
+ if (r.wins || !old(r.item.id.client, r.item.id.clock))
414
+ continue;
415
+ ops.remove(tr, r.item);
416
+ dropSearchMarkers(node);
417
+ report.records++;
418
+ }
419
+ });
420
+ // Text hold records past the horizon, with what they hold.
421
+ report.marks = purgeText(doc, freed);
422
+ // Release records the horizon covers: what they released is past it.
423
+ {
424
+ const tr = doc._transaction;
425
+ for (let it = firstItem(doc.get(RELEASE_ROOT)); it; it = it.right) {
426
+ if (it.deleted || !old(it.id.client, it.id.clock + it.length - 1))
427
+ continue;
428
+ it.delete(tr);
429
+ report.released++;
430
+ }
431
+ }
432
+ // Suggestions decided before the horizon (`sugg.purge`): the decision
433
+ // trail goes with them. A writer's oldest record first, so no record's
434
+ // clocks pass to another.
435
+ const suggest = facade.part && suggestionsOf(facade);
436
+ const records = suggest?.records() ?? [];
437
+ if (records.length > 0) {
438
+ const pending = new Set(suggest.changes().map((c) => c.suggestion));
439
+ const root = doc.get(SUGGEST_ROOT);
440
+ const seen = new Set();
441
+ for (const rec of [...records].sort((a, b) => clockOf(a) - clockOf(b))) {
442
+ if (seen.has(rec.writer))
443
+ continue;
444
+ seen.add(rec.writer);
445
+ const node = root.getAttr(rec.id);
446
+ const items = [node._item];
447
+ for (let it = firstItem(node); it; it = it.right)
448
+ if (!it.deleted)
449
+ items.push(it);
450
+ if (pending.has(rec.id) ||
451
+ !items.every((it) => old(it.id.client, it.id.clock + it.length - 1)))
452
+ continue;
453
+ root.deleteAttr(rec.id);
454
+ seen.delete(rec.writer);
455
+ report.suggestions++;
456
+ }
314
457
  }
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
458
  // The horizon record, last.
320
- doc.get(HORIZON_ROOT).setAttr(HORIZON_ATTR, { at: horizon.at, sv: horizon.sv });
459
+ if (horizon !== null)
460
+ doc.get(HORIZON_ROOT).setAttr(HORIZON_ATTR, { at: horizon.at, sv: horizon.sv });
321
461
  return report;
322
462
  };
323
463
  return { purge };
@@ -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 gets a placement candidate between its neighbours); a
18
- * snapshot block the registry no longer holds is created there;
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;
@@ -26,6 +26,7 @@
26
26
  import type { EngineDoc } from './engine-api.js';
27
27
  import { type BlockId, type ContentItem, type InlineSpec, type PlacementModel } from './placement/model.js';
28
28
  import type { DataPatch } from './data.js';
29
+ import { type SuggestFacade } from './doc/suggest.js';
29
30
  import type { JSONDoc } from '../utils/json.js';
30
31
  /** One display unit: a character (a code point) with its marks, or an inline atom, keyed for comparison. */
31
32
  export type Unit = {
@@ -60,7 +61,7 @@ export type RestoreReport = {
60
61
  deleted: number;
61
62
  };
62
63
  /** What a restore reads and writes through: the facade over the document (structurally). */
63
- export type RestoreFacade<Plan> = {
64
+ export type RestoreFacade<Plan> = Partial<SuggestFacade> & {
64
65
  model: PlacementModel;
65
66
  childSlots(parent: BlockId | null): readonly {
66
67
  id: BlockId;