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
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Slots and move records — where a block or an array item stands (fork
3
+ * patch YP22).
4
+ *
5
+ * Every data owner keeps ONE order sequence, its node's list part (a block's
6
+ * node, an atom's node, the registry root for the top-level blocks, the
7
+ * `docdata` root for the document's data). The sequence holds one countable
8
+ * unit per thing it orders:
9
+ *
10
+ * - a block's **home slot** `{ b: id }`, written once at the block's creation
11
+ * in its parent's sequence, right after its left sibling's unit (YATA's
12
+ * origin rule: later inserts there stay together, `order.insert.run`);
13
+ * - an array item's **slot** `{ p: path, i: id }` (`crdt/data.ts`): the
14
+ * item exists while its slot does;
15
+ * - a **move record** (the engine's `ContentMove`): one unit at the
16
+ * destination naming the units it moves, in order. Each named unit shows
17
+ * at the record that wins its movers — the greatest `(priority, client,
18
+ * clock)` — and reads as deleted at home, so a sequence walk sees it gone
19
+ * from there. Deleting a record (an undo) hands its units to the next
20
+ * record, else home.
21
+ *
22
+ * A reader of a sequence (`entriesOf`) walks its live items: a slot at home,
23
+ * or a record expanded to the units it shows. A writer places a new slot or a
24
+ * record after a displayed entry (`placeAfter`): when that entry is a unit in
25
+ * the middle of a record's display, the new record also carries the record's
26
+ * later units, so the new thing shows between them.
27
+ *
28
+ * The engine is read through `engine.js` (structural types, `engine-api.ts`).
29
+ */
30
+ import type { EngineDoc, EngineNode } from '../engine-api.js';
31
+ import { type StoreStruct } from '../structs.js';
32
+ export type BlockId = string;
33
+ /**
34
+ * A block's home slot in its parent's sequence; `s`, when the block was made
35
+ * by a split (Enter, a paste of lines): the id (`client, clock`) of the
36
+ * split's boundary item in the text, the engine's sort key among the slots
37
+ * of one origin (the boundaries' order in the text, so two peers' splits of
38
+ * one block keep its text's order, `order.split.text`).
39
+ */
40
+ export type BlockSlot = {
41
+ b: BlockId;
42
+ s?: [client: number, clock: number];
43
+ };
44
+ /** An array item's slot: the array's path (`crdt/data.ts`) and the item's id. */
45
+ export type DataSlot = {
46
+ p: string;
47
+ i: string;
48
+ };
49
+ /** One unit of a sequence: its engine id. */
50
+ export type Unit = {
51
+ c: number;
52
+ k: number;
53
+ };
54
+ export declare const isBlockSlot: (v: unknown) => v is BlockSlot;
55
+ export declare const isDataSlot: (v: unknown) => v is DataSlot;
56
+ /** A sequence item as the slot reads walk it. */
57
+ export type SeqItem = StoreStruct & {
58
+ left: SeqItem | null;
59
+ right: SeqItem | null;
60
+ content: {
61
+ arr?: unknown[];
62
+ priority?: number;
63
+ side?: number;
64
+ part?: number;
65
+ at?: unknown;
66
+ ids?: number[];
67
+ };
68
+ /** The record that shows this unit (fork patch YP22), `null` at home. */
69
+ mover?: SeqItem | null;
70
+ movers?: SeqItem[] | null;
71
+ /** Removed or held: shown nowhere. */
72
+ gone?: boolean;
73
+ };
74
+ /** Whether `it` is a move record. */
75
+ export declare const isRecord: (it: SeqItem) => boolean;
76
+ /** A record's stamp, the order its wins follow: `(priority, client, clock)`. */
77
+ export type Stamp = readonly [priority: number, client: number, clock: number];
78
+ export declare const stampOf: (r: SeqItem) => Stamp;
79
+ export declare const cmpStamp: (a: Stamp, b: Stamp) => number;
80
+ /** One displayed entry of a sequence: its value, its unit, and the record it shows through (`null`: at home; `at`: its place among the units read with it). */
81
+ export type Entry = {
82
+ value: unknown;
83
+ unit: Unit;
84
+ via: SeqItem | null;
85
+ at: number;
86
+ };
87
+ /** Whether `a` and `b` are one unit. */
88
+ export declare const sameUnit: (a: Unit, b: Unit) => boolean;
89
+ /** The item holding unit `u`, or `null` when `doc` lacks it (no value read: the fold's hot path). */
90
+ export declare const itemAt: (doc: EngineDoc, u: Unit) => SeqItem | null;
91
+ /** The item holding unit `u` and the value at it, or `null` when `doc` lacks it (a GC struct, not arrived). */
92
+ export declare const unitOf: (doc: EngineDoc, u: Unit) => {
93
+ item: SeqItem;
94
+ value: unknown;
95
+ } | null;
96
+ /**
97
+ * The units item `it` can show, in order, those `keep` takes: a live
98
+ * record's named units (present, not gone; `via` the record), a slot item's
99
+ * units, moved away or not (none when it is gone; `via` `null`). THE walk
100
+ * every reader of a sequence's units takes: the engine's (`shownBy`), the
101
+ * index's places (`placement/resolve.ts`) and the writers' view of them
102
+ * (`placement/model.ts`).
103
+ */
104
+ export declare const unitsOf: (doc: EngineDoc, it: SeqItem, keep: (item: SeqItem, value: unknown, unit: Unit) => boolean) => Entry[];
105
+ /** The units the engine shows at item `it`, in order: a live record's, those it wins; a live slot item's, all. */
106
+ export declare const shownBy: (doc: EngineDoc, it: SeqItem) => Entry[];
107
+ /** The displayed entries of `node`'s sequence, in order. */
108
+ export declare const entriesOf: (doc: EngineDoc, node: EngineNode) => Entry[];
109
+ /**
110
+ * The entry unit `home` displays as: at home when no live record wins it,
111
+ * else at its mover (`via`, `at`); `null` when it shows nowhere (gone).
112
+ */
113
+ export declare const anchorOf: (doc: EngineDoc, home: Unit) => Entry | null;
114
+ /** The sequence node that holds `e`'s displaying item (its record's, or its slot's). */
115
+ export declare const nodeOf: (doc: EngineDoc, e: Entry) => EngineNode;
116
+ /** `units` as `client, clock, len` triples (one unit each). */
117
+ export declare const triples: (units: readonly Unit[]) => number[];
118
+ /**
119
+ * Write new slots (`payloads`) and/or a move of `moved` (unit triples) into
120
+ * `node`'s sequence right after the displayed entry `left` (at the start when
121
+ * `null`; `right` the entry that follows, if any). When `left` is a unit in
122
+ * the middle of a record's display, the record written here carries that
123
+ * record's later units too, so the new entries show between them. `side` is
124
+ * the record's class among the items of its origin (`0` after, `1` before);
125
+ * `chain` the units the record stands at for the engine's sort of two
126
+ * records at one origin, given its first unit — the home slots from the root
127
+ * sequence down to the unit it stands at (`null`: the first unit alone) — and
128
+ * `part` its order among the records standing there. `shown` reads the units
129
+ * a record shows, as the reader sees them (the index's: a unit a cycle sent
130
+ * back elsewhere is not carried).
131
+ */
132
+ export declare const placeAfter: (doc: EngineDoc, node: EngineNode, left: Entry | null, right: Entry | null, payloads: readonly unknown[] | null, moved?: readonly number[] | null, side?: number, part?: number, chain?: ((first: Unit) => readonly Unit[]) | null, shown?: (r: SeqItem) => Entry[]) => void;
133
+ /** Remove unit `u` from its sequence (the engine's delete: it shows nowhere any more). */
134
+ export declare const removeUnit: (doc: EngineDoc, u: Unit) => void;
@@ -0,0 +1,152 @@
1
+ import { Y } from '../engine.js';
2
+ import { asYDoc, clientsOf, engineOps, firstItem, storeOf, structAt } from '../structs.js';
3
+ export const isBlockSlot = (v) => v != null && typeof v === 'object' && typeof v.b === 'string';
4
+ export const isDataSlot = (v) => v != null &&
5
+ typeof v === 'object' &&
6
+ typeof v.p === 'string' &&
7
+ typeof v.i === 'string';
8
+ /** Whether `it` is a move record. */
9
+ export const isRecord = (it) => it.content.priority !== undefined && it.content.ids !== undefined;
10
+ export const stampOf = (r) => [r.content.priority, r.id.client, r.id.clock];
11
+ export const cmpStamp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
12
+ /** Whether `a` and `b` are one unit. */
13
+ export const sameUnit = (a, b) => a.c === b.c && a.k === b.k;
14
+ const ops = engineOps(Y);
15
+ /** The item holding unit `u`, or `null` when `doc` lacks it (no value read: the fold's hot path). */
16
+ export const itemAt = (doc, u) => {
17
+ const structs = clientsOf(doc).get(u.c);
18
+ if (structs === undefined || u.k >= storeOf(doc).getClock(u.c))
19
+ return null;
20
+ const item = structAt(Y, structs, u.k);
21
+ return item === null || !ops.isItem(item) ? null : item;
22
+ };
23
+ /** The item holding unit `u` and the value at it, or `null` when `doc` lacks it (a GC struct, not arrived). */
24
+ export const unitOf = (doc, u) => {
25
+ const item = itemAt(doc, u);
26
+ return item === null ? null : { item, value: item.content.arr?.[u.k - item.id.clock] };
27
+ };
28
+ /**
29
+ * The units item `it` can show, in order, those `keep` takes: a live
30
+ * record's named units (present, not gone; `via` the record), a slot item's
31
+ * units, moved away or not (none when it is gone; `via` `null`). THE walk
32
+ * every reader of a sequence's units takes: the engine's (`shownBy`), the
33
+ * index's places (`placement/resolve.ts`) and the writers' view of them
34
+ * (`placement/model.ts`).
35
+ */
36
+ export const unitsOf = (doc, it, keep) => {
37
+ const out = [];
38
+ const add = (item, value, unit, via) => {
39
+ if (item.gone !== true && keep(item, value, unit))
40
+ out.push({ value, unit, via, at: out.length });
41
+ };
42
+ if (!isRecord(it))
43
+ it.content.arr?.forEach((value, j) => add(it, value, { c: it.id.client, k: it.id.clock + j }, null));
44
+ else if (!it.deleted) {
45
+ const ids = it.content.ids;
46
+ for (let i = 0; i < ids.length; i += 3)
47
+ for (let k = 0; k < ids[i + 2]; k++) {
48
+ const unit = { c: ids[i], k: ids[i + 1] + k };
49
+ const u = unitOf(doc, unit);
50
+ if (u !== null)
51
+ add(u.item, u.value, unit, it);
52
+ }
53
+ }
54
+ return out;
55
+ };
56
+ /** The units the engine shows at item `it`, in order: a live record's, those it wins; a live slot item's, all. */
57
+ export const shownBy = (doc, it) => {
58
+ const via = isRecord(it) ? it : null;
59
+ return it.deleted ? [] : unitsOf(doc, it, (item) => item.mover === via);
60
+ };
61
+ /** The displayed entries of `node`'s sequence, in order. */
62
+ export const entriesOf = (doc, node) => {
63
+ const out = [];
64
+ for (let it = firstItem(node); it !== null; it = it.right)
65
+ for (const e of shownBy(doc, it))
66
+ out.push(e);
67
+ return out;
68
+ };
69
+ /**
70
+ * The entry unit `home` displays as: at home when no live record wins it,
71
+ * else at its mover (`via`, `at`); `null` when it shows nowhere (gone).
72
+ */
73
+ export const anchorOf = (doc, home) => {
74
+ const u = unitOf(doc, home);
75
+ if (u === null || u.item.gone === true)
76
+ return null;
77
+ const r = u.item.mover ?? null;
78
+ if (r === null || r.deleted)
79
+ return { value: u.value, unit: home, via: null, at: 0 };
80
+ return shownBy(doc, r).find((e) => sameUnit(e.unit, home)) ?? null;
81
+ };
82
+ /** The sequence node that holds `e`'s displaying item (its record's, or its slot's). */
83
+ export const nodeOf = (doc, e) => (e.via !== null ? e.via.parent : unitOf(doc, e.unit).item.parent);
84
+ /** The live index of the item that displays `e` (its record, or its slot unit). */
85
+ const liveIndexOf = (doc, e) => {
86
+ const id = e.via === null ? Y.createID(e.unit.c, e.unit.k) : Y.createID(e.via.id.client, e.via.id.clock);
87
+ return Y.indexOfId(asYDoc(doc), id).index;
88
+ };
89
+ /** `units` as `client, clock, len` triples (one unit each). */
90
+ export const triples = (units) => units.flatMap((u) => [u.c, u.k, 1]);
91
+ /**
92
+ * Write new slots (`payloads`) and/or a move of `moved` (unit triples) into
93
+ * `node`'s sequence right after the displayed entry `left` (at the start when
94
+ * `null`; `right` the entry that follows, if any). When `left` is a unit in
95
+ * the middle of a record's display, the record written here carries that
96
+ * record's later units too, so the new entries show between them. `side` is
97
+ * the record's class among the items of its origin (`0` after, `1` before);
98
+ * `chain` the units the record stands at for the engine's sort of two
99
+ * records at one origin, given its first unit — the home slots from the root
100
+ * sequence down to the unit it stands at (`null`: the first unit alone) — and
101
+ * `part` its order among the records standing there. `shown` reads the units
102
+ * a record shows, as the reader sees them (the index's: a unit a cycle sent
103
+ * back elsewhere is not carried).
104
+ */
105
+ export const placeAfter = (doc, node, left, right, payloads, moved = null, side = 0, part = 0, chain = null, shown = (r) => shownBy(doc, r)) => {
106
+ let index;
107
+ let carry = [];
108
+ if (left === null)
109
+ index = right === null ? node.length : liveIndexOf(doc, right);
110
+ else {
111
+ if (left.via !== null)
112
+ carry = shown(left.via)
113
+ .slice(left.at + 1)
114
+ .map((e) => e.unit);
115
+ // A unit shown at home though its item moved away (a cycle sent it back)
116
+ // is no live unit: the gap after it starts at its live index.
117
+ const live = left.via !== null || unitOf(doc, left.unit)?.item.deleted !== true;
118
+ index = liveIndexOf(doc, left) + (live ? 1 : 0);
119
+ }
120
+ const units = moved === null ? [] : [...moved];
121
+ if (payloads !== null && payloads.length > 0) {
122
+ // The new slots' first unit is this writer's next clock.
123
+ if (carry.length > 0)
124
+ units.push(doc.clientID, storeOf(doc).getClock(doc.clientID), payloads.length);
125
+ node.insert(index, payloads);
126
+ }
127
+ // A carried unit that moves here anyway is named once.
128
+ const named = new Set();
129
+ for (let i = 0; i < units.length; i += 3)
130
+ for (let k = 0; k < units[i + 2]; k++)
131
+ named.add(`${units[i]}:${units[i + 1] + k}`);
132
+ for (const u of carry)
133
+ if (!named.has(`${u.c}:${u.k}`))
134
+ units.push(u.c, u.k, 1);
135
+ if (units.length > 0 && (moved !== null || carry.length > 0)) {
136
+ const first = { c: units[0], k: units[1] };
137
+ const at = chain === null ? null : chain(first);
138
+ // A chain of the first unit alone is the engine's default: not written.
139
+ const own = at !== null && at.length === 1 && sameUnit(at[0], first);
140
+ node.move(index, units, side, part, at === null || own ? null : at.map((u) => Y.createID(u.c, u.k)));
141
+ }
142
+ };
143
+ /** Remove unit `u` from its sequence (the engine's delete: it shows nowhere any more). */
144
+ export const removeUnit = (doc, u) => {
145
+ Y.transact(asYDoc(doc), (tr) => {
146
+ const it = Y.getItemCleanStart(tr, Y.createID(u.c, u.k));
147
+ if (it.length > 1)
148
+ Y.getItemCleanStart(tr, Y.createID(u.c, u.k + 1));
149
+ if (!it.removed)
150
+ ops.remove(tr, it);
151
+ });
152
+ };
@@ -139,13 +139,27 @@ export type LifecycleHost = {
139
139
  _leave: () => void;
140
140
  emit?(event: 'failed', args: [unknown, unknown]): void;
141
141
  };
142
+ /** When a subscriber of {@link onPageLeave} runs: a session's last writes, then the transports. */
143
+ export type PageLeavePhase = 'write' | 'transport';
144
+ /**
145
+ * Be told when the page leaves for good: a `pagehide` of a page the
146
+ * back/forward cache keeps nothing of (a kept page may come back as it was),
147
+ * or, in Node, the process's `exit`. One listener for the page; its
148
+ * subscribers run in two phases whatever order they subscribed in — a
149
+ * session's last writes (`write`: a history's release, `hist.release.end`)
150
+ * before the transports' teardown (`transport`: each provider), so those
151
+ * writes go out while the sockets and stores are still open (listener order
152
+ * on the window is registration order in Chromium, capturing or not).
153
+ * Returns the unsubscribe.
154
+ */
155
+ export declare const onPageLeave: (fn: () => void, phase: PageLeavePhase) => (() => void);
142
156
  /**
143
157
  * Install the lifecycle on a provider: `hasSynced`, `whenSynced`, and the
144
158
  * departure announcement. `beforeunload` only announces it (`depart`): an
145
159
  * app's unsaved-changes prompt fires it and the user may stay, so the
146
160
  * provider keeps running and its presence returns at the next renewal.
147
- * Leaving the page (`pagehide` of a page not kept in the back/forward
148
- * cache; Node: `exit`) destroys the provider, whose teardown announces it.
161
+ * Leaving the page for good ({@link onPageLeave}'s `transport` phase)
162
+ * destroys the provider, whose teardown announces it.
149
163
  */
150
164
  export declare const initLifecycle: (host: LifecycleHost, destroy: () => unknown, depart?: () => void) => void;
151
165
  /** The first sync — lifetime. Returns `true` only the first time. */
@@ -45,6 +45,7 @@ import * as encoding from 'lib0-v14/encoding';
45
45
  import * as decoding from 'lib0-v14/decoding';
46
46
  import * as promise from 'lib0-v14/promise';
47
47
  import { applyAwarenessUpdate, encodeAwarenessUpdate } from '../protocols/awareness.js';
48
+ import { callEach } from '../protocols/observable.js';
48
49
  import { BARE_REPLY_LENGTH, frame, GENERATION, writeProtocolVersion } from '../protocols/envelope.js';
49
50
  import { checkSchema, SchemaMismatchError } from '../admission.js';
50
51
  import { asEngineDoc } from '../structs.js';
@@ -205,13 +206,68 @@ export const tabs = {
205
206
  };
206
207
  /** The outbound quarantine: a read-only document does not spread. */
207
208
  export const quarantined = (doc) => checkSchema(asEngineDoc(doc)) !== null;
209
+ /**
210
+ * The departures installed, by the page's `addEventListener` (one per realm
211
+ * in a browser; a harness simulating several pages in one realm gives each
212
+ * its own) or, in Node, by the process.
213
+ */
214
+ const departures = new WeakMap();
215
+ /** The page (Node: the process) leaves for good: the writes first, then the transports, each isolated. */
216
+ const leave = (departure) => {
217
+ for (const phase of ['write', 'transport'])
218
+ callEach(`[edytor] page leave (${phase})`, [...departure.leaving[phase]]);
219
+ };
220
+ /**
221
+ * Be told when the page leaves for good: a `pagehide` of a page the
222
+ * back/forward cache keeps nothing of (a kept page may come back as it was),
223
+ * or, in Node, the process's `exit`. One listener for the page; its
224
+ * subscribers run in two phases whatever order they subscribed in — a
225
+ * session's last writes (`write`: a history's release, `hist.release.end`)
226
+ * before the transports' teardown (`transport`: each provider), so those
227
+ * writes go out while the sockets and stores are still open (listener order
228
+ * on the window is registration order in Chromium, capturing or not).
229
+ * Returns the unsubscribe.
230
+ */
231
+ export const onPageLeave = (fn, phase) => {
232
+ const page = globalThis;
233
+ const proc = globalThis.process ?? {};
234
+ const key = page.addEventListener ?? proc;
235
+ let departure = departures.get(key);
236
+ if (departure === undefined) {
237
+ const created = {
238
+ leaving: { write: new Set(), transport: new Set() },
239
+ uninstall: () => { }
240
+ };
241
+ if (page.addEventListener) {
242
+ const hide = (event) => void (event.persisted || leave(created));
243
+ const remove = page.removeEventListener;
244
+ page.addEventListener('pagehide', hide);
245
+ created.uninstall = () => remove?.call(page, 'pagehide', hide);
246
+ }
247
+ else {
248
+ const exit = () => leave(created);
249
+ proc.on?.('exit', exit);
250
+ created.uninstall = () => proc.off?.('exit', exit);
251
+ }
252
+ departures.set(key, (departure = created));
253
+ }
254
+ const own = departure;
255
+ own.leaving[phase].add(fn);
256
+ return () => {
257
+ own.leaving[phase].delete(fn);
258
+ if (own.leaving.write.size + own.leaving.transport.size === 0 && departures.get(key) === own) {
259
+ own.uninstall();
260
+ departures.delete(key);
261
+ }
262
+ };
263
+ };
208
264
  /**
209
265
  * Install the lifecycle on a provider: `hasSynced`, `whenSynced`, and the
210
266
  * departure announcement. `beforeunload` only announces it (`depart`): an
211
267
  * app's unsaved-changes prompt fires it and the user may stay, so the
212
268
  * provider keeps running and its presence returns at the next renewal.
213
- * Leaving the page (`pagehide` of a page not kept in the back/forward
214
- * cache; Node: `exit`) destroys the provider, whose teardown announces it.
269
+ * Leaving the page for good ({@link onPageLeave}'s `transport` phase)
270
+ * destroys the provider, whose teardown announces it.
215
271
  */
216
272
  export const initLifecycle = (host, destroy, depart = () => { }) => {
217
273
  host.hasSynced = false;
@@ -222,21 +278,12 @@ export const initLifecycle = (host, destroy, depart = () => { }) => {
222
278
  // A consumer that never attaches a catch must not crash the process.
223
279
  host.whenSynced.catch(() => { });
224
280
  const page = globalThis;
225
- if (page.addEventListener) {
226
- const leave = (event) => void (event.persisted || destroy());
227
- page.addEventListener('beforeunload', depart);
228
- page.addEventListener('pagehide', leave);
229
- host._leave = () => {
230
- page.removeEventListener?.('beforeunload', depart);
231
- page.removeEventListener?.('pagehide', leave);
232
- };
233
- }
234
- else {
235
- const leave = () => void destroy();
236
- const proc = globalThis.process ?? {};
237
- proc.on?.('exit', leave);
238
- host._leave = () => proc.off?.('exit', leave);
239
- }
281
+ page.addEventListener?.('beforeunload', depart);
282
+ const off = onPageLeave(() => void destroy(), 'transport');
283
+ host._leave = () => {
284
+ page.removeEventListener?.('beforeunload', depart);
285
+ off();
286
+ };
240
287
  };
241
288
  /** The first sync — lifetime. Returns `true` only the first time. */
242
289
  export const markSynced = (host) => {
@@ -59,7 +59,7 @@ import { frame } from '../protocols/envelope.js';
59
59
  import { assertRoomId, beginDestroy, bindRoomProtocol, tabs, chunkFrame, CLOSE, createChunkReader, emitFailed, initLifecycle, isRefusal, LOCAL_PRESENCE_LOSS, markSynced, messageAuth, messageChunk, messageComments, messageSaved, messageSync, MAX_FRAME_BYTES, SyncRefusedError } from './room.js';
60
60
  import { asEngineDoc } from '../structs.js';
61
61
  import { ATTRIBUTION_ROOT } from '../schema.js';
62
- import { ATTRIBUTION_ORIGIN } from '../attribution/attribution.js';
62
+ import { ATTRIBUTION_ORIGIN, writerOf } from '../attribution/attribution.js';
63
63
  // @todo - this should depend on awareness.outdatedTime
64
64
  const messageReconnectTimeout = 30000;
65
65
  /** Silence after which the socket is sent one text `ping` (a room auto-answers `pong`). */
@@ -262,10 +262,12 @@ export const bindWebsocketProvider = (Y) => {
262
262
  * reload, its other tabs), and a seed writer (content any replica may
263
263
  * write, never stripped). Another actor's structs are not this replica's
264
264
  * to save: a room restored from a lagging snapshot strips them for good.
265
- * A doc without a binding for its own id (a raw doc) owns all.
265
+ * The actor is the binding of the doc's own id, written with its first
266
+ * write, and before it the actor the document attached (`writerOf`). A
267
+ * raw doc, with neither, owns all.
266
268
  */
267
269
  const ownerOf = (doc) => {
268
- const actor = actorOf(doc, doc.clientID);
270
+ const actor = writerOf(asEngineDoc(doc));
269
271
  return (client) => actor === undefined ||
270
272
  client === doc.clientID ||
271
273
  client < seedBand ||
@@ -9,8 +9,11 @@
9
9
  * The horizon is a state vector: an item whose clock is below it was
10
10
  * stored by the room before the horizon's time (`room.purge.timing`). A
11
11
  * deletion's time is the struct that made it: a block's oldest live delete
12
- * mark (a withdrawn block: its withdraw mark, and the marks of what it last
13
- * held), a text delete mark record, a placement candidate.
12
+ * mark (a withdrawn block: its withdraw mark, and the records of what it last
13
+ * held), a text hold record, a move record. A delete of a session whose
14
+ * history ended (a release record, `hist.release.end`) is past the horizon
15
+ * whatever its time: a delete or withdraw mark, a text hold record of that
16
+ * writer below the record's clock. A move or a merge waits for the horizon.
14
17
  */
15
18
  import type { EngineApi, EngineDoc } from './engine-api.js';
16
19
  import { type PlacementModel } from './placement/model.js';
@@ -24,14 +27,12 @@ export type Horizon = {
24
27
  export type PurgeReport = {
25
28
  /** Blocks removed: registry entry deleted, node collected. */
26
29
  removed: number;
27
- /** Deleted blocks kept (they delimit a live text, or a block is placed under them) whose stream, data and claims were deleted. */
30
+ /** Deleted blocks kept (a live block streams in their text, two boundaries name them, or a block is placed under them) whose stream, data and claims were deleted. */
28
31
  emptied: number;
29
- /** Text delete mark records deleted. */
32
+ /** Text hold records deleted (what they held deleted with them). */
30
33
  marks: number;
31
- /** Restoration records deleted (their copies collected). */
34
+ /** Move records dropped: old ones no block shows at any more (a lost move, a move of removed blocks). */
32
35
  records: number;
33
- /** Placement candidates dropped (runner-ups of an old, accepted winner). */
34
- candidates: number;
35
36
  /** Claims naming a removed block, deleted from the blocks that hold them. */
36
37
  claims: number;
37
38
  /**
@@ -40,13 +41,21 @@ export type PurgeReport = {
40
41
  * contributors at the merge): deleted, their registry entries kept.
41
42
  */
42
43
  merged: number;
44
+ /** Release records the horizon covers (`hist.release.end`): deleted. */
45
+ released: number;
43
46
  };
47
+ /**
48
+ * The ended histories a document holds (`hist.release.end`): per writer,
49
+ * the clock its latest live release record ended at (what it wrote below
50
+ * it is released).
51
+ */
52
+ export declare const releasesOf: (doc: EngineDoc) => Map<number, number>;
44
53
  /** The horizon a document holds (`null`: never purged). */
45
54
  export declare const readHorizon: (doc: EngineDoc) => Horizon | null;
46
55
  export declare const bindPurge: (Y: EngineApi) => {
47
56
  purge: (doc: EngineDoc, facade: {
48
57
  model: PlacementModel;
49
58
  text: TextEngine;
50
- }, horizon: Horizon) => PurgeReport;
59
+ }, horizon: Horizon | null) => PurgeReport;
51
60
  };
52
61
  export type PurgeBinding = ReturnType<typeof bindPurge>;