@panphora/clayjs 1.13.1 → 1.15.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 (146) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69385 -40992
  3. package/dist/server/checkpoint.cjs +1 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/core.cjs +72210 -0
  6. package/dist/server/rooms.cjs +1 -0
  7. package/dist/server/rooms.mjs +66 -0
  8. package/docs/guide.md +127 -99
  9. package/entries/clay.js +22 -0
  10. package/entries/sap.js +5 -5
  11. package/package.json +16 -2
  12. package/packed-contract.json +18 -3
  13. package/src/core/boot-phase.js +6 -0
  14. package/src/core/conflict-download.js +12 -3
  15. package/src/core/host-meta.js +75 -0
  16. package/src/core/persist.js +11 -0
  17. package/src/core/save-core.js +266 -31
  18. package/src/core/save.js +154 -36
  19. package/src/core/snapshot.js +51 -13
  20. package/src/core/style-comparisons.js +37 -0
  21. package/src/dom/insert-style-tag.js +11 -12
  22. package/src/lib/derived.js +116 -0
  23. package/src/lib/effect.js +130 -0
  24. package/src/lib/mutation.js +3 -1
  25. package/src/lib/style-adoption.js +9 -0
  26. package/src/lib/style.js +47 -0
  27. package/src/live/fast-lane.js +347 -60
  28. package/src/live/local-lane.js +32 -6
  29. package/src/live/ops.js +77 -0
  30. package/src/live/provenance.js +129 -0
  31. package/src/live/save-history.js +188 -0
  32. package/src/live/server-lane.js +9 -2
  33. package/src/live/store.js +194 -16
  34. package/src/loader-logic.js +16 -4
  35. package/src/loader.js +37 -1
  36. package/src/page/address.js +20 -0
  37. package/src/page/adopt-boot.js +189 -0
  38. package/src/page/adopt.js +240 -0
  39. package/src/page/ai-tool.js +188 -0
  40. package/src/page/announce.js +21 -0
  41. package/src/page/blocks.js +1170 -0
  42. package/src/page/blocksel.js +269 -0
  43. package/src/page/board-drag.js +226 -0
  44. package/src/page/board-panel.js +353 -0
  45. package/src/page/board-style.js +146 -0
  46. package/src/page/board-view.js +447 -0
  47. package/src/page/board.js +41 -0
  48. package/src/page/chart.js +220 -0
  49. package/src/page/checklist.js +143 -0
  50. package/src/page/chrome.js +587 -0
  51. package/src/page/csv-import.js +169 -0
  52. package/src/page/drag.js +202 -0
  53. package/src/page/drop.js +112 -0
  54. package/src/page/editor.js +386 -0
  55. package/src/page/highlight.js +268 -0
  56. package/src/page/inline.js +60 -1
  57. package/src/page/items.js +159 -0
  58. package/src/page/link.js +247 -0
  59. package/src/page/markdown.js +71 -0
  60. package/src/page/media.js +570 -0
  61. package/src/page/merge.js +130 -0
  62. package/src/page/model.js +71 -9
  63. package/src/page/placeholder.js +23 -0
  64. package/src/page/projection.js +647 -0
  65. package/src/page/recover.js +193 -0
  66. package/src/page/rich/cell.js +16 -0
  67. package/src/page/rich/chart.js +104 -0
  68. package/src/page/rich/csv.js +118 -0
  69. package/src/page/rich/page-tool.js +86 -0
  70. package/src/page/rich/tool-runtime.js +195 -0
  71. package/src/page/save-projection.js +53 -0
  72. package/src/page/save-states.js +85 -0
  73. package/src/page/schema.js +12 -5
  74. package/src/page/table-kinds.js +20 -0
  75. package/src/page/table-paste.js +57 -0
  76. package/src/page/table-select.js +244 -0
  77. package/src/page/table-style.js +25 -0
  78. package/src/page/table.js +650 -0
  79. package/src/page/themes/content.css +46 -0
  80. package/src/page/themes/light-1.css +269 -0
  81. package/src/page/themes/light-2.css +211 -0
  82. package/src/page/themes/light-3.css +130 -0
  83. package/src/page/themes/light-4.css +299 -0
  84. package/src/page/tool-data.js +25 -0
  85. package/src/page/tool-view.js +68 -0
  86. package/src/page/tools.js +133 -0
  87. package/src/page/view.js +136 -0
  88. package/src/page/ybind/author.js +25 -0
  89. package/src/page/ybind/binding.js +92 -0
  90. package/src/page/ybind/board-commands.js +170 -0
  91. package/src/page/ybind/board.js +89 -0
  92. package/src/page/ybind/bridge.js +382 -0
  93. package/src/page/ybind/conversion.js +26 -0
  94. package/src/page/ybind/core.js +7 -0
  95. package/src/page/ybind/decode.js +208 -0
  96. package/src/page/ybind/doc.js +58 -0
  97. package/src/page/ybind/external.js +190 -0
  98. package/src/page/ybind/group.js +124 -0
  99. package/src/page/ybind/index.js +12 -0
  100. package/src/page/ybind/layout.js +27 -0
  101. package/src/page/ybind/marks.js +18 -0
  102. package/src/page/ybind/migrate.js +152 -0
  103. package/src/page/ybind/moves.js +288 -0
  104. package/src/page/ybind/origins.js +257 -0
  105. package/src/page/ybind/parts.js +44 -0
  106. package/src/page/ybind/projection-map.js +92 -0
  107. package/src/page/ybind/provider.js +285 -0
  108. package/src/page/ybind/recovery.js +411 -0
  109. package/src/page/ybind/seed.js +27 -0
  110. package/src/page/ybind/table.js +166 -0
  111. package/src/page/ybind/tokens.js +93 -0
  112. package/src/page/ybind/walk.js +333 -0
  113. package/src/page/ybind/writer.js +307 -0
  114. package/src/plugins/derive.js +12 -0
  115. package/src/plugins/indicator.js +16 -6
  116. package/src/plugins/live.js +742 -65
  117. package/src/plugins/page.js +547 -0
  118. package/src/plugins/undo.js +62 -4
  119. package/src/server/checkpoint/index.js +218 -0
  120. package/src/server/rooms/index.js +59 -0
  121. package/src/server/rooms/live-data-ops.js +369 -0
  122. package/src/server/rooms/live-data-rooms.js +965 -0
  123. package/src/server/rooms/live-data-tags.js +49 -0
  124. package/src/server/rooms/live-page-history.js +41 -0
  125. package/src/server/rooms/live-provenance.js +142 -0
  126. package/src/server/rooms/page-admission.js +79 -0
  127. package/src/server/rooms/page-conversion.js +62 -0
  128. package/src/server/rooms/page-rebase.js +108 -0
  129. package/src/server/rooms/page-state.js +91 -0
  130. package/src/server/rooms/page-write-admission.js +63 -0
  131. package/src/server/rooms/page-write-guard.js +23 -0
  132. package/src/server/rooms/recent.js +18 -0
  133. package/src/server/rooms/spec-wire.js +17 -0
  134. package/src/standalone.js +22 -0
  135. package/src/sync/conflict-revert.js +2 -0
  136. package/src/sync/conflicts.js +2 -2
  137. package/src/sync/live-sync.js +36 -8
  138. package/src/sync/stream.js +62 -16
  139. package/src/ui/index.js +4 -0
  140. package/src/ui/toast.js +21 -5
  141. package/src/vendor/pierre/pierre.vendor.js +3 -0
  142. package/src/vendor/sap-core.vendor.js +9 -0
  143. package/src/vendor/sap-ownership.vendor.js +111 -0
  144. package/src/vendor/yjs.vendor.js +7 -0
  145. package/src/page/binding.js +0 -647
  146. package/src/page/replay.js +0 -578
