@earendil-works/chord 0.85.0 → 0.86.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 +8 -8
- package/dist/delta/index.d.ts +3 -3
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +1057 -366
- package/dist/delta/index.js.map +1 -1
- package/package.json +2 -2
- package/src/delta/README.md +127 -42
package/dist/delta/index.js
CHANGED
|
@@ -46,14 +46,13 @@ const cloneJson = (value) => {
|
|
|
46
46
|
return value;
|
|
47
47
|
if (Array.isArray(value))
|
|
48
48
|
return value.map((item) => cloneJson(item));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
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);
|
|
57
56
|
}
|
|
58
57
|
return result;
|
|
59
58
|
};
|
|
@@ -61,7 +60,6 @@ const INDEX = /^(?:0|[1-9]\d*)$/;
|
|
|
61
60
|
const norm = (target, key) => typeof key === "symbol" ? key : Array.isArray(target) && INDEX.test(key) ? Number(key) : key;
|
|
62
61
|
const MUTATORS = new Set(["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"]);
|
|
63
62
|
const MISSING = Symbol("missing");
|
|
64
|
-
const dirtyNode = () => ({ children: new Map() });
|
|
65
63
|
const spliceItems = (target, index, remove, items) => {
|
|
66
64
|
const removed = Reflect.apply(Array.prototype.splice, target, [index, remove]);
|
|
67
65
|
const chunkSize = 10_000;
|
|
@@ -96,11 +94,6 @@ const jsonEqual = (left, right) => {
|
|
|
96
94
|
}
|
|
97
95
|
return true;
|
|
98
96
|
};
|
|
99
|
-
const ownValue = (value, segment) => {
|
|
100
|
-
if (!isObj(value) || !Object.hasOwn(value, segment))
|
|
101
|
-
return MISSING;
|
|
102
|
-
return value[segment];
|
|
103
|
-
};
|
|
104
97
|
const emitSet = (path, value, out) => {
|
|
105
98
|
const snapshot = cloneJson(value);
|
|
106
99
|
if (path.length === 0)
|
|
@@ -220,197 +213,461 @@ function diffArray(before, after, path, scan, out) {
|
|
|
220
213
|
out.push(["p", [...path], after.length, before.length - after.length, []]);
|
|
221
214
|
}
|
|
222
215
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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)
|
|
232
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);
|
|
233
271
|
}
|
|
234
|
-
|
|
235
|
-
|
|
272
|
+
log = compacted;
|
|
273
|
+
tombstones = 0;
|
|
274
|
+
};
|
|
275
|
+
const killSlot = (slot) => {
|
|
276
|
+
if (slot.dead)
|
|
236
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;
|
|
237
294
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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))
|
|
241
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;
|
|
242
334
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const current = ownValue(after, segment);
|
|
248
|
-
if (previous === MISSING || current === MISSING || child.valueDirty) {
|
|
249
|
-
diffValue(previous, current, [...path, segment], scan, out);
|
|
335
|
+
if (at.retiredKids !== undefined) {
|
|
336
|
+
for (const generation of at.retiredKids) {
|
|
337
|
+
for (const child of generation.values())
|
|
338
|
+
killSubtree(child);
|
|
250
339
|
}
|
|
251
|
-
|
|
252
|
-
|
|
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;
|
|
253
361
|
}
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
}
|
|
256
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;
|
|
257
384
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const previous = ownValue(before, segment);
|
|
265
|
-
const current = ownValue(after, segment);
|
|
266
|
-
if (previous === MISSING || current === MISSING || child.valueDirty) {
|
|
267
|
-
diffValue(previous, current, [...path, segment], scan, out);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (!isObj(previous) || !isObj(current)) {
|
|
271
|
-
diffValue(previous, current, [...path, segment], scan, out);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
walkDirty(previous, current, child, [...path, segment], scan, out);
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
/**
|
|
278
|
-
* Bring `baseline` up to `root` along the dirty paths by sharing references.
|
|
279
|
-
* Returns false — having changed nothing — if any dirty node is an array
|
|
280
|
-
* change other than a pure append, so the caller can replay ops instead.
|
|
281
|
-
* Cloning a whole array there is O(n) per flush; replay is O(changes).
|
|
282
|
-
*
|
|
283
|
-
* Strings are immutable, so root's `after` is shared outright, and sharing it
|
|
284
|
-
* also means the next flush compares against a flat string rather than a cons.
|
|
285
|
-
* Objects are cloned because root keeps mutating them.
|
|
286
|
-
*/
|
|
287
|
-
const syncBaseline = (baseline, root, node) => {
|
|
288
|
-
if (!canSync(node))
|
|
289
|
-
return false;
|
|
290
|
-
syncInto(baseline, root, node);
|
|
291
|
-
return true;
|
|
292
|
-
};
|
|
293
|
-
const canSync = (node) => {
|
|
294
|
-
if (node.array !== undefined && node.array.kind !== "append")
|
|
295
|
-
return false;
|
|
296
|
-
for (const child of node.children.values())
|
|
297
|
-
if (!canSync(child))
|
|
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)
|
|
298
391
|
return false;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const start = node.array.start;
|
|
305
|
-
for (const [index, child] of node.children) {
|
|
306
|
-
if (typeof index === "number" && index < start)
|
|
307
|
-
syncChild(parent, root, index, child);
|
|
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]];
|
|
308
397
|
}
|
|
309
|
-
|
|
310
|
-
|
|
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;
|
|
311
437
|
}
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
for (const [segment, child] of node.children)
|
|
315
|
-
syncChild(parent, root, segment, child);
|
|
316
|
-
};
|
|
317
|
-
const syncChild = (parent, root, segment, child) => {
|
|
318
|
-
const current = ownValue(root, segment);
|
|
319
|
-
const previous = ownValue(parent, segment);
|
|
320
|
-
if (current === MISSING) {
|
|
321
|
-
if (Array.isArray(parent))
|
|
322
|
-
parent.splice(segment, 1);
|
|
323
|
-
else
|
|
324
|
-
delete parent[segment];
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (child.valueDirty || !isObj(current) || !isObj(previous) || Array.isArray(current) !== Array.isArray(previous)) {
|
|
328
|
-
parent[segment] = isObj(current) ? cloneJson(current) : current;
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
syncInto(previous, current, child);
|
|
332
|
-
};
|
|
333
|
-
const cloneOp = (op) => {
|
|
334
|
-
switch (op[0]) {
|
|
335
|
-
case "r":
|
|
336
|
-
return ["r", cloneJson(op[1])];
|
|
337
|
-
case "s":
|
|
338
|
-
return ["s", op[1], cloneJson(op[2])];
|
|
339
|
-
case "p":
|
|
340
|
-
return ["p", op[1], op[2], op[3], cloneJson(op[4])];
|
|
341
|
-
default:
|
|
342
|
-
return op;
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
export function track(root, options = {}) {
|
|
346
|
-
const scan = options.maxOverlapScan ?? 65_536;
|
|
347
|
-
let pending = dirtyNode();
|
|
348
|
-
let hasPending = false;
|
|
349
|
-
let baseline;
|
|
350
|
-
let forceBase = true;
|
|
351
|
-
const clearPending = () => {
|
|
352
|
-
pending = dirtyNode();
|
|
353
|
-
hasPending = false;
|
|
354
438
|
};
|
|
355
|
-
const
|
|
439
|
+
const recordString = (path, previous, value) => {
|
|
440
|
+
if (forceBase)
|
|
441
|
+
return;
|
|
356
442
|
hasPending = true;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
}
|
|
368
461
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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);
|
|
378
487
|
}
|
|
379
|
-
|
|
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
|
+
});
|
|
380
496
|
};
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
if (node === undefined)
|
|
497
|
+
const record = (op) => {
|
|
498
|
+
if (forceBase)
|
|
384
499
|
return;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 });
|
|
388
618
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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);
|
|
392
633
|
return;
|
|
393
634
|
}
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
400
641
|
return;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
}
|
|
407
664
|
return;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
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);
|
|
414
671
|
};
|
|
415
672
|
const guard = (segment) => {
|
|
416
673
|
if (typeof segment === "symbol")
|
|
@@ -419,7 +676,6 @@ export function track(root, options = {}) {
|
|
|
419
676
|
throw new UnsafePathError(segment);
|
|
420
677
|
return segment;
|
|
421
678
|
};
|
|
422
|
-
const adoptItems = (values) => values;
|
|
423
679
|
const integer = (value) => {
|
|
424
680
|
const number = Number(value);
|
|
425
681
|
if (Number.isNaN(number) || number === 0)
|
|
@@ -436,189 +692,619 @@ export function track(root, options = {}) {
|
|
|
436
692
|
: Math.max(0, Math.min(integer(args[1]), length - index));
|
|
437
693
|
return { index, remove };
|
|
438
694
|
};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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;
|
|
507
864
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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;
|
|
521
938
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
childBlocked = rawSegment;
|
|
939
|
+
const at = entry.target.indexOf(cell.target);
|
|
940
|
+
if (at < 0) {
|
|
941
|
+
cell.dead = true;
|
|
942
|
+
childCells.delete(ref);
|
|
527
943
|
}
|
|
528
944
|
else
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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));
|
|
542
1003
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
}
|
|
551
1078
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
target
|
|
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
|
+
}
|
|
556
1106
|
}
|
|
557
|
-
return
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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);
|
|
565
1166
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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)], []]);
|
|
570
1173
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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();
|
|
574
1179
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return true;
|
|
581
|
-
if (Array.isArray(target)) {
|
|
582
|
-
const index = segment;
|
|
583
|
-
if (index === target.length)
|
|
584
|
-
markArrayAppend(path, target.length);
|
|
585
|
-
else {
|
|
586
|
-
const start = appendStart(path);
|
|
587
|
-
if (start === undefined || index < start)
|
|
588
|
-
markValue(at);
|
|
589
|
-
}
|
|
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]);
|
|
590
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);
|
|
591
1220
|
else
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
+
}
|
|
604
1244
|
}
|
|
605
|
-
|
|
606
|
-
|
|
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)) {
|
|
607
1254
|
return Reflect.deleteProperty(target, key);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
throw new TypeError("
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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);
|
|
619
1297
|
return proxy;
|
|
1298
|
+
}
|
|
1299
|
+
const rootCell = {
|
|
1300
|
+
parent: undefined,
|
|
1301
|
+
owner: undefined,
|
|
1302
|
+
entry: undefined,
|
|
1303
|
+
target: root,
|
|
1304
|
+
seg: "",
|
|
1305
|
+
dead: false,
|
|
620
1306
|
};
|
|
621
|
-
let state = wrap(root,
|
|
1307
|
+
let state = wrap(root, rootCell);
|
|
622
1308
|
return {
|
|
623
1309
|
get state() {
|
|
624
1310
|
return state;
|
|
@@ -627,15 +1313,22 @@ export function track(root, options = {}) {
|
|
|
627
1313
|
return root;
|
|
628
1314
|
},
|
|
629
1315
|
set state(next) {
|
|
630
|
-
|
|
1316
|
+
const rawNext = unwrap(next);
|
|
1317
|
+
if (rawNext === root) {
|
|
631
1318
|
clearPending();
|
|
632
1319
|
forceBase = true;
|
|
633
1320
|
return;
|
|
634
1321
|
}
|
|
635
1322
|
clearPending();
|
|
636
|
-
root =
|
|
637
|
-
state = wrap(root,
|
|
638
|
-
|
|
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
|
+
});
|
|
639
1332
|
forceBase = true;
|
|
640
1333
|
},
|
|
641
1334
|
rebase() {
|
|
@@ -643,33 +1336,31 @@ export function track(root, options = {}) {
|
|
|
643
1336
|
forceBase = true;
|
|
644
1337
|
},
|
|
645
1338
|
get dirty() {
|
|
1339
|
+
// conservative: true if anything was written since the last flush, even
|
|
1340
|
+
// if the writes cancelled out
|
|
646
1341
|
return forceBase || hasPending;
|
|
647
1342
|
},
|
|
648
1343
|
discard() {
|
|
649
|
-
baseline = cloneJson(root);
|
|
650
1344
|
clearPending();
|
|
651
1345
|
},
|
|
652
1346
|
flush() {
|
|
653
1347
|
if (forceBase) {
|
|
654
1348
|
const value = cloneJson(root);
|
|
655
|
-
baseline = cloneJson(root);
|
|
656
1349
|
forceBase = false;
|
|
657
1350
|
clearPending();
|
|
658
1351
|
return [["r", value]];
|
|
659
1352
|
}
|
|
660
|
-
if (!hasPending
|
|
1353
|
+
if (!hasPending)
|
|
661
1354
|
return [];
|
|
662
1355
|
const out = [];
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (out.length > 0)
|
|
672
|
-
baseline = apply(baseline, out.map(cloneOp));
|
|
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);
|
|
673
1364
|
}
|
|
674
1365
|
clearPending();
|
|
675
1366
|
return out;
|