edytor 0.1.0-next.21 → 0.1.0-next.23

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 (69) hide show
  1. package/README.md +1 -1
  2. package/dist/cloudflare/DocumentRoom.d.ts +302 -10
  3. package/dist/cloudflare/DocumentRoom.js +965 -88
  4. package/dist/cloudflare/index.d.ts +1 -1
  5. package/dist/cloudflare/index.js +1 -1
  6. package/dist/cloudflare/routeDocumentSocket.js +30 -6
  7. package/dist/collaboration/awarenessSelection.d.ts +7 -1
  8. package/dist/collaboration/awarenessSelection.js +7 -1
  9. package/dist/collaboration/index.d.ts +2 -2
  10. package/dist/collaboration/index.js +2 -2
  11. package/dist/collaboration/providers.d.ts +4 -0
  12. package/dist/collaboration/providers.js +4 -0
  13. package/dist/components/Edytor.svelte +14 -2
  14. package/dist/components/Edytor.svelte.d.ts +8 -1
  15. package/dist/crdt/admission.d.ts +6 -1
  16. package/dist/crdt/admission.js +9 -2
  17. package/dist/crdt/data.d.ts +3 -1
  18. package/dist/crdt/data.js +42 -32
  19. package/dist/crdt/document.d.ts +31 -8
  20. package/dist/crdt/document.js +53 -18
  21. package/dist/crdt/edytor-doc.d.ts +31 -3
  22. package/dist/crdt/edytor-doc.js +131 -14
  23. package/dist/crdt/engine.d.ts +6 -0
  24. package/dist/crdt/engine.js +7 -0
  25. package/dist/crdt/flow.d.ts +2 -0
  26. package/dist/crdt/flow.js +2 -2
  27. package/dist/crdt/index.d.ts +20 -9
  28. package/dist/crdt/index.js +3 -3
  29. package/dist/crdt/migration/migrate.js +9 -3
  30. package/dist/crdt/placement/model.d.ts +9 -1
  31. package/dist/crdt/placement/model.js +29 -10
  32. package/dist/crdt/placement/rank.d.ts +20 -0
  33. package/dist/crdt/placement/rank.js +79 -0
  34. package/dist/crdt/protocols/envelope.d.ts +20 -1
  35. package/dist/crdt/protocols/envelope.js +14 -2
  36. package/dist/crdt/providers/container.d.ts +15 -0
  37. package/dist/crdt/providers/container.js +17 -3
  38. package/dist/crdt/providers/index.d.ts +21 -1
  39. package/dist/crdt/providers/index.js +86 -3
  40. package/dist/crdt/providers/indexeddb.js +60 -40
  41. package/dist/crdt/providers/room.d.ts +18 -2
  42. package/dist/crdt/providers/room.js +29 -5
  43. package/dist/crdt/providers/websocket.d.ts +9 -1
  44. package/dist/crdt/providers/websocket.js +21 -6
  45. package/dist/crdt/storage.d.ts +21 -0
  46. package/dist/crdt/storage.js +37 -0
  47. package/dist/crdt/structs.d.ts +17 -0
  48. package/dist/crdt/structs.js +41 -0
  49. package/dist/crdt/text/model.d.ts +9 -1
  50. package/dist/crdt/text/model.js +24 -14
  51. package/dist/crdt/text/rows.d.ts +94 -0
  52. package/dist/crdt/text/rows.js +193 -0
  53. package/dist/crdt/text/runs.d.ts +30 -10
  54. package/dist/crdt/text/runs.js +1235 -264
  55. package/dist/edytor.svelte.d.ts +3 -1
  56. package/dist/edytor.svelte.js +9 -6
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/plugins/image/ImageEmpty.svelte +19 -6
  60. package/dist/plugins/image/ImagePlugin.svelte +9 -3
  61. package/dist/plugins/image/ImagePlugin.svelte.d.ts +2 -2
  62. package/dist/plugins/image/image.d.ts +16 -0
  63. package/dist/plugins/image/image.js +19 -0
  64. package/dist/plugins/index.d.ts +1 -1
  65. package/dist/plugins/index.js +1 -1
  66. package/dist/plugins.d.ts +11 -0
  67. package/dist/session/commands.d.ts +1 -0
  68. package/dist/session/commands.js +4 -1
  69. package/package.json +1 -1