@@ -0,0 +1,288 @@
1
+ import * as Y from "../../vendor/yjs.vendor.js";
2
+ import { LOCAL, REMOTE_META, mintOp } from "./layout.js";
3
+ import { state as pmState } from "../../vendor/prosemirror.vendor.js";
4
+ import { walk } from "./walk.js";
5
+ import { docOf } from "./doc.js";
6
+ import { createProjectionMap } from "./projection-map.js";
7
+ import { unitIds } from "./origins.js";
8
+ import { attrKey, copyActiveKey, parseGoneIntents, physicalKeyOf, recoveredRoot, removeKey, restoreKey } from "./recovery.js";
9
+
10
+ const refuse = (code) => ({ status: "refused", code });
11
+
12
+ const attrsKey = (attrs) => {
13
+ if (!attrs) return "";
14
+ const out = [];
15
+ for (const key of Object.keys(attrs).sort()) out.push(`${key}=${JSON.stringify(attrs[key])}`);
16
+ return out.join("\n");
17
+ };
18
+
19
+ const visibleBlocks = (pd, current, view) => {
20
+ const doc = view ? view.state.doc : null;
21
+ const settled = doc && doc.childCount === current.visible.length && current.visible.every((seg, index) => doc.child(index).attrs.id === seg.id);
22
+ const source = settled ? doc : docOf(pd, current);
23
+ const out = [];
24
+ source.forEach((node) => {
25
+ if (node.attrs.id === null || node.attrs.id === undefined) return;
26
+ out.push({ id: node.attrs.id, parent: node.attrs.parent ?? null, t: node.type.name });
27
+ });
28
+ return out;
29
+ };
30
+
31
+ const ancestors = (byId, id) => {
32
+ const out = [];
33
+ let cur = byId.get(id)?.parent ?? null;
34
+ let guard = 0;
35
+ while (cur !== null && cur !== undefined && guard++ <= byId.size) {
36
+ out.push(cur);
37
+ cur = byId.get(cur)?.parent ?? null;
38
+ }
39
+ return out;
40
+ };
41
+
42
+ const isAncestor = (byId, ancestor, id) => ancestors(byId, id).includes(ancestor);
43
+ const inSubtree = (byId, id, root) => id === root || isAncestor(byId, root, id);
44
+
45
+ const setAttr = (pd, seg, id, field, value) => {
46
+ if (seg?.recovered) {
47
+ const key = attrKey(seg.sourceKey, field);
48
+ if ((pd.gone.has(key) ? pd.gone.get(key) : seg.fields?.[field] ?? null) !== value) pd.gone.set(key, value);
49
+ return;
50
+ }
51
+ let map = pd.attrs.get(id);
52
+ if (!map) { map = new Y.Map(); pd.attrs.set(id, map); }
53
+ if (map.get(field) !== value) map.set(field, value);
54
+ };
55
+
56
+ const setParent = (pd, seg, id, parent) => setAttr(pd, seg, id, "parent", parent);
57
+
58
+ const MAX_INDENT = 8;
59
+ const ATTR_RULES = {
60
+ indent: (value, block) => block.t === "list_item" && Number.isInteger(value) && value >= 0 && value <= MAX_INDENT,
61
+ open: (value, block) => block.t === "toggle" && typeof value === "boolean",
62
+ };
63
+
64
+ const attrChanges = (args, byId) => {
65
+ if (args?.attrs === undefined) return [];
66
+ if (!args.attrs || typeof args.attrs !== "object" || Array.isArray(args.attrs)) return null;
67
+ const out = [];
68
+ for (const [id, fields] of Object.entries(args.attrs)) {
69
+ const block = byId.get(id);
70
+ if (!block || !fields || typeof fields !== "object" || Array.isArray(fields)) return null;
71
+ for (const [field, value] of Object.entries(fields)) {
72
+ if (!ATTR_RULES[field] || !ATTR_RULES[field](value, block)) return null;
73
+ out.push({ id, field, value });
74
+ }
75
+ }
76
+ return out;
77
+ };
78
+
79
+ const flowUnits = (pd, origins) => {
80
+ const ids = unitIds(pd.flow, 0, pd.flow.length);
81
+ const out = [];
82
+ let at = 0;
83
+ for (const part of pd.flow.toDelta()) {
84
+ const attrs = part.attributes;
85
+ if (typeof part.insert === "string") {
86
+ for (let i = 0; i < part.insert.length; i++) out.push({ ch: part.insert[i], attrs, source: origins.resolve(ids[at++]) });
87
+ } else out.push({ embed: part.insert, attrs, source: origins.resolve(ids[at++]) });
88
+ }
89
+ if (at !== ids.length) throw new Error(`The flow delta has ${at} units but the document has ${ids.length}`);
90
+ return out;
91
+ };
92
+
93
+ const liveRemovals = (pd, current, selected) => {
94
+ const observed = new Map();
95
+ for (const seg of current.visible) if (selected.has(seg.id) && seg.brk) observed.set(physicalKeyOf(seg.brk), seg.id);
96
+ for (const id of selected) for (const key of current.copies.get(id) ?? []) observed.set(key, id);
97
+ const out = [];
98
+ for (let it = pd.flow._start; it; it = it.right) {
99
+ if (it.deleted || !(it.content instanceof Y.ContentEmbed)) continue;
100
+ const id = it.content.embed?.b;
101
+ if (id === undefined || observed.get(physicalKeyOf(it.id)) !== id) continue;
102
+ let end = null;
103
+ for (let next = it.right; next; next = next.right) {
104
+ if (next.deleted || !(next.content instanceof Y.ContentEmbed)) continue;
105
+ const other = next.content.embed?.b;
106
+ if (other === undefined || other === id) continue;
107
+ end = physicalKeyOf(next.id);
108
+ break;
109
+ }
110
+ out.push({ key: physicalKeyOf(it.id), blockId: id, end });
111
+ }
112
+ return out;
113
+ };
114
+
115
+ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
116
+ const ids = Array.isArray(args?.ids) ? [...new Set(args.ids)] : [];
117
+ const afterId = args?.afterId === undefined ? null : args.afterId;
118
+ const hasParent = args?.parent !== undefined;
119
+
120
+ const current = walk(pd, origins);
121
+ const list = visibleBlocks(pd, current, view);
122
+ const byId = new Map(list.map((block) => [block.id, block]));
123
+ const order = list.map((block) => block.id);
124
+
125
+ if (!ids.length) return refuse("missing-source");
126
+ for (const id of ids) if (!byId.has(id)) return refuse("missing-source");
127
+ const changes = attrChanges(args, byId);
128
+ if (!changes) return refuse("invalid-attrs");
129
+
130
+ const roots = new Set(ids.filter((id) => !ids.some((other) => other !== id && isAncestor(byId, other, id))));
131
+ const selected = list.filter((block) => ids.some((root) => inSubtree(byId, block.id, root)));
132
+ const selectedSet = new Set(selected.map((block) => block.id));
133
+
134
+ const segments = new Map(current.visible.map((seg) => [seg.id, seg]));
135
+ for (const block of selected) {
136
+ const seg = segments.get(block.id);
137
+ if (!seg || seg.implicit || !seg.brk) return refuse("missing-source");
138
+ }
139
+
140
+ let parent;
141
+ if (hasParent) parent = args.parent ?? null;
142
+ else {
143
+ const parents = new Set(selected.filter((block) => roots.has(block.id)).map((block) => block.parent));
144
+ if (parents.size !== 1) return refuse("invalid-parent");
145
+ parent = [...parents][0];
146
+ }
147
+ if (hasParent && parent !== null) {
148
+ const node = byId.get(parent);
149
+ if (!node || node.t !== "toggle") return refuse("invalid-parent");
150
+ if (selectedSet.has(parent)) return refuse("cycle");
151
+ }
152
+ if (afterId !== null) {
153
+ if (typeof afterId !== "string" || !byId.has(afterId)) return refuse("missing-destination");
154
+ if (selectedSet.has(afterId)) return refuse("cycle");
155
+ if (byId.get(afterId).parent !== parent) return refuse("missing-destination");
156
+ }
157
+
158
+ const remaining = list.filter((block) => !selectedSet.has(block.id));
159
+ let insertAt = 0;
160
+ if (afterId === null) {
161
+ if (parent !== null) {
162
+ const first = remaining.findIndex((block) => block.parent === parent);
163
+ const own = remaining.findIndex((block) => block.id === parent);
164
+ insertAt = first >= 0 ? first : own < 0 ? 0 : own + 1;
165
+ }
166
+ } else {
167
+ insertAt = remaining.findIndex((block) => block.id === afterId) + 1;
168
+ while (insertAt < remaining.length && inSubtree(byId, remaining[insertAt].id, afterId)) insertAt++;
169
+ }
170
+
171
+ const desired = [...remaining.slice(0, insertAt), ...selected, ...remaining.slice(insertAt)];
172
+ const sameOrder = desired.length === order.length && desired.every((block, index) => block.id === order[index]);
173
+ const movedRoots = selected.filter((block) => roots.has(block.id) && block.parent !== parent);
174
+ const movedIds = selected.map((block) => block.id);
175
+ if (sameOrder && !movedRoots.length && !changes.length) return { status: "unchanged" };
176
+
177
+ if (sameOrder) {
178
+ um?.stopCapturing();
179
+ pd.ydoc.transact(() => {
180
+ for (const block of movedRoots) setParent(pd, segments.get(block.id), block.id, parent);
181
+ for (const change of changes) setAttr(pd, segments.get(change.id), change.id, change.field, change.value);
182
+ }, LOCAL);
183
+ um?.stopCapturing();
184
+ render();
185
+ return { status: "moved", ids: movedIds };
186
+ }
187
+
188
+ const sv = [...Y.decodeStateVector(Y.encodeStateVector(pd.ydoc)).entries()].sort((left, right) => left[0] - right[0]);
189
+ const operationId = mintOp(pd);
190
+ const units = flowUnits(pd, origins);
191
+ const before = createProjectionMap(current, pd.flow.length);
192
+ const captured = [];
193
+ let cursor = 0;
194
+ for (const seg of current.visible) {
195
+ if (selectedSet.has(seg.id)) {
196
+ const block = [];
197
+ for (const [from, to] of before.physicalRanges(cursor, cursor + seg.len)) for (let unit = from; unit < to; unit++) block.push(units[unit]);
198
+ captured.push({ seg, units: block });
199
+ }
200
+ cursor += seg.len;
201
+ }
202
+ for (let index = 0; index < captured.length; index++) {
203
+ const block = captured[index];
204
+ if (block.seg.id !== movedIds[index]) throw new Error(`Captured ${block.seg.id} where ${movedIds[index]} was expected`);
205
+ if (!block.units.length || block.units[0].embed === undefined || block.units[0].embed.b !== block.seg.id) throw new Error(`The selected block ${block.seg.id} has no physical marker to copy`);
206
+ }
207
+
208
+ const removals = liveRemovals(pd, current, selectedSet);
209
+ const intents = parseGoneIntents(pd.gone);
210
+ const promotions = new Map();
211
+ for (const block of selected) {
212
+ const seg = segments.get(block.id);
213
+ if (!seg.recovered) continue;
214
+ const removalsOf = current.removals.filter((removal) => removal.sourceKey === seg.sourceKey);
215
+ const winner = recoveredRoot(seg.sourceKey, removalsOf, intents);
216
+ const baseline = intents.restores.find((restore) => restore.key === winner?.key)?.baseline ?? { type: seg.t, attrs: seg.fields ?? {} };
217
+ promotions.set(seg.sourceKey, { covers: removalsOf.map((removal) => removal.key), baseline: JSON.parse(JSON.stringify(baseline)) });
218
+ }
219
+ const caret = (() => {
220
+ const sel = view?.state.selection;
221
+ if (!(sel instanceof pmState.TextSelection)) return null;
222
+ const at = (pos) => { const $pos = view.state.doc.resolve(pos); return $pos.depth ? { id: $pos.node(1).attrs.id, offset: $pos.parentOffset } : null; };
223
+ const anchor = at(sel.anchor), head = at(sel.head);
224
+ return anchor && head && (selectedSet.has(anchor.id) || selectedSet.has(head.id)) ? { anchor, head } : null;
225
+ })();
226
+ if (!removals.length) throw new Error("The selected blocks have no live physical markers to retire");
227
+
228
+ um?.stopCapturing();
229
+ pd.ydoc.transact(() => {
230
+ for (const removal of removals) pd.gone.set(removeKey(removal.key, operationId), { blockId: removal.blockId, end: removal.end, sv });
231
+
232
+ const after = walk(pd, origins);
233
+ const map = createProjectionMap(after, pd.flow.length);
234
+ const afterIds = after.visible.map((seg) => seg.id);
235
+ let index = 0;
236
+ if (insertAt > 0) {
237
+ const found = afterIds.indexOf(remaining[insertAt - 1].id);
238
+ index = found < 0 ? 0 : found + 1;
239
+ }
240
+ let position = 0;
241
+ for (let i = 0; i < index; i++) position += after.visible[i].len;
242
+ let cursor = map.toPhysical(position, "before");
243
+ const spans = [];
244
+ for (const block of captured) {
245
+ const start = cursor;
246
+ let text = null, textAttrs = null;
247
+ const flush = () => {
248
+ if (text === null) return;
249
+ pd.flow.insert(cursor, text, textAttrs ?? undefined);
250
+ cursor += text.length;
251
+ text = null;
252
+ };
253
+ for (const unit of block.units) {
254
+ if (unit.ch !== undefined) {
255
+ if (text !== null && attrsKey(unit.attrs) !== attrsKey(textAttrs)) flush();
256
+ if (text === null) { text = ""; textAttrs = unit.attrs; }
257
+ text += unit.ch;
258
+ } else {
259
+ flush();
260
+ pd.flow.insertEmbed(cursor, unit.embed, unit.attrs);
261
+ cursor += 1;
262
+ }
263
+ }
264
+ flush();
265
+ const dest = unitIds(pd.flow, start, cursor);
266
+ if (dest.length !== block.units.length) throw new Error(`Copied ${block.seg.id} as ${dest.length} units instead of ${block.units.length}`);
267
+ for (let i = 1; i < block.units.length; i++) spans.push({ dest: dest[i], source: block.units[i].source });
268
+ origins.recordBoundary(dest[0], block.seg.recovered ? { blockId: block.seg.id, kind: "copy", recoveryOf: block.seg.sourceKey } : { blockId: block.seg.id, kind: "copy" });
269
+ if (block.seg.recovered) pd.gone.set(restoreKey(block.seg.sourceKey, operationId), promotions.get(block.seg.sourceKey));
270
+ pd.gone.set(copyActiveKey(physicalKeyOf(dest[0])), true);
271
+ }
272
+ if (spans.length) origins.recordSpans(spans);
273
+ for (const block of movedRoots) setParent(pd, segments.get(block.id), block.id, parent);
274
+ for (const change of changes) setAttr(pd, segments.get(change.id), change.id, change.field, change.value);
275
+ }, LOCAL);
276
+ um?.stopCapturing();
277
+ render();
278
+ if (caret && view) {
279
+ const place = ({ id, offset }) => {
280
+ let found = null;
281
+ view.state.doc.forEach((node, pos) => { if (found === null && node.attrs.id === id) found = pos + 1 + Math.min(offset, node.content.size); });
282
+ return found;
283
+ };
284
+ const anchor = place(caret.anchor), head = place(caret.head);
285
+ if (anchor !== null && head !== null) view.dispatch(view.state.tr.setSelection(pmState.TextSelection.create(view.state.doc, anchor, head)).setMeta(REMOTE_META, true).setMeta("addToHistory", false));
286
+ }
287
+ return { status: "moved", ids: movedIds };
288
+ }
@@ -0,0 +1,257 @@
1
+ import * as Y from "../../vendor/yjs.vendor.js";
2
+
3
+ export const ORIGINS_ORIGIN = "page-origins";
4
+ export const UNDO_OVERLAP_ORIGIN = "page-undo-overlap-repair";
5
+
6
+ export const spanKey = (client, clock) => JSON.stringify(["span", client, clock]);
7
+ export const aliasKey = (client, clock) => JSON.stringify(["alias", client, clock]);
8
+ export const boundaryKey = (client, clock) => JSON.stringify(["boundary", client, clock]);
9
+
10
+ const BOUNDARY_KINDS = ["copy", "removed", "restore"];
11
+ const DIGITS = /^(?:0|[1-9]\d*)$/;
12
+ const idKey = (client, clock) => `${client}.${clock}`;
13
+ const isClock = (value) => Number.isSafeInteger(value) && value >= 0;
14
+ const isLength = (value) => Number.isSafeInteger(value) && value > 0;
15
+ const isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ const conflicting = (key) => new Error(`Conflicting immutable origin record: ${key}`);
17
+
18
+ const isSourceKey = (value) => {
19
+ if (typeof value !== "string") return false;
20
+ const dot = value.indexOf(".");
21
+ if (dot < 1 || dot === value.length - 1) return false;
22
+ const client = value.slice(0, dot), clock = value.slice(dot + 1);
23
+ return DIGITS.test(client) && DIGITS.test(clock) && isClock(Number(client)) && isClock(Number(clock));
24
+ };
25
+
26
+ const boundaryRecord = (value) => {
27
+ if (!isPlain(value)) return null;
28
+ for (const field of Object.keys(value)) if (field !== "blockId" && field !== "kind" && field !== "recoveryOf") return null;
29
+ if (typeof value.blockId !== "string" || !value.blockId) return null;
30
+ if (!BOUNDARY_KINDS.includes(value.kind)) return null;
31
+ if (value.recoveryOf !== undefined && !isSourceKey(value.recoveryOf)) return null;
32
+ return value.recoveryOf === undefined ? { blockId: value.blockId, kind: value.kind } : { blockId: value.blockId, kind: value.kind, recoveryOf: value.recoveryOf };
33
+ };
34
+
35
+ const sameBoundary = (a, b) => a.blockId === b.blockId && a.kind === b.kind && a.recoveryOf === b.recoveryOf;
36
+
37
+ const recordKey = (key) => {
38
+ if (typeof key !== "string" || key.charCodeAt(0) !== 91) return null;
39
+ let parts;
40
+ try { parts = JSON.parse(key); } catch { return null; }
41
+ if (!Array.isArray(parts) || parts.length !== 3) return null;
42
+ if (parts[0] !== "span" && parts[0] !== "alias" && parts[0] !== "boundary") return null;
43
+ return isClock(parts[1]) && isClock(parts[2]) ? parts : null;
44
+ };
45
+
46
+ export function createOriginIndex(origins) {
47
+ const spans = new Map();
48
+ const aliases = new Map();
49
+ const boundaries = new Map();
50
+
51
+ const lower = (list, clock) => {
52
+ let lo = 0, hi = list.length;
53
+ while (lo < hi) { const mid = (lo + hi) >> 1; if (list[mid].clock < clock) lo = mid + 1; else hi = mid; }
54
+ return lo;
55
+ };
56
+ const apply = (key, value) => {
57
+ const parts = recordKey(key);
58
+ if (!parts) return;
59
+ const [kind, client, clock] = parts;
60
+ if (kind === "alias") {
61
+ if (value && isClock(value.sourceClient) && isClock(value.sourceClock)) aliases.set(idKey(client, clock), { client: value.sourceClient, clock: value.sourceClock });
62
+ else aliases.delete(idKey(client, clock));
63
+ return;
64
+ }
65
+ if (kind === "boundary") {
66
+ const record = boundaryRecord(value);
67
+ if (record) boundaries.set(idKey(client, clock), record);
68
+ else boundaries.delete(idKey(client, clock));
69
+ return;
70
+ }
71
+ const list = spans.get(client) ?? [];
72
+ const at = lower(list, clock);
73
+ if (list[at]?.clock === clock) list.splice(at, 1);
74
+ if (value && isLength(value.length) && isClock(value.sourceClient) && isClock(value.sourceClock)) {
75
+ list.splice(lower(list, clock), 0, { client, clock, length: value.length, sourceClient: value.sourceClient, sourceClock: value.sourceClock });
76
+ }
77
+ if (list.length) spans.set(client, list);
78
+ else spans.delete(client);
79
+ };
80
+ for (const [key, value] of origins) apply(key, value);
81
+ const observer = (event) => { for (const key of event.keysChanged) apply(key, origins.get(key)); };
82
+ origins.observe(observer);
83
+
84
+ const resolve = (id) => {
85
+ const alias = aliases.get(idKey(id.client, id.clock));
86
+ if (alias) return { client: alias.client, clock: alias.clock };
87
+ const list = spans.get(id.client);
88
+ if (!list) return { client: id.client, clock: id.clock };
89
+ let lo = 0, hi = list.length - 1, found = -1;
90
+ while (lo <= hi) { const mid = (lo + hi) >> 1; if (list[mid].clock <= id.clock) { found = mid; lo = mid + 1; } else hi = mid - 1; }
91
+ if (found < 0) return { client: id.client, clock: id.clock };
92
+ const record = list[found];
93
+ const offset = id.clock - record.clock;
94
+ if (offset >= record.length) return { client: id.client, clock: id.clock };
95
+ return { client: record.sourceClient, clock: record.sourceClock + offset };
96
+ };
97
+
98
+ const boundaryOf = (id) => {
99
+ if (!id || !isClock(id.client) || !isClock(id.clock)) return null;
100
+ let key = idKey(id.client, id.clock);
101
+ const seen = new Set();
102
+ while (!seen.has(key)) {
103
+ seen.add(key);
104
+ const alias = aliases.get(key);
105
+ if (!alias) break;
106
+ key = idKey(alias.client, alias.clock);
107
+ }
108
+ if (seen.has(key) && aliases.has(key)) return null;
109
+ const record = boundaries.get(key);
110
+ return record ? { ...record } : null;
111
+ };
112
+
113
+ const writeSpan = (client, clock, value) => {
114
+ const key = spanKey(client, clock);
115
+ const old = origins.get(key);
116
+ if (old !== undefined && (old.length !== value.length || old.sourceClient !== value.sourceClient || old.sourceClock !== value.sourceClock)) throw conflicting(key);
117
+ const list = spans.get(client) ?? [];
118
+ const at = lower(list, clock);
119
+ for (const other of [list[at - 1], list[at]]) {
120
+ if (other && other.clock !== clock && other.clock < clock + value.length && clock < other.clock + other.length) throw conflicting(key);
121
+ }
122
+ if (old === undefined) origins.set(key, value);
123
+ };
124
+
125
+ const recordSpans = (entries) => {
126
+ const run = [];
127
+ const flush = () => {
128
+ if (!run.length) return;
129
+ const first = run[0];
130
+ const value = { length: run.length, sourceClient: first.source.client, sourceClock: first.source.clock };
131
+ if (!isClock(value.sourceClock + value.length - 1)) throw new Error(`Unsafe origin span: ${spanKey(first.dest.client, first.dest.clock)}`);
132
+ writeSpan(first.dest.client, first.dest.clock, value);
133
+ run.length = 0;
134
+ };
135
+ for (const entry of entries) {
136
+ if (!entry?.dest || !entry?.source) throw new Error("Invalid origin destination");
137
+ const dest = entry.dest, source = entry.source;
138
+ if (!isClock(dest.client) || !isClock(dest.clock) || !isClock(source.client) || !isClock(source.clock)) throw new Error("Invalid origin destination");
139
+ const flat = resolve(source);
140
+ const prev = run[run.length - 1];
141
+ if (prev && prev.dest.client === dest.client && prev.dest.clock + 1 === dest.clock && prev.source.client === flat.client && prev.source.clock + 1 === flat.clock) run.push({ dest, source: flat });
142
+ else { flush(); run.push({ dest, source: flat }); }
143
+ }
144
+ flush();
145
+ };
146
+
147
+ const recordAlias = (dest, source) => {
148
+ if (!dest || !source || !isClock(dest.client) || !isClock(dest.clock) || !isClock(source.client) || !isClock(source.clock)) throw new Error("Invalid origin destination");
149
+ const flat = resolve(source);
150
+ const key = aliasKey(dest.client, dest.clock);
151
+ const old = origins.get(key);
152
+ if (old !== undefined && (old.sourceClient !== flat.client || old.sourceClock !== flat.clock)) throw conflicting(key);
153
+ if (old === undefined) origins.set(key, { sourceClient: flat.client, sourceClock: flat.clock });
154
+ };
155
+
156
+ const recordBoundary = (dest, value) => {
157
+ if (!dest || !isClock(dest.client) || !isClock(dest.clock)) throw new Error("Invalid origin destination");
158
+ const record = boundaryRecord(value);
159
+ if (!record) throw new Error(`Invalid boundary record: ${boundaryKey(dest.client, dest.clock)}`);
160
+ const key = boundaryKey(dest.client, dest.clock);
161
+ const existing = origins.get(key);
162
+ if (existing === undefined) { origins.set(key, record); return; }
163
+ const previous = boundaryRecord(existing);
164
+ if (!previous || !sameBoundary(previous, record)) throw conflicting(key);
165
+ };
166
+
167
+ return {
168
+ origins, resolve, recordSpans, recordAlias, recordBoundary, boundaryOf,
169
+ get size() { return origins.size; },
170
+ dispose() { origins.unobserve(observer); },
171
+ };
172
+ }
173
+
174
+ export function unitIds(flow, from, to) {
175
+ const out = [];
176
+ let at = 0;
177
+ for (let item = flow._start; item && at < to; item = item.right) {
178
+ if (item.deleted || !item.countable) continue;
179
+ const start = at;
180
+ at += item.length;
181
+ if (at <= from) continue;
182
+ const lo = Math.max(0, from - start);
183
+ const hi = Math.min(item.length, to - start);
184
+ for (let offset = lo; offset < hi; offset++) out.push({ client: item.id.client, clock: item.id.clock + offset });
185
+ }
186
+ return out;
187
+ }
188
+
189
+ const coversOriginal = (records, self, id) => records.some((record) => {
190
+ if (record.actor === self || record.client !== id.client) return false;
191
+ if (id.clock < record.clock || id.clock >= record.clock + record.length) return false;
192
+ return record.sv.some(([client, clock]) => client === id.client && clock > id.clock);
193
+ });
194
+
195
+ const livePhysicalIndex = (flow, id) => {
196
+ let at = 0;
197
+ for (let item = flow._start; item; item = item.right) {
198
+ if (item.deleted || !item.countable) continue;
199
+ if (item.id.client === id.client && item.id.clock <= id.clock && id.clock < item.id.clock + item.length) return at + id.clock - item.id.clock;
200
+ at += item.length;
201
+ }
202
+ return null;
203
+ };
204
+
205
+ const repairOverlaps = (pd, dests) => {
206
+ const indexes = [];
207
+ for (const dest of dests) {
208
+ const at = livePhysicalIndex(pd.flow, dest);
209
+ if (at !== null) indexes.push(at);
210
+ }
211
+ indexes.sort((a, b) => a - b);
212
+ const ranges = [];
213
+ for (const at of indexes) {
214
+ const last = ranges[ranges.length - 1];
215
+ if (last && at <= last[1]) last[1] = Math.max(last[1], at + 1);
216
+ else ranges.push([at, at + 1]);
217
+ }
218
+ for (let i = ranges.length - 1; i >= 0; i--) {
219
+ const [from, to] = ranges[i];
220
+ if (to > from) pd.flow.delete(from, to - from);
221
+ }
222
+ };
223
+
224
+ export function installUndoOrigins(pd, um, index = createOriginIndex(pd.origins), textDeletes = () => []) {
225
+ const hook = (transaction) => {
226
+ if (transaction.origin !== um || !um.currStackItem) return;
227
+ const spans = [];
228
+ const aliases = [];
229
+ const repairs = [];
230
+ const records = textDeletes(pd.gone);
231
+ Y.iterateDeletedStructs(transaction, um.currStackItem.deletions, (item) => {
232
+ if (!(item instanceof Y.Item) || item.parent !== pd.flow || !item.countable || !item.redone) return;
233
+ const marker = item.content instanceof Y.ContentEmbed && item.content.embed?.b !== undefined;
234
+ for (let offset = 0; offset < item.length; offset++) {
235
+ const original = { client: item.id.client, clock: item.id.clock + offset };
236
+ const source = index.resolve(original);
237
+ let dest = { client: item.redone.client, clock: item.redone.clock + offset };
238
+ let live = Y.getItem(pd.ydoc.store, dest);
239
+ while (live instanceof Y.Item && live.redone) {
240
+ dest = { client: live.redone.client, clock: live.redone.clock + dest.clock - live.id.clock };
241
+ live = Y.getItem(pd.ydoc.store, dest);
242
+ }
243
+ if (!(live instanceof Y.Item) || live.deleted) continue;
244
+ if (dest.clock < (transaction.beforeState.get(dest.client) ?? 0)) continue;
245
+ if (!marker && coversOriginal(records, pd.ydoc.clientID, original)) { repairs.push(dest); continue; }
246
+ (marker ? aliases : spans).push({ dest, source });
247
+ }
248
+ });
249
+ if (spans.length || aliases.length) pd.ydoc.transact(() => {
250
+ if (spans.length) index.recordSpans(spans);
251
+ for (const alias of aliases) index.recordAlias(alias.dest, alias.source);
252
+ }, ORIGINS_ORIGIN);
253
+ if (repairs.length) pd.ydoc.transact(() => repairOverlaps(pd, repairs), UNDO_OVERLAP_ORIGIN);
254
+ };
255
+ pd.ydoc.on("beforeObserverCalls", hook);
256
+ return () => pd.ydoc.off("beforeObserverCalls", hook);
257
+ }
@@ -0,0 +1,44 @@
1
+ export const PART_KEY = /^(.+)\.(\d+)\.(\d+)$/;
2
+ export const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
3
+ const PART_CHARS = /^[A-Za-z0-9+/=\t\n\f\r ]*$/;
4
+
5
+ export function entriesOf(y) {
6
+ const whole = [];
7
+ const parts = new Map();
8
+ for (const k of Object.keys(y).sort()) {
9
+ const m = PART_KEY.exec(k);
10
+ if (!m) { whole.push([k, y[k]]); continue; }
11
+ const group = parts.get(m[1]) ?? { n: Number(m[3]), got: [] };
12
+ group.got[Number(m[2])] = y[k];
13
+ parts.set(m[1], group);
14
+ }
15
+ for (const [k, group] of parts) {
16
+ let have = 0;
17
+ for (let i = 0; i < group.n; i++) if (typeof group.got[i] === "string") have++;
18
+ if (have === group.n) whole.push([k, group.got.join("")]);
19
+ }
20
+ return whole;
21
+ }
22
+
23
+ export function captureYValue(value) {
24
+ if (value?.v !== 1 || !value.y || typeof value.y !== "object" || Array.isArray(value.y)) return value;
25
+ const groups = new Map();
26
+ for (const [key, text] of Object.entries(value.y)) {
27
+ const part = PART_KEY.exec(key);
28
+ if (!part) continue;
29
+ const base = part[1], index = Number(part[2]), count = Number(part[3]);
30
+ const group = groups.get(base) ?? { count, indices: new Set(), keys: [], invalid: false };
31
+ if (!Number.isSafeInteger(index) || !Number.isSafeInteger(count) || count < 1 || index >= count || group.count !== count || group.indices.has(index) || typeof text !== "string" || !PART_CHARS.test(text)) group.invalid = true;
32
+ group.indices.add(index);
33
+ group.keys.push(key);
34
+ groups.set(base, group);
35
+ }
36
+ const omit = new Set();
37
+ for (const [base, group] of groups) {
38
+ if (!group.invalid && !Object.hasOwn(value.y, base) && group.indices.size < group.count) {
39
+ for (const key of group.keys) omit.add(key);
40
+ }
41
+ }
42
+ if (!omit.size) return value;
43
+ return { ...value, y: Object.fromEntries(Object.entries(value.y).filter(([key]) => !omit.has(key))) };
44
+ }
@@ -0,0 +1,92 @@
1
+ const isCount = (value) => Number.isSafeInteger(value) && value >= 0;
2
+ const fail = (message) => { throw new RangeError(message); };
3
+ const sideOf = (affinity) => {
4
+ if (affinity !== "before" && affinity !== "after") fail(`Unknown affinity: ${affinity}`);
5
+ return affinity;
6
+ };
7
+ const firstAbove = (list, pick, value) => {
8
+ let lo = 0, hi = list.length;
9
+ while (lo < hi) { const mid = (lo + hi) >> 1; if (pick(list[mid]) <= value) lo = mid + 1; else hi = mid; }
10
+ return lo;
11
+ };
12
+
13
+ export function createProjectionMap(w, physicalLength) {
14
+ if (!w || typeof w !== "object" || !isCount(w.tokens)) fail("A projection map needs a walk result with a token count");
15
+ if (!isCount(physicalLength)) fail("A projection map needs a nonnegative physical length");
16
+ const holes = w.holes === undefined ? [] : w.holes;
17
+ if (!Array.isArray(holes)) fail("A projection map needs the walk holes");
18
+ const segments = [];
19
+ const spans = [];
20
+ const prefix = [0];
21
+ let logical = 0, physical = 0, previousAt = -1;
22
+ const show = (length) => {
23
+ segments.push({ logicalStart: logical, logicalLength: length, physicalStart: physical, physicalLength: length, kind: "visible" });
24
+ logical += length;
25
+ physical += length;
26
+ };
27
+ const hide = (length) => {
28
+ const last = spans[spans.length - 1];
29
+ if (last && last.at === logical) {
30
+ last.length += length;
31
+ last.end += length;
32
+ segments[segments.length - 1].physicalLength += length;
33
+ prefix[prefix.length - 1] += length;
34
+ } else {
35
+ spans.push({ at: logical, start: physical, end: physical + length, length });
36
+ segments.push({ logicalStart: logical, logicalLength: 0, physicalStart: physical, physicalLength: length, kind: "hidden" });
37
+ prefix.push(prefix[prefix.length - 1] + length);
38
+ }
39
+ physical += length;
40
+ };
41
+ for (const hole of holes) {
42
+ if (!hole || typeof hole !== "object" || !isCount(hole.at) || !isCount(hole.len)) fail("Malformed walk hole");
43
+ if (hole.at < previousAt) fail("Walk holes are out of order");
44
+ if (hole.at > w.tokens) fail("A walk hole sits past the logical length");
45
+ previousAt = hole.at;
46
+ if (hole.at > logical) show(hole.at - logical);
47
+ if (hole.len) hide(hole.len);
48
+ }
49
+ if (logical < w.tokens) show(w.tokens - logical);
50
+ if (physical !== physicalLength) fail("Walk holes do not account for the physical length");
51
+ const spansAt = (value) => firstAbove(spans, (span) => span.at, value);
52
+ const spansEnded = (value) => firstAbove(spans, (span) => span.end, value);
53
+ const physicalAt = (value) => value + prefix[spansAt(value)];
54
+ const spanAt = (value) => {
55
+ const span = spans[spansAt(value) - 1];
56
+ return span && span.at === value ? span : null;
57
+ };
58
+ const spanInside = (value) => {
59
+ const span = spans[spansEnded(value)];
60
+ return span && span.start <= value ? span : null;
61
+ };
62
+ const toPhysical = (value, affinity) => {
63
+ const side = sideOf(affinity);
64
+ if (!isCount(value) || value > w.tokens) fail(`Logical position ${value} is outside the map`);
65
+ const span = spanAt(value);
66
+ if (span) return side === "before" ? span.start : span.end;
67
+ return physicalAt(value);
68
+ };
69
+ const toLogical = (value, affinity) => {
70
+ sideOf(affinity);
71
+ if (!isCount(value) || value > physicalLength) fail(`Physical position ${value} is outside the flow`);
72
+ const span = spanInside(value);
73
+ if (span) return span.at;
74
+ return value - prefix[spansEnded(value)];
75
+ };
76
+ const physicalRanges = (from, to) => {
77
+ if (!isCount(from) || !isCount(to) || from > w.tokens || to > w.tokens) fail(`Logical range ${from}..${to} is outside the map`);
78
+ const ranges = [];
79
+ if (to <= from) return ranges;
80
+ let start = physicalAt(from);
81
+ for (const span of spans) {
82
+ if (span.at <= from) continue;
83
+ if (span.at > to) break;
84
+ if (span.start > start) ranges.push([start, span.start]);
85
+ start = span.end;
86
+ }
87
+ const end = physicalAt(to);
88
+ if (end > start) ranges.push([start, end]);
89
+ return ranges;
90
+ };
91
+ return { segments, logicalLength: w.tokens, physicalLength, toPhysical, toLogical, physicalRanges };
92
+ }