edytor 6.0.0 → 7.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 (76) hide show
  1. package/README.md +3 -3
  2. package/dist/cloudflare/DocumentRoom.d.ts +77 -2
  3. package/dist/cloudflare/DocumentRoom.js +37 -1
  4. package/dist/cloudflare/index.d.ts +1 -1
  5. package/dist/cloudflare/index.js +1 -1
  6. package/dist/cloudflare/room/access.d.ts +6 -1
  7. package/dist/cloudflare/room/access.js +30 -4
  8. package/dist/cloudflare/room/admission.d.ts +11 -0
  9. package/dist/cloudflare/room/admission.js +43 -10
  10. package/dist/cloudflare/room/context.d.ts +11 -2
  11. package/dist/cloudflare/room/context.js +17 -6
  12. package/dist/cloudflare/room/forged.d.ts +1 -1
  13. package/dist/cloudflare/room/forged.js +11 -9
  14. package/dist/cloudflare/room/frames.d.ts +2 -0
  15. package/dist/cloudflare/room/frames.js +2 -0
  16. package/dist/cloudflare/room/presence.js +1 -4
  17. package/dist/cloudflare/room/reseed.d.ts +93 -0
  18. package/dist/cloudflare/room/reseed.js +500 -0
  19. package/dist/cloudflare/room/scheduler.d.ts +1 -1
  20. package/dist/cloudflare/room/scheduler.js +9 -1
  21. package/dist/cloudflare/room/shared.d.ts +10 -0
  22. package/dist/cloudflare/room/shared.js +10 -0
  23. package/dist/cloudflare/room/storage.d.ts +7 -0
  24. package/dist/cloudflare/room/storage.js +33 -1
  25. package/dist/components/Edytor.svelte +18 -3
  26. package/dist/components/Edytor.svelte.d.ts +10 -1
  27. package/dist/components/EdytorRoot.svelte +17 -0
  28. package/dist/components/EdytorRoot.svelte.d.ts +4 -0
  29. package/dist/components/Suggestion.svelte +1 -1
  30. package/dist/crdt/attribution/block.d.ts +3 -0
  31. package/dist/crdt/attribution/block.js +3 -2
  32. package/dist/crdt/doc/suggest.d.ts +6 -2
  33. package/dist/crdt/doc/suggest.js +6 -5
  34. package/dist/crdt/edytor-doc.d.ts +1 -1
  35. package/dist/crdt/edytor-doc.js +1 -1
  36. package/dist/crdt/engine-room.d.ts +4 -0
  37. package/dist/crdt/engine-room.js +6 -0
  38. package/dist/crdt/index.d.ts +2 -1
  39. package/dist/crdt/protocol.d.ts +2 -2
  40. package/dist/crdt/protocol.js +2 -2
  41. package/dist/crdt/protocols/auth.d.ts +8 -1
  42. package/dist/crdt/protocols/auth.js +14 -1
  43. package/dist/crdt/providers/index.d.ts +1 -0
  44. package/dist/crdt/providers/index.js +22 -3
  45. package/dist/crdt/providers/room.d.ts +15 -0
  46. package/dist/crdt/providers/room.js +15 -0
  47. package/dist/crdt/providers/websocket.d.ts +4 -2
  48. package/dist/crdt/providers/websocket.js +36 -9
  49. package/dist/crdt/reseed.d.ts +127 -0
  50. package/dist/crdt/reseed.js +533 -0
  51. package/dist/crdt/restore.d.ts +13 -1
  52. package/dist/crdt/restore.js +8 -3
  53. package/dist/crdt/schema.d.ts +6 -0
  54. package/dist/crdt/schema.js +6 -0
  55. package/dist/crdt/structs.d.ts +8 -11
  56. package/dist/crdt/structs.js +25 -44
  57. package/dist/crdt/text/index/build.d.ts +3 -9
  58. package/dist/crdt/text/index/build.js +87 -181
  59. package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
  60. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  61. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
  62. package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
  63. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  64. package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
  65. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
  66. package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/selection/selection.svelte.d.ts +1 -1
  70. package/dist/selection/selection.svelte.js +87 -106
  71. package/dist/session/commands.js +49 -51
  72. package/dist/surface/attributes.d.ts +3 -3
  73. package/dist/surface/attributes.js +2 -2
  74. package/dist/surface/observer.svelte.d.ts +7 -7
  75. package/dist/surface/observer.svelte.js +213 -309
  76. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ import type { EngineDoc } from './engine-api.js';
