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
@@ -13,15 +13,21 @@
13
13
  * - It ends at the next delimiting boundary of the same text, or at the text
14
14
  * end. A boundary whose nonce matches no record is inert: skipped by reads,
15
15
  * never a delimiter.
16
- * - `display(b)` = `stream(b)`, then `display(m)` for each merge claim `{m}`
17
- * on `b`'s `claims` list whose max-stamp live claimer is `b`. Owner
18
- * resolution over claims (`claimGraph`: max-stamp claim, cycles, claims held
19
- * by deleted blocks are inert) is unchanged.
16
+ * - A merge writes one CLAIM ITEM `{m}` (one countable unit, never
17
+ * displayed) at the end of the gap at the end of the holder's stream (fork
18
+ * patch YP21: among the items of one origin it sorts after content and
19
+ * boundaries, so YATA decides which piece of a concurrent split keeps it).
20
+ * A block's claims are the claim items of its stream, in text order.
21
+ * - `display(b)` = `stream(b)`, then `display(m)` for each claim `{m}` of `b`
22
+ * whose max-stamp live claimer is `b`. Owner resolution over claims
23
+ * (`claimGraph`: max-stamp claim, cycles, claims held by deleted blocks are
24
+ * inert) is unchanged.
20
25
  *
21
26
  * Every position therefore lies in exactly one stream: the block that displays
22
27
  * it is a pure function of replicated items and is never repaired. Typing at a
23
28
  * stream's start inserts after its boundary, at its end before the next one;
24
- * the per-stream delete never removes a boundary; every read skips them.
29
+ * the per-stream delete never removes a boundary or a claim; every read skips
30
+ * them.
25
31
  *
26
32
  * Anchors are `{b, a}`: `b` the home block of the backing text, `a` an
27
33
  * engine relative position (`i` the bound item, `a` the side: `< 0` left). A
@@ -36,9 +42,9 @@
36
42
  */
37
43
  import type { EngineApi, EngineDoc, EngineNode } from '../engine-api.js';
38
44
  import { type IdSetOf } from '../structs.js';
39
- export { isBoundary, nodeStart } from './items.js';
45
+ export { isBoundary, isClaimItem, isRowMark, nodeStart } from './items.js';
40
46
  export { canonKey, deepFreeze, inlineItemOf, protectItems, readRange } from './ranges.js';
41
- export { byId, delimiters, displayOf, locate, ownedLength, pieces, placeText, scanText } from './streams.js';
47
+ export { byId, claimsIn, delimiters, displayOf, locate, ownedLength, pieces, placeText, removedKeys, scanText } from './streams.js';
42
48
  export type BlockId = string;
43
49
  /** A serialized relative position inside a backing text (`i: null`: a text end). */
44
50
  export type Anchor = {
@@ -48,26 +54,15 @@ export type Anchor = {
48
54
  } | null;
49
55
  a: number;
50
56
  };
51
- /** An engine item id as a claim payload stores it. */
52
- export type ItemRef = {
53
- c: number;
54
- k: number;
55
- };
56
57
  /**
57
- * Merge-claim payload: "this block's display continues with block `m`'s".
58
- * `a` and `r` (since 0.1.0-next.26, `merge.claim.anchor`) anchor it to the
59
- * end of the holder's stream as the claim's writer saw it: `a` the last
60
- * unit of that stream (its opening boundary when empty; absent at the start
61
- * of an own text) and `r` the item right after it (the next stream's
62
- * boundary; `null` at the text's end). A split the writer did not see moves
63
- * the claim to the piece that ends the region between them.
58
+ * A claim item's payload: "the display of the block whose stream holds me
59
+ * continues with block `m`'s" (`merge.claim.anchor`: YATA places it, so a
60
+ * split its writer did not see leaves it in the piece that ends the
61
+ * holder's region).
64
62
  */
65
63
  export type MergeClaim = {
66
64
  m: BlockId;
67
- a?: ItemRef;
68
- r?: ItemRef | null;
69
65
  };
70
- export declare const isMergeClaim: (v: unknown) => v is MergeClaim;
71
66
  /** Deterministic claim stamp = engine item id (client-major order). */
