@panphora/clayjs 1.13.1 → 1.14.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/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69667 -41708
- package/dist/server/checkpoint.cjs +70690 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/rooms.cjs +72039 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +94 -93
- package/entries/clay.js +22 -0
- package/package.json +16 -2
- package/packed-contract.json +17 -3
- package/src/core/conflict-download.js +11 -2
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +62 -32
- package/src/core/snapshot.js +39 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/effect.js +130 -0
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +44 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +7 -3
- package/src/loader.js +6 -0
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1121 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +384 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +60 -3
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/live-sync.js +13 -2
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- package/src/page/replay.js +0 -578
package/src/page/replay.js
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
// The replayed merge for one text block: pure functions, no store, no lane, no
|
|
2
|
-
// timers. Every tab feeds a block's ops to `step` (a set or a delete) and
|
|
3
|
-
// `tombstone` in room order, and gets the same agreed value S.
|
|
4
|
-
//
|
|
5
|
-
// A text block is { t, c, m?, ...fields }: c is its flat text, m (optional) one
|
|
6
|
-
// mark key per UTF-16 unit of c ("" for none). A write made by the page binding
|
|
7
|
-
// also carries w, which never reaches S or the saved file:
|
|
8
|
-
// a the writing tab
|
|
9
|
-
// e the epoch it was made in
|
|
10
|
-
// k the seq its editor was last released at: its steps extend S at k
|
|
11
|
-
// s its splices in order, [p, e, removed, n, removedMarks?]: offsets into the
|
|
12
|
-
// text before that splice, whose length is n. The base is derived from the
|
|
13
|
-
// write itself (chainOf), so a tab with no history can still place it.
|
|
14
|
-
// g on a repair: the seq the writer computed S through
|
|
15
|
-
// h on a heal: the seq the healing tab had reached in the lost room
|
|
16
|
-
// u on a heal: per splice, the lost room's seq of its write (null: unknown)
|
|
17
|
-
// f the non-text fields the write changed: S takes only those from it, so a
|
|
18
|
-
// typing write never undoes a peer's toggle or move. Without f, S takes
|
|
19
|
-
// every field of the write.
|
|
20
|
-
//
|
|
21
|
-
// Placement, the first that applies:
|
|
22
|
-
// 1. the step mapper, when this replay's history reaches back to k: positions
|
|
23
|
-
// map through every write after k, ties at one spot go to the lower tab id,
|
|
24
|
-
// and a delete removes only characters its writer had seen;
|
|
25
|
-
// 2. the self-described base placed by a diff (placeSplice), with a notice
|
|
26
|
-
// when a delete cannot be placed exactly;
|
|
27
|
-
// 3. merge3Text, with the losing side in a notice.
|
|
28
|
-
import { HyperMorph } from "../vendor/hyper-morph.vendor.js";
|
|
29
|
-
const { merge3Text, diff } = HyperMorph;
|
|
30
|
-
|
|
31
|
-
export const LIMITS = { logEntries: 1024, logBytes: 256 * 1024 };
|
|
32
|
-
|
|
33
|
-
export const strip = (v) => {
|
|
34
|
-
if (!v) return null;
|
|
35
|
-
const { w, ...rest } = v;
|
|
36
|
-
return rest;
|
|
37
|
-
};
|
|
38
|
-
const canon = (v) => (Array.isArray(v) ? `[${v.map(canon).join(",")}]` : v && typeof v === "object" ? `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",")}}` : JSON.stringify(v ?? null));
|
|
39
|
-
// 64 bits in two 32-bit lanes, so a divergent value never passes as agreed.
|
|
40
|
-
function hash64(text) {
|
|
41
|
-
let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
|
|
42
|
-
for (let i = 0; i < text.length; i++) {
|
|
43
|
-
const ch = text.charCodeAt(i);
|
|
44
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
45
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
46
|
-
}
|
|
47
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
48
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
49
|
-
return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
50
|
-
}
|
|
51
|
-
export const hashOf = (v) => hash64(canon(strip(v)));
|
|
52
|
-
|
|
53
|
-
const blank = (n) => new Array(n).fill("");
|
|
54
|
-
const put = (arr, from, to, items) => [...arr.slice(0, from), ...items, ...arr.slice(to)];
|
|
55
|
-
const marksOf = (v) => (v && Array.isArray(v.m) && v.m.length === v.c.length ? v.m : null);
|
|
56
|
-
// The fields S takes from write X (see f above).
|
|
57
|
-
function fieldsOf(r, X) {
|
|
58
|
-
const f = X.w && X.w.f;
|
|
59
|
-
if (!Array.isArray(f) || !r.S) return strip(X);
|
|
60
|
-
const out = { ...r.S };
|
|
61
|
-
for (const name of f) {
|
|
62
|
-
if (name === "c" || name === "m" || name === "w") continue;
|
|
63
|
-
if (Object.prototype.hasOwnProperty.call(X, name)) out[name] = X[name];
|
|
64
|
-
else delete out[name];
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function withMarks(fields, c, marks) {
|
|
70
|
-
const { c: _c, m: _m, w: _w, ...rest } = fields;
|
|
71
|
-
return marks && marks.some(Boolean) ? { ...rest, c, m: marks } : { ...rest, c };
|
|
72
|
-
}
|
|
73
|
-
const isHigh = (u) => u >= 0xd800 && u <= 0xdbff;
|
|
74
|
-
const isLow = (u) => u >= 0xdc00 && u <= 0xdfff;
|
|
75
|
-
|
|
76
|
-
// The single splice that turns a into b, never cutting a surrogate pair.
|
|
77
|
-
export function spliceOf(a, b) {
|
|
78
|
-
const n = Math.min(a.length, b.length);
|
|
79
|
-
let p = 0;
|
|
80
|
-
while (p < n && a.charCodeAt(p) === b.charCodeAt(p)) p++;
|
|
81
|
-
if (p > 0 && isHigh(a.charCodeAt(p - 1))) p--;
|
|
82
|
-
let s = 0;
|
|
83
|
-
while (s < n - p && a.charCodeAt(a.length - 1 - s) === b.charCodeAt(b.length - 1 - s)) s++;
|
|
84
|
-
if (s > 0 && isLow(a.charCodeAt(a.length - s))) s--;
|
|
85
|
-
return { p, e: a.length - s, t: b.slice(p, b.length - s) };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// The changes from oldText to newText as { from, to, t } in old offsets.
|
|
89
|
-
export function regions(oldText, newText) {
|
|
90
|
-
if (oldText === newText) return [];
|
|
91
|
-
return diff(oldText, newText).map((h) => {
|
|
92
|
-
const sp = spliceOf(oldText.slice(h.bs, h.be), h.text);
|
|
93
|
-
return { from: h.bs + sp.p, to: h.bs + sp.e, t: sp.t };
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// A caret through those changes; text inserted at the caret goes after it.
|
|
98
|
-
export function mapOffset(oldText, newText, offset) {
|
|
99
|
-
let delta = 0;
|
|
100
|
-
for (const g of regions(oldText, newText)) {
|
|
101
|
-
if (offset <= g.from) break;
|
|
102
|
-
if (offset >= g.to) { delta += g.t.length - (g.to - g.from); continue; }
|
|
103
|
-
return g.from + delta + g.t.length;
|
|
104
|
-
}
|
|
105
|
-
return offset + delta;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Where offset q of the base sits in `side`: a change starting at q stays after
|
|
109
|
-
// it; inside a range the other side removed, the start of that range; inside a
|
|
110
|
-
// range the other side replaced with other text, null.
|
|
111
|
-
function mapHunks(hunks, q) {
|
|
112
|
-
let delta = 0;
|
|
113
|
-
for (const h of hunks) {
|
|
114
|
-
if (h.from >= q) break;
|
|
115
|
-
if (h.to <= q) { delta += h.t.length - (h.to - h.from); continue; }
|
|
116
|
-
if (h.t === "") return h.from + delta;
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
return q + delta;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// A writer's splice { from, to, t } of `base`, placed in `side`, or null when
|
|
123
|
-
// the other side changed text inside its range.
|
|
124
|
-
export function placeSplice(base, side, sp) {
|
|
125
|
-
const hunks = regions(base, side);
|
|
126
|
-
for (const h of hunks) {
|
|
127
|
-
if (h.t !== "" && h.to > h.from && h.from < sp.to && h.to > sp.from) return null;
|
|
128
|
-
if (h.from === h.to && h.from > sp.from && h.from < sp.to) return null;
|
|
129
|
-
}
|
|
130
|
-
const from = mapHunks(hunks, sp.from);
|
|
131
|
-
const to = sp.to === sp.from ? from : mapHunks(hunks, sp.to);
|
|
132
|
-
if (from === null || to === null || to < from) return null;
|
|
133
|
-
return { from, to, t: sp.t };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// The write's base and steps, derived backward from its own text and splices.
|
|
137
|
-
// Each step: { from, to, t, tm, removed, rm }, offsets into the text before it.
|
|
138
|
-
export function chainOf(X) {
|
|
139
|
-
const s = X && X.w && X.w.s;
|
|
140
|
-
if (!Array.isArray(s) || !s.length || typeof X.c !== "string") return null;
|
|
141
|
-
let c = X.c;
|
|
142
|
-
let m = marksOf(X) ?? blank(c.length);
|
|
143
|
-
const steps = [];
|
|
144
|
-
for (let i = s.length - 1; i >= 0; i--) {
|
|
145
|
-
if (!Array.isArray(s[i])) return null;
|
|
146
|
-
const [p, e, removed, n, rm] = s[i];
|
|
147
|
-
if (typeof removed !== "string" || !Number.isInteger(p) || !Number.isInteger(n) || p < 0 || e < p || e > n || e - p !== removed.length) return null;
|
|
148
|
-
const len = c.length - p - (n - e);
|
|
149
|
-
if (len < 0 || p + len > c.length) return null;
|
|
150
|
-
const removedMarks = Array.isArray(rm) && rm.length === removed.length ? rm : blank(removed.length);
|
|
151
|
-
steps.unshift({ from: p, to: e, t: c.slice(p, p + len), tm: m.slice(p, p + len), removed, rm: removedMarks });
|
|
152
|
-
c = c.slice(0, p) + removed + c.slice(p + len);
|
|
153
|
-
m = put(m, p, p + len, removedMarks);
|
|
154
|
-
}
|
|
155
|
-
return { base: { c, m }, steps };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// The splice a local edit makes: [from, to) of `before` replaced.
|
|
159
|
-
export function spliceFor(before, beforeMarks, from, to) {
|
|
160
|
-
const rm = beforeMarks ? beforeMarks.slice(from, to) : [];
|
|
161
|
-
return [from, to, before.slice(from, to), before.length, ...(rm.some(Boolean) ? [rm] : [])];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Splices that turn fromText into toText, right to left so each one's offsets
|
|
165
|
-
// still hold.
|
|
166
|
-
export function chainFromDiff(fromText, fromMarks, toText) {
|
|
167
|
-
const out = [];
|
|
168
|
-
let n = fromText.length;
|
|
169
|
-
for (const g of regions(fromText, toText).reverse()) {
|
|
170
|
-
out.push(spliceFor(fromText, fromMarks, g.from, g.to).map((v, i) => (i === 3 ? n : v)));
|
|
171
|
-
n += g.t.length - (g.to - g.from);
|
|
172
|
-
}
|
|
173
|
-
return out;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Private-use encoding for merge3Text: every marked UTF-16 unit, and every
|
|
177
|
-
// private-use unit, becomes one code of its own, so text and marks merge
|
|
178
|
-
// together and private-use input never decodes as something else.
|
|
179
|
-
export function encodeForMerge(values) {
|
|
180
|
-
const pua = (u) => u >= 0xe000 && u <= 0xf8ff;
|
|
181
|
-
const keyOf = (v, i) => (v.m[i] || pua(v.c.charCodeAt(i)) ? v.c[i] + "\u0000" + v.m[i] : null);
|
|
182
|
-
const keys = new Set();
|
|
183
|
-
for (const v of values) for (let i = 0; i < v.c.length; i++) { const k = keyOf(v, i); if (k !== null) keys.add(k); }
|
|
184
|
-
const sorted = [...keys].sort();
|
|
185
|
-
if (sorted.length > 0xf8ff - 0xe000 + 1) return null;
|
|
186
|
-
const code = new Map(sorted.map((k, i) => [k, String.fromCharCode(0xe000 + i)]));
|
|
187
|
-
const back = new Map(sorted.map((k, i) => [String.fromCharCode(0xe000 + i), k]));
|
|
188
|
-
const encode = (v) => { let out = ""; for (let i = 0; i < v.c.length; i++) { const k = keyOf(v, i); out += k === null ? v.c[i] : code.get(k); } return out; };
|
|
189
|
-
const decode = (s) => {
|
|
190
|
-
let c = "";
|
|
191
|
-
const m = [];
|
|
192
|
-
for (let i = 0; i < s.length; i++) {
|
|
193
|
-
const k = back.get(s[i]);
|
|
194
|
-
c += k ? k[0] : s[i];
|
|
195
|
-
m.push(k ? k.slice(2) : "");
|
|
196
|
-
}
|
|
197
|
-
return { c, m };
|
|
198
|
-
};
|
|
199
|
-
return { strings: values.map(encode), decode };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function createReplay(seed, { seq = 0, epoch = null, me = null } = {}) {
|
|
203
|
-
const r = {
|
|
204
|
-
me, epoch, seq, S: null, marks: null, prov: null, tomb: false,
|
|
205
|
-
log: [], logBytes: 0, floor: seq, kept: seq, baseHash: null, epochStart: seq, healH: -1, behind: false,
|
|
206
|
-
conflicts: [], stats: { ot: 0, fallback: 0, merge: 0, whole: 0, inexact: 0, rewinds: 0, refused: 0, agreed: 0, healMerges: 0 },
|
|
207
|
-
};
|
|
208
|
-
setWhole(r, seed, null, seq);
|
|
209
|
-
r.baseHash = hashOf(r.S);
|
|
210
|
-
return r;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function setWhole(r, v, a, q) {
|
|
214
|
-
if (!v || r.tomb) { r.S = null; r.marks = null; r.prov = null; return; }
|
|
215
|
-
const S = strip(v);
|
|
216
|
-
r.S = withMarks(S, S.c, marksOf(S));
|
|
217
|
-
r.marks = marksOf(S) ? [...S.m] : blank(S.c.length);
|
|
218
|
-
r.prov = Array.from({ length: S.c.length }, () => ({ a, q }));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function note(r, c) {
|
|
222
|
-
if (r.conflicts.some((x) => x.kind === c.kind && x.lost === c.lost && x.seq === c.seq && x.by === c.by)) return;
|
|
223
|
-
r.conflicts.push(c);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Text this tab typed that S no longer has, after a change that is not a
|
|
227
|
-
// placed write (a repair, a heal, a join, a whole write): it goes in a notice.
|
|
228
|
-
function noticeJump(r, before, beforeProv, kind, seq) {
|
|
229
|
-
if (!before || !beforeProv || !r.me) return;
|
|
230
|
-
const after = r.S ? r.S.c : "";
|
|
231
|
-
for (const g of regions(before.c, after)) {
|
|
232
|
-
if (g.to <= g.from) continue;
|
|
233
|
-
let mine = false;
|
|
234
|
-
for (let i = g.from; i < g.to; i++) if (beforeProv[i] && beforeProv[i].a === r.me) mine = true;
|
|
235
|
-
const lost = before.c.slice(g.from, g.to);
|
|
236
|
-
if (mine && lost.trim() && !g.t.includes(lost)) note(r, { kind, lost, kept: g.t, seq });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function push(r, entry) {
|
|
241
|
-
entry.bytes = entry.X ? JSON.stringify(entry.X).length : 8;
|
|
242
|
-
r.log.push(entry);
|
|
243
|
-
r.logBytes += entry.bytes;
|
|
244
|
-
while (r.log.length > LIMITS.logEntries || r.logBytes > LIMITS.logBytes) {
|
|
245
|
-
const gone = r.log.shift();
|
|
246
|
-
r.logBytes -= gone.bytes;
|
|
247
|
-
r.floor = Math.max(r.floor, gone.seq);
|
|
248
|
-
r.kept = Math.max(r.kept, gone.seq);
|
|
249
|
-
r.baseHash = gone.hash;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function hashAt(r, n) {
|
|
254
|
-
for (let i = r.log.length - 1; i >= 0; i--) if (r.log[i].seq <= n) return r.log[i].hash;
|
|
255
|
-
return n >= r.kept ? r.baseHash : null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const newEntry = (seq, X) => ({ seq, a: X && X.w ? X.w.a : null, X, events: [], authored: [], applied: [], whole: false, hash: null });
|
|
259
|
-
|
|
260
|
-
// The shard's tombstone for the block landed: it stays deleted. Writes under it
|
|
261
|
-
// are kept in notices; the writer's repair deletes the block again.
|
|
262
|
-
export function tombstone(r, seq) {
|
|
263
|
-
const entry = newEntry(seq, null);
|
|
264
|
-
entry.tombstone = true;
|
|
265
|
-
entry.whole = true;
|
|
266
|
-
r.tomb = true;
|
|
267
|
-
setWhole(r, null);
|
|
268
|
-
push(r, entry);
|
|
269
|
-
entry.hash = hashOf(r.S);
|
|
270
|
-
r.floor = Math.max(r.floor, seq);
|
|
271
|
-
r.seq = seq;
|
|
272
|
-
return r.S;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// One op on the block, in room order: X is the value set there, or null for
|
|
276
|
-
// its delete.
|
|
277
|
-
export function step(r, X, seq) {
|
|
278
|
-
const w = X && X.w;
|
|
279
|
-
const entry = newEntry(seq, X);
|
|
280
|
-
push(r, entry);
|
|
281
|
-
if (w && w.g !== undefined) repair(r, X, seq, entry);
|
|
282
|
-
else if (r.tomb) {
|
|
283
|
-
if (X && w && typeof w.a === "string") note(r, { kind: "removed", lost: X.c, by: w.a, seq });
|
|
284
|
-
entry.skipped = true;
|
|
285
|
-
} else if (w && w.h !== undefined) heal(r, X, seq, entry);
|
|
286
|
-
else apply(r, X, seq, entry, false);
|
|
287
|
-
if (entry.whole) r.floor = Math.max(r.floor, seq);
|
|
288
|
-
entry.hash = hashOf(r.S);
|
|
289
|
-
r.seq = seq;
|
|
290
|
-
return r.S;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function apply(r, X, seq, entry, forced) {
|
|
294
|
-
if (!X) { setWhole(r, null); entry.whole = true; return; }
|
|
295
|
-
const w = X.w;
|
|
296
|
-
if (!w || typeof w.a !== "string" || !r.S) {
|
|
297
|
-
if (!r.S && w && typeof w.a === "string") { note(r, { kind: "removed", lost: X.c, by: w.a, seq }); entry.skipped = true; return; }
|
|
298
|
-
// A create, a restore, a data API write: it wins whole.
|
|
299
|
-
return whole(r, X, null, seq, entry);
|
|
300
|
-
}
|
|
301
|
-
// Made in another epoch and sent again by its lane: its tab's heal carries it.
|
|
302
|
-
if (typeof w.e === "string" && w.e !== r.epoch) { entry.skipped = true; return; }
|
|
303
|
-
const ch = chainOf(X);
|
|
304
|
-
if (!ch) return whole(r, X, w.a, seq, entry);
|
|
305
|
-
entry.k = w.k;
|
|
306
|
-
if (!forced && w.e === r.epoch && Number.isInteger(w.k) && w.k >= r.floor && w.k <= r.seq) placeSteps(r, X, entry, ch.steps);
|
|
307
|
-
else placeByBase(r, X, entry, ch, seq);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function whole(r, X, a, seq, entry) {
|
|
311
|
-
const before = r.S, prov = r.prov;
|
|
312
|
-
setWhole(r, X, a, seq);
|
|
313
|
-
noticeJump(r, before, prov, "replaced", seq);
|
|
314
|
-
entry.whole = true;
|
|
315
|
-
r.stats.whole++;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Offset q of writer x's editor (S at k, then x's own steps since k) in S now.
|
|
319
|
-
function mapPos(r, x, k, own, q) {
|
|
320
|
-
const rec = new Map();
|
|
321
|
-
for (let j = own.length - 1; j >= 0; j--) {
|
|
322
|
-
const s = own[j], end = s.from + s.t.length;
|
|
323
|
-
if (q >= s.from && q <= end) { rec.set(j, q - s.from); q = s.from; }
|
|
324
|
-
else if (q > end) q -= s.t.length - (s.to - s.from);
|
|
325
|
-
}
|
|
326
|
-
let i = 0;
|
|
327
|
-
for (const e of r.log) {
|
|
328
|
-
if (e.seq <= k) continue;
|
|
329
|
-
for (const ev of e.events) {
|
|
330
|
-
if (ev.own) { if (e.a === x) { if (rec.has(i)) q = ev.at + rec.get(i); i++; } continue; }
|
|
331
|
-
const { from, to, t, a } = ev;
|
|
332
|
-
if (a === x) {
|
|
333
|
-
if (q > from && q < to) q = from;
|
|
334
|
-
else if (q >= to && !(q === from && from === to)) q += t.length - (to - from);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (q < from) continue;
|
|
338
|
-
if (q > to) { q += t.length - (to - from); continue; }
|
|
339
|
-
if (q === to && to > from) { q = from + t.length; continue; }
|
|
340
|
-
q = x < a ? from : from + t.length;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return q;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const ownSteps = (r, x, k, except) => r.log.filter((e) => e !== except && e.seq > k && e.a === x).flatMap((e) => e.authored);
|
|
347
|
-
|
|
348
|
-
// 1. The step mapper.
|
|
349
|
-
function placeSteps(r, X, entry, steps) {
|
|
350
|
-
const { a: x, k } = X.w;
|
|
351
|
-
const own = ownSteps(r, x, k, entry);
|
|
352
|
-
const fields = fieldsOf(r, X);
|
|
353
|
-
let c = r.S.c, marks = r.marks, prov = r.prov;
|
|
354
|
-
const seen = (pv) => pv.q <= k || pv.a === x;
|
|
355
|
-
for (const s of steps) {
|
|
356
|
-
const F = mapPos(r, x, k, own, s.from);
|
|
357
|
-
const T = s.to === s.from ? F : Math.max(F, mapPos(r, x, k, own, s.to));
|
|
358
|
-
if (s.to > s.from && s.t === s.removed) {
|
|
359
|
-
// Only the marks changed: the range keeps its text, including what peers
|
|
360
|
-
// typed inside it, and takes the writer's marks.
|
|
361
|
-
const uniform = s.tm.every((key) => key === s.tm[0]);
|
|
362
|
-
const next = marks.slice();
|
|
363
|
-
let j = 0;
|
|
364
|
-
for (let p = F; p < T; p++) {
|
|
365
|
-
if (uniform) next[p] = s.tm[0];
|
|
366
|
-
else if (seen(prov[p])) { while (j < s.t.length && s.t[j] !== c[p]) j++; if (j < s.t.length) next[p] = s.tm[j++]; }
|
|
367
|
-
}
|
|
368
|
-
entry.applied.push([F, T, c.slice(F, T), c.length, marks.slice(F, T)]);
|
|
369
|
-
marks = next;
|
|
370
|
-
} else {
|
|
371
|
-
for (let p = T - 1; p >= F; p--) {
|
|
372
|
-
if (!seen(prov[p])) continue;
|
|
373
|
-
let q = p;
|
|
374
|
-
while (q - 1 >= F && seen(prov[q - 1])) q--;
|
|
375
|
-
entry.applied.push([q, p + 1, c.slice(q, p + 1), c.length, marks.slice(q, p + 1)]);
|
|
376
|
-
entry.events.push({ from: q, to: p + 1, t: "", a: x });
|
|
377
|
-
c = c.slice(0, q) + c.slice(p + 1);
|
|
378
|
-
marks = put(marks, q, p + 1, []);
|
|
379
|
-
prov = put(prov, q, p + 1, []);
|
|
380
|
-
p = q;
|
|
381
|
-
}
|
|
382
|
-
if (s.t) {
|
|
383
|
-
entry.applied.push([F, F, "", c.length]);
|
|
384
|
-
entry.events.push({ from: F, to: F, t: s.t, a: x });
|
|
385
|
-
c = c.slice(0, F) + s.t + c.slice(F);
|
|
386
|
-
marks = put(marks, F, F, s.tm);
|
|
387
|
-
prov = put(prov, F, F, Array.from({ length: s.t.length }, () => ({ a: x, q: entry.seq })));
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
entry.events.push({ own: true, at: F });
|
|
391
|
-
own.push(s);
|
|
392
|
-
entry.authored.push(s);
|
|
393
|
-
r.S = withMarks(fields, c, marks);
|
|
394
|
-
r.marks = marks;
|
|
395
|
-
r.prov = prov;
|
|
396
|
-
}
|
|
397
|
-
r.stats.ot++;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 2. The base the write describes, placed by a diff; 3. merge3Text.
|
|
401
|
-
function placeByBase(r, X, entry, ch, seq, { skip = () => false, heal = false } = {}) {
|
|
402
|
-
const x = X.w.a;
|
|
403
|
-
const fields = fieldsOf(r, X);
|
|
404
|
-
let base = ch.base.c;
|
|
405
|
-
let c = r.S.c, marks = r.marks, prov = r.prov;
|
|
406
|
-
const events = [], applied = [], notes = [];
|
|
407
|
-
let ok = true;
|
|
408
|
-
ch.steps.forEach((s, i) => {
|
|
409
|
-
if (!ok) return;
|
|
410
|
-
const nextBase = base.slice(0, s.from) + s.t + base.slice(s.to);
|
|
411
|
-
if (skip(i)) { base = nextBase; return; }
|
|
412
|
-
const placed = placeSplice(base, c, s);
|
|
413
|
-
if (!placed) { ok = false; return; }
|
|
414
|
-
const { from, to } = placed;
|
|
415
|
-
const gone = c.slice(from, to);
|
|
416
|
-
if (s.to > s.from && gone !== s.removed && s.t !== s.removed) notes.push({ kind: "inexact", lost: s.removed, kept: gone, by: x, seq });
|
|
417
|
-
applied.push([from, to, gone, c.length, marks.slice(from, to)]);
|
|
418
|
-
events.push({ from, to, t: s.t, a: x }, { own: true, at: from });
|
|
419
|
-
c = c.slice(0, from) + s.t + c.slice(to);
|
|
420
|
-
marks = put(marks, from, to, s.tm);
|
|
421
|
-
prov = put(prov, from, to, Array.from({ length: s.t.length }, () => ({ a: x, q: seq })));
|
|
422
|
-
base = nextBase;
|
|
423
|
-
});
|
|
424
|
-
if (ok) {
|
|
425
|
-
entry.events.push(...events);
|
|
426
|
-
entry.applied.push(...applied);
|
|
427
|
-
if (!heal) entry.authored.push(...ch.steps);
|
|
428
|
-
for (const n of notes) { note(r, n); r.stats.inexact++; }
|
|
429
|
-
r.S = withMarks(fields, c, marks);
|
|
430
|
-
r.marks = marks;
|
|
431
|
-
r.prov = prov;
|
|
432
|
-
r.stats.fallback++;
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (heal) {
|
|
436
|
-
ch.steps.forEach((s, i) => { if (!skip(i) && s.t.trim()) note(r, { kind: "heal", lost: s.t, by: x, seq }); });
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
const values = [ch.base, { c: r.S.c, m: r.marks }, { c: X.c, m: marksOf(X) ?? blank(X.c.length) }];
|
|
440
|
-
const enc = encodeForMerge(values);
|
|
441
|
-
const merged = enc ? merge3Text(enc.strings[0], enc.strings[1], enc.strings[2], "remote") : merge3Text(values[0].c, values[1].c, values[2].c, "remote");
|
|
442
|
-
const out = enc ? enc.decode(merged.text) : { c: merged.text, m: blank(merged.text.length) };
|
|
443
|
-
for (const cf of merged.conflicts) {
|
|
444
|
-
const lost = enc ? enc.decode(cf.local).c : cf.local;
|
|
445
|
-
if (lost.trim()) note(r, { kind: "merge", lost, kept: enc ? enc.decode(cf.remote).c : cf.remote, by: x, seq });
|
|
446
|
-
}
|
|
447
|
-
if (!enc) note(r, { kind: "marks", lost: "", by: x, seq });
|
|
448
|
-
r.S = withMarks(fields, out.c, out.m);
|
|
449
|
-
r.marks = out.m;
|
|
450
|
-
r.prov = Array.from({ length: out.c.length }, () => ({ a: null, q: seq }));
|
|
451
|
-
entry.whole = true;
|
|
452
|
-
r.stats.merge++;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// A repair: S as the writer computed it through seq g. A replay with the same
|
|
456
|
-
// hash at g changes nothing. One that differs takes it and places every later
|
|
457
|
-
// write it holds on top, when it holds them all (g at or above `kept`);
|
|
458
|
-
// otherwise it refuses the repair and is behind until it can take one.
|
|
459
|
-
function repair(r, X, seq, entry) {
|
|
460
|
-
const n = X.w.g;
|
|
461
|
-
entry.repair = n;
|
|
462
|
-
if (X.w.e !== r.epoch || r.tomb) return;
|
|
463
|
-
const mine = hashAt(r, n);
|
|
464
|
-
if (mine !== null && mine === hashOf(X)) { r.stats.agreed++; settled(r, n); return; }
|
|
465
|
-
if (Number.isInteger(n) && n >= r.kept && n <= r.seq) {
|
|
466
|
-
rewind(r, n, X, seq, entry);
|
|
467
|
-
r.behind = false;
|
|
468
|
-
settled(r, n);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
r.stats.refused++;
|
|
472
|
-
r.behind = true;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// The writer placed every write through seq n exactly: a delete this replay
|
|
476
|
-
// could only place by a diff before then is no longer in doubt.
|
|
477
|
-
function settled(r, n) {
|
|
478
|
-
r.conflicts = r.conflicts.filter((c) => !(c.kind === "inexact" && c.seq <= n));
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// S at seq n becomes V, and every later op this replay holds is applied again
|
|
482
|
-
// on top, by its base (its steps were made against the old S). The step mapper
|
|
483
|
-
// resumes for writes made at or after seq m.
|
|
484
|
-
function rewind(r, n, V, m, trigger) {
|
|
485
|
-
r.stats.rewinds++;
|
|
486
|
-
const before = r.S, beforeProv = r.prov;
|
|
487
|
-
const again = r.log.filter((e) => e.seq > n && e !== trigger);
|
|
488
|
-
r.log = [];
|
|
489
|
-
r.logBytes = 0;
|
|
490
|
-
r.tomb = false;
|
|
491
|
-
setWhole(r, V, null, n);
|
|
492
|
-
r.floor = n;
|
|
493
|
-
r.kept = n;
|
|
494
|
-
r.baseHash = hashOf(r.S);
|
|
495
|
-
for (const old of again) {
|
|
496
|
-
const entry = newEntry(old.seq, old.X);
|
|
497
|
-
push(r, entry);
|
|
498
|
-
const w = old.X && old.X.w;
|
|
499
|
-
if (old.tombstone) { entry.tombstone = true; entry.whole = true; r.tomb = true; setWhole(r, null); }
|
|
500
|
-
else if (w && w.g !== undefined) entry.repair = w.g;
|
|
501
|
-
else if (r.tomb) entry.skipped = true;
|
|
502
|
-
else if (w && w.h !== undefined) { entry.heal = true; healMerge(r, old.X, old.seq, entry); }
|
|
503
|
-
else apply(r, old.X, old.seq, entry, true);
|
|
504
|
-
entry.hash = hashOf(r.S);
|
|
505
|
-
}
|
|
506
|
-
push(r, trigger);
|
|
507
|
-
trigger.whole = true;
|
|
508
|
-
r.floor = Math.max(r.floor, m);
|
|
509
|
-
noticeJump(r, before, beforeProv, "repair", m);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// A heal after a room loss: the heal from the longest stream of the lost room
|
|
513
|
-
// is S at the new epoch's start, and every other op of the epoch is applied on
|
|
514
|
-
// it again; a shorter heal adds only its writes the longer stream had not seen.
|
|
515
|
-
function heal(r, X, seq, entry) {
|
|
516
|
-
entry.heal = true;
|
|
517
|
-
if (X.w.e !== r.epoch) { entry.skipped = true; return; }
|
|
518
|
-
if (X.w.h > r.healH) {
|
|
519
|
-
r.healH = X.w.h;
|
|
520
|
-
if (r.kept <= r.epochStart) return rewind(r, r.epochStart, X, seq, entry);
|
|
521
|
-
whole(r, X, null, seq, entry);
|
|
522
|
-
r.behind = true;
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
healMerge(r, X, seq, entry);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function healMerge(r, X, seq, entry) {
|
|
529
|
-
const ch = chainOf(X);
|
|
530
|
-
if (!ch || !r.S) return;
|
|
531
|
-
const u = Array.isArray(X.w.u) ? X.w.u : [];
|
|
532
|
-
r.stats.healMerges++;
|
|
533
|
-
placeByBase(r, X, entry, ch, seq, { heal: true, skip: (i) => Number.isInteger(u[i]) && u[i] <= r.healH });
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// The block's value from a state read: S becomes it. `moves` are the room's ops
|
|
537
|
-
// after `since` that the value already holds ({ seq, X } or { seq, tombstone }),
|
|
538
|
-
// kept so a later repair or heal can apply them again. In a new epoch whose
|
|
539
|
-
// moves hold a heal, the ops from the first heal on are stepped as every other
|
|
540
|
-
// tab stepped them, so this tab computes the same S as one that was there.
|
|
541
|
-
export function join(r, X, seq, { epoch = r.epoch, moves = null, since = seq, tomb = false } = {}) {
|
|
542
|
-
const before = r.S, beforeProv = r.prov;
|
|
543
|
-
const fresh = epoch !== r.epoch;
|
|
544
|
-
r.epoch = epoch;
|
|
545
|
-
r.tomb = tomb;
|
|
546
|
-
setWhole(r, X, null, seq);
|
|
547
|
-
r.log = [];
|
|
548
|
-
r.logBytes = 0;
|
|
549
|
-
r.seq = seq;
|
|
550
|
-
r.floor = seq;
|
|
551
|
-
r.kept = moves ? since : seq;
|
|
552
|
-
r.baseHash = hashOf(r.S);
|
|
553
|
-
r.behind = false;
|
|
554
|
-
if (fresh) { r.healH = -1; r.epochStart = r.kept; }
|
|
555
|
-
const list = moves ?? [];
|
|
556
|
-
const first = fresh ? list.findIndex((mv) => mv.X && mv.X.w && mv.X.w.h !== undefined) : -1;
|
|
557
|
-
const held = first < 0 ? list : list.slice(0, first);
|
|
558
|
-
for (const mv of held) {
|
|
559
|
-
const entry = newEntry(mv.seq, mv.X ?? null);
|
|
560
|
-
entry.whole = true;
|
|
561
|
-
entry.tombstone = mv.tombstone === true;
|
|
562
|
-
push(r, entry);
|
|
563
|
-
}
|
|
564
|
-
if (held.length) { r.log[r.log.length - 1].hash = first < 0 ? hashOf(r.S) : null; r.baseHash = null; }
|
|
565
|
-
if (first >= 0) {
|
|
566
|
-
r.seq = held.length ? held[held.length - 1].seq : since;
|
|
567
|
-
for (const mv of list.slice(first)) mv.tombstone ? tombstone(r, mv.seq) : step(r, mv.X ?? null, mv.seq);
|
|
568
|
-
r.seq = Math.max(r.seq, seq);
|
|
569
|
-
}
|
|
570
|
-
if (!fresh) noticeJump(r, before, beforeProv, "joined", seq);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// The caret of writer `me`, whose editor is S at k plus its own steps since.
|
|
574
|
-
export function mapCaret(r, me, k, q, viewText) {
|
|
575
|
-
if (!r.S) return 0;
|
|
576
|
-
if (Number.isInteger(k) && k >= r.floor && k <= r.seq) return Math.max(0, Math.min(r.S.c.length, mapPos(r, me, k, ownSteps(r, me, k), q)));
|
|
577
|
-
return Math.max(0, Math.min(r.S.c.length, mapOffset(viewText ?? r.S.c, r.S.c, q)));
|
|
578
|
-
}
|