edytor 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +2 -2
  2. package/dist/cloudflare/DocumentRoom.d.ts +13 -9
  3. package/dist/cloudflare/DocumentRoom.js +10 -6
  4. package/dist/cloudflare/room/context.d.ts +27 -26
  5. package/dist/cloudflare/room/context.js +2 -2
  6. package/dist/cloudflare/room/purge.d.ts +6 -3
  7. package/dist/cloudflare/room/purge.js +44 -19
  8. package/dist/cloudflare/room/updates.d.ts +3 -4
  9. package/dist/cloudflare/room/updates.js +6 -8
  10. package/dist/crdt/attribution/attribution.d.ts +31 -12
  11. package/dist/crdt/attribution/attribution.js +56 -18
  12. package/dist/crdt/attribution/block.d.ts +3 -3
  13. package/dist/crdt/attribution/block.js +7 -8
  14. package/dist/crdt/attribution/index.d.ts +1 -1
  15. package/dist/crdt/attribution/index.js +1 -1
  16. package/dist/crdt/data.d.ts +62 -31
  17. package/dist/crdt/data.js +211 -103
  18. package/dist/crdt/doc/delete.d.ts +1 -1
  19. package/dist/crdt/doc/delete.js +13 -30
  20. package/dist/crdt/doc/funnel.js +29 -24
  21. package/dist/crdt/doc/gate.d.ts +3 -3
  22. package/dist/crdt/doc/history.d.ts +25 -9
  23. package/dist/crdt/doc/history.js +150 -40
  24. package/dist/crdt/doc/layout.js +11 -22
  25. package/dist/crdt/doc/meta.js +12 -17
  26. package/dist/crdt/doc/moves.d.ts +1 -1
  27. package/dist/crdt/doc/moves.js +52 -32
  28. package/dist/crdt/doc/reads.js +2 -2
  29. package/dist/crdt/doc/seed.js +2 -2
  30. package/dist/crdt/doc/split-merge.js +12 -15
  31. package/dist/crdt/doc/steps.d.ts +25 -13
  32. package/dist/crdt/doc/steps.js +52 -122
  33. package/dist/crdt/doc/types.d.ts +21 -9
  34. package/dist/crdt/document.d.ts +16 -5
  35. package/dist/crdt/document.js +21 -4
  36. package/dist/crdt/edytor-doc.d.ts +62 -64
  37. package/dist/crdt/edytor-doc.js +18 -13
  38. package/dist/crdt/engine-api.d.ts +33 -1
  39. package/dist/crdt/engine.d.ts +0 -1
  40. package/dist/crdt/engine.js +1 -4
  41. package/dist/crdt/flow.d.ts +1 -1
  42. package/dist/crdt/flow.js +25 -27
  43. package/dist/crdt/index.d.ts +26 -25
  44. package/dist/crdt/index.js +1 -1
  45. package/dist/crdt/placement/display.d.ts +5 -30
  46. package/dist/crdt/placement/display.js +39 -144
  47. package/dist/crdt/placement/keys.d.ts +58 -0
  48. package/dist/crdt/placement/keys.js +90 -0
  49. package/dist/crdt/placement/model.d.ts +119 -118
  50. package/dist/crdt/placement/model.js +242 -142
  51. package/dist/crdt/placement/resolve.d.ts +123 -35
  52. package/dist/crdt/placement/resolve.js +187 -153
  53. package/dist/crdt/placement/slots.d.ts +134 -0
  54. package/dist/crdt/placement/slots.js +152 -0
  55. package/dist/crdt/providers/room.d.ts +16 -2
  56. package/dist/crdt/providers/room.js +64 -17
  57. package/dist/crdt/providers/websocket.js +5 -3
  58. package/dist/crdt/purge.d.ts +17 -8
  59. package/dist/crdt/purge.js +236 -126
  60. package/dist/crdt/restore.d.ts +2 -2
  61. package/dist/crdt/restore.js +18 -34
  62. package/dist/crdt/schema.d.ts +22 -21
  63. package/dist/crdt/schema.js +29 -23
  64. package/dist/crdt/storage.d.ts +1 -1
  65. package/dist/crdt/storage.js +1 -1
  66. package/dist/crdt/structs.d.ts +15 -15
  67. package/dist/crdt/structs.js +8 -22
  68. package/dist/crdt/text/index/cache.js +4 -0
  69. package/dist/crdt/text/index/claims.js +12 -0
  70. package/dist/crdt/text/index/fold.d.ts +2 -3
  71. package/dist/crdt/text/index/fold.js +194 -44
  72. package/dist/crdt/text/index/layout.js +5 -8
  73. package/dist/crdt/text/index/placement.d.ts +26 -18
  74. package/dist/crdt/text/index/placement.js +404 -138
  75. package/dist/crdt/text/index/records.d.ts +3 -3
  76. package/dist/crdt/text/index/records.js +24 -31
  77. package/dist/crdt/text/index/shared.d.ts +11 -3
  78. package/dist/crdt/text/index/shared.js +12 -3
  79. package/dist/crdt/text/index/state.d.ts +21 -17
  80. package/dist/crdt/text/index/state.js +15 -17
  81. package/dist/crdt/text/index/streams.d.ts +10 -5
  82. package/dist/crdt/text/index/streams.js +78 -27
  83. package/dist/crdt/text/items.d.ts +10 -3
  84. package/dist/crdt/text/items.js +7 -0
  85. package/dist/crdt/text/model.d.ts +77 -62
  86. package/dist/crdt/text/model.js +84 -169
  87. package/dist/crdt/text/ranges.d.ts +1 -1
  88. package/dist/crdt/text/ranges.js +3 -3
  89. package/dist/crdt/text/rows.d.ts +13 -7
  90. package/dist/crdt/text/rows.js +24 -7
  91. package/dist/crdt/text/runs.d.ts +17 -12
  92. package/dist/crdt/text/runs.js +24 -7
  93. package/dist/crdt/text/streams.d.ts +26 -13
  94. package/dist/crdt/text/streams.js +41 -14
  95. package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
  96. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  97. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  98. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  99. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
  100. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  101. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  102. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  103. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  104. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  105. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  106. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  107. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
  108. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  109. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  110. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  111. package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
  112. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  113. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  114. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  115. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  116. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  117. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  118. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  119. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  120. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  121. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  122. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  123. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  124. package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
  125. package/dist/edytor.svelte.js +4 -1
  126. package/dist/events/beforeInputCommands.js +2 -2
  127. package/dist/events/onPageHide.d.ts +3 -0
  128. package/dist/events/onPageHide.js +16 -0
  129. package/dist/export/forms.d.ts +25 -25
  130. package/dist/surface/observer.svelte.js +2 -2
  131. package/dist/surface/projector.svelte.d.ts +15 -3
  132. package/dist/surface/projector.svelte.js +59 -8
  133. package/package.json +2 -2
  134. package/dist/crdt/placement/rank.d.ts +0 -87
  135. package/dist/crdt/placement/rank.js +0 -332
  136. package/dist/crdt/placement/source.d.ts +0 -43
  137. package/dist/crdt/placement/source.js +0 -66
  138. package/dist/crdt/text/delete-index.d.ts +0 -129
  139. package/dist/crdt/text/delete-index.js +0 -365
  140. package/dist/crdt/text/deletes.d.ts +0 -76
  141. package/dist/crdt/text/deletes.js +0 -362
  142. package/dist/crdt/text/index/anchored.d.ts +0 -17
  143. package/dist/crdt/text/index/anchored.js +0 -197