2
+ import { type BlockId } from './placement/model.js';
3
+ import { type DataPatch } from './data.js';
4
+ import { restoreDocument, type RestoreFacade } from './restore.js';
5
+ import type { ModelView } from './placement/model.js';
6
+ import { type SuggestContext } from './doc/suggest.js';
7
+ import type { JSONDoc } from '../utils/json.js';
8
+ /** What the rebase reads and writes through: a facade over a document (structurally). */
9
+ export type ReseedFacade<Plan = unknown> = RestoreFacade<Plan> & {
10
+ order(): readonly BlockId[];
11
+ displayLength(id: BlockId): number;
12
+ toJSON(options?: {
13
+ suggestions?: boolean;
14
+ }): JSONDoc;
15
+ /** The facade's reads beyond its operations: the index's view, the base view (`doc/suggest.ts` builds over it too). */
16
+ part: {
17
+ view(): ModelView;
18
+ base?: SuggestContext['base'];
19
+ };
20
+ prepare: RestoreFacade<Plan>['prepare'] & {
21
+ patchData(target: {
22
+ block: BlockId;
23
+ atom: string;
24
+ }, ops: DataPatch[]): Plan;
25
+ formatRange(id: BlockId, offset: number, length: number, marks: Record<string, unknown>): Plan;
26
+ splitBlock(id: BlockId, offset: number, newId: BlockId): Plan;
27
+ mergeBlocks(from: BlockId, into: BlockId): Plan;
28
+ };
29
+ };
30
+ /** A document and the facade over it. */
31
+ export type Replica = {
32
+ doc: EngineDoc;
33
+ facade: ReseedFacade;
34
+ };
35
+ /**
36
+ * A document without its pending suggestions (`sugg.json.base`), its JSON
37
+ * and each unit's identity read from one view: what a cut is made from.
38
+ */
39
+ export type Base = {
40
+ json: JSONDoc;
41
+ ids: Map<BlockId, string[]>;
42
+ };
43
+ export declare const baseOf: (facade: ReseedFacade) => Base;
44
+ /** What the room keeps of a re-seed (`room.reseed.cut`). */
45
+ export type Cut = {
46
+ /** The old document's state vector at the cut. */
47
+ sv: Uint8Array;
48
+ /** The old document's deletes at the cut (an update without structs). */
49
+ ds: Uint8Array;
50
+ /** The cut map (`encodeRuns`). */
51
+ map: Uint8Array;
52
+ };
53
+ /** A cut as one value (`Cut` → bytes and back), for the room's storage. */
54
+ export declare const encodeCut: (cut: Cut) => Uint8Array<ArrayBuffer>;
55
+ export declare const decodeCut: (bytes: Uint8Array) => Cut;
56
+ /**
57
+ * The new epoch's document (`room.reseed.cut`): `seed` builds it from the
58
+ * old one's base (`baseOf`: the caller's semantics and actor), then its
59
+ * blocks take the old ones' attribution, and it takes the old one's actor
60
+ * profiles (`u/<actorId>`: names and colors). Reads `old`, writes only the
61
+ * new document.
62
+ */
63
+ export declare const seedFrom: (old: Replica, base: Base, seed: (json: JSONDoc) => Replica) => Replica;
64
+ /**
65
+ * The cut between `old` and `fresh`, the new epoch seeded from its base
66
+ * ({@link seedFrom}): each seed unit stands for the base's unit at its
67
+ * place.
68
+ */
69
+ export declare const cutBetween: (old: Replica, base: Base, fresh: Replica) => Cut;
70
+ /** Cut `old` (`room.reseed.cut`): the new epoch's document and the cut ({@link seedFrom}, {@link cutBetween}). */
71
+ export declare const cutDocument: (old: Replica, seed: (json: JSONDoc) => Replica) => {
72
+ document: Replica;
73
+ cut: Cut;
74
+ };
75
+ /**
76
+ * Make `doc` show `json`, where `ids` names each unit of each of its blocks
77
+ * (an identity `doc` holds is that unit, kept where it can be; any other
78
+ * is new text). Splits and merges first: each block of `doc` is cut where
79
+ * its units go to another block of `json` (or where another block's text
80
+ * comes between them), then each block of `json` is assembled from its
81
+ * pieces in order (its first piece takes its id, the others are merged
82
+ * into it; a unit `json` no longer shows stays in the piece before it).
83
+ * Then `restoreDocument` writes the rest, its content by identity, and
84
+ * keeps the pending suggestions. An identity `doc` does not hold (`new:`,
85
+ * `~`, an old unit it never had) names no unit of it: a unit this restore
86
+ * wrote and `ids` names nowhere else stands for it, so it is written once
87
+ * (a `new:` one, the caller's own new unit, any such unit). Runs inside the
88
+ * caller's transaction.
89
+ */
90
+ export declare const restoreByIdentity: (doc: EngineDoc, facade: ReseedFacade, json: JSONDoc, ids: ReadonlyMap<BlockId, readonly string[]>) => import("./restore.js").RestoreReport;
91
+ /** What a rebase did: nothing to merge, or the restore's report of its write. */
92
+ export type RebaseReport = {
93
+ status: 'noop';
94
+ } | ({
95
+ status: 'applied';
96
+ } & ReturnType<typeof restoreDocument>);
97
+ /**
98
+ * What one user's rebases from one cut merged so far (`room.reseed.rebase`):
99
+ * the old document's state they covered (a state vector, and the deletes
100
+ * as an update without structs), so a second upload of the same edits
101
+ * (another tab's) merges nothing twice, and the merged maps: `maps[i]`,
102
+ * written while the `i`th cut's epoch was current, reads each unit those
103
+ * rebases wrote as the old unit it stands for, so an edit that builds on
104
+ * them still finds it. Starts as the cut's own state, with no map.
105
+ */
106
+ export type Merged = {
107
+ sv: Uint8Array;
108
+ ds: Uint8Array;
109
+ maps: Uint8Array[];
110
+ };
111
+ export declare const encodeMerged: (merged: Merged) => Uint8Array<ArrayBuffer>;
112
+ export declare const decodeMerged: (bytes: Uint8Array) => Merged;
113
+ /** Whether `upload` holds anything `merged` does not cover: what a rebase merges, if anything. */
114
+ export declare const hasEdits: (upload: Uint8Array, merged: Merged) => boolean;
115
+ /**
116
+ * Merge a client's unsaved edits, `upload` (its old document, whole),
117
+ * into `target`, the current document: `cuts` are the re-seeds since the
118
+ * client's epoch, oldest first (each one's map reads the next epoch's seed
119
+ * units as the previous epoch's), `merged` what its user's earlier rebases
120
+ * from the oldest cut merged (default: the cut's own state), `scratch`
121
+ * builds a document from an update. Writes `target` inside the caller's
122
+ * transaction; answers the report and `merged` grown by this rebase.
123
+ */
124
+ export declare const rebaseOnto: (target: Replica, cuts: readonly Cut[], upload: Uint8Array, scratch: (update: Uint8Array) => Replica, merged?: Merged) => {
125
+ report: RebaseReport;
126
+ merged: Merged;
127
+ };
@@ -0,0 +1,533 @@
1
+ /**
2
+ * A document's re-seed (`room.reseed.*` in `docs/editor-delete-contract.md`):
3
+ * the room rebuilds a document that grew near its quota from its visible
4
+ * content, with fresh identities, and keeps a cut so that an offline
5
+ * client's unsaved edits made against the old document still merge.
6
+ * Worker-safe; the room (`cloudflare/room/reseed.ts`) owns the storage,
7
+ * the trigger and the protocol, this module the cut and the merge.
8
+ *
9
+ * The cut (`cutDocument`): the new epoch's document, seeded from the old
10
+ * one without its pending suggestions (`baseOf`: same block ids, kinds,
11
+ * data, content, marks, block attribution), and what the room keeps of
12
+ * the old one: its state vector and its deletes (which part of an
13
+ * uploaded old document the room had) and the cut map (each seed
14
+ * character's old identity, read from the same view as the seed).
15
+ *
16
+ * The rebase (`rebaseOnto`), from a client's old document, against what
17
+ * its user's earlier rebases from the same cut merged (`Merged`, the cut's
18
+ * own state at first):
19
+ *
20
+ * 1. its base, the part already merged (`Y.clipUpdate`, fork patch YP34),
21
+ * into a scratch document, the shadow, without its pending suggestions
22
+ * (the cut dropped them); its own edits, the rest (`Y.diffUpdate`);
23
+ * 2. the current document written onto the shadow by identity
24
+ * (`restoreByIdentity`): a seed character is its old character (the
25
+ * cut map), a character an earlier rebase wrote the one it stood for
26
+ * (the merged map), so the shadow keeps it and the client's edits still
27
+ * find it; the new document's splits and merges are written as splits
28
+ * and merges;
29
+ * 3. the client's edits merged into the shadow by the CRDT, its pending
30
+ * suggestions dropped (`room.reseed.suggestions`);
31
+ * 4. the shadow written onto the current document by identity, in the
32
+ * caller's transaction; each character it writes is added to the
33
+ * merged map.
34
+ *
35
+ * Exact for text; when both sides restructured the same blocks the CRDT's
36
+ * outcome can depend on the old document's structural history, which the
37
+ * cut does not keep (`room.reseed.residue`).
38
+ */
39
+ import { Y } from './engine.js';
40
+ import { clipUpdate, diffUpdate } from './engine-room.js';
41
+ import { asYDoc } from './structs.js';
42
+ import * as encoding from 'lib0-v14/encoding';
43
+ import * as decoding from 'lib0-v14/decoding';
44
+ import { blockNodeOf, view as modelView } from './placement/model.js';
45
+ import { bindBlockAttribution, blockAttributionOf, CONTRIBUTOR_PREFIX, CREATED_KEY } from './attribution/block.js';
46
+ import { ATTRIBUTION_ROOT, LAST_CHANGED_ATTR } from './schema.js';
47
+ import { ACTOR_PREFIX } from './attribution/attribution.js';
48
+ import { common } from './data.js';
49
+ import { canonKey } from './text/model.js';
50
+ import { restoreDocument, runsOf, unitsOf } from './restore.js';
51
+ import { displayOf, isRowMark, openRangeCursor } from './text/model.js';
52
+ import { suggestionsOf } from './doc/suggest.js';
53
+ const width = (u) => (u.item.kind === 'text' ? u.item.text.length : 1);
54
+ const sum = (units) => units.reduce((n, u) => n + width(u), 0);
55
+ /**
56
+ * Each unit's identity in block `id` (its character's or atom's item: the
57
+ * engine id of its first code unit), in display order, as `unitsOf` lists
58
+ * the block's units: one walk over the pieces the block displays.
59
+ */
60
+ const idsOf = (facade, id) => {
61
+ const { blocks, own } = facade.part.view();
62
+ const out = [];
63
+ for (const seg of displayOf(id, blocks, own, undefined, true) ?? []) {
64
+ for (const piece of openRangeCursor(seg.text).read(seg.i0, seg.i1)) {
65
+ if (piece.deleted || piece.len === 0)
66
+ continue;
67
+ const { client, clock } = piece.id;
68
+ const c = piece.content;
69
+ if (typeof c.str === 'string') {
70
+ let k = piece.offset;
71
+ for (const ch of c.str.slice(piece.offset, piece.offset + piece.len)) {
72
+ out.push(`${client}:${clock + k}`);
73
+ k += ch.length;
74
+ }
75
+ }
76
+ else if (typeof c.getContent === 'function') {
77
+ const all = c.getContent();
78
+ for (let k = piece.offset; k < piece.offset + piece.len; k++)
79
+ if (!isRowMark(all[k]))
80
+ out.push(`${client}:${clock + k}`);
81
+ }
82
+ }
83
+ }
84
+ return out;
85
+ };
86
+ /** Every displayed block's unit identities. */
87
+ const identities = (facade) => new Map(facade.order().map((id) => [id, idsOf(facade, id)]));
88
+ /** A unit's identity and what it is (its character, or its atom's id and kind): a wrong identity never keeps another unit. */
89
+ const keyed = (ids, units) => ids.map((id, i) => {
90
+ const item = units[i]?.item;
91
+ return `${id}|${item?.kind === 'text' ? item.text : `@${item?.id}|${item?.type}`}`;
92
+ });
93
+ export const baseOf = (facade) => facade.part.base?.((other) => ({
94
+ json: other.toJSON({ suggestions: true }),
95
+ ids: identities(other)
96
+ })) ?? { json: facade.toJSON(), ids: identities(facade) };
97
+ /** Reject every pending suggestion of `facade`'s document (`room.reseed.suggestions`). */
98
+ const dropSuggestions = (facade) => {
99
+ const all = suggestionsOf(facade);
100
+ const pending = all.changes();
101
+ if (pending.length > 0)
102
+ all.run(all.decide(pending, false));
103
+ };
104
+ const encodeRuns = (runs) => {
105
+ const e = encoding.createEncoder();
106
+ encoding.writeVarUint(e, runs.length);
107
+ for (const r of runs)
108
+ for (const n of [r.c, r.k, r.len, r.oc, r.ok])
109
+ encoding.writeVarUint(e, n);
110
+ return encoding.toUint8Array(e);
111
+ };
112
+ const decodeRuns = (bytes) => {
113
+ const d = decoding.createDecoder(bytes);
114
+ return Array.from({ length: decoding.readVarUint(d) }, () => {
115
+ const [c, k, len, oc, ok] = Array.from({ length: 5 }, () => decoding.readVarUint(d));
116
+ return { c: c, k: k, len: len, oc: oc, ok: ok };
117
+ });
118
+ };
119
+ /** Add `id` (`client:clock`) standing for `was` to `runs`, joined to the last run when it continues it. */
120
+ const addRun = (runs, id, was) => {
121
+ const [c, k] = id.split(':').map(Number);
122
+ const [oc, ok] = was.split(':').map(Number);
123
+ const last = runs.at(-1);
124
+ if (last &&
125
+ last.c === c &&
126
+ last.k + last.len === k &&
127
+ last.oc === oc &&
128
+ last.ok + last.len === ok)
129
+ last.len++;
130
+ else
131
+ runs.push({ c, k, len: 1, oc, ok });
132
+ };
133
+ /** A cut as one value (`Cut` → bytes and back), for the room's storage. */
134
+ export const encodeCut = (cut) => {
135
+ const e = encoding.createEncoder();
136
+ for (const part of [cut.sv, cut.ds, cut.map])
137
+ encoding.writeVarUint8Array(e, part);
138
+ return encoding.toUint8Array(e);
139
+ };
140
+ export const decodeCut = (bytes) => {
141
+ const d = decoding.createDecoder(bytes);
142
+ const [sv, ds, map] = Array.from({ length: 3 }, () => decoding.readVarUint8Array(d));
143
+ return { sv: sv, ds: ds, map: map };
144
+ };
145
+ /**
146
+ * The new epoch's document (`room.reseed.cut`): `seed` builds it from the
147
+ * old one's base (`baseOf`: the caller's semantics and actor), then its
148
+ * blocks take the old ones' attribution, and it takes the old one's actor
149
+ * profiles (`u/<actorId>`: names and colors). Reads `old`, writes only the
150
+ * new document.
151
+ */
152
+ export const seedFrom = (old, base, seed) => {
153
+ const fresh = seed(base.json);
154
+ const { ensureRecord } = bindBlockAttribution(Y);
155
+ fresh.doc.transact(() => {
156
+ const [was, now] = [old.doc.get(ATTRIBUTION_ROOT), fresh.doc.get(ATTRIBUTION_ROOT)];
157
+ for (const key of was.attrKeys())
158
+ if (key.startsWith(ACTOR_PREFIX))
159
+ now.setAttr(key, was.getAttr(key));
160
+ for (const id of fresh.facade.order()) {
161
+ const node = blockNodeOf(fresh.doc, id);
162
+ if (node === null)
163
+ continue;
164
+ // The record and `lastChangedBy` become the old block's, whatever the seed wrote.
165
+ const att = blockAttributionOf(old.doc, id);
166
+ const rec = ensureRecord(fresh.doc, id, node);
167
+ for (const key of rec.attrKeys())
168
+ if (key.startsWith(CONTRIBUTOR_PREFIX))
169
+ rec.deleteAttr(key);
170
+ for (const actor of att?.contributors ?? [])
171
+ rec.setAttr(CONTRIBUTOR_PREFIX + actor, true);
172
+ if (att?.createdBy === undefined)
173
+ rec.deleteAttr(CREATED_KEY);
174
+ else
175
+ rec.setAttr(CREATED_KEY, att.createdBy);
176
+ if (att?.lastChangedBy === undefined)
177
+ node.deleteAttr(LAST_CHANGED_ATTR);
178
+ else
179
+ node.setAttr(LAST_CHANGED_ATTR, att.lastChangedBy);
180
+ }
181
+ });
182
+ return fresh;
183
+ };
184
+ /**
185
+ * The cut between `old` and `fresh`, the new epoch seeded from its base
186
+ * ({@link seedFrom}): each seed unit stands for the base's unit at its
187
+ * place.
188
+ */
189
+ export const cutBetween = (old, base, fresh) => {
190
+ const runs = [];
191
+ for (const [block, ids] of identities(fresh.facade)) {
192
+ const was = base.ids.get(block) ?? [];
193
+ ids.forEach((id, i) => {
194
+ if (was[i] !== undefined)
195
+ addRun(runs, id, was[i]);
196
+ });
197
+ }
198
+ const update = Y.encodeStateAsUpdate(asYDoc(old.doc));
199
+ const sv = Y.encodeStateVector(asYDoc(old.doc));
200
+ return { sv, ds: diffUpdate(update, sv), map: encodeRuns(runs) };
201
+ };
202
+ /** Cut `old` (`room.reseed.cut`): the new epoch's document and the cut ({@link seedFrom}, {@link cutBetween}). */
203
+ export const cutDocument = (old, seed) => {
204
+ const base = baseOf(old.facade);
205
+ const document = seedFrom(old, base, seed);
206
+ return { document, cut: cutBetween(old, base, document) };
207
+ };
208
+ /** A map of runs as a lookup: an item's id → the old item's it stands for (`undefined`: none). */
209
+ const lookupOf = (map) => {
210
+ const byClient = new Map();
211
+ for (const run of decodeRuns(map)) {
212
+ const list = byClient.get(run.c);
213
+ if (list)
214
+ list.push(run);
215
+ else
216
+ byClient.set(run.c, [run]);
217
+ }
218
+ for (const list of byClient.values())
219
+ list.sort((a, b) => a.k - b.k);
220
+ return (id) => {
221
+ const [c, k] = id.split(':').map(Number);
222
+ const list = byClient.get(c);
223
+ if (!list)
224
+ return undefined;
225
+ let [lo, hi] = [0, list.length];
226
+ while (lo < hi) {
227
+ const mid = (lo + hi) >> 1;
228
+ if (list[mid].k + list[mid].len <= k)
229
+ lo = mid + 1;
230
+ else
231
+ hi = mid;
232
+ }
233
+ const run = list[lo];
234
+ return run && run.k <= k ? `${run.oc}:${run.ok + (k - run.k)}` : undefined;
235
+ };
236
+ };
237
+ /**
238
+ * Make block `id`'s content read `want` keeping every unit whose identity
239
+ * the two key lists share in order: each gap between kept units deleted
240
+ * and written again, a kept character whose marks differ formatted (runs
241
+ * of the same change in one write), a kept atom whose data differs patched
242
+ * in place (written again when it has no id, or another kind). Edits
243
+ * through the display.
244
+ */
245
+ const rewriteKeyed = (facade, id, have, want, haveKeys, wantKeys) => {
246
+ let [at, i, j] = [0, 0, 0];
247
+ let run = null;
248
+ const flush = () => {
249
+ if (run !== null)
250
+ facade.apply(facade.prepare.formatRange(id, run.at, run.len, run.marks));
251
+ run = null;
252
+ };
253
+ for (const [pi, pj] of [...common(haveKeys, wantKeys), [have.length, want.length]]) {
254
+ const gone = sum(have.slice(i, pi));
255
+ if (gone > 0 || pj > j)
256
+ flush();
257
+ if (gone > 0)
258
+ facade.apply(facade.prepare.deleteText(id, at, gone));
259
+ for (const item of runsOf(want.slice(j, pj))) {
260
+ if (item.kind === 'text')
261
+ facade.apply(facade.prepare.insertText(id, at, item.text, item.marks ?? {}));
262
+ else
263
+ facade.apply(facade.prepare.insertInline(id, at, { id: item.id, type: item.type, data: item.data }));
264
+ at += item.kind === 'text' ? item.text.length : 1;
265
+ }
266
+ if (pi === have.length)
267
+ break;
268
+ const [h, w] = [have[pi], want[pj]];
269
+ if (h.key !== w.key && w.item.kind === 'inline') {
270
+ flush();
271
+ const { item } = h;
272
+ if (item.kind === 'inline' &&
273
+ item.id !== '' &&
274
+ item.id === w.item.id &&
275
+ item.type === w.item.type)
276
+ facade.apply(facade.prepare.patchData({ block: id, atom: item.id }, [{ path: [], value: w.item.data }]));
277
+ else {
278
+ facade.apply(facade.prepare.deleteText(id, at, 1));
279
+ facade.apply(facade.prepare.insertInline(id, at, {
280
+ id: w.item.id,
281
+ type: w.item.type,
282
+ data: w.item.data
283
+ }));
284
+ }
285
+ }
286
+ else if (h.key !== w.key && h.item.kind === 'text' && w.item.kind === 'text') {
287
+ const [hm, wm] = [h.item.marks ?? {}, w.item.marks ?? {}];
288
+ const marks = {};
289
+ for (const k of new Set([...Object.keys(hm), ...Object.keys(wm)]))
290
+ if (canonKey({ v: hm[k] ?? null }) !== canonKey({ v: wm[k] ?? null }))
291
+ marks[k] = wm[k] ?? null;
292
+ const tag = canonKey(marks);
293
+ if (run !== null && run.tag === tag && run.at + run.len === at)
294
+ run.len += width(h);
295
+ else {
296
+ flush();
297
+ run = { at, len: width(h), marks, tag };
298
+ }
299
+ }
300
+ else
301
+ flush();
302
+ at += width(h);
303
+ [i, j] = [pi + 1, pj + 1];
304
+ }
305
+ flush();
306
+ };
307
+ /**
308
+ * Make `doc` show `json`, where `ids` names each unit of each of its blocks
309
+ * (an identity `doc` holds is that unit, kept where it can be; any other
310
+ * is new text). Splits and merges first: each block of `doc` is cut where
311
+ * its units go to another block of `json` (or where another block's text
312
+ * comes between them), then each block of `json` is assembled from its
313
+ * pieces in order (its first piece takes its id, the others are merged
314
+ * into it; a unit `json` no longer shows stays in the piece before it).
315
+ * Then `restoreDocument` writes the rest, its content by identity, and
316
+ * keeps the pending suggestions. An identity `doc` does not hold (`new:`,
317
+ * `~`, an old unit it never had) names no unit of it: a unit this restore
318
+ * wrote and `ids` names nowhere else stands for it, so it is written once
319
+ * (a `new:` one, the caller's own new unit, any such unit). Runs inside the
320
+ * caller's transaction.
321
+ */
322
+ export const restoreByIdentity = (doc, facade, json, ids) => {
323
+ const where = new Map();
324
+ for (const [x, list] of ids)
325
+ list.forEach((id, i) => where.set(id, { x, i }));
326
+ const base = modelView(doc).order.ids.slice();
327
+ const srcOf = new Map();
328
+ for (const a of base)
329
+ for (const k of idsOf(facade, a))
330
+ srcOf.set(k, a);
331
+ const pieces = new Map();
332
+ let temp = 0;
333
+ for (const a of base) {
334
+ const list = [];
335
+ let cur = null;
336
+ idsOf(facade, a).forEach((k, u) => {
337
+ const w = where.get(k);
338
+ if (!w)
339
+ return;
340
+ const between = (ids.get(w.x) ?? []).slice((cur?.last ?? 0) + 1, w.i);
341
+ if (cur !== null &&
342
+ cur.dest === w.x &&
343
+ w.i > cur.last &&
344
+ between.every((key) => (srcOf.get(key) ?? a) === a)) {
345
+ cur.last = w.i;
346
+ return;
347
+ }
348
+ const first = list.length === 0;
349
+ cur = { start: first ? 0 : u, dest: w.x, first: w.i, last: w.i, id: first ? a : '' };
350
+ list.push(cur);
351
+ });
352
+ pieces.set(a, list);
353
+ }
354
+ const byDest = new Map();
355
+ for (const list of pieces.values())
356
+ for (const piece of list)
357
+ byDest.set(piece.dest, [...(byDest.get(piece.dest) ?? []), piece]);
358
+ for (const [x, list] of byDest) {
359
+ list.sort((a, b) => a.first - b.first);
360
+ if (list[0].id === '' && blockNodeOf(doc, x) === null)
361
+ list[0].id = x;
362
+ }
363
+ const free = () => {
364
+ let id;
365
+ do
366
+ id = `~cut${temp++}`;
367
+ while (blockNodeOf(doc, id) !== null || ids.has(id));
368
+ return id;
369
+ };
370
+ for (const list of pieces.values())
371
+ for (const piece of list)
372
+ if (piece.id === '')
373
+ piece.id = free();
374
+ for (const [a, list] of pieces)
375
+ for (let k = list.length - 1; k >= 1; k--) {
376
+ const at = sum(unitsOf(facade.contentItems(a)).slice(0, list[k].start));
377
+ facade.apply(facade.prepare.splitBlock(a, at, list[k].id));
378
+ }
379
+ for (const [x, list] of byDest) {
380
+ const head = list[0];
381
+ if (blockNodeOf(doc, x) === null && head.id !== x) {
382
+ facade.apply(facade.prepare.splitBlock(head.id, 0, x));
383
+ head.id = x;
384
+ }
385
+ for (const piece of list)
386
+ if (piece.id !== x)
387
+ facade.apply(facade.prepare.mergeBlocks(piece.id, x));
388
+ }
389
+ const want = (id) => [...(ids.get(id) ?? [])];
390
+ const report = restoreDocument(doc, facade, json, {
391
+ rewrite: (f, id, have, wanted) => {
392
+ const r = f;
393
+ rewriteKeyed(r, id, have, wanted, keyed(idsOf(r, id), have), keyed(want(id), wanted));
394
+ return true;
395
+ },
396
+ suggestions: 'keep'
397
+ });
398
+ // Same text, another identity (a unit deleted and typed again): by identity too.
399
+ const named = new Set([...ids.values()].flat());
400
+ for (const [id, list] of ids) {
401
+ const have = idsOf(facade, id);
402
+ // A unit this restore wrote stands for it; a unit `doc` held stands only for the
403
+ // caller's own new one (`new:`): another unit of `doc` would answer for a unit
404
+ // it is not, which a later edit of `doc`'s own unit would then hit.
405
+ const wanted = list.map((k, i) => !srcOf.has(k) &&
406
+ have[i] !== undefined &&
407
+ !named.has(have[i]) &&
408
+ (k.startsWith('new:') || !srcOf.has(have[i]))
409
+ ? have[i]
410
+ : k);
411
+ if (have.length === wanted.length && have.every((k, i) => k === wanted[i]))
412
+ continue;
413
+ const units = unitsOf(facade.contentItems(id));
414
+ rewriteKeyed(facade, id, units, units, keyed(have, units), keyed(wanted, units));
415
+ }
416
+ return report;
417
+ };
418
+ export const encodeMerged = (merged) => {
419
+ const e = encoding.createEncoder();
420
+ for (const part of [merged.sv, merged.ds])
421
+ encoding.writeVarUint8Array(e, part);
422
+ encoding.writeVarUint(e, merged.maps.length);
423
+ for (const map of merged.maps)
424
+ encoding.writeVarUint8Array(e, map);
425
+ return encoding.toUint8Array(e);
426
+ };
427
+ export const decodeMerged = (bytes) => {
428
+ const d = decoding.createDecoder(bytes);
429
+ const [sv, ds] = [decoding.readVarUint8Array(d), decoding.readVarUint8Array(d)];
430
+ const maps = Array.from({ length: decoding.readVarUint(d) }, () => decoding.readVarUint8Array(d));
431
+ return { sv, ds, maps };
432
+ };
433
+ /** Whether `upload` holds anything `merged` does not cover: what a rebase merges, if anything. */
434
+ export const hasEdits = (upload, merged) => {
435
+ const own = Y.decodeUpdate(diffUpdate(upload, merged.sv));
436
+ return (!own.structs.every((s) => s instanceof Y.Skip) ||
437
+ !Y.diffIdSet(own.ds, Y.decodeUpdate(merged.ds).ds).isEmpty());
438
+ };
439
+ /**
440
+ * Merge a client's unsaved edits, `upload` (its old document, whole),
441
+ * into `target`, the current document: `cuts` are the re-seeds since the
442
+ * client's epoch, oldest first (each one's map reads the next epoch's seed
443
+ * units as the previous epoch's), `merged` what its user's earlier rebases
444
+ * from the oldest cut merged (default: the cut's own state), `scratch`
445
+ * builds a document from an update. Writes `target` inside the caller's
446
+ * transaction; answers the report and `merged` grown by this rebase.
447
+ */
448
+ export const rebaseOnto = (target, cuts, upload, scratch, merged = { sv: cuts[0].sv, ds: cuts[0].ds, maps: [] }) => {
449
+ const mine = diffUpdate(upload, merged.sv);
450
+ const own = Y.decodeUpdate(mine);
451
+ const done = Y.decodeUpdate(merged.ds).ds;
452
+ if (own.structs.every((s) => s instanceof Y.Skip) && Y.diffIdSet(own.ds, done).isEmpty())
453
+ return { report: { status: 'noop' }, merged };
454
+ // 1 · The base, without the pending suggestions the cut dropped.
455
+ const shadow = scratch(clipUpdate(upload, merged.sv, done));
456
+ dropSuggestions(shadow.facade);
457
+ // 2 · The current document, each unit read down the cuts to its old one.
458
+ const cutLookups = cuts.map((cut) => lookupOf(cut.map));
459
+ const mapLookups = merged.maps.map(lookupOf);
460
+ const old = (k) => {
461
+ let id = k;
462
+ for (let i = cuts.length - 1; i >= 0; i--) {
463
+ const was = mapLookups[i]?.(id);
464
+ if (was !== undefined)
465
+ return was;
466
+ const before = cutLookups[i](id);
467
+ // A unit of a later epoch the cuts do not name: no old unit stands for it.
468
+ if (before === undefined)
469
+ return `~${k}`;
470
+ id = before;
471
+ }
472
+ return id;
473
+ };
474
+ const theirs = target.facade.toJSON({ suggestions: true });
475
+ const current = identities(target.facade);
476
+ const asOld = new Map([...current].map(([id, list]) => [id, list.map(old)]));
477
+ shadow.doc.transact(() => restoreByIdentity(shadow.doc, shadow.facade, theirs, asOld));
478
+ // Each shadow unit's identity in the current document (they read the same now).
479
+ const toCurrent = new Map();
480
+ for (const [id, list] of identities(shadow.facade)) {
481
+ const now = current.get(id) ?? [];
482
+ list.forEach((k, i) => {
483
+ if (now[i] !== undefined)
484
+ toCurrent.set(k, now[i]);
485
+ });
486
+ }
487
+ // 3 · The client's edits, merged by the CRDT; its pending suggestions dropped.
488
+ Y.applyUpdate(asYDoc(shadow.doc), mine);
489
+ dropSuggestions(shadow.facade);
490
+ // 4 · Back onto the current document, by identity.
491
+ const result = shadow.facade.toJSON({ suggestions: true });
492
+ const ids = new Map([...identities(shadow.facade)].map(([id, list]) => [
493
+ id,
494
+ list.map((k) => toCurrent.get(k) ?? `new:${k}`)
495
+ ]));
496
+ // The units this rebase writes: the target's writer's, past its state now.
497
+ const writer = asYDoc(target.doc).clientID;
498
+ const from = Y.decodeStateVector(Y.encodeStateVector(asYDoc(target.doc))).get(writer) ?? 0;
499
+ const report = restoreByIdentity(target.doc, target.facade, result, ids);
500
+ // What this rebase merged: the structs the shadow took in, the deletes, the units it wrote.
501
+ const took = Y.decodeStateVector(Y.encodeStateVector(asYDoc(shadow.doc)));
502
+ const sv = Y.decodeStateVector(merged.sv);
503
+ const top = new Map();
504
+ for (const struct of own.structs) {
505
+ const { client, clock } = struct.id;
506
+ top.set(client, Math.max(top.get(client) ?? 0, clock + struct.length));
507
+ if ((took.get(client) ?? 0) > (sv.get(client) ?? 0))
508
+ sv.set(client, took.get(client));
509
+ }
510
+ // The upload's deletes alone: its structs past every one it holds.
511
+ const deletes = diffUpdate(mine, Y.encodeStateVector(top));
512
+ const runs = decodeRuns(merged.maps[cuts.length - 1] ?? encodeRuns([]));
513
+ for (const [id, list] of ids) {
514
+ const now = idsOf(target.facade, id);
515
+ if (now.length !== list.length)
516
+ continue;
517
+ list.forEach((k, i) => {
518
+ const [c, clock] = now[i].split(':').map(Number);
519
+ if (k.startsWith('new:') && c === writer && clock >= from)
520
+ addRun(runs, now[i], k.slice(4));
521
+ });
522
+ }
523
+ const maps = Array.from({ length: cuts.length }, (_, i) => merged.maps[i] ?? encodeRuns([]));
524
+ maps[cuts.length - 1] = encodeRuns(runs);
525
+ return {
526
+ report: { status: 'applied', ...report },
527
+ merged: {
528
+ sv: Y.encodeStateVector(sv),
529
+ ds: Y.mergeUpdates([merged.ds, deletes]),
530
+ maps
531
+ }
532
+ };
533
+ };