72
67
  export type Stamp = {
73
68
  c: number;
@@ -75,19 +70,13 @@ export type Stamp = {
75
70
  };
76
71
  export declare const cmpStamp: (a: Stamp, b: Stamp) => number;
77
72
  /**
78
- * One live merge claim: `seqIndex` is its live index in the list that
79
- * stores it — its holder's, or `holder`'s when its anchor moved it to
80
- * another block (`merge.claim.anchor`); `-1` for an implicit claim, which
81
- * no list stores (`id.same.concurrent`). `a`/`r`: its anchor (see
82
- * {@link MergeClaim}).
73
+ * One live merge claim: its stamp is its claim item's id; an implicit
74
+ * claim, which no item stores (`id.same.concurrent`), stamps below every
75
+ * item (`stamp.c < 0`).
83
76
  */
84
77
  export type Claim = {
85
78
  m: BlockId;
86
79
  stamp: Stamp;
87
- seqIndex: number;
88
- a?: ItemRef;
89
- r?: ItemRef | null;
90
- holder?: BlockId;
91
80
  };
92
81
  /** Boundary-item payload in a backing text. */
93
82
  export type Boundary = {
@@ -111,8 +100,6 @@ export type SeqItem = {
111
100
  type?: EngineNode;
112
101
  };
113
102
  };
114
- /** The live merge claims of a `claims` list, with their stamps. */
115
- export declare const readClaims: (node: EngineNode | undefined) => Claim[];
116
103
  /** One emitted range-read element — the `ContentItem` shape. */
117
104
  export type RangeItem = {
118
105
  kind: 'text';
@@ -164,16 +151,10 @@ export type TextBlockRec = {
164
151
  n: unknown;
165
152
  /** The block's own backing text, when it has one. */
166
153
  content: EngineNode | undefined;
167
- /** The `claims` list node (write handle). */
168
- claimsNode: EngineNode | undefined;
169
- /**
170
- * Its effective merge claims (`merge.claim.anchor`): its list's claims
171
- * that stay with it, in list order, then those other holders' anchors
172
- * moved to it (each with its `holder`), by stamp.
173
- */
154
+ /** Its merge claims: its implicit ones, then the claim items of its stream, in text order. */
174
155
  claims: Claim[];
175
- /** Its list's live claims, in list order (absent: `claims` is the list). */
176
- listClaims?: Claim[];
156
+ /** Its implicit claims (`id.same.concurrent`), which no item stores. */
157
+ implicit?: Claim[];
177
158
  };
178
159
  /** The internal "no display owner" verdict — a symbol, never a block id. */
179
160
  export declare const DEAD: unique symbol;
@@ -191,12 +172,23 @@ export declare const claimGraph: (blocks: ReadonlyMap<BlockId, TextBlockRec>) =>
191
172
  };
192
173
  /** `owner(b)` for every block (see {@link claimGraph}). */
193
174
  export declare const computeOwners: (blocks: ReadonlyMap<BlockId, TextBlockRec>) => Map<BlockId, Owner>;
194
- /** A live boundary at live index `at` of its text; `key` is its item id. */
195
- export type Bound = Boundary & {
175
+ /**
176
+ * A unit a row tracks at live index `at` of its text (`key`: its item id): a
177
+ * live boundary, or a live claim item (`m`, with its stamp), which never
178
+ * delimits.
179
+ */
180
+ export type Bound = {
196
181
  at: number;
197
182
  key: string;
198
- };
199
- /** One scanned backing text: its live boundaries in order and its live length. */
183
+ } & ((Boundary & {
184
+ m?: undefined;
185
+ }) | {
186
+ m: BlockId;
187
+ stamp: Stamp;
188
+ s?: undefined;
189
+ n?: undefined;
190
+ });
191
+ /** One scanned backing text: its live boundaries and claim items in order, and its live length. */
200
192
  export type TextRow = {
201
193
  home: BlockId;
202
194
  text: EngineNode;
@@ -204,7 +196,7 @@ export type TextRow = {
204
196
  len: number;
205
197
  key: string;
206
198
  };
207
- /** One stream: `[start, end)` of `home`'s text, minus the inert boundaries at `inert`. */
199
+ /** One stream: `[start, end)` of `home`'s text, minus the inert boundaries and claim items at `inert`. */
208
200
  export type Stream = {
209
201
  block: BlockId;
210
202
  home: BlockId;
@@ -232,6 +224,8 @@ export type Ownership = {
232
224
  hidden: (b: BlockId) => boolean;
233
225
  /** The claimed block's max-stamp live claimer. */
234
226
  top: (m: BlockId) => BlockId | undefined;
227
+ /** The position of `m` among `holder`'s claims (`-1`: none): the order the claimed blocks' children show in. */
228
+ claimIndex: (holder: BlockId, m: BlockId) => number;
235
229
  streamOf: (b: BlockId) => Stream | undefined;
236
230
  /** The streams of `home`'s text, in text order. */
237
231
  streamsIn: (home: BlockId) => readonly Stream[];
@@ -239,8 +233,6 @@ export type Ownership = {
239
233
  streamAt: (home: BlockId, i: number) => Stream | undefined;
240
234
  /** `display(b)` as boundary-free pieces in reading order; `null` when hidden or dead. */
241
235
  display: (b: BlockId) => Seg[] | null;
242
- /** `b`'s effective claims (`merge.claim.anchor`), when the context computed them. */
243
- claimsOf?: (b: BlockId) => readonly Claim[];
244
236
  };
245
237
  /** Construct a detached v14 node, viewed through the structural interface. */
246
238
  export declare const newNode: (name: string) => EngineNode;
@@ -257,6 +249,11 @@ export declare const openRangeCursor: (text: EngineNode) => RangeCursor;
257
249
  export declare const itemsOfRange: (text: EngineNode, i0: number, i1: number) => RangeItem[];
258
250
  /** The items of `segs`, one cursor per backing text. */
259
251
  export declare const readSegs: (segs: readonly Seg[], stats?: RangeReadStats) => RangeItem[];
252
+ /**
253
+ * A from-scratch ownership context over `given` (every text scanned; the
254
+ * index's oracle): each block's claims are its implicit ones, then the claim
255
+ * items of its stream.
256
+ */
260
257
  export declare const computeOwnership: (doc: EngineDoc, given: ReadonlyMap<BlockId, TextBlockRec>) => Ownership;
261
258
  /** `b`'s display pieces (`[]` when hidden or streamless and claimless). */
262
259
  export declare const flatten: (b: BlockId, blocks: unknown, own: Ownership) => Seg[];
@@ -271,7 +268,14 @@ export declare const blockTextOf: (b: BlockId, blocks: unknown, own: Ownership)
271
268
  * `marks` (an operation for each mark its gap gave it otherwise).
272
269
  */
273
270
  export declare const insertIntoText: (doc: EngineDoc, blocks: unknown, own: Ownership, b: BlockId, offset: number, payload: string | EngineNode, marks?: Record<string, unknown>) => void;
274
- /** The per-stream delete: each range lies inside one stream's pieces, so no boundary is ever removed. */
271
+ /**
272
+ * The per-stream delete: each range lies inside one stream's pieces, so no
273
+ * boundary is ever removed. The text is held by this writer's record (fork
274
+ * patch YP20, root `textdel`, in the history's scope): an undo deletes the
275
+ * record and the text shows again where it was, unless another writer's
276
+ * record holds it. Text the open undo step inserted is removed instead, so
277
+ * that step's undo still changes nothing.
278
+ */
275
279
  export declare const deleteRange: (doc: EngineDoc, blocks: unknown, own: Ownership, b: BlockId, offset: number, length: number) => void;
276
280
  export declare const formatRangeIn: (doc: EngineDoc, blocks: unknown, own: Ownership, b: BlockId, offset: number, length: number, formats: Record<string, unknown>) => void;
277
281
  /**
@@ -290,19 +294,25 @@ export declare const findAtom: (own: Ownership, b: BlockId, inlineId: string) =>
290
294
  } | null;
291
295
  /**
292
296
  * Split `b`'s display at `offset` for the new block `newId` (incarnation
293
- * `n`): one boundary through fork patch YP7 at the end of the gap, and the claims that
294
- * follow the split point — every claim of the stream's own block, then each
295
- * holder's claims after the one that led down — returned for re-insertion
296
- * on the new block and deleted from their holders. No text is copied.
297
+ * `n`): one boundary through fork patch YP7 at the end of the gap. The
298
+ * claims that follow the split point — every claim of the stream's own
299
+ * block, then each holder's claims after the one that led down — are
300
+ * written again, in that order, at the end of the new block's stream, and
301
+ * their items deleted (an implicit claim is written on the new block, whose
302
+ * claim outranks it). No text is copied.
297
303
  */
298
- export declare const splitAt: (blocks: ReadonlyMap<BlockId, TextBlockRec>, own: Ownership, b: BlockId, offset: number, newId: BlockId, n: number) => MergeClaim[];
304
+ export declare const splitAt: (doc: EngineDoc, blocks: ReadonlyMap<BlockId, TextBlockRec>, own: Ownership, b: BlockId, offset: number, newId: BlockId, n: number) => void;
299
305
  /**
300
- * Append a merge claim `{m: from}` to `into`'s claims list, anchored to
301
- * the end of `into`'s stream (`merge.claim.anchor`): its last unit (its
302
- * opening boundary when empty; none at an own text's start) and the item
303
- * after it.
306
+ * Merge `from` into `into`: one claim item `{m: from}` at the end of the gap
307
+ * at the end of `into`'s stream (fork patch YP7; YP21 sorts it after the
308
+ * content and boundaries a concurrent writer puts at its origin, so a split
309
+ * the merge's writer did not see leaves it in the piece that ends `into`'s
310
+ * region: `merge.claim.anchor`). `into` has a stream: the caller gives a
311
+ * streamless block its own text first.
304
312
  */
305
- export declare const claimInto: (blocks: ReadonlyMap<BlockId, TextBlockRec>, own: Pick<Ownership, "streamOf">, from: BlockId, into: BlockId) => void;
313
+ export declare const claimInto: (own: Pick<Ownership, "streamOf">, from: BlockId, into: BlockId) => void;
314
+ /** Delete the items of `claims` (an implicit claim has none). */
315
+ export declare const deleteClaims: (doc: EngineDoc, claims: readonly Claim[]) => void;
306
316
  /**
307
317
  * Give streamless block `rec` its own text and re-mint its nonce, so a late
308
318
  * copy of its old boundary is inert. Both writes carry a writer id
@@ -340,8 +350,8 @@ export declare const bindText: (_Y: EngineApi) => {
340
350
  at: number;
341
351
  node: EngineNode;
342
352
  } | null;
343
- splitAt: (blocks: ReadonlyMap<BlockId, TextBlockRec>, own: Ownership, b: BlockId, offset: number, newId: BlockId, n: number) => MergeClaim[];
344
- claimInto: (blocks: ReadonlyMap<BlockId, TextBlockRec>, own: Pick<Ownership, "streamOf">, from: BlockId, into: BlockId) => void;
353
+ splitAt: (doc: EngineDoc, blocks: ReadonlyMap<BlockId, TextBlockRec>, own: Ownership, b: BlockId, offset: number, newId: BlockId, n: number) => void;
354
+ claimInto: (own: Pick<Ownership, "streamOf">, from: BlockId, into: BlockId) => void;
345
355
  ownText: (doc: EngineDoc, rec: TextBlockRec) => {
346
356
  from: unknown;
347
357
  to: number;
@@ -353,4 +363,9 @@ export declare const bindText: (_Y: EngineApi) => {
353
363
  * typing and keeps the text every replica shares.
354
364
  */
355
365
  export declare const ownTextIds: WeakMap<EngineDoc, IdSetOf>;
366
+ /**
367
+ * Each history's open step on a doc (`doc/history.ts` registers them): what
368
+ * the step still capturing inserted, `null` when none is open.
369
+ */
370
+ export declare const openSteps: WeakMap<EngineDoc, Set<() => IdSetOf | null>>;
356
371
  export type TextEngine = ReturnType<typeof bindText>;
@@ -1,45 +1,16 @@
1
1
  import { Y } from '../engine.js';
2
2
  import { asYDoc, asYNode, attrItems, engineOps, newNode as makeNode, rangeCursorOf, storeOf } from '../structs.js';
3
- import { CONTENT, CONTENT_NODE, ID, NONCE, SCHEMA } from '../schema.js';
3
+ import { CONTENT, CONTENT_NODE, ID, NONCE, SCHEMA, TEXT_DELETES_ROOT } from '../schema.js';
4
4
  import { DEV } from 'esm-env';
5
5
  import { hash32, hash53 } from '../rand.js';
6
- import { bindDeletes } from './deletes.js';
7
6
  import { fitMarks, writeMarks } from './marks.js';
8
7
  import { nodeStart } from './items.js';
9
8
  import { readRange } from './ranges.js';
10
- import { delimiters, displayOf, locate, ownedLength, pieces, placeText, rangesOf, scanText } from './streams.js';
11
- export { isBoundary, nodeStart } from './items.js';
9
+ import { claimsIn, delimiters, displayOf, locate, ownedLength, pieces, placeText, rangesOf, removedKeys, scanText } from './streams.js';
10
+ export { isBoundary, isClaimItem, isRowMark, nodeStart } from './items.js';
12
11
  export { canonKey, deepFreeze, inlineItemOf, protectItems, readRange } from './ranges.js';
13
- export { byId, delimiters, displayOf, locate, ownedLength, pieces, placeText, scanText } from './streams.js';
14
- const isItemRef = (v) => v != null &&
15
- typeof v === 'object' &&
16
- Number.isInteger(v.c) &&
17
- Number.isInteger(v.k);
18
- export const isMergeClaim = (v) => v != null && typeof v === 'object' && typeof v.m === 'string';
12
+ export { byId, claimsIn, delimiters, displayOf, locate, ownedLength, pieces, placeText, removedKeys, scanText } from './streams.js';
19
13
  export const cmpStamp = (a, b) => a.c - b.c || a.k - b.k;
20
- /** The live merge claims of a `claims` list, with their stamps. */
21
- export const readClaims = (node) => {
22
- const out = [];
23
- let seqIndex = 0;
24
- for (let it = node ? nodeStart(node) : null; it !== null; it = it.right) {
25
- if (it.deleted || it.countable === false)
26
- continue;
27
- const arr = it.content.getContent();
28
- for (let j = 0; j < it.length; j++) {
29
- const p = arr[j];
30
- if (isMergeClaim(p)) {
31
- const claim = { m: p.m, stamp: { c: it.id.client, k: it.id.clock + j }, seqIndex };
32
- if (isItemRef(p.a)) {
33
- claim.a = p.a;
34
- claim.r = isItemRef(p.r) ? p.r : null;
35
- }
36
- out.push(claim);
37
- }
38
- seqIndex++;
39
- }
40
- }
41
- return out;
42
- };
43
14
  /** The internal "no display owner" verdict — a symbol, never a block id. */
44
15
  export const DEAD = Symbol('edytor.crdt.dead');
45
16
  /**
@@ -106,7 +77,6 @@ export const claimGraph = (blocks) => {
106
77
  };
107
78
  /** `owner(b)` for every block (see {@link claimGraph}). */
108
79
  export const computeOwners = (blocks) => claimGraph(blocks).owners;
109
- const D = bindDeletes(Y);
110
80
  const ops = engineOps(Y);
111
81
  /** Construct a detached v14 node, viewed through the structural interface. */
112
82
  export const newNode = (name) => makeNode(Y, name);
@@ -144,116 +114,44 @@ export const readSegs = (segs, stats) => {
144
114
  }
145
115
  return out;
146
116
  };
147
- /** A from-scratch ownership context over `blocks` (every text scanned). */
148
117
  /**
149
- * The effective claims of every block, from scratch (`merge.claim.anchor`;
150
- * the index keeps them incrementally, `text/runs.ts`): an anchored claim
151
- * goes to the block of the segment just before its `r` (the text's last
152
- * segment when `r` is null) in the row of its holder's stream, when that
153
- * segment is the holder's or a later one.
118
+ * A from-scratch ownership context over `given` (every text scanned; the
119
+ * index's oracle): each block's claims are its implicit ones, then the claim
120
+ * items of its stream.
154
121
  */
155
- const effectiveClaims = (doc, blocks, rows, delim, streams) => {
156
- /** Live index of unit `ref` (assoc 0) in `text`, or `null` when it is not `text`'s. */
157
- const indexIn = (text, ref) => {
158
- if (!text._item?.id)
159
- return null;
160
- const abs = Y.indexOfId(asYDoc(doc), Y.createID(ref.c, ref.k));
161
- return abs === null || abs.type !== text ? null : abs.index;
162
- };
163
- const targetOf = (holder, c) => {
164
- if (c.a === undefined)
165
- return holder;
166
- const s = streams.get(holder);
167
- const row = s && rows.get(s.text);
168
- if (row === undefined || indexIn(row.text, c.a) === null)
169
- return holder;
170
- const cuts = [];
171
- row.bounds.forEach((b, j) => {
172
- if (delim.get(b.s) === b.key)
173
- cuts.push(j);
174
- });
175
- const head = delim.has(row.home) ? null : row.home;
176
- // The gap before `r`: a live boundary's own index, else the boundaries before it.
177
- let gap = row.bounds.length;
178
- if (c.r != null) {
179
- const key = `${c.r.c}:${c.r.k}`;
180
- const at = row.bounds.findIndex((b) => b.key === key);
181
- if (at >= 0)
182
- gap = at;
183
- else {
184
- const p = indexIn(row.text, c.r);
185
- if (p === null)
186
- return holder;
187
- gap = row.bounds.filter((b) => b.at < p).length;
188
- }
189
- }
190
- const segment = (g) => cuts.filter((j) => j < g).length;
191
- const k = segment(gap);
192
- // The holder's own segment: the one its stream starts in.
193
- const k0 = segment(row.bounds.filter((b) => b.at < s.start).length);
194
- if (k < k0)
195
- return holder;
196
- return (k === 0 ? head : row.bounds[cuts[k - 1]].s) ?? holder;
197
- };
198
- const own = new Map();
199
- const moved = new Map();
200
- for (const [h, rec] of blocks) {
201
- const stays = [];
202
- for (const c of rec.listClaims ?? rec.claims) {
203
- const t = targetOf(h, c);
204
- if (t === h)
205
- stays.push(c);
206
- else {
207
- let into = moved.get(t);
208
- if (into === undefined)
209
- moved.set(t, (into = []));
210
- into.push({ ...c, holder: h });
211
- }
212
- }
213
- own.set(h, stays);
214
- }
215
- for (const [t, list] of moved) {
216
- if (!own.has(t))
217
- continue;
218
- list.sort((x, y) => cmpStamp(x.stamp, y.stamp));
219
- own.set(t, [...own.get(t), ...list]);
220
- }
221
- return own;
222
- };
223
122
  export const computeOwnership = (doc, given) => {
224
123
  const rows = [];
225
124
  for (const rec of given.values())
226
125
  if (rec.content)
227
126
  rows.push(scanText(rec.id, rec.content));
228
- const delim = delimiters(given, rows);
127
+ const delim = delimiters(given, rows, removedKeys(doc.get(SCHEMA.roots.registry)));
229
128
  const streams = new Map();
230
129
  const inText = new Map();
130
+ const rowOf = new Map();
231
131
  for (const row of rows) {
232
- const list = placeText(row, delim);
132
+ const list = placeText(row, delim, (s) => !given.has(s));
233
133
  inText.set(row.home, list);
134
+ rowOf.set(row.text, row);
234
135
  for (const s of list)
235
136
  streams.set(s.block, s);
236
137
  }
237
- // The records with their effective claims (`merge.claim.anchor`).
238
- const effective = effectiveClaims(doc, given, new Map(rows.map((row) => [row.text, row])), delim, streams);
239
138
  const blocks = new Map();
240
- for (const [id, rec] of given)
241
- blocks.set(id, {
242
- ...rec,
243
- listClaims: rec.listClaims ?? rec.claims,
244
- claims: effective.get(id) ?? rec.claims
245
- });
139
+ for (const [id, rec] of given) {
140
+ const s = streams.get(id);
141
+ const items = s === undefined ? [] : claimsIn(rowOf.get(s.text), s);
142
+ blocks.set(id, { ...rec, claims: [...(rec.implicit ?? []), ...items] });
143
+ }
246
144
  const { owners, top } = claimGraph(blocks);
247
145
  const ownerOf = (b) => owners.get(b) ?? DEAD;
248
146
  const own = {
249
147
  ownerOf,
250
148
  hidden: (b) => ownerOf(b) !== b,
251
149
  top: (m) => top.get(m),
150
+ claimIndex: (holder, m) => blocks.get(holder)?.claims.findIndex((c) => c.m === m) ?? -1,
252
151
  streamOf: (b) => streams.get(b),
253
152
  streamsIn: (home) => inText.get(home) ?? [],
254
153
  streamAt: (home, i) => (inText.get(home) ?? []).find((x) => x.start <= i && i <= x.end),
255
- display: (b) => displayOf(b, blocks, own),
256
- claimsOf: (b) => blocks.get(b)?.claims ?? []
154
+ display: (b) => displayOf(b, blocks, own)
257
155
  };
258
156
  return own;
259
157
  };
@@ -288,16 +186,19 @@ export const insertIntoText = (doc, blocks, own, b, offset, payload, marks) => {
288
186
  const have = at.find((p) => !p.deleted && p.len > 0)?.formats;
289
187
  fitMarks(doc, text, hit.idx, payload.length, have, marks);
290
188
  };
291
- /** The per-stream delete: each range lies inside one stream's pieces, so no boundary is ever removed. */
189
+ /**
190
+ * The per-stream delete: each range lies inside one stream's pieces, so no
191
+ * boundary is ever removed. The text is held by this writer's record (fork
192
+ * patch YP20, root `textdel`, in the history's scope): an undo deletes the
193
+ * record and the text shows again where it was, unless another writer's
194
+ * record holds it. Text the open undo step inserted is removed instead, so
195
+ * that step's undo still changes nothing.
196
+ */
292
197
  export const deleteRange = (doc, blocks, own, b, offset, length) => {
293
- const spans = [];
294
- for (const r of rangesOf(flatten(b, blocks, own), offset, offset + length)) {
295
- for (const p of openRangeCursor(r.text).read(r.a, r.b))
296
- if (!p.deleted && p.len > 0 && (p.content.str !== undefined || 'type' in p.content))
297
- spans.push({ c: p.id.client, k: p.id.clock, n: p.len });
298
- r.text.delete(r.a, r.b - r.a);
299
- }
300
- D.markDeleted(doc, spans);
198
+ const records = doc.get(TEXT_DELETES_ROOT);
199
+ const fresh = openInserts(doc);
200
+ for (const r of rangesOf(flatten(b, blocks, own), offset, offset + length))
201
+ r.text.delete(r.a, r.b - r.a, records, fresh);
301
202
  };
302
203
  export const formatRangeIn = (doc, blocks, own, b, offset, length, formats) => {
303
204
  for (const r of rangesOf(flatten(b, blocks, own), offset, offset + length))
@@ -337,60 +238,52 @@ export const findAtom = (own, b, inlineId) => {
337
238
  };
338
239
  /**
339
240
  * Split `b`'s display at `offset` for the new block `newId` (incarnation
340
- * `n`): one boundary through fork patch YP7 at the end of the gap, and the claims that
341
- * follow the split point — every claim of the stream's own block, then each
342
- * holder's claims after the one that led down — returned for re-insertion
343
- * on the new block and deleted from their holders. No text is copied.
241
+ * `n`): one boundary through fork patch YP7 at the end of the gap. The
242
+ * claims that follow the split point — every claim of the stream's own
243
+ * block, then each holder's claims after the one that led down — are
244
+ * written again, in that order, at the end of the new block's stream, and
245
+ * their items deleted (an implicit claim is written on the new block, whose
246
+ * claim outranks it). No text is copied.
344
247
  */
345
- export const splitAt = (blocks, own, b, offset, newId, n) => {
248
+ export const splitAt = (doc, blocks, own, b, offset, newId, n) => {
346
249
  const segs = flatten(b, blocks, own);
347
250
  const hit = locate(segs, Math.max(0, Math.min(offset, ownedLength(segs))));
348
251
  if (hit === null)
349
252
  throw new Error(`splitAt: "${b}" has no stream`);
350
253
  const { seg, idx } = hit;
351
- const moved = [];
352
- // A claim another block stores (its anchor moved it here) is deleted from that list.
353
- const after = (holder, from) => blocks
354
- .get(holder)
355
- .claims.slice(from)
356
- .forEach((c) => moved.push({ holder: c.holder ?? holder, claim: c }));
357
- after(seg.block, 0);
254
+ const moved = [...blocks.get(seg.block).claims];
358
255
  for (let i = seg.path.length - 1; i >= 0; i--)
359
- after(seg.path[i].holder, seg.path[i].entry + 1);
256
+ moved.push(...blocks.get(seg.path[i].holder).claims.slice(seg.path[i].entry + 1));
360
257
  const text = seg.text;
258
+ // The new block's stream ends where the source's did, one boundary later.
259
+ const end = own.streamOf(seg.block).end + 1;
361
260
  text.insertAtGapEnd(idx, [{ s: newId, n }]);
362
- const byHolder = new Map();
363
- // An implicit claim (a losing incarnation) has no list entry: it is
364
- // only written, explicitly, on the new block, whose claim outranks it.
365
- for (const { holder, claim } of moved)
366
- if (claim.seqIndex >= 0)
367
- byHolder.set(holder, [...(byHolder.get(holder) ?? []), claim.seqIndex]);
368
- for (const [holder, idxs] of byHolder) {
369
- const list = blocks.get(holder).claimsNode;
370
- for (const i of idxs.sort((x, y) => y - x))
371
- list.delete(i, 1);
372
- }
373
- // Re-inserted with their anchors: the region they end is the new block's now.
374
- return moved.map(({ claim: c }) => c.a === undefined ? { m: c.m } : { m: c.m, a: c.a, r: c.r ?? null });
261
+ if (moved.length === 0)
262
+ return;
263
+ text.insertAtGapEnd(end, moved.map((c) => ({ m: c.m })));
264
+ deleteClaims(doc, moved);
375
265
  };
376
266
  /**
377
- * Append a merge claim `{m: from}` to `into`'s claims list, anchored to
378
- * the end of `into`'s stream (`merge.claim.anchor`): its last unit (its
379
- * opening boundary when empty; none at an own text's start) and the item
380
- * after it.
267
+ * Merge `from` into `into`: one claim item `{m: from}` at the end of the gap
268
+ * at the end of `into`'s stream (fork patch YP7; YP21 sorts it after the
269
+ * content and boundaries a concurrent writer puts at its origin, so a split
270
+ * the merge's writer did not see leaves it in the piece that ends `into`'s
271
+ * region: `merge.claim.anchor`). `into` has a stream: the caller gives a
272
+ * streamless block its own text first.
381
273
  */
382
- export const claimInto = (blocks, own, from, into) => {
383
- const list = blocks.get(into).claimsNode;
384
- const claim = { m: from };
274
+ export const claimInto = (own, from, into) => {
385
275
  const s = own.streamOf(into);
386
- if (s !== undefined) {
387
- const a = anchorAt(s.text, s.end, -1).i;
388
- if (a !== null) {
389
- claim.a = a;
390
- claim.r = anchorAt(s.text, s.end, 0).i;
391
- }
276
+ s.text.insertAtGapEnd(s.end, [{ m: from }]);
277
+ };
278
+ /** Delete the items of `claims` (an implicit claim has none). */
279
+ export const deleteClaims = (doc, claims) => {
280
+ const store = storeOf(doc);
281
+ for (const { stamp } of claims) {
282
+ if (stamp.c < 0 || store.getItem({ client: stamp.c, clock: stamp.k }).deleted)
283
+ continue;
284
+ const at = Y.indexOfId(asYDoc(doc), Y.createID(stamp.c, stamp.k));
285
+ at.type.delete(at.index, 1);
392
286
  }
393
- list.insert(list.length, [claim]);
394
287
  };
395
288
  /**
396
289
  * Give streamless block `rec` its own text and re-mint its nonce, so a late
@@ -467,3 +360,25 @@ export const bindText = (_Y) => ({
467
360
  * typing and keeps the text every replica shares.
468
361
  */
469
362
  export const ownTextIds = new WeakMap();
363
+ /**
364
+ * Each history's open step on a doc (`doc/history.ts` registers them): what
365
+ * the step still capturing inserted, `null` when none is open.
366
+ */
367
+ export const openSteps = new WeakMap();
368
+ /**
369
+ * What this writer inserted in the open undo steps of `doc` (`null`: none):
370
+ * a history capturing a peer's frames (the room's validation) opens steps of
371
+ * that peer's writes, never fresh here.
372
+ */
373
+ const openInserts = (doc) => {
374
+ let fresh = null;
375
+ for (const open of openSteps.get(doc) ?? []) {
376
+ const ranges = open()?.clients.get(doc.clientID);
377
+ if (ranges === undefined)
378
+ continue;
379
+ fresh ??= ops.idSet();
380
+ for (const r of ranges.getIds())
381
+ fresh.add(doc.clientID, r.clock, r.len);
382
+ }
383
+ return fresh;
384
+ };
@@ -15,6 +15,6 @@ export declare const inlineItemOf: (entry: unknown) => RangeItem;
15
15
  /**
16
16
  * Read `[i0, i1)` through the cursor: text pieces as UTF-16 slices under their
17
17
  * folded marks (adjacent equal marks merge), inline atoms one item each.
18
- * Boundary items are skipped — the one read predicate stream ownership needs.
18
+ * Boundary and claim items are skipped — the one read predicate stream ownership needs.
19
19
  */
20
20
  export declare const readRange: (cur: RangeCursor, i0: number, i1: number, stats?: RangeReadStats, items?: RangeItem[]) => RangeItem[];
@@ -1,6 +1,6 @@
1
1
  import { ID, TYPE } from '../schema.js';
2
2
  import { readData } from '../data.js';
3
- import { isBoundary } from './items.js';
3
+ import { isRowMark } from './items.js';
4
4
  /** Canonical JSON key (sorted keys, recursive) — mark-set equality/interning. */
5
5
  export const canonKey = (v) => {
6
6
  if (v === null || typeof v !== 'object')
@@ -52,7 +52,7 @@ export const inlineItemOf = (entry) => {
52
52
  /**
53
53
  * Read `[i0, i1)` through the cursor: text pieces as UTF-16 slices under their
54
54
  * folded marks (adjacent equal marks merge), inline atoms one item each.
55
- * Boundary items are skipped — the one read predicate stream ownership needs.
55
+ * Boundary and claim items are skipped — the one read predicate stream ownership needs.
56
56
  */
57
57
  export const readRange = (cur, i0, i1, stats, items = []) => {
58
58
  for (const piece of cur.read(i0, i1, stats)) {
@@ -73,7 +73,7 @@ export const readRange = (cur, i0, i1, stats, items = []) => {
73
73
  else if (typeof c.getContent === 'function') {
74
74
  const arr = c.getContent();
75
75
  for (let k = piece.offset; k < piece.offset + piece.len; k++) {
76
- if (!isBoundary(arr[k]))
76
+ if (!isRowMark(arr[k]))
77
77
  items.push(inlineItemOf(arr[k]));
78
78
  }
79
79
  }