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,13 +1,7 @@
1
- /**
2
- * The write funnel: every facade write goes through `write` (disposal and the
3
- * document's writable guard), and every prepared plan through `apply` — one
4
- * transaction, its result read from what the transaction wrote, and the
5
- * frame's one attribution pass.
6
- */
7
- import { blockNodeOf, buildInline, materializeSpec, ownText, view as modelView, writePlacement, writeSplit } from '../placement/model.js';
8
- import { ENTRY_FACET } from '../text/runs.js';
9
- import { AT, DEL_PREFIX, ID, LAST_CHANGED_ATTR, TYPE } from '../schema.js';
10
- import { writeLeaves } from '../data.js';
1
+ import { blockNodeOf, buildInline, insertSpecsAt, moveBlocksTo, ownText, view as modelView, writeSplit } from '../placement/model.js';
2
+ import { ENTRY_FACET, SEQ_FACET } from '../text/runs.js';
3
+ import { DEL_PREFIX, ID, LAST_CHANGED_ATTR, TYPE } from '../schema.js';
4
+ import { writeData } from '../data.js';
11
5
  import { NOOP } from './plan.js';
12
6
  import { claimInto, deleteRange, formatRangeIn, insertIntoText } from '../text/model.js';
13
7
  /**
@@ -55,8 +49,8 @@ export const writeFunnel = (c) => {
55
49
  // Each apply opens a frame that tracks the index's folds. When it
56
50
  // closes, ONE pass over the facets the fold saw stamps attribution: a
57
51
  // block whose registry entry was created gets its `createdBy` record; a block whose content, claims, type or data
58
- // changed gets a contributor stamp; moves and delete marks stamp
59
- // nothing. Two intents the effects cannot name are recorded by their
52
+ // changed gets a contributor stamp; moves (a record or a slot in a
53
+ // block's sequence) and delete marks stamp nothing. Two intents the effects cannot name are recorded by their
60
54
  // steps: a split-born block inherits its source's contributors, and a
61
55
  // merge survivor unions the absorbed block's. With no configured
62
56
  // `actor` the funnel writes no attribution at all.
@@ -74,9 +68,12 @@ export const writeFunnel = (c) => {
74
68
  else
75
69
  for (const facet of facets)
76
70
  if (!facet.startsWith(DEL_PREFIX) &&
77
- ![ENTRY_FACET, AT, ID, LAST_CHANGED_ATTR].includes(facet))
71
+ ![ENTRY_FACET, SEQ_FACET, ID, LAST_CHANGED_ATTR].includes(facet))
78
72
  changed.add(id);
79
73
  }
74
+ // A merge changed its survivor (its claim item, written quietly, is no edit of the text holding it).
75
+ for (const [into] of f.unions)
76
+ changed.add(into);
80
77
  for (const id of created) {
81
78
  const node = blockNodeOf(doc, id);
82
79
  if (node !== null)
@@ -93,38 +90,46 @@ export const writeFunnel = (c) => {
93
90
  /**
94
91
  * A streamless block gets its own text before the first write that
95
92
  * needs a stream (`ownText`: a writer derived from its dead
96
- * incarnation); its attribution record follows the re-minted nonce.
93
+ * incarnation); its attribution record follows the re-minted nonce. A
94
+ * claim item needs the block's own stream (`claim`); text, any piece of
95
+ * its display.
97
96
  */
