@earendil-works/chord 0.87.1 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -27
- package/dist/api.d.ts +6 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +8 -3
- package/dist/api.js.map +1 -1
- package/dist/bundler.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/delta/apply-immutable-trusted.d.ts +4 -0
- package/dist/delta/apply-immutable-trusted.d.ts.map +1 -0
- package/dist/delta/apply-immutable-trusted.js +127 -0
- package/dist/delta/apply-immutable-trusted.js.map +1 -0
- package/dist/{state → delta}/diff.d.ts +1 -1
- package/dist/delta/diff.d.ts.map +1 -0
- package/dist/delta/diff.js +433 -0
- package/dist/delta/diff.js.map +1 -0
- package/dist/delta/draft.d.ts +5 -0
- package/dist/delta/draft.d.ts.map +1 -0
- package/dist/delta/draft.js +2 -0
- package/dist/delta/draft.js.map +1 -0
- package/dist/delta/index.d.ts +11 -21
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +43 -1341
- package/dist/delta/index.js.map +1 -1
- package/dist/delta/revision-validator.d.ts +7 -0
- package/dist/delta/revision-validator.d.ts.map +1 -0
- package/dist/delta/revision-validator.js +71 -0
- package/dist/delta/revision-validator.js.map +1 -0
- package/dist/delta/tracker.d.ts +26 -0
- package/dist/delta/tracker.d.ts.map +1 -0
- package/dist/delta/tracker.js +2069 -0
- package/dist/delta/tracker.js.map +1 -0
- package/dist/facets/host.d.ts.map +1 -1
- package/dist/facets/host.js.map +1 -1
- package/dist/facets/loader.d.ts.map +1 -1
- package/dist/facets/loader.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +6 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +73 -6
- package/dist/json.js.map +1 -1
- package/dist/node/bundle-loader.d.ts.map +1 -1
- package/dist/node/bundle-loader.js.map +1 -1
- package/dist/node/bundle.d.ts.map +1 -1
- package/dist/node/bundle.js.map +1 -1
- package/dist/node/manifest.d.ts.map +1 -1
- package/dist/node/package.d.ts.map +1 -1
- package/dist/node/package.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/services/consumer.d.ts.map +1 -1
- package/dist/services/consumer.js +49 -1
- package/dist/services/consumer.js.map +1 -1
- package/dist/services/errors.d.ts.map +1 -1
- package/dist/services/errors.js.map +1 -1
- package/dist/services/handle.d.ts.map +1 -1
- package/dist/services/handle.js +3 -1
- package/dist/services/handle.js.map +1 -1
- package/dist/services/instances.d.ts +1 -0
- package/dist/services/instances.d.ts.map +1 -1
- package/dist/services/instances.js +3 -0
- package/dist/services/instances.js.map +1 -1
- package/dist/services/loopback.d.ts.map +1 -1
- package/dist/services/loopback.js.map +1 -1
- package/dist/services/provider.d.ts.map +1 -1
- package/dist/services/provider.js +48 -29
- package/dist/services/provider.js.map +1 -1
- package/dist/services/state-codec.d.ts.map +1 -1
- package/dist/services/state-codec.js +18 -0
- package/dist/services/state-codec.js.map +1 -1
- package/dist/services/state-internals.d.ts +5 -2
- package/dist/services/state-internals.d.ts.map +1 -1
- package/dist/services/state-internals.js.map +1 -1
- package/dist/services/state.d.ts +8 -3
- package/dist/services/state.d.ts.map +1 -1
- package/dist/services/state.js +288 -88
- package/dist/services/state.js.map +1 -1
- package/dist/services/wire.d.ts +3 -0
- package/dist/services/wire.d.ts.map +1 -1
- package/dist/services/wire.js +13 -0
- package/dist/services/wire.js.map +1 -1
- package/dist/types.d.ts +74 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/delta/README.md +143 -360
- package/dist/state/diff.d.ts.map +0 -1
- package/dist/state/diff.js +0 -259
- package/dist/state/diff.js.map +0 -1
- package/dist/state/draft.d.ts +0 -13
- package/dist/state/draft.d.ts.map +0 -1
- package/dist/state/draft.js +0 -540
- package/dist/state/draft.js.map +0 -1
- package/dist/state/value.d.ts +0 -9
- package/dist/state/value.d.ts.map +0 -1
- package/dist/state/value.js +0 -175
- package/dist/state/value.js.map +0 -1
package/dist/delta/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const isObj = (value) => value !== null && typeof value === "object";
|
|
1
2
|
// ─── Classification ──────────────────────────────────────────────────────────
|
|
2
3
|
export const isReplace = (op) => op[0] === "r";
|
|
3
4
|
/**
|
|
@@ -40,1333 +41,9 @@ export function overlap(a, b, scan, probe = 64, maxCandidates = 8) {
|
|
|
40
41
|
}
|
|
41
42
|
return 0;
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return value;
|
|
47
|
-
if (Array.isArray(value))
|
|
48
|
-
return value.map((item) => cloneJson(item));
|
|
49
|
-
// Spread preserves compact object layouts instead of reserving extra slots
|
|
50
|
-
// while growing an empty object. Both branches create own writable properties.
|
|
51
|
-
const result = (Object.getPrototypeOf(value) === null ? Object.assign(Object.create(null), value) : { ...value });
|
|
52
|
-
for (const key of Object.keys(result)) {
|
|
53
|
-
const child = result[key];
|
|
54
|
-
if (isObj(child))
|
|
55
|
-
result[key] = cloneJson(child);
|
|
56
|
-
}
|
|
57
|
-
return result;
|
|
58
|
-
};
|
|
59
|
-
const INDEX = /^(?:0|[1-9]\d*)$/;
|
|
60
|
-
const norm = (target, key) => typeof key === "symbol" ? key : Array.isArray(target) && INDEX.test(key) ? Number(key) : key;
|
|
61
|
-
const MUTATORS = new Set(["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"]);
|
|
62
|
-
const MISSING = Symbol("missing");
|
|
63
|
-
const spliceItems = (target, index, remove, items) => {
|
|
64
|
-
const removed = Reflect.apply(Array.prototype.splice, target, [index, remove]);
|
|
65
|
-
const chunkSize = 10_000;
|
|
66
|
-
for (let offset = 0; offset < items.length; offset += chunkSize) {
|
|
67
|
-
Reflect.apply(Array.prototype.splice, target, [index + offset, 0, ...items.slice(offset, offset + chunkSize)]);
|
|
68
|
-
}
|
|
69
|
-
return removed;
|
|
70
|
-
};
|
|
71
|
-
const jsonEqual = (left, right) => {
|
|
72
|
-
if (left === right)
|
|
73
|
-
return true;
|
|
74
|
-
if (!isObj(left) || !isObj(right) || Array.isArray(left) !== Array.isArray(right))
|
|
75
|
-
return false;
|
|
76
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
77
|
-
if (left.length !== right.length)
|
|
78
|
-
return false;
|
|
79
|
-
for (let index = 0; index < left.length; index++) {
|
|
80
|
-
if (!jsonEqual(left[index], right[index]))
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
const leftObject = left;
|
|
86
|
-
const rightObject = right;
|
|
87
|
-
const leftKeys = Object.keys(leftObject);
|
|
88
|
-
const rightKeys = Object.keys(rightObject);
|
|
89
|
-
if (leftKeys.length !== rightKeys.length)
|
|
90
|
-
return false;
|
|
91
|
-
for (const key of leftKeys) {
|
|
92
|
-
if (!Object.hasOwn(rightObject, key) || !jsonEqual(leftObject[key], rightObject[key]))
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
return true;
|
|
96
|
-
};
|
|
97
|
-
const emitSet = (path, value, out) => {
|
|
98
|
-
const snapshot = cloneJson(value);
|
|
99
|
-
if (path.length === 0)
|
|
100
|
-
out.push(["r", snapshot]);
|
|
101
|
-
else
|
|
102
|
-
out.push(["s", [...path], snapshot]);
|
|
103
|
-
};
|
|
104
|
-
const emitDelete = (path, out) => {
|
|
105
|
-
if (path.length === 0)
|
|
106
|
-
throw new TypeError("the tracked root cannot be deleted");
|
|
107
|
-
out.push(["d", [...path]]);
|
|
108
|
-
};
|
|
109
|
-
const diffString = (before, after, path, scan, out) => {
|
|
110
|
-
if (before === after)
|
|
111
|
-
return;
|
|
112
|
-
if (path.length === 0) {
|
|
113
|
-
emitSet(path, after, out);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const at = [...path];
|
|
117
|
-
// NOT `after.startsWith(before)`. `after` is usually a cons string — the
|
|
118
|
-
// producer just did `s += chunk` — and V8's startsWith walks a cons char by
|
|
119
|
-
// char. `slice(...) === before` flattens once and compares with memcmp.
|
|
120
|
-
// Measured on a 200 KB string growing by 8 bytes per flush: 845 us -> 42 us.
|
|
121
|
-
if (after.length > before.length && after.slice(0, before.length) === before) {
|
|
122
|
-
out.push(["a", at, after.slice(before.length)]);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const shared = overlap(before, after, scan);
|
|
126
|
-
if (shared === 0) {
|
|
127
|
-
out.push(["s", at, after]);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
out.push(["t", at, before.length - shared]);
|
|
131
|
-
if (after.length > shared)
|
|
132
|
-
out.push(["a", at, after.slice(shared)]);
|
|
133
|
-
};
|
|
134
|
-
const diffValue = (before, after, path, scan, out) => {
|
|
135
|
-
if (before === MISSING) {
|
|
136
|
-
if (after !== MISSING)
|
|
137
|
-
emitSet(path, after, out);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (after === MISSING) {
|
|
141
|
-
emitDelete(path, out);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (before === after)
|
|
145
|
-
return;
|
|
146
|
-
if (typeof before === "string" && typeof after === "string") {
|
|
147
|
-
diffString(before, after, path, scan, out);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (Array.isArray(before) && Array.isArray(after)) {
|
|
151
|
-
diffArray(before, after, path, scan, out);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (isObj(before) && isObj(after) && !Array.isArray(before) && !Array.isArray(after)) {
|
|
155
|
-
diffObject(before, after, path, scan, out);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
emitSet(path, after, out);
|
|
159
|
-
};
|
|
160
|
-
function diffObject(before, after, path, scan, out) {
|
|
161
|
-
if ([...Object.keys(before), ...Object.keys(after)].some((key) => RESERVED_SEGMENTS.has(key))) {
|
|
162
|
-
emitSet(path, after, out);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
for (const key of Object.keys(after)) {
|
|
166
|
-
diffValue(Object.hasOwn(before, key) ? before[key] : MISSING, after[key], [...path, key], scan, out);
|
|
167
|
-
}
|
|
168
|
-
for (const key of Object.keys(before)) {
|
|
169
|
-
if (!Object.hasOwn(after, key))
|
|
170
|
-
emitDelete([...path, key], out);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
function diffArray(before, after, path, scan, out) {
|
|
174
|
-
if (before.length === after.length) {
|
|
175
|
-
for (let index = 0; index < after.length; index++) {
|
|
176
|
-
diffValue(before[index], after[index], [...path, index], scan, out);
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
let prefix = 0;
|
|
181
|
-
while (prefix < before.length && prefix < after.length && jsonEqual(before[prefix], after[prefix]))
|
|
182
|
-
prefix++;
|
|
183
|
-
let suffix = 0;
|
|
184
|
-
while (suffix < before.length - prefix &&
|
|
185
|
-
suffix < after.length - prefix &&
|
|
186
|
-
jsonEqual(before[before.length - 1 - suffix], after[after.length - 1 - suffix])) {
|
|
187
|
-
suffix++;
|
|
188
|
-
}
|
|
189
|
-
const shorter = Math.min(before.length, after.length);
|
|
190
|
-
if (prefix + suffix === shorter) {
|
|
191
|
-
const remove = before.length - prefix - suffix;
|
|
192
|
-
const items = after.slice(prefix, after.length - suffix);
|
|
193
|
-
if (prefix === 0 && remove === before.length)
|
|
194
|
-
emitSet(path, after, out);
|
|
195
|
-
else
|
|
196
|
-
out.push(["p", [...path], prefix, remove, cloneJson(items)]);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// Structural movement combined with retained-index edits has no unique
|
|
200
|
-
// alignment. Preserve the retained index deltas and express only the tail
|
|
201
|
-
// length change structurally. It may be broader than the producer's intent,
|
|
202
|
-
// but never degrades those edits to a whole-array replacement.
|
|
203
|
-
for (let index = 0; index < shorter; index++) {
|
|
204
|
-
diffValue(before[index], after[index], [...path, index], scan, out);
|
|
205
|
-
}
|
|
206
|
-
if (after.length > before.length) {
|
|
207
|
-
out.push(["p", [...path], before.length, 0, cloneJson(after.slice(before.length))]);
|
|
208
|
-
}
|
|
209
|
-
else if (before.length > after.length) {
|
|
210
|
-
if (after.length === 0)
|
|
211
|
-
emitSet(path, after, out);
|
|
212
|
-
else
|
|
213
|
-
out.push(["p", [...path], after.length, before.length - after.length, []]);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
export function track(root, options = {}) {
|
|
217
|
-
const scan = options.maxOverlapScan ?? 65_536;
|
|
218
|
-
let log = [];
|
|
219
|
-
let trie = {};
|
|
220
|
-
let nextOrder = 0;
|
|
221
|
-
let tombstones = 0;
|
|
222
|
-
let liveSlots = 0;
|
|
223
|
-
let nodeCount = 1;
|
|
224
|
-
let lastAddedSlot;
|
|
225
|
-
let hasPending = false;
|
|
226
|
-
let forceBase = true;
|
|
227
|
-
const clearPending = () => {
|
|
228
|
-
log = [];
|
|
229
|
-
trie = {};
|
|
230
|
-
nextOrder = 0;
|
|
231
|
-
tombstones = 0;
|
|
232
|
-
liveSlots = 0;
|
|
233
|
-
nodeCount = 1;
|
|
234
|
-
lastAddedSlot = undefined;
|
|
235
|
-
hasPending = false;
|
|
236
|
-
};
|
|
237
|
-
const logNode = (path) => {
|
|
238
|
-
let at = trie;
|
|
239
|
-
for (const segment of path) {
|
|
240
|
-
if (!at.kids)
|
|
241
|
-
at.kids = new Map();
|
|
242
|
-
let next = at.kids.get(segment);
|
|
243
|
-
if (next === undefined) {
|
|
244
|
-
next = {};
|
|
245
|
-
at.kids.set(segment, next);
|
|
246
|
-
nodeCount++;
|
|
247
|
-
}
|
|
248
|
-
at = next;
|
|
249
|
-
}
|
|
250
|
-
return at;
|
|
251
|
-
};
|
|
252
|
-
const findLogNode = (path) => {
|
|
253
|
-
let at = trie;
|
|
254
|
-
for (const segment of path) {
|
|
255
|
-
const next = at.kids?.get(segment);
|
|
256
|
-
if (next === undefined)
|
|
257
|
-
return undefined;
|
|
258
|
-
at = next;
|
|
259
|
-
}
|
|
260
|
-
return at;
|
|
261
|
-
};
|
|
262
|
-
const compactLog = () => {
|
|
263
|
-
if (tombstones < 1_024 || tombstones * 2 < log.length)
|
|
264
|
-
return;
|
|
265
|
-
const compacted = [];
|
|
266
|
-
for (const slot of log) {
|
|
267
|
-
if (slot === undefined)
|
|
268
|
-
continue;
|
|
269
|
-
slot.index = compacted.length;
|
|
270
|
-
compacted.push(slot);
|
|
271
|
-
}
|
|
272
|
-
log = compacted;
|
|
273
|
-
tombstones = 0;
|
|
274
|
-
};
|
|
275
|
-
const killSlot = (slot) => {
|
|
276
|
-
if (slot.dead)
|
|
277
|
-
return;
|
|
278
|
-
slot.dead = true;
|
|
279
|
-
liveSlots--;
|
|
280
|
-
if (log[slot.index] === slot) {
|
|
281
|
-
log[slot.index] = undefined;
|
|
282
|
-
tombstones++;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
const liveSlot = (at) => {
|
|
286
|
-
const slots = at.slots;
|
|
287
|
-
if (slots === undefined)
|
|
288
|
-
return undefined;
|
|
289
|
-
while (slots.length > 0 && slots[slots.length - 1].dead)
|
|
290
|
-
slots.pop();
|
|
291
|
-
if (slots.length === 0) {
|
|
292
|
-
at.slots = undefined;
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
return slots[slots.length - 1];
|
|
296
|
-
};
|
|
297
|
-
const killHere = (at) => {
|
|
298
|
-
if (at.slots === undefined)
|
|
299
|
-
return;
|
|
300
|
-
for (const slot of at.slots)
|
|
301
|
-
killSlot(slot);
|
|
302
|
-
at.slots = undefined;
|
|
303
|
-
};
|
|
304
|
-
const addSlot = (at, slot) => {
|
|
305
|
-
compactLog();
|
|
306
|
-
slot.order = nextOrder++;
|
|
307
|
-
slot.index = log.length;
|
|
308
|
-
at.lastOrder = slot.order;
|
|
309
|
-
if (at.slots === undefined)
|
|
310
|
-
at.slots = [];
|
|
311
|
-
at.slots.push(slot);
|
|
312
|
-
log.push(slot);
|
|
313
|
-
liveSlots++;
|
|
314
|
-
lastAddedSlot = slot;
|
|
315
|
-
};
|
|
316
|
-
const collapsePending = () => {
|
|
317
|
-
// Long mutation windows can otherwise retain operations and retired path
|
|
318
|
-
// generations indefinitely. Fall back to a complete snapshot once either
|
|
319
|
-
// history exceeds a bounded coalescing window. Payload bytes and transient
|
|
320
|
-
// allocation remain workload-dependent.
|
|
321
|
-
if (forceBase || (liveSlots <= 4_096 && nodeCount <= 4_096))
|
|
322
|
-
return;
|
|
323
|
-
const value = cloneJson(root);
|
|
324
|
-
clearPending();
|
|
325
|
-
hasPending = true;
|
|
326
|
-
addSlot(trie, { op: ["r", value], dead: false, order: 0, index: 0 });
|
|
327
|
-
};
|
|
328
|
-
const killSubtree = (at) => {
|
|
329
|
-
killHere(at);
|
|
330
|
-
if (at.kids !== undefined) {
|
|
331
|
-
for (const child of at.kids.values())
|
|
332
|
-
killSubtree(child);
|
|
333
|
-
at.kids = undefined;
|
|
334
|
-
}
|
|
335
|
-
if (at.retiredKids !== undefined) {
|
|
336
|
-
for (const generation of at.retiredKids) {
|
|
337
|
-
for (const child of generation.values())
|
|
338
|
-
killSubtree(child);
|
|
339
|
-
}
|
|
340
|
-
at.retiredKids = undefined;
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
const retireKids = (at) => {
|
|
344
|
-
if (at.kids === undefined)
|
|
345
|
-
return;
|
|
346
|
-
if (at.retiredKids === undefined)
|
|
347
|
-
at.retiredKids = [];
|
|
348
|
-
at.retiredKids.push(at.kids);
|
|
349
|
-
at.kids = undefined;
|
|
350
|
-
};
|
|
351
|
-
// Deepest live ancestor op carrying a payload we can fold a later write into.
|
|
352
|
-
// `s`/`r` carry the whole subtree; `p` carries the items it inserted, so a
|
|
353
|
-
// write to one of those indices belongs inside the payload rather than after it.
|
|
354
|
-
const foldTarget = (path) => {
|
|
355
|
-
let at = trie;
|
|
356
|
-
let found;
|
|
357
|
-
let ancestorMax = -1;
|
|
358
|
-
for (let depth = 0; depth < path.length; depth++) {
|
|
359
|
-
if (found !== undefined && at.lastOrder !== undefined && at.lastOrder > found.slot.order) {
|
|
360
|
-
found = undefined;
|
|
361
|
-
}
|
|
362
|
-
const slot = liveSlot(at);
|
|
363
|
-
// A fold is sound only if nothing has been recorded at this path or above it
|
|
364
|
-
// since: a later op there (a splice on the same array, a replacement of an
|
|
365
|
-
// ancestor) would have to apply after this write, not before it. Writes to
|
|
366
|
-
// other branches are irrelevant, which is why this is not "the most recent op".
|
|
367
|
-
if (slot !== undefined && slot.order >= ancestorMax && at.lastOrder === slot.order) {
|
|
368
|
-
if (slot.op[0] === "s" || slot.op[0] === "r")
|
|
369
|
-
found = { slot, depth };
|
|
370
|
-
else if (slot.op[0] === "p") {
|
|
371
|
-
const index = path[depth];
|
|
372
|
-
const items = slot.op[4];
|
|
373
|
-
if (typeof index === "number" && index >= slot.op[2] && index < slot.op[2] + items.length) {
|
|
374
|
-
found = { slot, depth: depth + 1, item: index - slot.op[2] };
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (at.lastOrder !== undefined && at.lastOrder > ancestorMax)
|
|
379
|
-
ancestorMax = at.lastOrder;
|
|
380
|
-
const next = at.kids?.get(path[depth]);
|
|
381
|
-
if (next === undefined)
|
|
382
|
-
break;
|
|
383
|
-
at = next;
|
|
384
|
-
}
|
|
385
|
-
if (found === undefined)
|
|
386
|
-
return undefined;
|
|
387
|
-
return { slot: found.slot, rest: path.slice(found.depth), item: found.item };
|
|
388
|
-
};
|
|
389
|
-
const foldInto = (container, rest, op) => {
|
|
390
|
-
if (rest.length === 0)
|
|
391
|
-
return false;
|
|
392
|
-
let target = container;
|
|
393
|
-
for (let index = 0; index < rest.length - 1; index++) {
|
|
394
|
-
if (!isObj(target))
|
|
395
|
-
return false;
|
|
396
|
-
target = target[rest[index]];
|
|
397
|
-
}
|
|
398
|
-
if (!isObj(target))
|
|
399
|
-
return false;
|
|
400
|
-
const key = rest[rest.length - 1];
|
|
401
|
-
const holder = target;
|
|
402
|
-
const write = (value) => {
|
|
403
|
-
Object.defineProperty(holder, key, { value, writable: true, enumerable: true, configurable: true });
|
|
404
|
-
};
|
|
405
|
-
switch (op[0]) {
|
|
406
|
-
case "s":
|
|
407
|
-
if (key === "__proto__")
|
|
408
|
-
return false;
|
|
409
|
-
write(cloneJson(op[2]));
|
|
410
|
-
return true;
|
|
411
|
-
case "d":
|
|
412
|
-
delete holder[key];
|
|
413
|
-
return true;
|
|
414
|
-
case "a": {
|
|
415
|
-
const value = holder[key];
|
|
416
|
-
if (typeof value !== "string")
|
|
417
|
-
return false;
|
|
418
|
-
write(value + op[2]);
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
case "t": {
|
|
422
|
-
const value = holder[key];
|
|
423
|
-
if (typeof value !== "string")
|
|
424
|
-
return false;
|
|
425
|
-
write(value.slice(op[2]));
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
case "p": {
|
|
429
|
-
const value = holder[key];
|
|
430
|
-
if (!Array.isArray(value))
|
|
431
|
-
return false;
|
|
432
|
-
spliceItems(value, op[2], op[3], cloneJson(op[4]));
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
default:
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
const recordString = (path, previous, value) => {
|
|
440
|
-
if (forceBase)
|
|
441
|
-
return;
|
|
442
|
-
hasPending = true;
|
|
443
|
-
// a string inside a pending payload belongs in that payload, as for any write
|
|
444
|
-
const fold = foldTarget(path);
|
|
445
|
-
if (fold !== undefined) {
|
|
446
|
-
const op = ["s", path, value];
|
|
447
|
-
if (fold.item !== undefined) {
|
|
448
|
-
const items = fold.slot.op[4];
|
|
449
|
-
if (fold.rest.length === 0) {
|
|
450
|
-
items[fold.item] = value;
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (foldInto(items[fold.item], fold.rest, op))
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
const payload = fold.slot.op[0] === "r" ? fold.slot.op[1] : fold.slot.op[2];
|
|
458
|
-
if (foldInto(payload, fold.rest, op))
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
const at = logNode(path);
|
|
463
|
-
const live = liveSlot(at);
|
|
464
|
-
if (live?.str !== undefined) {
|
|
465
|
-
live.str.value = value;
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (live !== undefined) {
|
|
469
|
-
// a pending set or delete at this path already replaced the value; keep that
|
|
470
|
-
// op and carry the new value in it rather than anchoring to it
|
|
471
|
-
if (live.op[0] === "s" || live.op[0] === "r") {
|
|
472
|
-
live.op = live.op[0] === "r" ? ["r", value] : ["s", live.op[1], value];
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (live.op[0] === "d") {
|
|
476
|
-
killHere(at);
|
|
477
|
-
addSlot(at, {
|
|
478
|
-
op: ["s", path, value],
|
|
479
|
-
dead: false,
|
|
480
|
-
order: 0,
|
|
481
|
-
index: 0,
|
|
482
|
-
});
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
// a truncate/append pair from an earlier string diff: both must go
|
|
486
|
-
killHere(at);
|
|
487
|
-
}
|
|
488
|
-
killSubtree(at);
|
|
489
|
-
addSlot(at, {
|
|
490
|
-
op: ["s", path, value],
|
|
491
|
-
dead: false,
|
|
492
|
-
order: 0,
|
|
493
|
-
index: 0,
|
|
494
|
-
str: { anchor: previous, value },
|
|
495
|
-
});
|
|
496
|
-
};
|
|
497
|
-
const record = (op) => {
|
|
498
|
-
if (forceBase)
|
|
499
|
-
return;
|
|
500
|
-
hasPending = true;
|
|
501
|
-
const path = (op[0] === "r" ? [] : op[1]);
|
|
502
|
-
const existing = findLogNode(path);
|
|
503
|
-
const anchored = existing === undefined ? undefined : liveSlot(existing);
|
|
504
|
-
if (anchored?.str !== undefined) {
|
|
505
|
-
switch (op[0]) {
|
|
506
|
-
case "a":
|
|
507
|
-
anchored.str.value += op[2];
|
|
508
|
-
return;
|
|
509
|
-
case "t":
|
|
510
|
-
anchored.str.value = anchored.str.value.slice(op[2]);
|
|
511
|
-
return;
|
|
512
|
-
case "s":
|
|
513
|
-
if (typeof op[2] === "string") {
|
|
514
|
-
anchored.str.value = op[2];
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
if (existing !== undefined)
|
|
519
|
-
killSubtree(existing);
|
|
520
|
-
}
|
|
521
|
-
else if (existing !== undefined && (op[0] === "s" || op[0] === "d" || op[0] === "r")) {
|
|
522
|
-
// A replacement absorbed into an ancestor payload must still invalidate
|
|
523
|
-
// operations already recorded at and below its destination.
|
|
524
|
-
killSubtree(existing);
|
|
525
|
-
}
|
|
526
|
-
if (path.length > 0) {
|
|
527
|
-
const fold = foldTarget(path);
|
|
528
|
-
if (fold !== undefined) {
|
|
529
|
-
if (fold.item !== undefined) {
|
|
530
|
-
const items = fold.slot.op[4];
|
|
531
|
-
if (fold.rest.length === 0) {
|
|
532
|
-
if (op[0] === "s") {
|
|
533
|
-
items[fold.item] = cloneJson(op[2]);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
else if (foldInto(items[fold.item], fold.rest, op))
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
const payload = fold.slot.op[0] === "r" ? fold.slot.op[1] : fold.slot.op[2];
|
|
542
|
-
if (foldInto(payload, fold.rest, op))
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
const at = logNode(path);
|
|
548
|
-
const live = liveSlot(at);
|
|
549
|
-
if (live !== undefined) {
|
|
550
|
-
const previous = live.op;
|
|
551
|
-
if (op[0] === "a" && previous[0] === "a") {
|
|
552
|
-
live.op = ["a", previous[1], previous[2] + op[2]];
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (op[0] === "a" && (previous[0] === "s" || previous[0] === "r")) {
|
|
556
|
-
const value = previous[0] === "r" ? previous[1] : previous[2];
|
|
557
|
-
if (typeof value === "string") {
|
|
558
|
-
live.op = previous[0] === "r" ? ["r", value + op[2]] : ["s", previous[1], value + op[2]];
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (op[0] === "t" && (previous[0] === "s" || previous[0] === "r")) {
|
|
563
|
-
const value = previous[0] === "r" ? previous[1] : previous[2];
|
|
564
|
-
if (typeof value === "string") {
|
|
565
|
-
const cut = value.slice(op[2]);
|
|
566
|
-
live.op = previous[0] === "r" ? ["r", cut] : ["s", previous[1], cut];
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (op[0] === "p" && previous[0] === "p") {
|
|
571
|
-
const previousItems = previous[4];
|
|
572
|
-
// These rewrites are sound only for adjacent recorded operations. A
|
|
573
|
-
// tombstoned operation remains a barrier through lastAddedSlot.
|
|
574
|
-
if (previous[3] === 0 &&
|
|
575
|
-
op[3] === 0 &&
|
|
576
|
-
previous[2] + previousItems.length === op[2] &&
|
|
577
|
-
lastAddedSlot === live) {
|
|
578
|
-
const items = op[4];
|
|
579
|
-
for (let index = 0; index < items.length; index++)
|
|
580
|
-
previousItems.push(items[index]);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (previous[3] === 0 &&
|
|
584
|
-
lastAddedSlot === live &&
|
|
585
|
-
op[2] >= previous[2] &&
|
|
586
|
-
op[2] + op[3] <= previous[2] + previousItems.length) {
|
|
587
|
-
spliceItems(previousItems, op[2] - previous[2], op[3], op[4]);
|
|
588
|
-
if (previousItems.length === 0 && previous[3] === 0)
|
|
589
|
-
killSlot(live);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
if (op[3] > 0 &&
|
|
593
|
-
op[4].length === 0 &&
|
|
594
|
-
previousItems.length > 0 &&
|
|
595
|
-
lastAddedSlot === live) {
|
|
596
|
-
const from = op[2] - previous[2];
|
|
597
|
-
if (from >= 0 && from + op[3] === previousItems.length) {
|
|
598
|
-
previousItems.length = from;
|
|
599
|
-
if (previousItems.length === 0 && previous[3] === 0)
|
|
600
|
-
killSlot(live);
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (op[0] === "s" || op[0] === "d" || op[0] === "r")
|
|
606
|
-
killHere(at);
|
|
607
|
-
}
|
|
608
|
-
if (op[0] === "s" || op[0] === "r" || op[0] === "d") {
|
|
609
|
-
// Replacements dominate all earlier descendants, including generations
|
|
610
|
-
// detached by array splices.
|
|
611
|
-
killSubtree(at);
|
|
612
|
-
}
|
|
613
|
-
else if (op[0] === "p") {
|
|
614
|
-
// Splices preserve earlier writes but form a barrier for later folding.
|
|
615
|
-
retireKids(at);
|
|
616
|
-
}
|
|
617
|
-
addSlot(at, { op, dead: false, order: 0, index: 0 });
|
|
618
|
-
};
|
|
619
|
-
// Local diff, used when a whole container is assigned: keeps op quality
|
|
620
|
-
// without a baseline by comparing the outgoing value with the incoming one at
|
|
621
|
-
// the moment of the write. String leaves go through recordString so every
|
|
622
|
-
// string path keeps a single anchored slot; otherwise a later write to the
|
|
623
|
-
// same string would have to supersede ops whose starting value it no longer
|
|
624
|
-
// knows.
|
|
625
|
-
const diffInto = (before, after, at) => {
|
|
626
|
-
if (forceBase)
|
|
627
|
-
return;
|
|
628
|
-
hasPending = true;
|
|
629
|
-
if (before === after)
|
|
630
|
-
return;
|
|
631
|
-
if (typeof before === "string" && typeof after === "string") {
|
|
632
|
-
recordString(at.slice(), before, after);
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
|
|
636
|
-
for (let index = 0; index < after.length; index++) {
|
|
637
|
-
at.push(index);
|
|
638
|
-
diffInto(before[index], after[index], at);
|
|
639
|
-
at.pop();
|
|
640
|
-
}
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
if (isObj(before) && isObj(after) && !Array.isArray(before) && !Array.isArray(after)) {
|
|
644
|
-
const beforeObject = before;
|
|
645
|
-
const afterObject = after;
|
|
646
|
-
const beforeKeys = Object.keys(beforeObject);
|
|
647
|
-
const afterKeys = Object.keys(afterObject);
|
|
648
|
-
if ([...beforeKeys, ...afterKeys].some((key) => RESERVED_SEGMENTS.has(key))) {
|
|
649
|
-
record(["s", at.slice(), cloneJson(after)]);
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
for (const key of afterKeys) {
|
|
653
|
-
at.push(key);
|
|
654
|
-
if (Object.hasOwn(beforeObject, key))
|
|
655
|
-
diffInto(beforeObject[key], afterObject[key], at);
|
|
656
|
-
else
|
|
657
|
-
record(["s", at.slice(), cloneJson(afterObject[key])]);
|
|
658
|
-
at.pop();
|
|
659
|
-
}
|
|
660
|
-
for (const key of beforeKeys) {
|
|
661
|
-
if (!Object.hasOwn(afterObject, key))
|
|
662
|
-
record(["d", [...at, key]]);
|
|
663
|
-
}
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
// arrays of differing length, and everything else: chord's own diff
|
|
667
|
-
const out = [];
|
|
668
|
-
diffValue(before, after, at, scan, out);
|
|
669
|
-
for (const op of out)
|
|
670
|
-
record(op);
|
|
671
|
-
};
|
|
672
|
-
const guard = (segment) => {
|
|
673
|
-
if (typeof segment === "symbol")
|
|
674
|
-
throw new UnsafePathError(String(segment));
|
|
675
|
-
if (typeof segment === "string" && RESERVED_SEGMENTS.has(segment))
|
|
676
|
-
throw new UnsafePathError(segment);
|
|
677
|
-
return segment;
|
|
678
|
-
};
|
|
679
|
-
const integer = (value) => {
|
|
680
|
-
const number = Number(value);
|
|
681
|
-
if (Number.isNaN(number) || number === 0)
|
|
682
|
-
return 0;
|
|
683
|
-
return Number.isFinite(number) ? Math.trunc(number) : number;
|
|
684
|
-
};
|
|
685
|
-
const spliceRange = (length, args) => {
|
|
686
|
-
const rawStart = args.length === 0 ? 0 : integer(args[0]);
|
|
687
|
-
const index = rawStart < 0 ? Math.max(0, length + rawStart) : Math.min(rawStart, length);
|
|
688
|
-
const remove = args.length === 0
|
|
689
|
-
? 0
|
|
690
|
-
: args.length === 1
|
|
691
|
-
? length - index
|
|
692
|
-
: Math.max(0, Math.min(integer(args[1]), length - index));
|
|
693
|
-
return { index, remove };
|
|
694
|
-
};
|
|
695
|
-
// Paths are walked from placement cells rather than baked into proxies. A held
|
|
696
|
-
// descendant keeps its cells and their owner entries alive, so an ancestor can
|
|
697
|
-
// be re-created after GC without losing array-renumbering metadata. Entries keep
|
|
698
|
-
// only weak references to public proxies; parent caches keep only weak cells.
|
|
699
|
-
let shape = 0;
|
|
700
|
-
// Ordinary tree entries are weak values. Explicit aliases are sparse and stay
|
|
701
|
-
// strong while their raw weak key is alive so alias locations survive a period
|
|
702
|
-
// in which no public proxy exists.
|
|
703
|
-
const entries = new WeakMap();
|
|
704
|
-
const aliases = new WeakMap();
|
|
705
|
-
// A live public proxy retains its entry through this ephemeron. Raw proxy RHS
|
|
706
|
-
// values can therefore be unwrapped before they are written into plain targets.
|
|
707
|
-
const proxyEntries = new WeakMap();
|
|
708
|
-
// WeakRef targets already stay alive through the current JavaScript job. These
|
|
709
|
-
// strong job-local caches avoid repeated deref bookkeeping without extending
|
|
710
|
-
// that lifetime; one microtask drops both maps before the next job.
|
|
711
|
-
let jobCells = new WeakMap();
|
|
712
|
-
let jobProxies = new WeakMap();
|
|
713
|
-
let jobCleanupScheduled = false;
|
|
714
|
-
const scheduleJobCleanup = () => {
|
|
715
|
-
if (jobCleanupScheduled)
|
|
716
|
-
return;
|
|
717
|
-
jobCleanupScheduled = true;
|
|
718
|
-
queueMicrotask(() => {
|
|
719
|
-
jobCells = new WeakMap();
|
|
720
|
-
jobProxies = new WeakMap();
|
|
721
|
-
jobCleanupScheduled = false;
|
|
722
|
-
});
|
|
723
|
-
};
|
|
724
|
-
const keepCellForJob = (ref, cell) => {
|
|
725
|
-
jobCells.set(ref, cell);
|
|
726
|
-
scheduleJobCleanup();
|
|
727
|
-
};
|
|
728
|
-
const derefCell = (ref) => {
|
|
729
|
-
const cached = jobCells.get(ref);
|
|
730
|
-
if (cached !== undefined)
|
|
731
|
-
return cached;
|
|
732
|
-
const cell = ref.deref();
|
|
733
|
-
if (cell !== undefined)
|
|
734
|
-
keepCellForJob(ref, cell);
|
|
735
|
-
return cell;
|
|
736
|
-
};
|
|
737
|
-
const entryFinalizer = new FinalizationRegistry(({ target, token }) => {
|
|
738
|
-
const raw = target.deref();
|
|
739
|
-
if (raw !== undefined && entries.get(raw)?.token === token)
|
|
740
|
-
entries.delete(raw);
|
|
741
|
-
});
|
|
742
|
-
const cellFinalizer = new FinalizationRegistry(({ owner, key, ref }) => {
|
|
743
|
-
const entry = owner.deref();
|
|
744
|
-
if (entry === undefined)
|
|
745
|
-
return;
|
|
746
|
-
if (key !== undefined && entry.childProxies?.get(key) === ref)
|
|
747
|
-
entry.childProxies.delete(key);
|
|
748
|
-
entry.childCells?.delete(ref);
|
|
749
|
-
});
|
|
750
|
-
// Almost no document ever puts one object at two positions. Until one does,
|
|
751
|
-
// every write has exactly one path, so the per-write cell walk is skipped.
|
|
752
|
-
let aliased = false;
|
|
753
|
-
const isDetached = (cell) => {
|
|
754
|
-
for (let c = cell; c !== undefined; c = c.parent)
|
|
755
|
-
if (c.dead)
|
|
756
|
-
return true;
|
|
757
|
-
return false;
|
|
758
|
-
};
|
|
759
|
-
const pathOf = (cell) => {
|
|
760
|
-
if (cell.at === shape && cell.cached !== undefined)
|
|
761
|
-
return cell.cached;
|
|
762
|
-
const out = [];
|
|
763
|
-
for (let c = cell; c !== undefined && c.parent !== undefined; c = c.parent)
|
|
764
|
-
out.push(c.seg);
|
|
765
|
-
out.reverse();
|
|
766
|
-
cell.at = shape;
|
|
767
|
-
cell.cached = out;
|
|
768
|
-
return out;
|
|
769
|
-
};
|
|
770
|
-
const liveCells = (entry) => {
|
|
771
|
-
const out = [];
|
|
772
|
-
for (const cell of entry.cells)
|
|
773
|
-
if (!isDetached(cell))
|
|
774
|
-
out.push(cell);
|
|
775
|
-
return out;
|
|
776
|
-
};
|
|
777
|
-
const primary = (entry) => (aliased ? (liveCells(entry)[0] ?? entry.fallback) : entry.fallback);
|
|
778
|
-
const pathNow = (entry) => pathOf(primary(entry));
|
|
779
|
-
const findEntry = (target) => {
|
|
780
|
-
const alias = aliases.get(target);
|
|
781
|
-
if (alias !== undefined)
|
|
782
|
-
return alias;
|
|
783
|
-
const slot = entries.get(target);
|
|
784
|
-
const entry = slot?.ref.deref();
|
|
785
|
-
if (slot !== undefined && entry === undefined)
|
|
786
|
-
entries.delete(target);
|
|
787
|
-
return entry;
|
|
788
|
-
};
|
|
789
|
-
const indexEntry = (entry) => {
|
|
790
|
-
const token = {};
|
|
791
|
-
entries.set(entry.target, { ref: new WeakRef(entry), token });
|
|
792
|
-
entryFinalizer.register(entry, { target: new WeakRef(entry.target), token });
|
|
793
|
-
};
|
|
794
|
-
const addPlacement = (entry, cell) => {
|
|
795
|
-
if (entry.cells.has(cell))
|
|
796
|
-
return;
|
|
797
|
-
let hasLive = false;
|
|
798
|
-
for (const existing of entry.cells) {
|
|
799
|
-
if (!isDetached(existing)) {
|
|
800
|
-
hasLive = true;
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
cell.entry = entry;
|
|
805
|
-
cell.target = entry.target;
|
|
806
|
-
entry.cells.add(cell);
|
|
807
|
-
if (!hasLive) {
|
|
808
|
-
const previous = entry.fallback;
|
|
809
|
-
entry.fallback = cell;
|
|
810
|
-
if (previous !== cell) {
|
|
811
|
-
shape++;
|
|
812
|
-
// Reattaching a held container moves every known immediate child to
|
|
813
|
-
// its new placement. Array registrations survive cache clears.
|
|
814
|
-
const children = new Set();
|
|
815
|
-
for (const ref of entry.childProxies?.values() ?? []) {
|
|
816
|
-
const child = derefCell(ref);
|
|
817
|
-
if (child !== undefined)
|
|
818
|
-
children.add(child);
|
|
819
|
-
}
|
|
820
|
-
for (const ref of entry.childCells ?? []) {
|
|
821
|
-
const child = derefCell(ref);
|
|
822
|
-
if (child !== undefined)
|
|
823
|
-
children.add(child);
|
|
824
|
-
}
|
|
825
|
-
for (const child of children) {
|
|
826
|
-
if (!child.dead && child.parent === previous)
|
|
827
|
-
child.parent = cell;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
else if (entry.blocked === undefined) {
|
|
832
|
-
aliased = true;
|
|
833
|
-
aliases.set(entry.target, entry);
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
const cachePlacement = (owner, key, cell) => {
|
|
837
|
-
let cleanup = cell.cleanup;
|
|
838
|
-
if (cleanup === undefined) {
|
|
839
|
-
cleanup = { owner: new WeakRef(owner), key: undefined, ref: new WeakRef(cell) };
|
|
840
|
-
cell.cleanup = cleanup;
|
|
841
|
-
cellFinalizer.register(cell, cleanup);
|
|
842
|
-
}
|
|
843
|
-
keepCellForJob(cleanup.ref, cell);
|
|
844
|
-
if (cleanup.key !== undefined && cleanup.key !== key && owner.childProxies?.get(cleanup.key) === cleanup.ref) {
|
|
845
|
-
owner.childProxies.delete(cleanup.key);
|
|
846
|
-
}
|
|
847
|
-
if (owner.childProxies === undefined)
|
|
848
|
-
owner.childProxies = new Map();
|
|
849
|
-
owner.childProxies.set(key, cleanup.ref);
|
|
850
|
-
cleanup.key = key;
|
|
851
|
-
if (Array.isArray(owner.target)) {
|
|
852
|
-
if (owner.childCells === undefined)
|
|
853
|
-
owner.childCells = new Set();
|
|
854
|
-
owner.childCells.add(cleanup.ref);
|
|
855
|
-
}
|
|
856
|
-
};
|
|
857
|
-
const findPlacement = (owner, parent, segment, target, blocked) => {
|
|
858
|
-
if (blocked === undefined) {
|
|
859
|
-
const known = findEntry(target);
|
|
860
|
-
if (known !== undefined) {
|
|
861
|
-
for (const cell of known.cells) {
|
|
862
|
-
if (!cell.dead && cell.owner === owner && cell.parent === parent && cell.seg === segment)
|
|
863
|
-
return cell;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return undefined;
|
|
867
|
-
}
|
|
868
|
-
// Blocked views are intentionally absent from the ordinary raw-entry index.
|
|
869
|
-
for (const ref of owner.childCells ?? []) {
|
|
870
|
-
const cell = derefCell(ref);
|
|
871
|
-
if (cell !== undefined &&
|
|
872
|
-
!cell.dead &&
|
|
873
|
-
cell.target === target &&
|
|
874
|
-
cell.entry?.blocked === blocked &&
|
|
875
|
-
cell.parent === parent &&
|
|
876
|
-
cell.seg === segment) {
|
|
877
|
-
return cell;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
return undefined;
|
|
881
|
-
};
|
|
882
|
-
const unwrap = (value) => (isObj(value) ? (proxyEntries.get(value)?.target ?? value) : value);
|
|
883
|
-
const adoptItems = (values) => values.map(unwrap);
|
|
884
|
-
// Every operation is emitted once per live location of an explicitly aliased
|
|
885
|
-
// object. Alias cells are independent of public proxy lifetimes.
|
|
886
|
-
const emit = (entry, op) => {
|
|
887
|
-
record(op);
|
|
888
|
-
if (!aliased)
|
|
889
|
-
return;
|
|
890
|
-
const live = liveCells(entry);
|
|
891
|
-
if (live.length <= 1)
|
|
892
|
-
return;
|
|
893
|
-
const base = pathNow(entry);
|
|
894
|
-
const head = primary(entry);
|
|
895
|
-
for (const cell of live) {
|
|
896
|
-
if (cell === head || op[0] === "r")
|
|
897
|
-
continue;
|
|
898
|
-
const rest = op[1].slice(base.length);
|
|
899
|
-
const cloned = [...op];
|
|
900
|
-
cloned[1] = [...pathOf(cell), ...rest];
|
|
901
|
-
record(cloned);
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
// Structural mutation updates every still-live placement cell, including cells
|
|
905
|
-
// retained only by held descendants. Dead weak registrations are pruned lazily;
|
|
906
|
-
// finalizers keep idle live parents from accumulating them indefinitely.
|
|
907
|
-
const renumber = (entry, key, before, after, spliceAt, spliceRemove, spliceInsert) => {
|
|
908
|
-
shape++;
|
|
909
|
-
const childCells = entry.childCells;
|
|
910
|
-
if (key === "push" || !Array.isArray(entry.target) || childCells === undefined || childCells.size === 0)
|
|
911
|
-
return;
|
|
912
|
-
let index;
|
|
913
|
-
let remove = 0;
|
|
914
|
-
let insert = 0;
|
|
915
|
-
if (key === "pop") {
|
|
916
|
-
index = before - 1;
|
|
917
|
-
remove = before > 0 ? 1 : 0;
|
|
918
|
-
}
|
|
919
|
-
else if (key === "shift") {
|
|
920
|
-
index = 0;
|
|
921
|
-
remove = before > 0 ? 1 : 0;
|
|
922
|
-
}
|
|
923
|
-
else if (key === "unshift") {
|
|
924
|
-
index = 0;
|
|
925
|
-
insert = after - before;
|
|
926
|
-
}
|
|
927
|
-
else if (key === "splice") {
|
|
928
|
-
index = spliceAt;
|
|
929
|
-
remove = spliceRemove;
|
|
930
|
-
insert = spliceInsert;
|
|
931
|
-
}
|
|
932
|
-
else {
|
|
933
|
-
for (const ref of [...childCells]) {
|
|
934
|
-
const cell = derefCell(ref);
|
|
935
|
-
if (cell === undefined) {
|
|
936
|
-
childCells.delete(ref);
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
const at = entry.target.indexOf(cell.target);
|
|
940
|
-
if (at < 0) {
|
|
941
|
-
cell.dead = true;
|
|
942
|
-
childCells.delete(ref);
|
|
943
|
-
}
|
|
944
|
-
else
|
|
945
|
-
cell.seg = at;
|
|
946
|
-
}
|
|
947
|
-
entry.childProxies?.clear();
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
const delta = insert - remove;
|
|
951
|
-
for (const ref of [...childCells]) {
|
|
952
|
-
const cell = derefCell(ref);
|
|
953
|
-
if (cell === undefined) {
|
|
954
|
-
childCells.delete(ref);
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
957
|
-
const at = cell.seg;
|
|
958
|
-
if (typeof at !== "number")
|
|
959
|
-
continue;
|
|
960
|
-
if (at >= index && at < index + remove) {
|
|
961
|
-
cell.dead = true;
|
|
962
|
-
childCells.delete(ref);
|
|
963
|
-
}
|
|
964
|
-
else if (at >= index + remove)
|
|
965
|
-
cell.seg = at + delta;
|
|
966
|
-
}
|
|
967
|
-
entry.childProxies?.clear();
|
|
968
|
-
};
|
|
969
|
-
const wrap = (object, cell, blockedSegment) => {
|
|
970
|
-
const placed = cell.entry;
|
|
971
|
-
if (placed !== undefined && placed.target === object && placed.blocked === blockedSegment) {
|
|
972
|
-
return proxyFor(placed);
|
|
973
|
-
}
|
|
974
|
-
const existing = blockedSegment === undefined ? findEntry(object) : undefined;
|
|
975
|
-
if (existing !== undefined) {
|
|
976
|
-
addPlacement(existing, cell);
|
|
977
|
-
return proxyFor(existing);
|
|
978
|
-
}
|
|
979
|
-
const entry = {
|
|
980
|
-
target: object,
|
|
981
|
-
cells: new Set(),
|
|
982
|
-
fallback: cell,
|
|
983
|
-
blocked: blockedSegment,
|
|
984
|
-
proxy: undefined,
|
|
985
|
-
childProxies: undefined,
|
|
986
|
-
childCells: undefined,
|
|
987
|
-
};
|
|
988
|
-
addPlacement(entry, cell);
|
|
989
|
-
if (blockedSegment === undefined)
|
|
990
|
-
indexEntry(entry);
|
|
991
|
-
return proxyFor(entry);
|
|
992
|
-
};
|
|
993
|
-
const handlerPrototype = {
|
|
994
|
-
get(target, key, receiver) {
|
|
995
|
-
const entry = this.entry;
|
|
996
|
-
if (Array.isArray(target) && typeof key === "string" && MUTATORS.has(key)) {
|
|
997
|
-
return (...args) => {
|
|
998
|
-
if (entry.blocked !== undefined)
|
|
999
|
-
throw new UnsafePathError(entry.blocked);
|
|
1000
|
-
const detached = aliased ? liveCells(entry).length === 0 : isDetached(entry.fallback);
|
|
1001
|
-
if (detached) {
|
|
1002
|
-
return Reflect.apply(Array.prototype[key], target, args.map(unwrap));
|
|
1003
|
-
}
|
|
1004
|
-
const before = target.length;
|
|
1005
|
-
let spliceAt = -1;
|
|
1006
|
-
let spliceRemove = 0;
|
|
1007
|
-
let spliceInsert = 0;
|
|
1008
|
-
let insertAt = -1;
|
|
1009
|
-
let insertCount = 0;
|
|
1010
|
-
let result;
|
|
1011
|
-
switch (key) {
|
|
1012
|
-
case "push": {
|
|
1013
|
-
const items = adoptItems(args);
|
|
1014
|
-
if (items.length > 0)
|
|
1015
|
-
emit(entry, ["p", [...pathNow(entry)], before, 0, cloneJson(items)]);
|
|
1016
|
-
insertAt = before;
|
|
1017
|
-
insertCount = items.length;
|
|
1018
|
-
spliceItems(target, before, 0, items);
|
|
1019
|
-
result = target.length;
|
|
1020
|
-
break;
|
|
1021
|
-
}
|
|
1022
|
-
case "unshift": {
|
|
1023
|
-
const items = adoptItems(args);
|
|
1024
|
-
if (items.length > 0)
|
|
1025
|
-
emit(entry, ["p", [...pathNow(entry)], 0, 0, cloneJson(items)]);
|
|
1026
|
-
insertAt = 0;
|
|
1027
|
-
insertCount = items.length;
|
|
1028
|
-
spliceItems(target, 0, 0, items);
|
|
1029
|
-
result = target.length;
|
|
1030
|
-
break;
|
|
1031
|
-
}
|
|
1032
|
-
case "pop":
|
|
1033
|
-
if (before > 0)
|
|
1034
|
-
emit(entry, ["p", [...pathNow(entry)], before - 1, 1, []]);
|
|
1035
|
-
result = Reflect.apply(Array.prototype.pop, target, args);
|
|
1036
|
-
break;
|
|
1037
|
-
case "shift":
|
|
1038
|
-
if (before > 0)
|
|
1039
|
-
emit(entry, ["p", [...pathNow(entry)], 0, 1, []]);
|
|
1040
|
-
result = Reflect.apply(Array.prototype.shift, target, args);
|
|
1041
|
-
break;
|
|
1042
|
-
case "splice": {
|
|
1043
|
-
const items = adoptItems(args.slice(2));
|
|
1044
|
-
const { index, remove } = spliceRange(before, args);
|
|
1045
|
-
spliceAt = index;
|
|
1046
|
-
spliceRemove = remove;
|
|
1047
|
-
spliceInsert = items.length;
|
|
1048
|
-
insertAt = index;
|
|
1049
|
-
insertCount = items.length;
|
|
1050
|
-
if (remove > 0 || items.length > 0) {
|
|
1051
|
-
if (index === 0 && remove === before) {
|
|
1052
|
-
if (pathNow(entry).length === 0)
|
|
1053
|
-
emit(entry, ["r", cloneJson(items)]);
|
|
1054
|
-
else
|
|
1055
|
-
emit(entry, [
|
|
1056
|
-
"s",
|
|
1057
|
-
[...pathNow(entry)],
|
|
1058
|
-
cloneJson(items),
|
|
1059
|
-
]);
|
|
1060
|
-
}
|
|
1061
|
-
else
|
|
1062
|
-
emit(entry, ["p", [...pathNow(entry)], index, remove, cloneJson(items)]);
|
|
1063
|
-
}
|
|
1064
|
-
result = spliceItems(target, index, remove, items);
|
|
1065
|
-
break;
|
|
1066
|
-
}
|
|
1067
|
-
default: {
|
|
1068
|
-
result = Reflect.apply(Array.prototype[key], target, args.map(unwrap));
|
|
1069
|
-
if (pathNow(entry).length === 0)
|
|
1070
|
-
emit(entry, ["r", cloneJson(target)]);
|
|
1071
|
-
else
|
|
1072
|
-
emit(entry, [
|
|
1073
|
-
"s",
|
|
1074
|
-
[...pathNow(entry)],
|
|
1075
|
-
cloneJson(target),
|
|
1076
|
-
]);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
collapsePending();
|
|
1080
|
-
renumber(entry, key, before, target.length, spliceAt, spliceRemove, spliceInsert);
|
|
1081
|
-
for (let index = insertAt; insertCount > 0 && index < insertAt + insertCount; index++) {
|
|
1082
|
-
const item = target[index];
|
|
1083
|
-
if (!isObj(item))
|
|
1084
|
-
continue;
|
|
1085
|
-
const known = findEntry(item);
|
|
1086
|
-
if (known === undefined || known.target === entry.target)
|
|
1087
|
-
continue;
|
|
1088
|
-
const parent = primary(entry);
|
|
1089
|
-
let seen = false;
|
|
1090
|
-
for (const existingCell of known.cells) {
|
|
1091
|
-
if (!existingCell.dead && existingCell.parent === parent && existingCell.seg === index)
|
|
1092
|
-
seen = true;
|
|
1093
|
-
}
|
|
1094
|
-
if (!seen) {
|
|
1095
|
-
const inserted = {
|
|
1096
|
-
parent,
|
|
1097
|
-
owner: entry,
|
|
1098
|
-
entry: undefined,
|
|
1099
|
-
target: item,
|
|
1100
|
-
seg: index,
|
|
1101
|
-
dead: false,
|
|
1102
|
-
};
|
|
1103
|
-
addPlacement(known, inserted);
|
|
1104
|
-
cachePlacement(entry, String(index), inserted);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
return key === "sort" || key === "reverse" || key === "fill" || key === "copyWithin" ? receiver : result;
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
const value = Reflect.get(target, key, receiver);
|
|
1111
|
-
if (!isObj(value))
|
|
1112
|
-
return value;
|
|
1113
|
-
const cachedRef = entry.childProxies?.get(key);
|
|
1114
|
-
const cached = cachedRef === undefined ? undefined : derefCell(cachedRef);
|
|
1115
|
-
if (cached !== undefined && cached.target === value && cached.entry !== undefined) {
|
|
1116
|
-
return proxyFor(cached.entry);
|
|
1117
|
-
}
|
|
1118
|
-
if (cachedRef !== undefined && cached === undefined && entry.childProxies?.get(key) === cachedRef) {
|
|
1119
|
-
entry.childProxies?.delete(key);
|
|
1120
|
-
}
|
|
1121
|
-
const rawSegment = norm(target, key);
|
|
1122
|
-
let segment;
|
|
1123
|
-
let childBlocked = entry.blocked;
|
|
1124
|
-
if (entry.blocked !== undefined) {
|
|
1125
|
-
if (typeof rawSegment === "symbol")
|
|
1126
|
-
throw new UnsafePathError(String(rawSegment));
|
|
1127
|
-
segment = rawSegment;
|
|
1128
|
-
}
|
|
1129
|
-
else if (typeof rawSegment === "string" && RESERVED_SEGMENTS.has(rawSegment) && Object.hasOwn(target, key)) {
|
|
1130
|
-
segment = rawSegment;
|
|
1131
|
-
childBlocked = rawSegment;
|
|
1132
|
-
}
|
|
1133
|
-
else
|
|
1134
|
-
segment = guard(rawSegment);
|
|
1135
|
-
const parent = primary(entry);
|
|
1136
|
-
const existingCell = findPlacement(entry, parent, segment, value, childBlocked);
|
|
1137
|
-
if (existingCell?.entry !== undefined) {
|
|
1138
|
-
cachePlacement(entry, key, existingCell);
|
|
1139
|
-
return proxyFor(existingCell.entry);
|
|
1140
|
-
}
|
|
1141
|
-
const childCell = {
|
|
1142
|
-
parent,
|
|
1143
|
-
owner: entry,
|
|
1144
|
-
entry: undefined,
|
|
1145
|
-
target: value,
|
|
1146
|
-
seg: segment,
|
|
1147
|
-
dead: false,
|
|
1148
|
-
};
|
|
1149
|
-
const child = wrap(value, childCell, childBlocked);
|
|
1150
|
-
cachePlacement(entry, key, childCell);
|
|
1151
|
-
return child;
|
|
1152
|
-
},
|
|
1153
|
-
set(target, key, value) {
|
|
1154
|
-
const entry = this.entry;
|
|
1155
|
-
if (entry.blocked !== undefined)
|
|
1156
|
-
throw new UnsafePathError(entry.blocked);
|
|
1157
|
-
const rawValue = unwrap(value);
|
|
1158
|
-
if (aliased ? liveCells(entry).length === 0 : isDetached(entry.fallback)) {
|
|
1159
|
-
return Reflect.set(target, key, rawValue);
|
|
1160
|
-
}
|
|
1161
|
-
if (Array.isArray(target) && key === "length") {
|
|
1162
|
-
const before = target.length;
|
|
1163
|
-
const next = Number(rawValue);
|
|
1164
|
-
if (!Number.isSafeInteger(next) || next < 0 || next > 4_294_967_295) {
|
|
1165
|
-
return Reflect.set(target, key, rawValue);
|
|
1166
|
-
}
|
|
1167
|
-
if (next < before) {
|
|
1168
|
-
if (next === 0) {
|
|
1169
|
-
if (pathNow(entry).length === 0)
|
|
1170
|
-
emit(entry, ["r", []]);
|
|
1171
|
-
else
|
|
1172
|
-
emit(entry, ["s", [...pathNow(entry)], []]);
|
|
1173
|
-
}
|
|
1174
|
-
else
|
|
1175
|
-
emit(entry, ["p", [...pathNow(entry)], next, before - next, []]);
|
|
1176
|
-
Reflect.set(target, key, next);
|
|
1177
|
-
renumber(entry, "splice", before, next, next, before - next, 0);
|
|
1178
|
-
entry.childProxies?.clear();
|
|
1179
|
-
}
|
|
1180
|
-
else if (next > before) {
|
|
1181
|
-
target.length = next;
|
|
1182
|
-
target.fill(null, before);
|
|
1183
|
-
const grown = new Array(next - before).fill(null);
|
|
1184
|
-
emit(entry, ["p", [...pathNow(entry)], before, 0, grown]);
|
|
1185
|
-
}
|
|
1186
|
-
collapsePending();
|
|
1187
|
-
return true;
|
|
1188
|
-
}
|
|
1189
|
-
const segment = guard(norm(target, key));
|
|
1190
|
-
if (Array.isArray(target)) {
|
|
1191
|
-
if (typeof segment !== "number")
|
|
1192
|
-
throw new UnsafePathError(segment);
|
|
1193
|
-
if (segment > target.length)
|
|
1194
|
-
throw new UnsafePathError(segment);
|
|
1195
|
-
}
|
|
1196
|
-
const at = [...pathNow(entry), segment];
|
|
1197
|
-
if (rawValue === undefined) {
|
|
1198
|
-
if (Array.isArray(target))
|
|
1199
|
-
throw new TypeError("undefined would create a sparse array; use splice instead");
|
|
1200
|
-
if (Object.hasOwn(target, key))
|
|
1201
|
-
emit(entry, ["d", at]);
|
|
1202
|
-
entry.childProxies?.delete(key);
|
|
1203
|
-
const deleted = Reflect.deleteProperty(target, key);
|
|
1204
|
-
if (deleted)
|
|
1205
|
-
collapsePending();
|
|
1206
|
-
return deleted;
|
|
1207
|
-
}
|
|
1208
|
-
const previous = target[key];
|
|
1209
|
-
if (previous === rawValue)
|
|
1210
|
-
return true;
|
|
1211
|
-
if (Array.isArray(target) && segment === target.length) {
|
|
1212
|
-
emit(entry, ["p", [...pathNow(entry)], target.length, 0, [cloneJson(rawValue)]]);
|
|
1213
|
-
}
|
|
1214
|
-
else if (isObj(previous) && isObj(rawValue)) {
|
|
1215
|
-
diffInto(previous, rawValue, [...pathNow(entry), segment]);
|
|
1216
|
-
}
|
|
1217
|
-
else if (typeof previous === "string" && typeof rawValue === "string") {
|
|
1218
|
-
if (!aliased)
|
|
1219
|
-
recordString([...pathNow(entry), segment], previous, rawValue);
|
|
1220
|
-
else
|
|
1221
|
-
for (const cell of liveCells(entry))
|
|
1222
|
-
recordString([...pathOf(cell), segment], previous, rawValue);
|
|
1223
|
-
}
|
|
1224
|
-
else {
|
|
1225
|
-
emit(entry, ["s", at, cloneJson(rawValue)]);
|
|
1226
|
-
}
|
|
1227
|
-
entry.childProxies?.delete(key);
|
|
1228
|
-
const updated = Reflect.set(target, key, rawValue);
|
|
1229
|
-
if (updated) {
|
|
1230
|
-
if (isObj(rawValue)) {
|
|
1231
|
-
const known = findEntry(rawValue);
|
|
1232
|
-
if (known !== undefined) {
|
|
1233
|
-
const placement = {
|
|
1234
|
-
parent: primary(entry),
|
|
1235
|
-
owner: entry,
|
|
1236
|
-
entry: undefined,
|
|
1237
|
-
target: rawValue,
|
|
1238
|
-
seg: segment,
|
|
1239
|
-
dead: false,
|
|
1240
|
-
};
|
|
1241
|
-
addPlacement(known, placement);
|
|
1242
|
-
cachePlacement(entry, key, placement);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
collapsePending();
|
|
1246
|
-
}
|
|
1247
|
-
return updated;
|
|
1248
|
-
},
|
|
1249
|
-
deleteProperty(target, key) {
|
|
1250
|
-
const entry = this.entry;
|
|
1251
|
-
if (entry.blocked !== undefined)
|
|
1252
|
-
throw new UnsafePathError(entry.blocked);
|
|
1253
|
-
if (aliased ? liveCells(entry).length === 0 : isDetached(entry.fallback)) {
|
|
1254
|
-
return Reflect.deleteProperty(target, key);
|
|
1255
|
-
}
|
|
1256
|
-
const segment = guard(norm(target, key));
|
|
1257
|
-
if (Array.isArray(target)) {
|
|
1258
|
-
if (typeof segment !== "number")
|
|
1259
|
-
throw new UnsafePathError(segment);
|
|
1260
|
-
throw new TypeError("delete would create a sparse array; use splice instead");
|
|
1261
|
-
}
|
|
1262
|
-
if (Object.hasOwn(target, key))
|
|
1263
|
-
emit(entry, ["d", [...pathNow(entry), segment]]);
|
|
1264
|
-
entry.childProxies?.delete(key);
|
|
1265
|
-
const deleted = Reflect.deleteProperty(target, key);
|
|
1266
|
-
if (deleted)
|
|
1267
|
-
collapsePending();
|
|
1268
|
-
return deleted;
|
|
1269
|
-
},
|
|
1270
|
-
defineProperty() {
|
|
1271
|
-
throw new TypeError("defineProperty is not supported on tracked state; use assignment");
|
|
1272
|
-
},
|
|
1273
|
-
setPrototypeOf() {
|
|
1274
|
-
throw new TypeError("setPrototypeOf is not supported on tracked state");
|
|
1275
|
-
},
|
|
1276
|
-
preventExtensions() {
|
|
1277
|
-
throw new TypeError("preventExtensions is not supported on tracked state");
|
|
1278
|
-
},
|
|
1279
|
-
};
|
|
1280
|
-
function proxyFor(entry) {
|
|
1281
|
-
const cached = jobProxies.get(entry);
|
|
1282
|
-
if (cached !== undefined)
|
|
1283
|
-
return cached;
|
|
1284
|
-
const existing = entry.proxy?.deref();
|
|
1285
|
-
if (existing !== undefined) {
|
|
1286
|
-
jobProxies.set(entry, existing);
|
|
1287
|
-
scheduleJobCleanup();
|
|
1288
|
-
return existing;
|
|
1289
|
-
}
|
|
1290
|
-
const handler = Object.create(handlerPrototype);
|
|
1291
|
-
handler.entry = entry;
|
|
1292
|
-
const proxy = new Proxy(entry.target, handler);
|
|
1293
|
-
entry.proxy = new WeakRef(proxy);
|
|
1294
|
-
jobProxies.set(entry, proxy);
|
|
1295
|
-
scheduleJobCleanup();
|
|
1296
|
-
proxyEntries.set(proxy, entry);
|
|
1297
|
-
return proxy;
|
|
1298
|
-
}
|
|
1299
|
-
const rootCell = {
|
|
1300
|
-
parent: undefined,
|
|
1301
|
-
owner: undefined,
|
|
1302
|
-
entry: undefined,
|
|
1303
|
-
target: root,
|
|
1304
|
-
seg: "",
|
|
1305
|
-
dead: false,
|
|
1306
|
-
};
|
|
1307
|
-
let state = wrap(root, rootCell);
|
|
1308
|
-
return {
|
|
1309
|
-
get state() {
|
|
1310
|
-
return state;
|
|
1311
|
-
},
|
|
1312
|
-
get target() {
|
|
1313
|
-
return root;
|
|
1314
|
-
},
|
|
1315
|
-
set state(next) {
|
|
1316
|
-
const rawNext = unwrap(next);
|
|
1317
|
-
if (rawNext === root) {
|
|
1318
|
-
clearPending();
|
|
1319
|
-
forceBase = true;
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
clearPending();
|
|
1323
|
-
root = rawNext;
|
|
1324
|
-
state = wrap(root, {
|
|
1325
|
-
parent: undefined,
|
|
1326
|
-
owner: undefined,
|
|
1327
|
-
entry: undefined,
|
|
1328
|
-
target: root,
|
|
1329
|
-
seg: "",
|
|
1330
|
-
dead: false,
|
|
1331
|
-
});
|
|
1332
|
-
forceBase = true;
|
|
1333
|
-
},
|
|
1334
|
-
rebase() {
|
|
1335
|
-
clearPending();
|
|
1336
|
-
forceBase = true;
|
|
1337
|
-
},
|
|
1338
|
-
get dirty() {
|
|
1339
|
-
// conservative: true if anything was written since the last flush, even
|
|
1340
|
-
// if the writes cancelled out
|
|
1341
|
-
return forceBase || hasPending;
|
|
1342
|
-
},
|
|
1343
|
-
discard() {
|
|
1344
|
-
clearPending();
|
|
1345
|
-
},
|
|
1346
|
-
flush() {
|
|
1347
|
-
if (forceBase) {
|
|
1348
|
-
const value = cloneJson(root);
|
|
1349
|
-
forceBase = false;
|
|
1350
|
-
clearPending();
|
|
1351
|
-
return [["r", value]];
|
|
1352
|
-
}
|
|
1353
|
-
if (!hasPending)
|
|
1354
|
-
return [];
|
|
1355
|
-
const out = [];
|
|
1356
|
-
for (const slot of log) {
|
|
1357
|
-
if (slot === undefined || slot.dead)
|
|
1358
|
-
continue;
|
|
1359
|
-
if (slot.str !== undefined) {
|
|
1360
|
-
diffValue(slot.str.anchor, slot.str.value, slot.op[1], scan, out);
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
out.push(slot.op);
|
|
1364
|
-
}
|
|
1365
|
-
clearPending();
|
|
1366
|
-
return out;
|
|
1367
|
-
},
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
44
|
+
// ─── Immutable revision tracking ─────────────────────────────────────────────
|
|
45
|
+
export { diffRevisions } from "./diff.js";
|
|
46
|
+
export { track } from "./tracker.js";
|
|
1370
47
|
// ─── Path safety ─────────────────────────────────────────────────────────────
|
|
1371
48
|
/**
|
|
1372
49
|
* Segments that reach the prototype chain.
|
|
@@ -1691,21 +368,33 @@ function applyOps(target, ops) {
|
|
|
1691
368
|
}
|
|
1692
369
|
return root;
|
|
1693
370
|
}
|
|
1694
|
-
/** Apply decoded
|
|
371
|
+
/** Apply one decoded operation batch without mutating the previous immutable value. */
|
|
1695
372
|
export function applyImmutable(target, ops) {
|
|
373
|
+
return applyImmutableBatches(target, [ops]);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Apply decoded operation batches as one final-result-only replay.
|
|
377
|
+
*
|
|
378
|
+
* Containers copied for an earlier batch may be mutated while applying a later
|
|
379
|
+
* batch, so this deliberately exposes no intermediate revisions.
|
|
380
|
+
*/
|
|
381
|
+
export function applyImmutableBatches(target, batches) {
|
|
1696
382
|
let root = target;
|
|
1697
|
-
|
|
1698
|
-
|
|
383
|
+
const owned = new WeakSet();
|
|
384
|
+
for (const ops of batches) {
|
|
385
|
+
for (const op of ops) {
|
|
1699
386
|
assertValidOp(op);
|
|
1700
|
-
|
|
1701
|
-
|
|
387
|
+
if (op[0] === "r") {
|
|
388
|
+
root = op[1];
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
root = copyContainers(root, op[0] === "p" || op[0] === "m" ? op[1] : op[1].slice(0, -1), owned);
|
|
392
|
+
root = applyOps(root, [op]);
|
|
1702
393
|
}
|
|
1703
|
-
root = copyContainers(root, op[0] === "p" || op[0] === "m" ? op[1] : op[1].slice(0, -1));
|
|
1704
|
-
root = applyOps(root, [op]);
|
|
1705
394
|
}
|
|
1706
395
|
return root;
|
|
1707
396
|
}
|
|
1708
|
-
function copyContainers(root, path) {
|
|
397
|
+
function copyContainers(root, path, owned) {
|
|
1709
398
|
const copy = (value) => {
|
|
1710
399
|
if (Array.isArray(value))
|
|
1711
400
|
return value.slice();
|
|
@@ -1722,15 +411,28 @@ function copyContainers(root, path) {
|
|
|
1722
411
|
}
|
|
1723
412
|
return result;
|
|
1724
413
|
};
|
|
1725
|
-
|
|
1726
|
-
|
|
414
|
+
if (!isObj(root))
|
|
415
|
+
throw new PathError(path);
|
|
416
|
+
let copiedRoot;
|
|
417
|
+
if (owned.has(root))
|
|
418
|
+
copiedRoot = root;
|
|
419
|
+
else {
|
|
420
|
+
copiedRoot = copy(root);
|
|
421
|
+
owned.add(copiedRoot);
|
|
422
|
+
}
|
|
1727
423
|
let destination = copiedRoot;
|
|
1728
424
|
for (const segment of path) {
|
|
1729
|
-
if (!
|
|
425
|
+
if (!Object.hasOwn(destination, segment))
|
|
1730
426
|
throw new PathError(path);
|
|
1731
|
-
if (Array.isArray(
|
|
427
|
+
if (Array.isArray(destination) && typeof segment !== "number")
|
|
1732
428
|
throw new UnsafePathError(segment);
|
|
1733
|
-
const child =
|
|
429
|
+
const child = destination[segment];
|
|
430
|
+
if (!isObj(child))
|
|
431
|
+
throw new PathError(path);
|
|
432
|
+
if (owned.has(child)) {
|
|
433
|
+
destination = child;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
1734
436
|
const copiedChild = copy(child);
|
|
1735
437
|
Object.defineProperty(destination, segment, {
|
|
1736
438
|
value: copiedChild,
|
|
@@ -1738,7 +440,7 @@ function copyContainers(root, path) {
|
|
|
1738
440
|
enumerable: true,
|
|
1739
441
|
configurable: true,
|
|
1740
442
|
});
|
|
1741
|
-
|
|
443
|
+
owned.add(copiedChild);
|
|
1742
444
|
destination = copiedChild;
|
|
1743
445
|
}
|
|
1744
446
|
return copiedRoot;
|