@@ -195,7 +195,7 @@ export const scanText = (home, text) => {
195
195
  }
196
196
  return { home, text, bounds, len: at, key: bounds.map((b) => b.key).join(',') };
197
197
  };
198
- const byId = (a, b) => {
198
+ export const byId = (a, b) => {
199
199
  const [ac, ak] = a.split(':').map(Number);
200
200
  const [bc, bk] = b.split(':').map(Number);
201
201
  return ac - bc || ak - bk;
@@ -214,21 +214,30 @@ export const delimiters = (blocks, rows) => {
214
214
  }
215
215
  return out;
216
216
  };
217
- /** The streams of one text, in text order: the home's head (unless delimited elsewhere), then one per delimiting boundary. */
217
+ /**
218
+ * The streams of one text, in text order: the home's head (unless delimited
219
+ * elsewhere), then one per delimiting boundary — one sweep over the
220
+ * boundaries (each non-delimiting one is an inert boundary of the stream it
221
+ * sits in).
222
+ */
218
223
  export const placeText = (row, delim) => {
219
- const cuts = row.bounds.filter((b) => delim.get(b.s) === b.key);
220
- const heads = [
221
- { block: delim.has(row.home) ? null : row.home, start: 0 },
222
- ...cuts.map((b) => ({ block: b.s, start: b.at + 1 }))
223
- ];
224
224
  const out = [];
225
- heads.forEach((h, i) => {
226
- const end = i + 1 < heads.length ? heads[i + 1].start - 1 : row.len;
227
- if (h.block === null)
228
- return;
229
- const inert = row.bounds.filter((b) => b.at >= h.start && b.at < end).map((b) => b.at);
230
- out.push({ block: h.block, home: row.home, text: row.text, start: h.start, end, inert });
231
- });
225
+ let block = delim.has(row.home) ? null : row.home;
226
+ let start = 0;
227
+ let inert = [];
228
+ const close = (end) => {
229
+ if (block !== null)
230
+ out.push({ block, home: row.home, text: row.text, start, end, inert });
231
+ };
232
+ for (const b of row.bounds) {
233
+ if (delim.get(b.s) !== b.key) {
234
+ inert.push(b.at);
235
+ continue;
236
+ }
237
+ close(b.at);
238
+ [block, start, inert] = [b.s, b.at + 1, []];
239
+ }
240
+ close(row.len);
232
241
  return out;
233
242
  };
234
243
  /** A stream's content pieces: the range minus its inert boundaries (at least one piece). */
@@ -382,6 +391,7 @@ export const bindText = (Y) => {
382
391
  top: (m) => top.get(m),
383
392
  streamOf: (b) => streams.get(b),
384
393
  streamsIn: (home) => inText.get(home) ?? [],
394
+ streamAt: (home, i) => (inText.get(home) ?? []).find((x) => x.start <= i && i <= x.end),
385
395
  display: (b) => displayOf(b, blocks, own)
386
396
  };
387
397
  return own;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The maintained form of one backing text's boundary table (P1) — the
3
+ * index's row: the text's live boundary items in text order and the live
4
+ * units between them, held in a Fenwick tree so an edit that writes or
5
+ * removes no boundary moves every later boundary in O(log B) instead of a
6
+ * rescan.
7
+ *
8
+ * `h[j]` is the units before boundary `j` since the one before it, plus
9
+ * boundary `j` itself (`h[B]`: the units after the last boundary), so the
10
+ * live index of boundary `j` is `H(j) - 1` (`H` the prefix sum) and the
11
+ * text's live length is `H(B)`. A row's boundary SET only changes through a
12
+ * rescan ({@link rowOf}); an insert or delete of other units changes one
13
+ * `h` entry ({@link shiftGap}), found by walking the item list from the
14
+ * edited item to its nearest boundary ({@link gapOfItem}).
15
+ *
16
+ * The streams of a row (`text/model.ts` `placeText`) are its segments
17
+ * between consecutive delimiting boundaries (`cuts`): segment `k` starts
18
+ * after cut `k - 1` (the text start for `k = 0`) and ends at cut `k` (the
19
+ * text end past the last). Segment 0 is the home block's stream unless the
20
+ * home is delimited elsewhere (`head: null` — that stretch is nobody's);
21
+ * segment `k > 0` is the stream of the block its opening cut names.
22
+ */
23
+ import type { EngineNode } from '../engine-api.js';
24
+ import { type BlockId, type Bound, type Stream, type TextRow } from './model.js';
25
+ export type LiveRow = {
26
+ home: BlockId;
27
+ text: EngineNode;
28
+ /** The live boundaries in text order (`at` is NOT maintained: read {@link boundAt}). */
29
+ bounds: readonly Bound[];
30
+ /** Fenwick tree over `h` (1-based, size `bounds.length + 1`). */
31
+ tree: Float64Array;
32
+ /** The bound keys joined — the boundary set's identity. */
33
+ key: string;
34
+ /** Each bound's index by its key. */
35
+ keyIndex: Map<string, number>;
36
+ /** Indexes into `bounds` of the delimiting boundaries, ascending ({@link placeRow}). */
37
+ cuts: number[];
38
+ /** The block of segment 0 (`null`: the home is delimited elsewhere). */
39
+ head: BlockId | null;
40
+ };
41
+ /** The live index of boundary `j`. */
42
+ export declare const boundAt: (row: LiveRow, j: number) => number;
43
+ /** The text's live length. */
44
+ export declare const rowLength: (row: LiveRow) => number;
45
+ /** A row from a scan (its segments are set by {@link placeRow}). */
46
+ export declare const rowOf: (scan: TextRow) => LiveRow;
47
+ /** A sequence item as the walk reads it (`text/model.ts` `SeqItem`, with its left neighbour). */
48
+ export type RowItem = {
49
+ id: {
50
+ client: number;
51
+ clock: number;
52
+ };
53
+ length: number;
54
+ deleted: boolean;
55
+ countable?: boolean;
56
+ left?: RowItem | null;
57
+ right?: RowItem | null;
58
+ content?: {
59
+ arr?: unknown[];
60
+ };
61
+ };
62
+ /**
63
+ * The gap (`h` entry) item `it` lies in — between the nearest live
64
+ * boundaries on its left and right, found by walking the item list both
65
+ * ways at once (the cost is the distance to the nearer one, never the
66
+ * text). It reads only the list, never the engine's position caches, so a
67
+ * write the caches have not seen yet places it all the same. `-1` when the
68
+ * walk finds a boundary the row does not hold (the caller rescans).
69
+ */
70
+ export declare const gapOfItem: (row: LiveRow, it: RowItem) => number;
71
+ /**
72
+ * Add `delta` units (an insert, or a delete when negative) to gap `gap`.
73
+ * `false` when a delete takes more units than the gap holds (a stale row:
74
+ * the caller rescans).
75
+ */
76
+ export declare const shiftGap: (row: LiveRow, gap: number, delta: number) => boolean;
77
+ /** The segment holding gap `gap` (the units before boundary `gap`). */
78
+ export declare const segmentOfGap: (row: LiveRow, gap: number) => number;
79
+ /** The segment whose `[start, end]` holds live index `x` (the first, at a seam). */
80
+ export declare const segmentAt: (row: LiveRow, x: number) => number;
81
+ /** The segment the cut keyed `cut` opens (`null`: segment 0); `-1` when it is no cut of the row. */
82
+ export declare const segmentOfCut: (row: LiveRow, cut: string | null) => number;
83
+ /** The block of segment `k` (`null`: nobody's). */
84
+ export declare const headOf: (row: LiveRow, k: number) => BlockId | null;
85
+ /** Segment `k` as a {@link Stream} of `block`. */
86
+ export declare const streamOfSegment: (row: LiveRow, k: number, block: BlockId) => Stream;
87
+ /**
88
+ * Set the row's segments from the delimiting boundaries (`delim`): the
89
+ * home's head (unless delimited elsewhere), then one per cut — the same
90
+ * streams `placeText` returns for the row's scan.
91
+ */
92
+ export declare const placeRow: (row: LiveRow, delim: ReadonlyMap<BlockId, string>) => void;
93
+ /** The row agrees with a fresh scan of its text (the index's self-check). */
94
+ export declare const sameRow: (row: LiveRow, scan: TextRow) => boolean;
@@ -0,0 +1,193 @@
1
+ import { isBoundary } from './model.js';
2
+ const add = (row, i, d) => {
3
+ const t = row.tree;
4
+ for (let x = i + 1; x < t.length; x += x & -x)
5
+ t[x] += d;
6
+ };
7
+ /** `H(i)`: the sum of `h[0..i]`. */
8
+ const prefix = (row, i) => {
9
+ const t = row.tree;
10
+ let s = 0;
11
+ for (let x = i + 1; x > 0; x -= x & -x)
12
+ s += t[x];
13
+ return s;
14
+ };
15
+ /** The smallest `j` with `H(j) >= target` (`bounds.length + 1` when none). */
16
+ const lowerBound = (row, target) => {
17
+ const t = row.tree;
18
+ const n = t.length - 1;
19
+ let pos = 0;
20
+ let rem = target;
21
+ let step = 1;
22
+ while (step * 2 <= n)
23
+ step *= 2;
24
+ for (; step > 0; step >>= 1) {
25
+ const next = pos + step;
26
+ if (next <= n && t[next] < rem) {
27
+ pos = next;
28
+ rem -= t[next];
29
+ }
30
+ }
31
+ return pos;
32
+ };
33
+ /** `h[j]` itself. */
34
+ const valueAt = (row, j) => prefix(row, j) - (j > 0 ? prefix(row, j - 1) : 0);
35
+ /** The live index of boundary `j`. */
36
+ export const boundAt = (row, j) => prefix(row, j) - 1;
37
+ /** The text's live length. */
38
+ export const rowLength = (row) => prefix(row, row.bounds.length);
39
+ /** A row from a scan (its segments are set by {@link placeRow}). */
40
+ export const rowOf = (scan) => {
41
+ const n = scan.bounds.length;
42
+ const keyIndex = new Map();
43
+ const t = new Float64Array(n + 2);
44
+ let prev = -1;
45
+ for (let j = 0; j <= n; j++) {
46
+ const at = j < n ? scan.bounds[j].at : scan.len;
47
+ if (j < n)
48
+ keyIndex.set(scan.bounds[j].key, j);
49
+ t[j + 1] += j < n ? at - prev : at - prev - 1;
50
+ prev = at;
51
+ // Linear Fenwick build: push the node's sum to its parent.
52
+ const parent = j + 1 + ((j + 1) & -(j + 1));
53
+ if (parent <= n + 1)
54
+ t[parent] += t[j + 1];
55
+ }
56
+ return {
57
+ home: scan.home,
58
+ text: scan.text,
59
+ bounds: scan.bounds,
60
+ tree: t,
61
+ key: scan.key,
62
+ keyIndex,
63
+ cuts: [],
64
+ head: scan.home
65
+ };
66
+ };
67
+ /**
68
+ * The index of the live boundary `it` holds last (`first`: first), `-1` when
69
+ * it holds none, `-2` when it holds one the row does not know (stale row).
70
+ */
71
+ const boundIn = (row, it, first) => {
72
+ if (it.deleted || it.countable === false)
73
+ return -1;
74
+ const arr = it.content?.arr;
75
+ if (arr === undefined)
76
+ return -1;
77
+ for (let n = 0; n < it.length; n++) {
78
+ const k = first ? n : it.length - 1 - n;
79
+ if (!isBoundary(arr[k]))
80
+ continue;
81
+ return row.keyIndex.get(`${it.id.client}:${it.id.clock + k}`) ?? -2;
82
+ }
83
+ return -1;
84
+ };
85
+ /**
86
+ * The gap (`h` entry) item `it` lies in — between the nearest live
87
+ * boundaries on its left and right, found by walking the item list both
88
+ * ways at once (the cost is the distance to the nearer one, never the
89
+ * text). It reads only the list, never the engine's position caches, so a
90
+ * write the caches have not seen yet places it all the same. `-1` when the
91
+ * walk finds a boundary the row does not hold (the caller rescans).
92
+ */
93
+ export const gapOfItem = (row, it) => {
94
+ if (row.bounds.length === 0)
95
+ return 0;
96
+ let l = it.left ?? null;
97
+ let r = it.right ?? null;
98
+ while (l !== null || r !== null) {
99
+ if (l !== null) {
100
+ const j = boundIn(row, l, false);
101
+ if (j === -2)
102
+ return -1;
103
+ if (j >= 0)
104
+ return j + 1;
105
+ l = l.left ?? null;
106
+ }
107
+ if (r !== null) {
108
+ const j = boundIn(row, r, true);
109
+ if (j === -2)
110
+ return -1;
111
+ if (j >= 0)
112
+ return j;
113
+ r = r.right ?? null;
114
+ }
115
+ }
116
+ return -1;
117
+ };
118
+ /**
119
+ * Add `delta` units (an insert, or a delete when negative) to gap `gap`.
120
+ * `false` when a delete takes more units than the gap holds (a stale row:
121
+ * the caller rescans).
122
+ */
123
+ export const shiftGap = (row, gap, delta) => {
124
+ if (delta < 0 && valueAt(row, gap) + delta < (gap < row.bounds.length ? 1 : 0))
125
+ return false;
126
+ add(row, gap, delta);
127
+ return true;
128
+ };
129
+ /** The segment holding gap `gap` (the units before boundary `gap`). */
130
+ export const segmentOfGap = (row, gap) => {
131
+ const cuts = row.cuts;
132
+ let lo = 0;
133
+ let hi = cuts.length;
134
+ while (lo < hi) {
135
+ const mid = (lo + hi) >> 1;
136
+ if (cuts[mid] < gap)
137
+ lo = mid + 1;
138
+ else
139
+ hi = mid;
140
+ }
141
+ return lo;
142
+ };
143
+ /** The segment whose `[start, end]` holds live index `x` (the first, at a seam). */
144
+ export const segmentAt = (row, x) => segmentOfGap(row, Math.min(lowerBound(row, x + 1), row.bounds.length));
145
+ /** The segment the cut keyed `cut` opens (`null`: segment 0); `-1` when it is no cut of the row. */
146
+ export const segmentOfCut = (row, cut) => {
147
+ if (cut === null)
148
+ return 0;
149
+ const j = row.keyIndex.get(cut);
150
+ if (j === undefined)
151
+ return -1;
152
+ const k = segmentOfGap(row, j);
153
+ return row.cuts[k] === j ? k + 1 : -1;
154
+ };
155
+ /** The block of segment `k` (`null`: nobody's). */
156
+ export const headOf = (row, k) => k === 0 ? row.head : row.bounds[row.cuts[k - 1]].s;
157
+ /** Segment `k` as a {@link Stream} of `block`. */
158
+ export const streamOfSegment = (row, k, block) => {
159
+ const lo = k === 0 ? -1 : row.cuts[k - 1];
160
+ const hi = k < row.cuts.length ? row.cuts[k] : row.bounds.length;
161
+ const start = lo < 0 ? 0 : boundAt(row, lo) + 1;
162
+ const end = hi === row.bounds.length ? rowLength(row) : boundAt(row, hi);
163
+ const inert = [];
164
+ for (let j = lo + 1; j < hi; j++)
165
+ inert.push(boundAt(row, j));
166
+ return { block, home: row.home, text: row.text, start, end, inert };
167
+ };
168
+ /**
169
+ * Set the row's segments from the delimiting boundaries (`delim`): the
170
+ * home's head (unless delimited elsewhere), then one per cut — the same
171
+ * streams `placeText` returns for the row's scan.
172
+ */
173
+ export const placeRow = (row, delim) => {
174
+ const cuts = [];
175
+ for (let j = 0; j < row.bounds.length; j++) {
176
+ const b = row.bounds[j];
177
+ if (delim.get(b.s) === b.key)
178
+ cuts.push(j);
179
+ }
180
+ row.cuts = cuts;
181
+ row.head = delim.has(row.home) ? null : row.home;
182
+ };
183
+ /** The row agrees with a fresh scan of its text (the index's self-check). */
184
+ export const sameRow = (row, scan) => {
185
+ if (row.key !== scan.key || row.bounds.length !== scan.bounds.length)
186
+ return false;
187
+ if (rowLength(row) !== scan.len)
188
+ return false;
189
+ for (let j = 0; j < scan.bounds.length; j++)
190
+ if (boundAt(row, j) !== scan.bounds[j].at)
191
+ return false;
192
+ return true;
193
+ };
@@ -22,15 +22,24 @@
22
22
  * mark, `type`/`data`, or the registry entry itself); one routine folds a
23
23
  * set of such pairs into the indexes.
24
24
  * - A transaction is folded ONCE at commit, from `transaction.changed`.
25
- * - A read inside an open transaction first folds the PENDING part: the
26
- * structs the transaction's insert/delete sets gained since the last fold
27
- * (the watermark is those sets' lengths).
28
- * - An edited backing text is rescanned for its boundary items; the stream
29
- * table is rebuilt only when a boundary set, a nonce or a registry entry
30
- * changed. Every cached block records which texts its display walked and
31
- * which blocks it walked or read a claim on (a skipped claim included); an
32
- * edit invalidates the display owners of the streams it touched (or every
33
- * reader of the text when it cannot say).
25
+ * - A read first folds the PENDING part of every transaction whose writes
26
+ * are in the document and whose commit fold has not run: the open one, and
27
+ * those a transaction's cleanup started (their bodies ran, their observers
28
+ * have not) — the structs their insert/delete sets gained since the last
29
+ * fold (the watermark is those sets' lengths). The commit fold takes only
30
+ * what no read folded.
31
+ * - Each backing text has a maintained row (`text/rows.ts`, P1): its
32
+ * boundaries and the live units between them. An edit that writes or
33
+ * removes no boundary moves the row by its units in the gap it lies in
34
+ * (found by walking the item list to the nearest boundary), never a
35
+ * rescan; a boundary written or removed rescans that text, re-decides the
36
+ * delimiters of the blocks its boundaries name, and re-places only the
37
+ * rows those cut. Every cached block records which texts its display
38
+ * walked and which blocks it walked or read a claim on (a skipped claim
39
+ * included); an edit invalidates the display owner of the stream it lies
40
+ * in, a placement the blocks whose stream's delimiting boundaries changed
41
+ * (or every reader of the text when the fold cannot say: a format marker,
42
+ * a change inside an atom).
34
43
  *
35
44
  * Publication is COMMIT-BOUND (R5): a mid-transaction read refreshes the
36
45
  * cache for read-your-writes, but the change report observes committed
@@ -40,7 +49,9 @@
40
49
  * The change report `{added, removed, moved, meta, content, order}` is the
41
50
  * fold against the last published index: the blocks the commit's folds
42
51
  * touched or invalidated are compared with what was published for them;
43
- * when the children index was rebuilt, the reachable tree is compared too.
52
+ * the child lists the commit patched are compared with the published ones,
53
+ * advancing the published tree in place (after a whole rebuild, the
54
+ * reachable tree is compared).
44
55
  */
45
56
  import type { EngineApi, EngineDoc } from '../engine-api.js';
46
57
  import type { BlockId, ContentItem, ModelView, ProjectedBlock } from '../placement/model.js';
@@ -218,6 +229,15 @@ export type DisplayRoles = {
218
229
  /** Every layout kind the roles declare, present in the document or not. */
219
230
  layoutKinds: () => Iterable<string>;
220
231
  };
232
+ /**
233
+ * Test lanes turn this on (`globalThis.__EDYTOR_INDEX_CHECKS__`, set before
234
+ * the index loads): after every fold the index checks each fact it keeps
235
+ * incrementally against a rebuild from the replicated state and throws on
236
+ * the first difference. Off in production.
237
+ */
238
+ export declare const indexChecks: {
239
+ on: boolean;
240
+ };
221
241
  export declare const bindRuns: (Y: EngineApi) => {
222
242
  attach: (doc: EngineDoc) => RunView;
223
243
  };