98
- const needStream = (id) => {
99
- if ((modelView(doc).own.display(id) ?? []).length > 0)
97
+ const needStream = (id, claim = false) => {
98
+ const { own } = modelView(doc);
99
+ if (claim ? own.streamOf(id) !== undefined : (own.display(id) ?? []).length > 0)
100
100
  return;
101
101
  const { from, to } = ownText(doc, id);
102
102
  BA.retarget(doc, id, from, to);
103
103
  };
104
104
  /** One planned step, written (the plan decided it; writers never refuse). */
105
105
  const writeStep = (w, f) => {
106
- // Structural steps need no view: delete marks and placements write one node.
106
+ // Delete marks need no view: they write one node.
107
107
  if (w.op === 'deleteBlock')
108
108
  return w.marks.forEach((id) => blockNodeOf(doc, id).setAttr(DEL_PREFIX + doc.clientID, true));
109
109
  if (w.op === 'splitBlock' || w.op === 'insertText' || w.op === 'insertInline')
110
110
  needStream(w.id);
111
- const { blocks, own } = modelView(doc);
111
+ // A claim item goes at the end of the stream of the block merged into.
112
+ if (w.op === 'mergeBlocks')
113
+ needStream(w.into, true);
114
+ const v = modelView(doc);
115
+ const { blocks, own } = v;
112
116
  const node = (id) => blockNodeOf(doc, id);
113
117
  switch (w.op) {
114
118
  case 'insertBlocks':
115
- return w.specs.forEach((sp, i) => materializeSpec(doc, sp, w.parent, w.ranks[i]));
119
+ return insertSpecsAt(doc, v, w.parent, w.index, w.specs, w.split === undefined ? null : (f.bounds.get(w.split) ?? null));
116
120
  case 'moveBlocks':
117
- return w.ids.forEach((id, i) => writePlacement(doc, node(id), w.parent, w.ranks[i]));
121
+ return moveBlocksTo(doc, v, w.ids, w.parent, w.index, w.side ?? 0, w.part ?? 0, w.at ?? null);
118
122
  case 'splitBlock':
119
123
  f.inherit.set(w.newId, w.id);
120
- return writeSplit(doc, w.id, w.offset, w.newId, w.tail, { p: w.parent, r: w.rank });
124
+ f.bounds.set(w.newId, writeSplit(doc, w.id, w.offset, w.newId, w.tail, w.parent, w.index));
125
+ return;
121
126
  case 'mergeBlocks':
122
127
  f.unions.push([w.into, w.from]);
123
- return claimInto(blocks, own, w.from, w.into);
128
+ return f.quiet(() => claimInto(own, w.from, w.into));
124
129
  case 'setBlockType':
125
130
  return void node(w.id).setAttr(TYPE, w.type);
126
131
  case 'patchData':
127
- return writeLeaves(dataNode(w.id, w.inlineId), w.leaves);
132
+ return writeData(dataNode(w.id, w.inlineId), w.writes);
128
133
  case 'insertText':
129
134
  return insertIntoText(doc, blocks, own, w.id, w.offset, w.text, w.marks);
130
135
  case 'insertInline':
@@ -141,8 +146,10 @@ export const writeFunnel = (c) => {
141
146
  * and fold the result from what that transaction did. A refusal writes
142
147
  * nothing. A plan is valid only at the version it was prepared against,
143
148
  * in the same synchronous turn: applying it anywhere else throws.
149
+ * `quiet`: it stamps no attribution (a suggestion's writer: its content
150
+ * is not the block's until accepted).
144
151
  */
145
- const apply = (p) => {
152
+ const apply = (p, quiet) => {
146
153
  if (!('writes' in p))
147
154
  return write(() => p);
148
155
  if (p.version !== runsView.version()) {
@@ -150,7 +157,7 @@ export const writeFunnel = (c) => {
150
157
  }
151
158
  return write(() => doc.transact(() => {
152
159
  const frame = runsView.track();
153
- const f = { inherit: new Map(), unions: [] };
160
+ const f = { inherit: new Map(), bounds: new Map(), unions: [], quiet: frame.quiet };
154
161
  try {
155
162
  for (const w of p.writes)
156
163
  writeStep(w, f);
@@ -161,7 +168,8 @@ export const writeFunnel = (c) => {
161
168
  throw error;
162
169
  }
163
170
  const folded = frame.end();
164
- close(f, folded);
171
+ if (!quiet)
172
+ close(f, folded);
165
173
  return folded.wrote ? { status: 'applied', ids: p.ids } : NOOP;
166
174
  }));
167
175
  };
@@ -17,15 +17,15 @@
17
17
  *
18
18
  * ```
19
19
  * doc.get('blocks') registry — flat map, blockId → node('block')
20
- * └ <blockId> id/n/type/data/del + content/claims/at
21
- * (n: incarnation nonce)
20
+ * └ <blockId> n/type/d/…/del./wd./l + content
21
+ * (n: incarnation nonce; the id is the key)
22
22
  * doc.get('meta') version record root
23
23
  * ├ v : number SCHEMA_VERSION (LWW attr — concurrent init converges)
24
24
  * └ schema : 'edytor-doc' SCHEMA_NAME
25
25
  * ```
26
26
  */
27
27
  import type { EngineDoc } from '../engine-api.js';
28
- export declare const SCHEMA_VERSION: 6;
28
+ export declare const SCHEMA_VERSION: 11;
29
29
  export declare const SCHEMA_NAME: "edytor-doc";
30
30
  export declare const META_KEY: "meta";
31
31
  /**
@@ -1,5 +1,4 @@
1
1
  import type { EngineApi, EngineDoc, EngineNode } from '../engine-api.js';
2
- import type { TextDeletes } from '../text/deletes.js';
3
2
  import { type IdSetOf, type StoreStruct } from '../structs.js';
4
3
  import { type WriteFunnel } from './funnel.js';
5
4
  import type { DocBase } from './reads.js';
@@ -11,6 +10,12 @@ export declare class StackItem {
11
10
  deletes: IdSetOf;
12
11
  /** Per-view metadata (`session/history.ts`: the selections around the step). */
13
12
  readonly meta: Map<string, unknown>;
13
+ /**
14
+ * The client it was written under when that is not the replica's own (a
15
+ * suggestion's writer: its transaction's `writer`): it replays under it,
16
+ * and its records release with the history's.
17
+ */
18
+ writer?: number;
14
19
  constructor(inserts: IdSetOf, deletes: IdSetOf);
15
20
  }
16
21
  /** A step taken into a stack: added (a new step) or updated (the open one took a transaction). */
@@ -33,15 +38,12 @@ export type HistoryOptions = {
33
38
  }) => boolean;
34
39
  };
35
40
  /** What a history is made over: the document and the rules of its replay. */
36
- type Rules = {
41
+ export type Rules = {
37
42
  Y: EngineApi;
38
43
  doc: EngineDoc;
39
44
  scope: EngineNode[];
40
- /** The text delete marks' restore and apply rules (`text/deletes.ts`). */
41
- marks: {
42
- restoreFilter(item: unknown): boolean;
43
- onApply(tr: unknown, step: StackItem): void;
44
- };
45
+ /** The records list a replay's text holds go to (fork patch YP20). */
46
+ records: EngineNode;
45
47
  /** Whether an item a replay would delete is withdrawn instead (`hist.undo.withdraw`). */
46
48
  withdraw(item: StoreStruct, step: StackItem): boolean;
47
49
  /** Repair the attrs a replay left undefined. */
@@ -85,16 +87,36 @@ export declare class UndoManager {
85
87
  redo(): StackItem | null;
86
88
  /** Drop every step (undo and redo) and release what they kept for their replay. */
87
89
  clear(): void;
90
+ /**
91
+ * The session ends: its page left for good (`hist.release.end`). Every
92
+ * step is dropped and released as a dropped step is (`#release`: what
93
+ * they kept is collected, the text this writer's records hold is
94
+ * deleted for good), in one transaction that, when a step marked a block
95
+ * deleted or withdrawn, also appends one release record (the
96
+ * {@link RELEASE_ROOT} list): its id names this writer and the clock the
97
+ * history ended at, so the room's purge removes what those marks hide at
98
+ * its next tick, past the horizon or not. Then the manager stops
99
+ * capturing (it is destroyed; the document attaches a fresh one if asked).
100
+ * A history that captured nothing writes nothing. Only for a page that
101
+ * cannot come back: a page the back/forward cache keeps, or one that
102
+ * only lost its connection, keeps its history.
103
+ */
104
+ end(): void;
88
105
  destroy: () => void;
89
106
  }
90
107
  /** What a facade's history is made over: its doc, the write funnel and the bound engine layers. */
91
108
  export type HistoryContext = DocBase & Pick<WriteFunnel, 'write' | 'disposed'> & {
92
- D: TextDeletes;
93
109
  /** Stamp the version record before the history attaches. */
94
110
  init: (doc: EngineDoc) => void;
95
111
  };
112
+ /**
113
+ * The document's history rules over `doc` (the one set every history of an
114
+ * edytor document runs): its scope (the block registry, the text hold
115
+ * records, the document's data), the records a replay's text holds go to,
116
+ * the withdraw rule (`hist.undo.withdraw`) and the attr repair below.
117
+ */
118
+ export declare const historyRules: (Y: EngineApi, doc: EngineDoc) => Rules;
96
119
  /** The history factory of one facade. */
97
120
  export declare const docHistory: (c: HistoryContext) => {
98
121
  createUndoManager: (options?: HistoryOptions) => UndoManager;
99
122
  };
100
- export {};
@@ -1,18 +1,21 @@
1
1
  /**
2
2
  * The document's history: the one undo manager (fork patch YP18 removed the
3
3
  * engine's). It captures the tracked local transactions that write in its
4
- * scope (the block registry, the text delete marks, the document's data)
4
+ * scope (the block registry, the text hold records, the document's data)
5
5
  * into steps, and an undo or a redo replays a step in one transaction over
6
- * the engine's struct primitives (`redoItem`, `keepItem`). Its rules: text
7
- * comes back as the delete marks allow (`hist.undo.restore`), an undone
6
+ * the engine's struct primitives (`redoItem`, `keepItem`). Its rules: a text
7
+ * delete is a hold record (fork patch YP20), so undoing it deletes the record
8
+ * and the text shows again unless another writer's record holds it
9
+ * (`hist.undo.restore`); undoing typing holds the typed text; an undone
8
10
  * creation is withdrawn (`hist.undo.withdraw`), an overwritten attr is
9
- * repaired, the stack keeps `limit` steps, and the purge horizon drops the
10
- * steps below it (`hist.purge.horizon`).
11
+ * repaired, the stack keeps `limit` steps, the purge horizon drops the
12
+ * steps below it (`hist.purge.horizon`), and a session's end releases them
13
+ * all (`hist.release.end`).
11
14
  */
12
15
  import { getUnixTime } from 'lib0-v14/time';
13
- import { ownTextIds } from '../text/model.js';
16
+ import { openSteps, ownTextIds } from '../text/model.js';
14
17
  import { readHorizon } from '../purge.js';
15
- import { BLOCK_NODE, DATA_LEAF_PREFIX, DOC_DATA_ROOT, HORIZON_ROOT, INLINE_NODE, LAST_CHANGED_ATTR, TYPE } from '../schema.js';
18
+ import { BLOCK_NODE, CONTENT_NODE, DATA_LEAF_PREFIX, DEL_PREFIX, DOC_DATA_ROOT, HORIZON_ROOT, INLINE_NODE, LAST_CHANGED_ATTR, RELEASE_ROOT, SUGGEST_ROOT, TEXT_DELETES_ROOT, TYPE, WITHDRAW_PREFIX } from '../schema.js';
16
19
  import { changedUnder, collectable, collectNow, collects, dropSearchMarkers, engineOps, walkIdSet } from '../structs.js';
17
20
  import { isInitialized } from './gate.js';
18
21
  import { DocumentDestroyedError } from './funnel.js';
@@ -27,6 +30,12 @@ export class StackItem {
27
30
  deletes;
28
31
  /** Per-view metadata (`session/history.ts`: the selections around the step). */
29
32
  meta = new Map();
33
+ /**
34
+ * The client it was written under when that is not the replica's own (a
35
+ * suggestion's writer: its transaction's `writer`): it replays under it,
36
+ * and its records release with the history's.
37
+ */
38
+ writer;
30
39
  constructor(inserts, deletes) {
31
40
  this.inserts = inserts;
32
41
  this.deletes = deletes;
@@ -37,6 +46,13 @@ const REPAIRED = {
37
46
  [BLOCK_NODE]: [TYPE, LAST_CHANGED_ATTR],
38
47
  [INLINE_NODE]: [TYPE]
39
48
  };
49
+ /** A unit of a block's text: a character or an inline atom (what a hold record holds). */
50
+ const isUnit = (it) => {
51
+ const content = it.content;
52
+ return (it.parentSub === null &&
53
+ it.parent?.name === CONTENT_NODE &&
54
+ (typeof content?.str === 'string' || content?.type?.name === INLINE_NODE));
55
+ };
40
56
  /**
41
57
  * A document's undo manager: the undo and redo stacks of the tracked local
42
58
  * transactions in its scope. `undo()`/`redo()` replay the top step and
@@ -64,6 +80,10 @@ export class UndoManager {
64
80
  /** What dropped steps released (an item is released once every step that deleted it is gone). */
65
81
  #released;
66
82
  #horizon;
83
+ /** A captured step wrote a block's delete or withdraw mark: the session's end records its release. */
84
+ #marked = false;
85
+ /** The session ended (`end()`): nothing more is released. */
86
+ #ended = false;
67
87
  constructor(rules, options = {}) {
68
88
  this.#rules = rules;
69
89
  this.#ops = engineOps(rules.Y);
@@ -78,7 +98,19 @@ export class UndoManager {
78
98
  this.trackedOrigins.add(this);
79
99
  this.#ops.on(rules.doc, 'afterTransaction', this.#afterTransaction);
80
100
  this.#ops.on(rules.doc, 'destroy', this.destroy);
101
+ const open = openSteps.get(rules.doc) ?? new Set();
102
+ openSteps.set(rules.doc, open.add(this.#open));
81
103
  }
104
+ /** What the step still capturing inserted (`null`: none is open): a text delete there removes. */
105
+ #open = () => {
106
+ const top = this.undoStack.at(-1);
107
+ const open = top !== undefined &&
108
+ !this.undoing &&
109
+ !this.redoing &&
110
+ this.lastChange > 0 &&
111
+ getUnixTime() - this.lastChange < this.captureTimeout;
112
+ return open ? top.inserts : null;
113
+ };
82
114
  /** Be told of every step taken into a stack (added, or the open one updated). Returns the unsubscribe. */
83
115
  observe(f) {
84
116
  this.#observers.add(f);
@@ -118,8 +150,39 @@ export class UndoManager {
118
150
  clear() {
119
151
  this.#release([...this.undoStack.splice(0), ...this.redoStack.splice(0)]);
120
152
  }
153
+ /**
154
+ * The session ends: its page left for good (`hist.release.end`). Every
155
+ * step is dropped and released as a dropped step is (`#release`: what
156
+ * they kept is collected, the text this writer's records hold is
157
+ * deleted for good), in one transaction that, when a step marked a block
158
+ * deleted or withdrawn, also appends one release record (the
159
+ * {@link RELEASE_ROOT} list): its id names this writer and the clock the
160
+ * history ended at, so the room's purge removes what those marks hide at
161
+ * its next tick, past the horizon or not. Then the manager stops
162
+ * capturing (it is destroyed; the document attaches a fresh one if asked).
163
+ * A history that captured nothing writes nothing. Only for a page that
164
+ * cannot come back: a page the back/forward cache keeps, or one that
165
+ * only lost its connection, keeps its history.
166
+ */
167
+ end() {
168
+ if (this.#ended)
169
+ return;
170
+ this.#ended = true;
171
+ const dropped = [...this.undoStack.splice(0), ...this.redoStack.splice(0)];
172
+ const marked = this.#marked;
173
+ if (dropped.length > 0 || marked)
174
+ this.doc.transact(() => {
175
+ this.#release(dropped);
176
+ if (marked) {
177
+ const records = this.doc.get(RELEASE_ROOT);
178
+ records.insert(records.length, [true]);
179
+ }
180
+ }, HISTORY_TRIM);
181
+ this.destroy();
182
+ }
121
183
  destroy = () => {
122
184
  this.trackedOrigins.delete(this);
185
+ openSteps.get(this.doc)?.delete(this.#open);
123
186
  this.#ops.off(this.doc, 'afterTransaction', this.#afterTransaction);
124
187
  this.#ops.off(this.doc, 'destroy', this.destroy);
125
188
  this.#observers.clear();
@@ -164,12 +227,22 @@ export class UndoManager {
164
227
  stack.push(new StackItem(tr.insertSet, tr.deleteSet));
165
228
  if (!undoing && !redoing)
166
229
  this.lastChange = now;
230
+ // A block's delete or withdraw mark: what it hides waits for this
231
+ // history (its end releases it to the purge, `end()`).
232
+ if (!this.#marked)
233
+ for (const keys of tr.changed.values())
234
+ for (const key of keys)
235
+ if (typeof key === 'string' &&
236
+ (key.startsWith(DEL_PREFIX) || key.startsWith(WITHDRAW_PREFIX)))
237
+ this.#marked = true;
167
238
  // What the step deleted is kept from collection while a step may bring it back.
168
239
  ops.iterate(tr, tr.deleteSet, (item) => {
169
240
  if (ops.isItem(item) && this.#inScope(item))
170
241
  ops.keep(item);
171
242
  });
172
243
  const step = stack.at(-1);
244
+ if (tr.writer)
245
+ step.writer = tr.writer;
173
246
  // A streamless block's own text is shared by every replica that
174
247
  // typed into it first: no step captures it, so undoing the first
175
248
  // typing removes the typing and keeps the text (and nonce).
@@ -190,59 +263,80 @@ export class UndoManager {
190
263
  }
191
264
  /**
192
265
  * Replay the top step of `stack` in one transaction (origin: this
193
- * manager): what it inserted is deleted (or withdrawn), what it deleted
194
- * comes back (text as its delete marks allow), the overwritten attrs are
266
+ * manager): what it inserted is deleted (text: held by a record of this
267
+ * writer; a block: withdrawn), what it deleted comes back (a text delete
268
+ * was a record: deleting it shows the text), the overwritten attrs are
195
269
  * repaired. A step that changes nothing is skipped for the one below.
196
270
  */
197
271
  #pop(stack) {
198
272
  const ops = this.#ops;
199
- const { marks, withdraw, repair } = this.#rules;
273
+ const { records, withdraw, repair } = this.#rules;
200
274
  const deletedByHistory = (id) => [this.undoStack, this.redoStack].some((s) => s.some((step) => step.deletes.has(id.client, id.clock)));
201
- this.doc.transact((transaction) => {
202
- const tr = transaction;
203
- while (stack.length > 0 && this.currStackItem === null) {
204
- const step = stack.pop();
205
- const restore = new Set();
206
- const remove = [];
207
- let changed = false;
208
- ops.iterate(tr, step.inserts, (struct) => {
209
- if (!ops.isItem(struct))
210
- return;
211
- const item = ops.latest(tr, struct);
212
- if (!item.deleted && this.#inScope(item))
213
- remove.push(item);
214
- });
215
- ops.iterate(tr, step.deletes, (struct) => {
216
- if (!ops.isItem(struct) || !this.#inScope(struct))
217
- return;
218
- // Created and deleted within the step: nothing to bring back.
219
- if (step.inserts.has(struct.id.client, struct.id.clock))
220
- return;
221
- // Text comes back through the delete marks (`onApply`): withheld here.
222
- if (!marks.restoreFilter(struct))
223
- changed = true;
224
- else
275
+ // A step written under another client replays under it, and so does its replay's step.
276
+ const local = this.doc.clientID;
277
+ const writer = stack.at(-1)?.writer;
278
+ this.doc.clientID = writer ?? local;
279
+ try {
280
+ this.doc.transact((transaction) => {
281
+ const tr = transaction;
282
+ tr.writer = writer;
283
+ while (stack.length > 0 && this.currStackItem === null) {
284
+ const step = stack.pop();
285
+ const restore = new Set();
286
+ const remove = [];
287
+ const held = [];
288
+ let changed = false;
289
+ ops.iterate(tr, step.inserts, (struct) => {
290
+ if (!ops.isItem(struct))
291
+ return;
292
+ const item = ops.latest(tr, struct);
293
+ // An atom the step inserted is held whole: its attrs stay with it.
294
+ const atom = item.parent?._item;
295
+ if (atom?.id &&
296
+ isUnit(atom) &&
297
+ step.inserts.has(atom.id.client, atom.id.clock))
298
+ return;
299
+ if (!item.deleted && this.#inScope(item))
300
+ remove.push(item);
301
+ });
302
+ ops.iterate(tr, step.deletes, (struct) => {
303
+ if (!ops.isItem(struct) || !this.#inScope(struct))
304
+ return;
305
+ // Created and deleted within the step: nothing to bring back.
306
+ if (step.inserts.has(struct.id.client, struct.id.clock))
307
+ return;
308
+ // Text a record holds stays hidden: the record decides (removed with its node).
309
+ if ((struct.holds ?? 0) > 0)
310
+ return;
225
311
  restore.add(struct);
226
- });
227
- for (const item of restore)
228
- changed = ops.redo(tr, item, restore, step.inserts, deletedByHistory) !== null || changed;
229
- // Children before parents: more is known when an item is judged.
230
- for (let i = remove.length - 1; i >= 0; i--) {
231
- if (!withdraw(remove[i], step))
232
- ops.remove(tr, remove[i]);
233
- changed = true;
234
- }
235
- this.currStackItem = changed ? step : null;
236
- if (changed) {
237
- marks.onApply(tr, step);
238
- repair(step);
312
+ });
313
+ for (const item of restore)
314
+ changed =
315
+ ops.redo(tr, item, restore, step.inserts, deletedByHistory) !== null || changed;
316
+ // Children before parents: more is known when an item is judged.
317
+ for (let i = remove.length - 1; i >= 0; i--) {
318
+ const item = remove[i];
319
+ // Text the step wrote is held, never removed: a redo shows it again where it was.
320
+ if (isUnit(item))
321
+ held.push(item.id.client, item.id.clock, item.length);
322
+ else if (!withdraw(item, step))
323
+ ops.remove(tr, item);
324
+ changed = true;
325
+ }
326
+ records.hold(held);
327
+ this.currStackItem = changed ? step : null;
328
+ if (changed)
329
+ repair(step);
239
330
  }
240
- }
241
- tr.changed.forEach((keys, node) => {
242
- if (keys.has(null))
243
- dropSearchMarkers(node);
244
- });
245
- }, this);
331
+ tr.changed.forEach((keys, node) => {
332
+ if (keys.has(null))
333
+ dropSearchMarkers(node);
334
+ });
335
+ }, this);
336
+ }
337
+ finally {
338
+ this.doc.clientID = local;
339
+ }
246
340
  const step = this.currStackItem;
247
341
  this.currStackItem = null;
248
342
  return step;
@@ -250,8 +344,8 @@ export class UndoManager {
250
344
  /**
251
345
  * Release what dropped steps kept for their replay: the engine collects
252
346
  * that content now, as it would have at the delete without a history
253
- * (the doc's `gcFilter` still decides: a text copy another replica may
254
- * have to copy again stays, `text/deletes.ts`). An item is released only
347
+ * (the doc's `gcFilter` still decides), and the text their hold records
348
+ * hold is deleted for good. An item is released only
255
349
  * once every step that deleted part of it was dropped (the engine merges
256
350
  * deleted items across steps): never split, so the store keeps its
257
351
  * merged items. Only the items themselves are released: a kept
@@ -269,6 +363,8 @@ export class UndoManager {
269
363
  return (released.clients.get(st.id.client)?.getIds() ?? []).some((r) => r.clock <= st.id.clock && end <= r.clock + r.len);
270
364
  };
271
365
  const gc = collects(doc);
366
+ const ops = this.#ops;
367
+ const { records } = this.#rules;
272
368
  doc.transact((tr) => {
273
369
  for (const step of dropped)
274
370
  walkIdSet(Y, doc, step.deletes, (st) => {
@@ -279,6 +375,34 @@ export class UndoManager {
279
375
  if (gc && it.deleted && collectable(doc, it))
280
376
  collectNow(it, tr);
281
377
  });
378
+ // A dropped step's text deletes can never be undone here: what this
379
+ // writer's live records hold is deleted for good, as the purge does,
380
+ // and the engine collects it (`hist.delete-marks.release`). A record
381
+ // a captured remote frame wrote (the room's validation) is its
382
+ // writer's to take back.
383
+ const own = ops.idSet();
384
+ for (const step of dropped)
385
+ walkIdSet(Y, doc, step.inserts, (st, from, to) => {
386
+ if (st.parent === records &&
387
+ !st.deleted &&
388
+ (st.id.client === doc.clientID || st.id.client === step.writer))
389
+ own.add(st.id.client, from, to - from);
390
+ });
391
+ const held = ops.idSet();
392
+ ops.iterate(tr, own, (st) => {
393
+ const ids = st.content?.ids;
394
+ if (st.deleted || ids === undefined)
395
+ return;
396
+ for (let i = 0; i < ids.length; i += 3)
397
+ held.add(ids[i], ids[i + 1], ids[i + 2]);
398
+ ops.remove(tr, st);
399
+ });
400
+ ops.iterate(tr, held, (it) => {
401
+ if (it.removed !== false)
402
+ return;
403
+ ops.remove(tr, it);
404
+ dropSearchMarkers(it.parent);
405
+ });
282
406
  }, HISTORY_TRIM);
283
407
  }
284
408
  /**
@@ -310,9 +434,13 @@ export class UndoManager {
310
434
  this.#release(dropped);
311
435
  }
312
436
  }
313
- /** The history factory of one facade. */
314
- export const docHistory = (c) => {
315
- const { Y, doc, D, init, write, disposed } = c;
437
+ /**
438
+ * The document's history rules over `doc` (the one set every history of an
439
+ * edytor document runs): its scope (the block registry, the text hold
440
+ * records, the document's data), the records a replay's text holds go to,
441
+ * the withdraw rule (`hist.undo.withdraw`) and the attr repair below.
442
+ */
443
+ export const historyRules = (Y, doc) => {
316
444
  /**
317
445
  * Undo repair, inside the replay's transaction. A replayed step
318
446
  * deletes its own attr write and re-creates the value that write
@@ -350,9 +478,25 @@ export const docHistory = (c) => {
350
478
  node.setAttr(key, value);
351
479
  }
352
480
  };
481
+ // The text hold records are in scope (an undo deletes the undoer's
482
+ // own, fork patch YP20). An undone creation withdraws the block instead
483
+ // of deleting it (`hist.undo.withdraw`).
484
+ const records = doc.get(TEXT_DELETES_ROOT);
485
+ return {
486
+ Y,
487
+ doc,
488
+ scope: [registryOf(doc), records, doc.get(DOC_DATA_ROOT), doc.get(SUGGEST_ROOT)],
489
+ records,
490
+ withdraw: withdrawOnUndo(doc),
491
+ repair: repairAttrs
492
+ };
493
+ };
494
+ /** The history factory of one facade. */
495
+ export const docHistory = (c) => {
496
+ const { Y, doc, init, write, disposed } = c;
353
497
  /**
354
498
  * The document's undo manager, SCOPED TO THE BLOCK REGISTRY (with the
355
- * text delete marks and the document's data), attached only after the
499
+ * text hold records and the document's data), attached only after the
356
500
  * doc carries the schema version record:
357
501
  *
358
502
  * - the `meta` root (version stamp, schema name) is outside the scope,
@@ -373,20 +517,7 @@ export const docHistory = (c) => {
373
517
  if (!isInitialized(doc))
374
518
  init(doc);
375
519
  });
376
- // Text delete marks (`text/deletes.ts`): the marks are in scope (an undo
377
- // removes the undoer's own), and the history restores text only as the
378
- // marks allow. An undone creation withdraws the block instead of
379
- // deleting it (`hist.undo.withdraw`).
380
- const marks = D.history(doc, () => um);
381
- const um = new UndoManager({
382
- Y,
383
- doc,
384
- scope: [registryOf(doc), D.scope(doc), doc.get(DOC_DATA_ROOT)],
385
- marks,
386
- withdraw: withdrawOnUndo(doc),
387
- repair: repairAttrs
388
- }, options);
389
- return um;
520
+ return new UndoManager(historyRules(Y, doc), options);
390
521
  };
391
522
  return { createUndoManager };
392
523
  };