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.
- package/README.md +2 -2
- package/dist/cloudflare/DocumentRoom.d.ts +13 -9
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/room/context.d.ts +27 -26
- package/dist/cloudflare/room/context.js +2 -2
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +44 -19
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/crdt/attribution/attribution.d.ts +31 -12
- package/dist/crdt/attribution/attribution.js +56 -18
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.js +29 -24
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +25 -9
- package/dist/crdt/doc/history.js +150 -40
- package/dist/crdt/doc/layout.js +11 -22
- package/dist/crdt/doc/meta.js +12 -17
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.js +2 -2
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +52 -122
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +16 -5
- package/dist/crdt/document.js +21 -4
- package/dist/crdt/edytor-doc.d.ts +62 -64
- package/dist/crdt/edytor-doc.js +18 -13
- package/dist/crdt/engine-api.d.ts +33 -1
- package/dist/crdt/engine.d.ts +0 -1
- package/dist/crdt/engine.js +1 -4
- package/dist/crdt/flow.d.ts +1 -1
- package/dist/crdt/flow.js +25 -27
- package/dist/crdt/index.d.ts +26 -25
- package/dist/crdt/index.js +1 -1
- package/dist/crdt/placement/display.d.ts +5 -30
- package/dist/crdt/placement/display.js +39 -144
- package/dist/crdt/placement/keys.d.ts +58 -0
- package/dist/crdt/placement/keys.js +90 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +187 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.js +5 -3
- package/dist/crdt/purge.d.ts +17 -8
- package/dist/crdt/purge.js +236 -126
- package/dist/crdt/restore.d.ts +2 -2
- package/dist/crdt/restore.js +18 -34
- package/dist/crdt/schema.d.ts +22 -21
- package/dist/crdt/schema.js +29 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +15 -15
- package/dist/crdt/structs.js +8 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +12 -0
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.js +5 -8
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +404 -138
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +12 -3
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +77 -62
- package/dist/crdt/text/model.js +84 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +17 -12
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
- package/dist/edytor.svelte.js +4 -1
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +25 -25
- package/dist/surface/observer.svelte.js +2 -2
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +59 -8
- package/package.json +2 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- 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
|
|
148
|
-
*
|
|
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
|
|
214
|
-
*
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
page.
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
*
|
|
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 =
|
|
270
|
+
const actor = writerOf(asEngineDoc(doc));
|
|
269
271
|
return (client) => actor === undefined ||
|
|
270
272
|
client === doc.clientID ||
|
|
271
273
|
client < seedBand ||
|
package/dist/crdt/purge.d.ts
CHANGED
|
@@ -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
|
|
13
|
-
* held), a text
|
|
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 (
|
|
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
|
|
32
|
+
/** Text hold records deleted (what they held deleted with them). */
|
|
30
33
|
marks: number;
|
|
31
|
-
/**
|
|
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>;
|