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
@@ -0,0 +1,579 @@
1
+ import { ATTRIBUTION_ROOT, CONTENT_NODE, isNodeLike, SUGGEST_ROOT, TEXT_DELETES_ROOT } from '../schema.js';
2
+ import { blockNodeOf, registryOf } from '../placement/model.js';
3
+ import { isRowMark, newNode, openRangeCursor, ownTextIds, proposing } from '../text/model.js';
4
+ import { asYDoc, changedUnder, clientsOf, engineOps, firstItem, itemOf, storeOf } from '../structs.js';
5
+ import { nonceOf } from '../rand.js';
6
+ import { id as mint } from '../../utils.js';
7
+ import { effectOf, NOOP, refused } from './plan.js';
8
+ import { keepingReplaced } from '../incarnations.js';
9
+ import { ACTOR_PREFIX, CLIENT_PREFIX, profileOf, sanitizeProfile } from '../attribution/attribution.js';
10
+ import { bindBlockAttribution } from '../attribution/block.js';
11
+ /** What a suggestion refuses, unless on its author's own pending block (`sugg.refused`). */
12
+ const REFUSE = ['mergeBlocks', 'setBlockType', 'patchData', 'formatRange'];
13
+ /**
14
+ * What a decision does with a change of each kind, accepted and rejected:
15
+ * `1` settles its ids (it stays), `2` holds them (its text goes), `3`
16
+ * deletes its block, `4` holds its writer's text and joins its block to the
17
+ * one it was split from.
18
+ */
19
+ const DECIDE = {
20
+ insert: [1, 2],
21
+ delete: [2, 1],
22
+ block: [1, 3],
23
+ split: [1, 4],
24
+ remove: [3, 1]
25
+ };
26
+ /** The list `map` keeps at `key` (made empty when absent). */
27
+ const listOf = (map, key) => map.get(key) ?? map.set(key, []).get(key);
28
+ /** Append unit `client:clock` to the id triples `ids`, extending the last one when it continues it. */
29
+ const push = (ids, client, clock) => {
30
+ const n = ids.length;
31
+ if (ids[n - 3] === client && ids[n - 2] + ids[n - 1] === clock)
32
+ ids[n - 1] = ids[n - 1] + 1;
33
+ else
34
+ ids.push(client, clock, 1);
35
+ };
36
+ const parts = new WeakMap();
37
+ /**
38
+ * The shared suggestions of the document `facade` is over, built once:
39
+ * from then on, its `toJSON()` leaves the pending ones out (`sugg.json.base`).
40
+ */
41
+ export const suggestionsOf = (facade) => parts.get(facade.part) ?? parts.set(facade.part, docSuggest(facade)).get(facade.part);
42
+ /**
43
+ * Build the shared suggestions over `document`, a document read without a
44
+ * view (on a server, in a worker): its `toJSON()` leaves the pending ones
45
+ * out from then on, as a view's with the shared suggestions plugin does. A
46
+ * document no one built them over reads their content as plain content.
47
+ */
48
+ export const adoptSharedSuggestions = (document) => void suggestionsOf(document.facade);
49
+ /** The document's shared suggestions ({@link suggestionsOf}), built over facade `f`. */
50
+ const docSuggest = (f) => {
51
+ const c = f.part;
52
+ const { Y, doc, runsView, view } = c;
53
+ const BA = bindBlockAttribution(Y);
54
+ const ops = engineOps(Y);
55
+ const { idSet } = ops;
56
+ const root = () => doc.get(SUGGEST_ROOT);
57
+ const actorOf = (client) => {
58
+ const bound = doc.get(ATTRIBUTION_ROOT).getAttr(`c/${client}`);
59
+ return typeof bound === 'string' ? bound : undefined;
60
+ };
61
+ /**
62
+ * Run `fn` in a transaction (`tr`) writing as `client` (a suggestion's
63
+ * writer), then as before: the transaction names the writer, so a
64
+ * history replays its step under it (`StackItem.writer`).
65
+ */
66
+ const as = (client, fn) => f.transact(() => doc.transact((tr) => {
67
+ const local = doc.clientID;
68
+ if (client !== local)
69
+ tr.writer = client;
70
+ doc.clientID = client;
71
+ try {
72
+ return fn(tr);
73
+ }
74
+ finally {
75
+ doc.clientID = local;
76
+ }
77
+ }));
78
+ /** The writers this replica minted: client ids it writes suggestions under besides its own. */
79
+ const writers = new Set();
80
+ /** Bumped by every change of the records (a commit, or a write of this part); the facade's `version` too. */
81
+ let version = 0;
82
+ const bump = () => {
83
+ version++;
84
+ c.v++;
85
+ };
86
+ let table = null;
87
+ const records = () => {
88
+ if (table?.v === version)
89
+ return table.t;
90
+ const t = { recs: [], byWriter: new Map(), proposing: [] };
91
+ const r = root();
92
+ for (const id of r.attrKeys()) {
93
+ const node = r.getAttr(id);
94
+ const item = isNodeLike(node) ? itemOf(node) : null;
95
+ if (!item)
96
+ continue;
97
+ const writer = item.id.client;
98
+ const attr = (key) => {
99
+ const v = node.getAttr(key);
100
+ return typeof v === 'string' ? v : undefined;
101
+ };
102
+ const rec = {
103
+ id,
104
+ node,
105
+ writer,
106
+ get by() {
107
+ return actorOf(writer);
108
+ },
109
+ via: attr('via'),
110
+ label: attr('lbl'),
111
+ at: Number(node.getAttr('at')) || 0,
112
+ start: item.id.clock,
113
+ settled: idSet(),
114
+ proposed: idSet()
115
+ };
116
+ for (let it = firstItem(node); it !== null; it = it.right) {
117
+ const ids = it.content.ids;
118
+ if (!it.deleted && ids)
119
+ for (let i = 0; i < ids.length; i += 3)
120
+ (it.id.client === writer ? rec.proposed : rec.settled).add(ids[i], ids[i + 1], ids[i + 2]);
121
+ }
122
+ if (rec.proposed.clients.size)
123
+ t.proposing.push(rec);
124
+ t.recs.push(rec);
125
+ listOf(t.byWriter, writer).push(rec);
126
+ }
127
+ for (const recs of t.byWriter.values())
128
+ recs.sort((a, b) => a.start - b.start);
129
+ t.recs.sort((a, b) => a.at - b.at || a.start - b.start);
130
+ table = { v: version, t };
131
+ return t;
132
+ };
133
+ /**
134
+ * The record whose clocks hold `client:clock` (its writer's latest opened
135
+ * before it), when it is pending there (not settled).
136
+ */
137
+ const pendingIn = (client, clock) => {
138
+ const rec = records()
139
+ .byWriter.get(client)
140
+ ?.findLast((r) => r.start <= clock);
141
+ return rec && !rec.settled.has(client, clock) ? rec : undefined;
142
+ };
143
+ /** The records proposing to delete unit `client:clock` (not settled there). */
144
+ const proposersOf = (client, clock) => records().proposing.filter((r) => r.proposed.has(client, clock) && !r.settled.has(client, clock));
145
+ /** The id of block `b`'s node: what its creation and a proposal of its deletion name. */
146
+ const bornOf = (b) => {
147
+ const node = b === undefined ? null : blockNodeOf(doc, b);
148
+ return node && itemOf(node).id;
149
+ };
150
+ // ── what is pending ─────────────────────────────────────────────────
151
+ /** The work done (`sugg.scale`): blocks read, units walked. */
152
+ const debug = { computes: 0, units: 0 };
153
+ /** The shown units of `b`, in display order (row marks skipped, as the reads skip them). */
154
+ const unitsOf = (b) => {
155
+ const out = [];
156
+ debug.computes++;
157
+ for (const seg of view().own.display(b) ?? [])
158
+ for (const piece of openRangeCursor(seg.text).read(seg.i0, seg.i1)) {
159
+ const { str } = piece.content;
160
+ const arr = str === undefined ? piece.content.getContent?.() : null;
161
+ for (let j = 0; !piece.deleted && j < piece.len; j++)
162
+ if (!arr || !isRowMark(arr[piece.offset + j]))
163
+ out.push({
164
+ client: piece.id.client,
165
+ clock: piece.id.clock + j,
166
+ offset: out.length,
167
+ char: str?.[piece.offset + j] ?? ''
168
+ });
169
+ }
170
+ debug.units += out.length;
171
+ return out;
172
+ };
173
+ /**
174
+ * The block a split-born `b` was split from: the one showing the nearest
175
+ * stream before `b`'s in its text (`null`: none
176
+ * is shown; `undefined`: `b` was not split).
177
+ */
178
+ const sourceOf = (b) => {
179
+ const { own } = view();
180
+ const s = own.streamOf(b);
181
+ if (s === undefined || s.home === b)
182
+ return undefined;
183
+ const streams = own.streamsIn(s.home);
184
+ for (let i = streams.findIndex((x) => x.block === b) - 1; i >= 0; i--) {
185
+ const owner = own.ownerOf(streams[i].block);
186
+ // Not itself: a block merged into it shows its stream there.
187
+ if (typeof owner === 'string' && owner !== b && c.live(owner))
188
+ return owner;
189
+ }
190
+ return null;
191
+ };
192
+ /** `b`'s pending changes, read from the state now (`null`: none). */
193
+ const compute = (b) => {
194
+ const units = unitsOf(b);
195
+ const born = bornOf(b);
196
+ // A split whose source is gone is no longer one: its writer's text is an insertion.
197
+ const split = sourceOf(b);
198
+ const maker = born && split !== null ? pendingIn(born.client, born.clock) : undefined;
199
+ const out = [];
200
+ const runs = new Map();
201
+ /** Its maker's units: shown as the block's change, in one piece. */
202
+ const made = [];
203
+ const add = (rec, kind, u) => {
204
+ const run = runs.get(kind + rec.id);
205
+ if (run && run.offset + run.length === u.offset) {
206
+ run.length++;
207
+ run.text += u.char;
208
+ return push(run.ids, u.client, u.clock);
209
+ }
210
+ if (run)
211
+ out.push(run);
212
+ runs.set(kind + rec.id, {
213
+ suggestion: rec.id,
214
+ kind,
215
+ block: b,
216
+ offset: u.offset,
217
+ length: 1,
218
+ text: u.char,
219
+ ids: [u.client, u.clock, 1]
220
+ });
221
+ };
222
+ for (const u of units) {
223
+ const rec = pendingIn(u.client, u.clock);
224
+ if (rec && rec === maker)
225
+ push(made, u.client, u.clock);
226
+ else if (rec)
227
+ add(rec, 'insert', u);
228
+ for (const r of proposersOf(u.client, u.clock))
229
+ add(r, 'delete', u);
230
+ }
231
+ out.push(...runs.values());
232
+ out.sort((x, y) => x.offset - y.offset);
233
+ const whole = (rec, kind, ids) => ({
234
+ suggestion: rec.id,
235
+ kind,
236
+ block: b,
237
+ offset: 0,
238
+ length: units.length,
239
+ text: units.map((u) => u.char).join(''),
240
+ ids: [born.client, born.clock, 1, ...ids]
241
+ });
242
+ if (maker)
243
+ out.unshift(whole(maker, split ? 'split' : 'block', made));
244
+ // A record whose id list names the block's node proposes deleting the block.
245
+ for (const r of born ? proposersOf(born.client, born.clock) : [])
246
+ out.push(whole(r, 'remove', []));
247
+ return out.length ? out : null;
248
+ };
249
+ /** Every block the records' writers and proposals are in (a proposal naming a block's node included). */
250
+ const candidates = () => {
251
+ const t = records();
252
+ const out = new Set();
253
+ const texts = new Set();
254
+ const clients = clientsOf(doc);
255
+ /** Note where the structs of `client` from `from` to `to` (exclusive) are. */
256
+ const each = (client, from, to) => {
257
+ const structs = clients.get(client) ?? [];
258
+ // Past what this replica holds: nothing.
259
+ if (from >= storeOf(doc).getClock(client))
260
+ return;
261
+ for (let k = Y.findIndexSS(structs, from); k < structs.length && structs[k].id.clock < to; k++) {
262
+ const st = structs[k];
263
+ const parent = st.parent;
264
+ if (st.deleted || !isNodeLike(parent))
265
+ continue;
266
+ if (parent.name === CONTENT_NODE)
267
+ texts.add(parent);
268
+ else if (st.parentSub && parent === registryOf(doc))
269
+ out.add(st.parentSub);
270
+ }
271
+ };
272
+ for (const [writer, recs] of t.byWriter)
273
+ each(writer, recs[0].start, Infinity);
274
+ for (const rec of t.proposing)
275
+ for (const [client, ranges] of rec.proposed.clients)
276
+ for (const { clock, len } of ranges.getIds())
277
+ each(client, clock, clock + len);
278
+ const { own } = view();
279
+ for (const text of texts) {
280
+ const home = text._item?.parent?._item?.parentSub;
281
+ for (const b of home ? [home, ...own.streamsIn(home).map((s) => s.block)] : []) {
282
+ const owner = own.ownerOf(b);
283
+ if (typeof owner === 'string')
284
+ out.add(owner);
285
+ }
286
+ }
287
+ return out;
288
+ };
289
+ /** The pending changes per block, read again after any write. */
290
+ let memo = null;
291
+ const pending = () => {
292
+ const v = `${version}.${runsView.version()}`;
293
+ if (memo?.v === v)
294
+ return memo.map;
295
+ const map = new Map();
296
+ for (const b of records().recs.length ? candidates() : []) {
297
+ const p = c.live(b) ? compute(b) : null;
298
+ if (p)
299
+ map.set(b, p);
300
+ }
301
+ memo = { v, map };
302
+ return map;
303
+ };
304
+ const listeners = new Set();
305
+ const onCommit = (tr) => {
306
+ if (!changedUnder(tr, [root()]))
307
+ return;
308
+ bump();
309
+ for (const f of listeners)
310
+ f();
311
+ };
312
+ doc.on('commit', onCommit);
313
+ // ── writes ──────────────────────────────────────────────────────────
314
+ /** A fresh writer this replica writes suggestions under. */
315
+ const writer = () => {
316
+ let w = 0;
317
+ while (!w || w === doc.clientID || storeOf(doc).getClock(w))
318
+ w = (nonceOf(doc) | 0x4000000) >>> 0;
319
+ writers.add(w);
320
+ return w;
321
+ };
322
+ /**
323
+ * Bind writer `w` to this replica's actor, written by `w` itself, once
324
+ * (`sugg.writer.binding`), and the actor's profile when the stored one
325
+ * differs: inside the writer's first transaction.
326
+ */
327
+ const bind = (w) => {
328
+ const actor = c.config.actor?.();
329
+ const attr = doc.get(ATTRIBUTION_ROOT);
330
+ if (!actor || !writers.has(w) || attr.getAttr(CLIENT_PREFIX + w) !== undefined)
331
+ return;
332
+ attr.setAttr(CLIENT_PREFIX + w, actor.id);
333
+ const next = profileOf(actor);
334
+ const stored = sanitizeProfile(attr.getAttr(ACTOR_PREFIX + actor.id));
335
+ if (stored.name !== next.name || stored.color !== next.color)
336
+ attr.setAttr(ACTOR_PREFIX + actor.id, next);
337
+ };
338
+ /**
339
+ * Open a suggestion written by `writer` (a fresh writer by default): its
340
+ * record, in no history step. Answers its id.
341
+ */
342
+ const open = (options = {}) => f.transact(() => {
343
+ const w = options.writer ?? writer();
344
+ const id = options.id ?? mint('s');
345
+ const from = storeOf(doc).getClock(w);
346
+ as(w, () => {
347
+ bind(w);
348
+ root().setAttr(id, newNode('suggestion'));
349
+ const rec = root().getAttr(id);
350
+ rec.setAttr('at', options.at ?? Date.now());
351
+ if (options.via)
352
+ rec.setAttr('via', options.via);
353
+ if (options.label)
354
+ rec.setAttr('lbl', options.label);
355
+ });
356
+ // No history step captures the record's creation: its clocks stay its own.
357
+ const own = ownTextIds.get(doc) ?? ownTextIds.set(doc, idSet()).get(doc);
358
+ own.add(w, from, storeOf(doc).getClock(w) - from);
359
+ bump();
360
+ return id;
361
+ });
362
+ const recOf = (id) => records().recs.find((r) => r.id === id);
363
+ /** Two writers of one author (the same writer, or bound to the same actor). */
364
+ const sameAuthor = (a, b) => a === b || (!!actorOf(a) && actorOf(a) === actorOf(b));
365
+ /** `client:clock` is a pending insertion of `w`'s author: its edits are direct. */
366
+ const ownPending = (client, clock, w) => !!pendingIn(client, clock) && sameAuthor(client, w);
367
+ /** Block `b` is a pending insertion of `w`'s author. */
368
+ const ownBlock = (b, w) => {
369
+ const id = bornOf(b);
370
+ return !!id && ownPending(id.client, id.clock, w);
371
+ };
372
+ /** Append id list `ids` (YP31) to record `rec`'s list, as the current client. */
373
+ const entry = (rec, ids) => {
374
+ rec.node.hold(ids);
375
+ bump();
376
+ };
377
+ /**
378
+ * The block each step of plan `p` is about: a merge's, the block it
379
+ * merges away; a retype of a child a block deletion of `p` promotes, the
380
+ * deleted block (it follows that deletion: written with it, or read again
381
+ * when the deletion is accepted).
382
+ */
383
+ const about = (p) => {
384
+ const removed = new Set(p.writes.flatMap((s) => (s.op === 'deleteBlock' ? s.removes : [])));
385
+ return (s) => {
386
+ const up = s.op === 'setBlockType' ? c.positionOf(s.id)?.parent : null;
387
+ return up && removed.has(up) ? up : 'from' in s ? s.from : 'id' in s ? s.id : undefined;
388
+ };
389
+ };
390
+ /**
391
+ * Write plan `p` as suggestion `id` (a new one of writer `w` when `null`
392
+ * or gone: the view's suggesting mode), by the suggestion's writer, in one frame:
393
+ * insertions are written; a text delete proposes the units that are not
394
+ * its author's pending ones (those go), the engine hiding them until the
395
+ * plan ends so its later steps read the offsets they were prepared with
396
+ * (fork patch YP32: an insertion at a proposed deletion lands after it);
397
+ * a block delete proposes the blocks it removes by their nodes (its
398
+ * author's own go). A plan that moves blocks, or merges, retypes,
399
+ * changes data or formats what is not its author's own pending block, is
400
+ * refused, nothing written (`sugg.refused`). Answers the result, the
401
+ * suggestion and where the plan's caret is now, the proposed text shown.
402
+ */
403
+ const apply = (id, p, w = recOf(id).writer) => {
404
+ if (!('writes' in p))
405
+ return { result: p };
406
+ const at = about(p);
407
+ const [writes, blocks] = [[], []];
408
+ for (const s of p.writes)
409
+ if (s.op === 'moveBlocks')
410
+ return { result: refused('suggesting') };
411
+ else if (ownBlock(at(s), w) || (!REFUSE.includes(s.op) && s.op !== 'deleteBlock'))
412
+ writes.push(s);
413
+ else if (s.op === 'deleteBlock')
414
+ for (const r of s.removes) {
415
+ const { client, clock } = bornOf(r);
416
+ blocks.push(client, clock, 1);
417
+ }
418
+ // A promoted child's retype follows its deletion: read again when it is accepted.
419
+ else if (s.op !== 'setBlockType' || at(s) === s.id)
420
+ return { result: refused('suggesting') };
421
+ const rec = (id !== null && recOf(id)) || recOf(open({ writer: w }));
422
+ return as(w, (tr) => {
423
+ let result = NOOP;
424
+ let anchor;
425
+ proposing.set(doc, {
426
+ into: rec.node,
427
+ sort: (client, clock) => ownPending(client, clock, w)
428
+ ? 1
429
+ : rec.proposed.has(client, clock) && !rec.settled.has(client, clock)
430
+ ? 3
431
+ : 2
432
+ });
433
+ try {
434
+ if (writes.length)
435
+ result = f.apply({ ...p, writes, effect: effectOf(writes), version: runsView.version() }, true);
436
+ anchor = p.at ? f.anchorAt(p.at.block, p.at.offset, 'right') : null;
437
+ }
438
+ finally {
439
+ proposing.delete(doc);
440
+ ops.unhide(tr);
441
+ }
442
+ if (blocks.length) {
443
+ entry(rec, blocks);
444
+ result = { status: 'applied', ids: p.ids };
445
+ }
446
+ bump();
447
+ const caret = anchor && f.resolveAnchor(anchor);
448
+ return {
449
+ result,
450
+ id: rec.id,
451
+ ...(caret && { at: { block: caret.blockId, offset: caret.offset } })
452
+ };
453
+ });
454
+ };
455
+ /**
456
+ * Write decision part `part` (an id list) by the current client: settled
457
+ * in its record, or held, the text that goes: its blocks stamped as
458
+ * changed by the decider, as a text delete stamps them.
459
+ */
460
+ const hold = ({ into, ids, blocks = [] }) => f.transact(() => {
461
+ if (into !== null)
462
+ return entry(recOf(into), ids);
463
+ doc.get(TEXT_DELETES_ROOT).hold(ids);
464
+ const actor = c.config.actor?.();
465
+ for (const b of actor ? blocks : []) {
466
+ const node = blockNodeOf(doc, b);
467
+ if (node)
468
+ BA.stampChange(doc, node, b, actor.id);
469
+ }
470
+ });
471
+ /**
472
+ * What accepting (`accept`) or rejecting `changes` writes, in the order
473
+ * it runs ({@link DECIDE}): the id lists first (what stays settled in its
474
+ * record, the text that goes held by the decider), then the merges, then
475
+ * the block deletes, each reading its arguments when it runs.
476
+ */
477
+ const decide = (changes, accept) => {
478
+ const lists = new Map();
479
+ const gone = new Set();
480
+ const [merges, deletes] = [[], []];
481
+ for (const { kind, block: b, suggestion, ids } of changes) {
482
+ const act = DECIDE[kind][accept ? 0 : 1];
483
+ if (act === 2)
484
+ gone.add(b);
485
+ if (act === 3)
486
+ deletes.push({ op: 'deleteBlock', calls: () => (c.live(b) ? [[b]] : []) });
487
+ else
488
+ listOf(lists, act === 1 ? suggestion : null).push(...(act === 4 ? ids.slice(3) : ids));
489
+ if (act === 4)
490
+ merges.push({
491
+ op: 'mergeBlocks',
492
+ calls: () => {
493
+ const into = sourceOf(b);
494
+ return into && c.live(b) ? [[b, into]] : [];
495
+ }
496
+ });
497
+ }
498
+ return [
499
+ ...[...lists].map(([into, ids]) => ({
500
+ into,
501
+ ids,
502
+ ...(into === null && { blocks: [...gone] })
503
+ })),
504
+ ...merges,
505
+ ...deletes
506
+ ];
507
+ };
508
+ /**
509
+ * Write decision `parts` by client `w` (the current one by default; its
510
+ * author's rejection, by a writer of theirs), in one transaction: the id
511
+ * lists, then each operation through `exec` (the bare document's own
512
+ * operation by default; a view passes its dispatched one).
513
+ */
514
+ const run = (parts, exec = (op, args) => f[op](...args), w = doc.clientID) => as(w, () => {
515
+ bind(w);
516
+ for (const part of parts)
517
+ if ('op' in part)
518
+ for (const args of part.calls())
519
+ exec(part.op, args);
520
+ else
521
+ hold(part);
522
+ });
523
+ /** Every pending change, in document order. */
524
+ const changes = () => {
525
+ const map = pending();
526
+ return [...map.keys()].sort(c.compare).flatMap((b) => map.get(b));
527
+ };
528
+ /**
529
+ * The document without its pending suggestions (`sugg.json.base`): on a
530
+ * copy, every pending one rejected; `undefined` while none is pending.
531
+ */
532
+ const base = () => {
533
+ if (root().attrKeys().next().done || !changes().length)
534
+ return;
535
+ const { Y } = c;
536
+ const copy = keepingReplaced(new Y.Doc());
537
+ Y.applyUpdate(copy, Y.encodeStateAsUpdate(asYDoc(doc)));
538
+ const other = c.create(copy, { ...c.config, actor: undefined });
539
+ const all = suggestionsOf(other);
540
+ all.run(all.decide(all.changes(), false));
541
+ const json = other.toJSON({ suggestions: true });
542
+ all.dispose();
543
+ other.dispose();
544
+ copy.destroy();
545
+ return json;
546
+ };
547
+ c.base = base;
548
+ return {
549
+ /** A fresh writer this replica writes suggestions under. */
550
+ writer,
551
+ /** The writers this replica minted (a history tags their steps, `StackItem.writer`). */
552
+ writers,
553
+ open,
554
+ apply,
555
+ decide,
556
+ run,
557
+ /** The suggestions the document holds, oldest first. */
558
+ records: () => records().recs,
559
+ /** Whether the document holds any suggestion record. */
560
+ any: () => !root().attrKeys().next().done,
561
+ /** Every pending change, in document order. */
562
+ changes,
563
+ /** The pending changes of block `b` (`[]`: none). */
564
+ at: (b) => pending().get(b) ?? [],
565
+ /** Bumped by every change of the records. */
566
+ version: () => version,
567
+ /** Be told of every change of the records. Returns the unsubscribe. */
568
+ onChange: (f) => {
569
+ listeners.add(f);
570
+ return () => void listeners.delete(f);
571
+ },
572
+ /** @internal The work done (blocks read, units walked): the scaling rows count it. */
573
+ debug,
574
+ dispose: () => {
575
+ doc.off('commit', onCommit);
576
+ listeners.clear();
577
+ }
578
+ };
579
+ };
@@ -8,7 +8,7 @@ import type { AttributionActor } from '../attribution/index.js';
8
8
  import type { BlockId, BlockSpec, InlineSpec, ProjectedBlock, SplitTail } from '../placement/model.js';