@@ -481,11 +481,11 @@ export const createDeleteSetFromStructStore = ss => {
481
481
  const dsitems = []
482
482
  for (let i = 0; i < structs.length; i++) {
483
483
  const struct = structs[i]
484
- if (struct.deleted) {
484
+ if (struct.removed) { // YP20: a held item is not deleted
485
485
  const clock = struct.id.clock
486
486
  let len = struct.length
487
487
  if (i + 1 < structs.length) {
488
- for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) {
488
+ for (let next = structs[i + 1]; i + 1 < structs.length && next.removed; next = structs[++i + 1]) {
489
489
  len += next.length
490
490
  }
491
491
  }
@@ -514,7 +514,7 @@ export const writeIdSet = (encoder, idSet) => {
514
514
  .forEach(([client, _idRanges]) => {
515
515
  const idRanges = _idRanges.getIds()
516
516
  encoder.resetIdSetCurVal()
517
- encoding.writeVarUint(encoder.restEncoder, client)
517
+ encoder.writeDsClient(client) // YP26
518
518
  const len = idRanges.length
519
519
  encoding.writeVarUint(encoder.restEncoder, len)
520
520
  for (let i = 0; i < len; i++) {
@@ -537,7 +537,7 @@ export const readIdSet = decoder => {
537
537
  const numClients = decoding.readVarUint(decoder.restDecoder)
538
538
  for (let i = 0; i < numClients; i++) {
539
539
  decoder.resetDsCurVal()
540
- const client = decoding.readVarUint(decoder.restDecoder)
540
+ const client = decoder.readDsClient() // YP26
541
541
  const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
542
542
  if (numberOfDeletes > 0) {
543
543
  /**
@@ -571,7 +571,7 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
571
571
  const numClients = decoding.readVarUint(decoder.restDecoder)
572
572
  for (let i = 0; i < numClients; i++) {
573
573
  decoder.resetDsCurVal()
574
- const client = decoding.readVarUint(decoder.restDecoder)
574
+ const client = decoder.readDsClient() // YP26
575
575
  const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
576
576
  const structs = store.clients.get(client) || []
577
577
  const state = store.getClock(client)
@@ -589,7 +589,7 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
589
589
  */
590
590
  let struct = structs[index]
591
591
  // split the first item if necessary
592
- if (!struct.deleted && struct.id.clock < clock && struct.isItem) {
592
+ if (!struct.removed && struct.id.clock < clock && struct.isItem) { // YP20: removed, not held
593
593
  // increment index, we now want to use the next struct
594
594
  structs.splice(++index, 0, /** @type {Item} */ (struct).split(transaction, clock - struct.id.clock))
595
595
  }
@@ -597,7 +597,7 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
597
597
  // @ts-ignore
598
598
  struct = structs[index++]
599
599
  if (struct.id.clock < clockEnd) {
600
- if (!struct.deleted) {
600
+ if (!struct.removed) { // YP20
601
601
  if (struct.isItem) {
602
602
  if (clockEnd < struct.id.clock + struct.length) {
603
603
  structs.splice(index, 0, /** @type {Item} */ (struct).split(transaction, clockEnd - struct.id.clock))
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import('./Transaction.js').Transaction} Transaction
3
+ */
4
+
1
5
  /**
2
6
  * Paired marks — an edytor fork addition (UPSTREAM.md YP13), not upstream source.
3
7
  *
@@ -117,33 +121,214 @@ export const formatValue = value => value instanceof MarkState ? value.value : v
117
121
  /**
118
122
  * The integration class of an item among the items of one origin: a
119
123
  * left-side mark item `0`, content and plain formats `1`, a right-side
120
- * mark item `2`. Deleted or collected items keep the class their content
121
- * gave them while it is there; a collected format item (its content gone)
122
- * would read `1`, which is why the fork never collects a paired mark item's
123
- * content (`ContentFormat#gc` keeps it).
124
+ * mark item `2`, a claim item `3` (YP21: a sequence unit `{ m }`, after
125
+ * everything else, so content and boundaries a concurrent writer puts at
126
+ * its origin land before it), a move record `4` or `5` by its side (YP22:
127
+ * after every slot of its origin — a split's pieces first — and a record
128
+ * standing after what precedes it before one standing before what follows
129
+ * it: the order every serial run of the gestures gives). Deleted or collected items keep the class
130
+ * their content gave them while it is there; a collected item (its content
131
+ * gone) would read `1`, which is why the fork never collects the content of
132
+ * an item of another class (`Item#gc` keeps it).
124
133
  *
125
134
  * @param {any} item
126
135
  * @return {number}
127
136
  */
128
137
  export const markClass = item => {
129
138
  const content = item.content
130
- if (content === undefined || typeof content.key !== 'string') return 1
139
+ if (content === undefined) return 1
140
+ if (content.cls !== undefined) return content.cls // YP29: a collected mark or claim (`ContentDeleted`)
141
+ if (typeof content.key !== 'string') {
142
+ // YP22: a move record (its side: 0 after, 1 before)
143
+ if (content.priority !== undefined && content.ids !== undefined) return 4 + content.side
144
+ // YP21: a claim item
145
+ return item.parentSub === null && typeof content.arr?.[0]?.m === 'string' ? 3 : 1
146
+ }
131
147
  const role = pairedRole(content)
132
148
  if (role < 0) return 1
133
149
  const side = role === 0 ? content.value[4] : content.value[2]
134
150
  return side === SIDE_LEFT ? 0 : 2
135
151
  }
136
152
 
153
+ /**
154
+ * YP22: the unit id `client:clock` names, as its item and clock, `null` where
155
+ * a GC struct replaced it.
156
+ *
157
+ * @param {import('./StructStore.js').StructStore} store
158
+ * @param {number} client
159
+ * @param {number} clock
160
+ * @return {{ item: any, clock: number } | null}
161
+ */
162
+ const unitAt = (store, client, clock) => {
163
+ const item = store.getItem({ client, clock })
164
+ return item.isItem ? { item, clock } : null
165
+ }
166
+
167
+ /**
168
+ * YP22: the order of two units (`unitAt`): by clock in one item, else by
169
+ * `sequenceOrder` (`NaN`: not in one sequence); `0` for one unit.
170
+ *
171
+ * @param {{ item: any, clock: number }} p
172
+ * @param {{ item: any, clock: number }} q
173
+ * @return {number}
174
+ */
175
+ const unitOrder = (p, q) => (p.item === q.item ? p.clock - q.clock : sequenceOrder(p.item, q.item))
176
+
177
+ /**
178
+ * YP22: a block slot's split key (`{ b, s: [client, clock] }`: the id of the
179
+ * boundary item of the split that made the block, kept when the slot is
180
+ * collected, `Item#gc`) as a unit (`unitAt`: `null` where a GC struct
181
+ * replaced the boundary), `undefined` for any other item (a slot no split
182
+ * made, a boundary, whose own `s` names a block).
183
+ *
184
+ * @param {any} item
185
+ * @param {import('./StructStore.js').StructStore} store
186
+ * @return {{ item: any, clock: number } | null | undefined}
187
+ */
188
+ const splitKey = (item, store) => {
189
+ const s = item.content.arr?.[0]?.s
190
+ return Array.isArray(s) ? unitAt(store, s[0], s[1]) : undefined
191
+ }
192
+
193
+ /**
194
+ * YP22: whether `item` is a list item outside a text (a block's or an
195
+ * array's slot: its parent is no `content` node) written by the writer of
196
+ * its origin: it extends that writer's run there, and sorts before every
197
+ * other writer's slot of the same origin (`order.insert.run`: the lines one
198
+ * writer types after its own line stay together around a peer's line
199
+ * inserted there meanwhile). Read from the item's ids and parent, which
200
+ * collection never changes, so every replica integrates alike.
201
+ *
202
+ * @param {any} item
203
+ * @return {boolean}
204
+ */
205
+ const extendsRun = (/** @type {Item} */ item) =>
206
+ item.parentSub === null && /** @type {YNode} */ (item.parent).name !== 'content' && item.origin !== null && item.origin.client === item.id.client
207
+
208
+ /**
209
+ * YP22: the order of two items of one sequence (one parent, one key):
210
+ * negative when `x` stands before `y`, positive after, `0` for one item,
211
+ * `NaN` when they are not in one sequence. The walk goes left from both at
212
+ * once, so it costs the distance between them.
213
+ *
214
+ * @param {any} x
215
+ * @param {any} y
216
+ * @return {number}
217
+ */
218
+ export const sequenceOrder = (x, y) => {
219
+ if (x === y) return 0
220
+ if (x.parent !== y.parent || x.parentSub !== y.parentSub) return NaN
221
+ for (let l = x, r = y; ;) {
222
+ l = l.left
223
+ r = r.left
224
+ if (l === y || r === null) return 1
225
+ if (r === x || l === null) return -1
226
+ }
227
+ }
228
+
229
+ /**
230
+ * YP22: the chain record `r` stands at, outermost first — the units its
231
+ * writer named (`at`: the home slots from the root sequence down to the unit
232
+ * it stands at), else the first unit it moves — each as its item and clock,
233
+ * `null` where a GC struct replaced it.
234
+ *
235
+ * @param {any} r
236
+ * @param {import('./StructStore.js').StructStore} store
237
+ * @return {Array<{ item: any, clock: number } | null>}
238
+ */
239
+ const chainOf = (r, store) => {
240
+ const c = r.content
241
+ return c.at !== null
242
+ ? c.at.map((/** @type {ID} */ id) => unitAt(store, id.client, id.clock))
243
+ : c.ids.length > 0 ? [unitAt(store, c.ids[0], c.ids[1])] : []
244
+ }
245
+
246
+ /**
247
+ * YP22: the order of two move records at one origin: by the chains they
248
+ * stand at (`chainOf`), level by level from the root — two units of one
249
+ * sequence by `sequenceOrder` (one item: by clock), a unit before the units
250
+ * inside its block (the shorter chain first); chains that meet no common
251
+ * sequence order nothing — then by their `part` (the writer's order among
252
+ * the records standing at one unit), then the stronger record first (the
253
+ * greater priority, then client: a unit both move shows at the first of the
254
+ * two). Two writers that move blocks out of one list into one gap at once (an
255
+ * outdent, a lift, a divider after an item) keep the list's order there and
256
+ * every serial order's placement of what stands at one item, whatever their
257
+ * client ids (the delete contract's CW-01 and DR-crdt-6 rules). It reads only
258
+ * the records and the units they name, all present when a record integrates
259
+ * (`getMissing`), so every replica sorts alike in any delivery order.
260
+ *
261
+ * @param {any} o
262
+ * @param {any} item
263
+ * @param {Transaction} tr
264
+ * @return {number}
265
+ */
266
+ const sourceOrder = (o, item, tr) => {
267
+ const store = tr.doc.store
268
+ const x = chainOf(o, store)
269
+ const y = chainOf(item, store)
270
+ let d = 0
271
+ for (let i = 0; ; i++) {
272
+ const p = x[i]
273
+ const q = y[i]
274
+ if (p === undefined || q === undefined) {
275
+ d = (p === undefined ? 0 : 1) - (q === undefined ? 0 : 1)
276
+ break
277
+ }
278
+ if (p === null || q === null) break
279
+ d = unitOrder(p, q)
280
+ if (d === 0) continue
281
+ if (Number.isNaN(d)) d = 0
282
+ break
283
+ }
284
+ if (d === 0) d = o.content.part - item.content.part
285
+ if (d === 0) d = item.content.priority - o.content.priority || item.id.client - o.id.client
286
+ return d
287
+ }
288
+
137
289
  /**
138
290
  * Whether `o` sorts before `item` among the items of one origin (the
139
- * integration's conflict rule): by class, then by client id as upstream.
291
+ * integration's conflict rule): by class, then (YP22) block slots by their
292
+ * split key and the origin writer's own first, records by `sourceOrder`,
293
+ * then by client id as upstream.
140
294
  *
141
295
  * @param {any} o
142
296
  * @param {any} item
297
+ * @param {Transaction} tr
143
298
  * @return {boolean}
144
299
  */
145
- export const sortsBefore = (o, item) => {
300
+ export const sortsBefore = (o, item, tr) => {
146
301
  const a = markClass(o)
147
302
  const b = markClass(item)
148
- return a !== b ? a < b : o.id.client < item.id.client
303
+ if (a !== b) return a < b
304
+ if (a === 1) {
305
+ // YP22: block slots a split made sort by their boundaries' order in the
306
+ // text (two writers' splits of one block keep its text's order), before
307
+ // the slots no split made; then the origin writer's own slots first.
308
+ const store = tr.doc.store
309
+ const x = splitKey(o, store)
310
+ const y = splitKey(item, store)
311
+ if (x !== undefined || y !== undefined) {
312
+ if (x === undefined || y === undefined) return y === undefined
313
+ if (x !== null && y !== null) {
314
+ // One total order over the keyed slots of an origin: the boundaries' text
315
+ // first (by the text node's item id, a root text first), then their place in
316
+ // it (two boundaries one item holds: their clocks).
317
+ const px = /** @type {YNode} */ (x.item.parent)._item
318
+ const py = /** @type {YNode} */ (y.item.parent)._item
319
+ if (x.item.parent !== y.item.parent) {
320
+ if (px === null || py === null) return px === null
321
+ return (px.id.client - py.id.client || px.id.clock - py.id.clock) < 0
322
+ }
323
+ const d = unitOrder(x, y)
324
+ if (d !== 0) return d < 0
325
+ }
326
+ }
327
+ const run = extendsRun(o)
328
+ if (run !== extendsRun(item)) return run
329
+ } else if (a >= 4) {
330
+ const d = sourceOrder(o, item, tr)
331
+ if (d !== 0) return d < 0
332
+ }
333
+ return o.id.client < item.id.client
149
334
  }
@@ -38,7 +38,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, deletedByH
38
38
  */
39
39
  let right
40
40
  // make sure that parent is redone
41
- if (parentItem !== null && parentItem.deleted === true) {
41
+ if (parentItem !== null && parentItem.removed) { // YP20: a held parent is alive
42
42
  // try to undo parent if it will be undone anyway
43
43
  if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, deletedByHistory) === null)) {
44
44
  return null
@@ -245,7 +245,7 @@ export const tryGcDeleteSet = (tr, ds, gcFilter) => {
245
245
  if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
246
246
  break
247
247
  }
248
- if (struct.isItem && struct.deleted && !(struct).keep && !isKeptReplaced(/** @type {Item} */ (struct)) && gcFilter(/** @type {Item} */ (struct))) { // YP14
248
+ if (struct.isItem && struct.removed && !(struct).keep && !isKeptReplaced(/** @type {Item} */ (struct)) && gcFilter(/** @type {Item} */ (struct))) { // YP14, YP20: removed
249
249
  /** @type {Item} */ (struct).gc(tr, false)
250
250
  }
251
251
  }
@@ -42,9 +42,9 @@ function * lazyStructReaderGenerator (decoder) {
42
42
  null, // right
43
43
  (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
44
44
  // @ts-ignore Force writing a string here.
45
- cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent
46
- cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
47
- readItemContent(decoder, info) // item content
45
+ cantCopyParentInfo ? ((info & binary.BIT5) === binary.BIT5 ? decoder.readKey() : decoder.readLeftID()) : null, // parent (YP27: bit 5 a root's name, YP24: a key)
46
+ cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readKey() : null, // parentSub (YP24: a key)
47
+ readItemContent(decoder, info, createID(client, clock)) // item content (YP25: with its id)
48
48
  )
49
49
  yield struct
50
50
  clock += struct.length
@@ -10,6 +10,9 @@ import {
10
10
  ContentFormat,
11
11
  ContentString,
12
12
  ContentType,
13
+ ContentHold, // YP20
14
+ ContentMove, // YP22
15
+ readFormatValue, // YP25
13
16
  YXmlFragmentRefID,
14
17
  YXmlElementRefID,
15
18
  YXmlHookRefID
@@ -248,7 +251,7 @@ const insertMarkItem = (transaction, parent, index, side, content) => {
248
251
  const markHelper = (transaction, parent, index, length, mark, value, startSide, endSide) => {
249
252
  const doc = transaction.doc
250
253
  const client = doc.clientID
251
- const l = /** @type {any} */ (doc)._markClock + 1
254
+ const l = /** @type {any} */ (doc)._clock + 1 // YP22: one clock with the move records
252
255
  const k = doc.store.getClock(client)
253
256
  insertMarkItem(transaction, parent, index, startSide, new ContentFormat(MARK_START + mark, [value, l, client, k, startSide]))
254
257
  insertMarkItem(transaction, parent, index + length, endSide, new ContentFormat(MARK_END + mark, [client, k, endSide]))
@@ -292,28 +295,74 @@ export const insertAtGapEndHelper = (transaction, parent, index, content) => {
292
295
  }
293
296
  // YP7 end
294
297
 
298
+ // YP20 begin (edytor fork: hold records — see UPSTREAM.md YP20)
295
299
  /**
300
+ * The first clock in `(clock, end)` where membership in `set` changes, else `end`.
301
+ *
302
+ * @param {import('./utils/ids.js').IdSet} set
303
+ * @param {number} client
304
+ * @param {number} clock
305
+ * @param {number} end
306
+ * @return {number}
307
+ */
308
+ const runEnd = (set, client, clock, end) => {
309
+ for (const r of set.clients.get(client)?.getIds() ?? []) {
310
+ if (r.clock > clock) return math.min(r.clock, end)
311
+ if (r.clock + r.len > clock) return math.min(r.clock + r.len, end)
312
+ }
313
+ return end
314
+ }
315
+
316
+ /**
317
+ * Append a hold record of `ids` to `list` (the document's records list).
318
+ *
319
+ * @param {Transaction} transaction
320
+ * @param {YNode} list
321
+ * @param {Array<number>} ids
322
+ */
323
+ const holdHelper = (transaction, list, ids) => {
324
+ insertInGapHelper(transaction, list, list._length, new ContentHold(ids))
325
+ }
326
+ // YP20 end
327
+
328
+ /**
329
+ * With `held`, the live units are not deleted: their ids go to `held`
330
+ * (`client, clock, len`), except those `fresh` names (YP20).
331
+ *
296
332
  * @param {Transaction} transaction
297
333
  * @param {ItemTextListPosition} currPos
298
334
  * @param {number} length
335
+ * @param {Array<number>|null} [held]
336
+ * @param {import('./utils/ids.js').IdSet|null} [fresh]
299
337
  * @return {ItemTextListPosition}
300
338
  *
301
339
  * @private
302
340
  * @function
303
341
  */
304
- export const deleteText = (transaction, currPos, length) => {
342
+ export const deleteText = (transaction, currPos, length, held = null, fresh = null) => {
305
343
  const startLength = length
344
+ let kept = 0 // YP20
306
345
  while (length > 0 && currPos.right !== null) {
307
346
  const item = currPos.right
308
347
  if (!item.deleted && item.countable) {
309
- if (length < item.length) {
310
- getItemCleanStart(transaction, createID(item.id.client, item.id.clock + length))
348
+ const { client, clock } = item.id
349
+ // YP20: a held run ends where the open step's inserts start or end
350
+ const n = fresh === null ? math.min(length, item.length) : runEnd(fresh, client, clock, clock + math.min(length, item.length)) - clock
351
+ if (n < item.length) {
352
+ getItemCleanStart(transaction, createID(client, clock + n))
311
353
  }
312
354
  length -= item.length
313
- item.delete(transaction)
355
+ if (held === null || (fresh !== null && fresh.has(client, clock))) {
356
+ item.delete(transaction)
357
+ } else {
358
+ held.push(client, clock, item.length)
359
+ kept += item.length
360
+ }
314
361
  }
315
362
  currPos.forward()
316
363
  }
364
+ // YP20: the held units hide when their record integrates
365
+ currPos.index -= kept
317
366
  const parent = /** @type {YNode} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
318
367
  if (parent._searchMarker) {
319
368
  updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length)
@@ -939,11 +988,17 @@ export class YNode {
939
988
 
940
989
  /**
941
990
  * Deletes elements starting from an index. A deleted node is left as it is.
991
+ * YP20: with `into` (the document's records list), the live units are not
992
+ * removed but held by one record appended to `into` (their writer's delete,
993
+ * which an undo of the record takes back), except those `fresh` names (the
994
+ * open undo step's inserts: removed, so that step's undo changes nothing).
942
995
  *
943
996
  * @param {number} index Index at which to start deleting elements
944
997
  * @param {number} length The number of elements to remove. Defaults to 1.
998
+ * @param {YNode|null} [into]
999
+ * @param {import('./utils/ids.js').IdSet|null} [fresh]
945
1000
  */
946
- delete (index, length = 1) {
1001
+ delete (index, length = 1, into = null, fresh = null) {
947
1002
  if (!(length > 0)) return
948
1003
  if (this.doc == null) {
949
1004
  const runs = prelimOf(this).runs
@@ -959,11 +1014,59 @@ export class YNode {
959
1014
  if (this._item !== null && this._item.deleted) return
960
1015
  transact(this.doc, transaction => {
961
1016
  const currPos = cursorAt(transaction, this, index)
962
- deleteText(transaction, currPos, length)
1017
+ const held = into === null ? null : [] // YP20
1018
+ deleteText(transaction, currPos, length, held, fresh)
1019
+ if (held !== null && held.length > 0) {
1020
+ // YP20: `deleteText` moved the markers as for a delete; the hold keeps them
1021
+ const ms = this._searchMarker === null ? null : this._searchMarker.slice()
1022
+ holdHelper(transaction, /** @type {YNode} */ (into), held)
1023
+ if (ms !== null && this._searchMarker !== null) this._searchMarker.splice(0, this._searchMarker.length, ...ms)
1024
+ }
963
1025
  plantMarker(this, currPos)
964
1026
  })
965
1027
  }
966
1028
 
1029
+ /**
1030
+ * YP20: append to this node (the document's records list) one hold record of
1031
+ * the items `ids` names (`client, clock, len` triples): they read as deleted
1032
+ * while the record is live.
1033
+ *
1034
+ * @param {Array<number>} ids
1035
+ */
1036
+ hold (ids) {
1037
+ if (this.doc == null) throw new Error('hold: node is not integrated')
1038
+ if (ids.length > 0) transact(this.doc, transaction => holdHelper(transaction, this, ids))
1039
+ }
1040
+
1041
+ // YP22 begin (edytor fork: move records — see UPSTREAM.md YP22)
1042
+ /**
1043
+ * YP22: move the units `ids` (`client, clock, len` ranges, in the order
1044
+ * they will show) to live index `index` of this sequence: one move record
1045
+ * there (`ContentMove`); `side` is its class among the items of its origin
1046
+ * (`0` after, `1` before), `at` the chain it stands at for the sort of two
1047
+ * records at one origin — the units from the root sequence down to the
1048
+ * unit it stands at, outermost first (`null`: the first unit it names) —
1049
+ * and `part` the writer's order among the records standing at that unit.
1050
+ *
1051
+ * @param {number} index
1052
+ * @param {Array<number>} ids
1053
+ * @param {number} [side]
1054
+ * @param {number} [part]
1055
+ * @param {Array<ID>|null} [at]
1056
+ */
1057
+ move (index, ids, side = 0, part = 0, at = null) {
1058
+ if (this.doc == null) throw new Error('move: node is not integrated')
1059
+ if (ids.length === 0) return
1060
+ transact(this.doc, transaction => {
1061
+ // Above every record seen from a peer (`_clock`, raised by remote records
1062
+ // only): this writer's records keep one priority until it sees another's,
1063
+ // so its gestures win or lose together against a concurrent peer's.
1064
+ const priority = /** @type {any} */ (transaction.doc)._clock + 1
1065
+ insertInGapHelper(transaction, this, index, new ContentMove(priority, side, ids, part, at))
1066
+ })
1067
+ }
1068
+ // YP22 end
1069
+
967
1070
  /**
968
1071
  * Executes a provided function on once on every key-value pair.
969
1072
  *
@@ -1081,9 +1184,13 @@ export const readContentString = decoder => new ContentString(decoder.readString
1081
1184
 
1082
1185
  /**
1083
1186
  * @param {UpdateDecoder} decoder
1187
+ * @param {import('./utils/ID.js').ID} id YP25: the item's id (a paired mark's operation id is usually it)
1084
1188
  * @return {ContentFormat}
1085
1189
  */
1086
- export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())
1190
+ export const readContentFormat = (decoder, id) => {
1191
+ const key = decoder.readKey()
1192
+ return new ContentFormat(key, readFormatValue(decoder, key, id)) // YP25
1193
+ }
1087
1194
 
1088
1195
  /**
1089
1196
  * @param {UpdateDecoder} decoder
@@ -1112,14 +1219,68 @@ export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
1112
1219
  */
1113
1220
  export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
1114
1221
 
1222
+ /**
1223
+ * YP20, YP22: a record's id ranges, a count then one `client, clock, len` each.
1224
+ *
1225
+ * @param {UpdateDecoder} decoder
1226
+ * @return {Array<number>}
1227
+ */
1228
+ const readIds = decoder => {
1229
+ const ids = []
1230
+ for (let n = decoder.readLen(); n > 0; n--) {
1231
+ const id = decoder.readLeftID()
1232
+ ids.push(id.client, id.clock, decoder.readLen())
1233
+ }
1234
+ return ids
1235
+ }
1236
+
1237
+ /**
1238
+ * YP20: a hold record, one `client, clock, len` per unit.
1239
+ *
1240
+ * @param {UpdateDecoder} decoder
1241
+ * @return {ContentHold}
1242
+ */
1243
+ export const readContentHold = decoder => new ContentHold(readIds(decoder))
1244
+
1245
+ /**
1246
+ * YP22: a move record: its priority, side, part and whether an anchor chain
1247
+ * follows in one word, the chain (a count, then its units), then one
1248
+ * `client, clock, len` per range.
1249
+ *
1250
+ * @param {UpdateDecoder} decoder
1251
+ * @return {ContentMove}
1252
+ */
1253
+ export const readContentMove = decoder => {
1254
+ const word = decoder.readLen()
1255
+ /** @type {Array<ID>|null} */
1256
+ let at = null
1257
+ if (word & 16) {
1258
+ at = []
1259
+ for (let n = decoder.readLen(); n > 0; n--) at.push(decoder.readLeftID())
1260
+ }
1261
+ return new ContentMove(word >> 5, word & 1, readIds(decoder), (word >> 1) & 7, at)
1262
+ }
1263
+
1264
+ /**
1265
+ * YP29: a collected mark or claim, its length and class.
1266
+ *
1267
+ * @param {UpdateDecoder} decoder
1268
+ * @return {ContentDeleted}
1269
+ */
1270
+ export const readContentClass = decoder => {
1271
+ const word = decoder.readLen()
1272
+ return new ContentDeleted(Math.floor(word / 4), word % 4)
1273
+ }
1274
+
1115
1275
  const refused = () => { error.unexpectedCase() }
1116
1276
 
1117
1277
  /**
1118
1278
  * A lookup map for reading Item content. YP19: JSON (2), embed (5) and
1119
1279
  * subdocument (9) contents are refused like GC (0) and Skip (10): edytor
1120
- * writes none, so an update holding one is malformed.
1280
+ * writes none, so an update holding one is malformed. YP20: 11 is a hold
1281
+ * record. YP22: 12 is a move record.
1121
1282
  *
1122
- * @type {Array<function(UpdateDecoder):AbstractContent>}
1283
+ * @type {Array<function(UpdateDecoder, import('./utils/ID.js').ID):AbstractContent>}
1123
1284
  */
1124
1285
  export const contentRefs = [
1125
1286
  refused, // 0 - GC is not ItemContent
@@ -1132,14 +1293,18 @@ export const contentRefs = [
1132
1293
  readContentType, // 7
1133
1294
  readContentAny, // 8
1134
1295
  refused, // 9 - subdocument
1135
- refused // 10 - Skip is not ItemContent
1296
+ refused, // 10 - Skip is not ItemContent
1297
+ readContentHold, // 11 - a hold record (YP20)
1298
+ readContentMove, // 12 - a move record (YP22)
1299
+ readContentClass // 13 - a collected mark or claim, its class (YP29)
1136
1300
  ]
1137
1301
 
1138
1302
  /**
1139
1303
  * @param {UpdateDecoder} decoder
1140
1304
  * @param {number} info
1305
+ * @param {import('./utils/ID.js').ID} id YP25: the item's id (a format item reads it)
1141
1306
  */
1142
- export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
1307
+ export const readItemContent = (decoder, info, id) => contentRefs[info & binary.BITS4](decoder, id) // YP27: four bits
1143
1308
 
1144
1309
  /**
1145
1310
  * @param {UpdateDecoder} decoder
@@ -8,6 +8,7 @@ import { onInput } from './events/onInput.js';
8
8
  import { onPaste } from './events/onPaste.js';
9
9
  import { onTextDragEnd, onTextDragStart, preventUnsupportedDrop } from './events/onDrop.js';
10
10
  import { attachFocus, selectionIsInside, takeKeys } from './events/onFocus.js';
11
+ import { attachPageHide } from './events/onPageHide.js';
11
12
  import { insertLineBreak, runIntent } from './events/beforeInputCommands.js';
12
13
  import { textPointAt } from './selection/selection.utils.js';
13
14
  import { Attempts } from './session/attempt.js';
@@ -794,7 +795,9 @@ export class Edytor {
794
795
  keydown(event);
795
796
  }),
796
797
  // Pointer and focus ownership, and the focus-time caret restore.
797
- ...attachFocus(this, node), on(node, 'mousedown', this.selection.pointer.preventTripleClick), on(node, 'click', this.selection.pointer.tripleClick),
798
+ ...attachFocus(this, node),
799
+ // The page leaving for good ends the session's history.
800
+ attachPageHide(this), on(node, 'mousedown', this.selection.pointer.preventTripleClick), on(node, 'click', this.selection.pointer.tripleClick),
798
801
  // Settle queued mutation repairs before the native menu opens —
799
802
  // spellcheck suggestions are computed against the DOM at this
800
803
  // moment (PM flushes its DOM observer on contextmenu for the
@@ -209,14 +209,14 @@ const splitKeepingChildren = (edytor, [id, offset, born, tail], child) => {
209
209
  return REFUSED;
210
210
  if (!child)
211
211
  return facade.compose({ ...split, writes: [step] });
212
- // The rank of a first child, and the document's answer on placing the kind there.
212
+ // The document's answer on placing the kind as a first child.
213
213
  const slot = facade.prepare.insertBlocks({ parent: id, index: 0 }, [
214
214
  { id: born, type: tail?.type ?? facade.defaultChild(id) }
215
215
  ]);
216
216
  const first = 'writes' in slot ? slot.writes[0] : undefined;
217
217
  if (first?.op !== 'insertBlocks')
218
218
  return REFUSED;
219
- const placed = { ...step, parent: id, rank: first.ranks[0] };
219
+ const placed = { ...step, parent: id, index: 0 };
220
220
  return facade.compose({ ...split, writes: [placed] });
221
221
  };
222
222
  /**
@@ -0,0 +1,3 @@
1
+ import type { Edytor } from '../edytor.svelte.js';
2
+ /** End the view's session when its page leaves for good; answers the teardown. */
3
+ export declare const attachPageHide: (edytor: Edytor) => (() => void);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The page leaves for good (installed by `Edytor.attach`): a `pagehide` the
3
+ * back/forward cache keeps nothing of. The view tells its document the
4
+ * session's history ended (`document.endHistory()`, `hist.release.end`): the
5
+ * history's rule releases what the session deleted. A page the cache keeps
6
+ * may come back with its history, and a lost connection fires nothing here:
7
+ * both keep it.
8
+ *
9
+ * The page's lifecycle is the sync layer's (`onPageLeave`, which the
10
+ * providers leave by too): the view subscribes to its `write` phase, run
11
+ * before the providers' teardown whatever order they subscribed in, so the
12
+ * release goes out on a socket or store that is still open.
13
+ */
14
+ import { onPageLeave } from '../crdt/providers/room.js';
15
+ /** End the view's session when its page leaves for good; answers the teardown. */
16
+ export const attachPageHide = (edytor) => onPageLeave(() => edytor.document.endHistory(), 'write');