9
9
  import type { Anchor } from '../text/model.js';
10
10
  import type { ContentRun } from '../text/runs.js';
11
- import type { DataPatch, LeafWrite } from '../data.js';
11
+ import type { DataPatch, DataWrite } from '../data.js';
12
12
  import type { DocPosition } from '../rangeDelete.js';
13
13
  /** Structural role of a block type (plugin-side policy, resolved per op). */
14
14
  export type BlockRole = {
@@ -180,22 +180,34 @@ export type TextRange = {
180
180
  /**
181
181
  * One planned write, named by the document operation that
182
182
  * performs it — the name a hook matches. Steps carry everything
183
- * their write needs (ranks, marks, offsets), decided at prepare time.
183
+ * their write needs (places, marks, offsets), decided at prepare time; a
184
+ * place is a display index among a parent's children, read against the
185
+ * document as the step is written (the steps before it applied).
184
186
  */
185
187
  export type PlanStep =
186
- /** `index`: the destination slot at prepare time, as hooks see it. */
188
+ /** `index`: the destination slot, as hooks see it. */
189
+ /** `split`: the split-born block of this plan whose boundary these blocks' slots take as their sort key (a paste's lines at its split). */
187
190
  {
188
191
  op: 'insertBlocks';
189
192
  parent: BlockId | null;
190
193
  index: number;
191
194
  specs: BlockSpec[];
192
- ranks: string[];
193
- } | {
195
+ split?: BlockId;
196
+ }
197
+ /**
198
+ * `side`: the move record's class among the items of its origin —
199
+ * absent or `0`, the blocks stand after what precedes them; `1`, before
200
+ * what follows them (blocks leaving a block for the gap before it).
201
+ */
202
+ /** `side`: the record's class at its origin (`1`: before); `at`, `part`: the block it stands at and its order there (`moveTo`). */
203
+ | {
194
204
  op: 'moveBlocks';
195
205
  ids: BlockId[];
196
206
  parent: BlockId | null;
197
207
  index: number;
198
- ranks: string[];
208
+ side?: 0 | 1;
209
+ part?: number;
210
+ at?: BlockId;
199
211
  }
200
212
  /** `marks`: the blocks that get this writer's mark; `removes`: those that leave the document. */
201
213
  | {
@@ -212,7 +224,7 @@ export type PlanStep =
212
224
  newId: BlockId;
213
225
  tail: SplitTail;
214
226
  parent: BlockId | null;
215
- rank: string;
227
+ index: number;
216
228
  }
217
229
  /** `at`/`length`: where `from`'s display lands in `into`'s. */
218
230
  | {
@@ -229,7 +241,7 @@ export type PlanStep =
229
241
  /**
230
242
  * A data patch (`crdt/data.ts`): of block `id`, of its atom `inlineId`
231
243
  * (at display `offset`), or of the document (no `id`). `ops` as asked;
232
- * `leaves` the attr writes they plan.
244
+ * `writes` the attr and slot writes they plan.
233
245
  */
234
246
  | {
235
247
  op: 'patchData';
@@ -237,7 +249,7 @@ export type PlanStep =
237
249
  inlineId?: string;
238
250
  offset?: number;
239
251
  ops: DataPatch[];
240
- leaves: LeafWrite[];
252
+ writes: DataWrite[];
241
253
  } | {
242
254
  op: 'insertText';
243
255
  id: BlockId;