@rohal12/spindle 0.59.3 → 0.59.5
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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +318 -116
- package/package.json +1 -1
- package/src/components/macros/Include.tsx +7 -2
- package/src/define-macro.ts +6 -3
- package/src/execute-mutation.ts +192 -123
- package/src/saves/save-manager.ts +6 -6
- package/src/saves/storage.ts +24 -1
- package/src/store.ts +71 -35
- package/src/story-api.ts +15 -1
- package/src/structural.ts +183 -35
- package/src/utils/object-path.ts +3 -5
package/src/store.ts
CHANGED
|
@@ -44,8 +44,16 @@ import {
|
|
|
44
44
|
deletePlaythroughData as smDeletePlaythroughData,
|
|
45
45
|
} from './saves/save-manager';
|
|
46
46
|
|
|
47
|
-
import {
|
|
48
|
-
|
|
47
|
+
import {
|
|
48
|
+
changesBetween,
|
|
49
|
+
deepClone,
|
|
50
|
+
existingObjects,
|
|
51
|
+
locateObjects,
|
|
52
|
+
mergesWith,
|
|
53
|
+
shareEqual,
|
|
54
|
+
type PathChange,
|
|
55
|
+
} from './structural';
|
|
56
|
+
import { getByPath } from './utils/object-path';
|
|
49
57
|
import { noPassageError, showRuntimeError } from './runtime-errors';
|
|
50
58
|
import {
|
|
51
59
|
snapshotPRNG,
|
|
@@ -708,11 +716,13 @@ function loadedEntryMoment(
|
|
|
708
716
|
* the `beforesave` hooks) into the payload's snapshot of the saved moment. A
|
|
709
717
|
* load restores that snapshot, the state on entering the passage, and runs
|
|
710
718
|
* the passage again, so data a hook adds to a save would otherwise be lost
|
|
711
|
-
* on load (#227). Only the property paths the hooks changed are written (
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
719
|
+
* on load (#227). Only the property paths the hooks changed are written (see
|
|
720
|
+
* changesBetween): a whole variable would bring along what the passage did
|
|
721
|
+
* to the rest of it, which the passage then does again on load (#232). The
|
|
722
|
+
* references the hooks made stay: an object they put in two variables is one
|
|
723
|
+
* object in the snapshot, and one they made another variable refer to is the
|
|
724
|
+
* snapshot's own object of it (#302). Only the payload's copy changes: the
|
|
725
|
+
* live history keeps the recorded snapshot (#159).
|
|
716
726
|
*/
|
|
717
727
|
function keepHookWrites(
|
|
718
728
|
payload: SavePayload,
|
|
@@ -721,41 +731,67 @@ function keepHookWrites(
|
|
|
721
731
|
): void {
|
|
722
732
|
const moment = payload.history[payload.historyIndex];
|
|
723
733
|
if (!moment) return;
|
|
724
|
-
|
|
734
|
+
const changes = changesBetween(before, after, true);
|
|
735
|
+
if (changes.length === 0) return;
|
|
736
|
+
// A copy of its own, so the writes keep the snapshot's references (which
|
|
737
|
+
// the history shares with other moments) as they are
|
|
738
|
+
const work = deepClone(moment.variables);
|
|
739
|
+
const seen = existingObjects(locateObjects(after, changes), work);
|
|
740
|
+
const own = <T>(value: T): T => deepClone(value, { seen });
|
|
741
|
+
for (const change of changes) writeHookChange(work, after, change, own);
|
|
742
|
+
moment.variables = shareEqual(moment.variables, work);
|
|
725
743
|
}
|
|
726
744
|
|
|
727
745
|
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
746
|
+
* Write `change` (of the hooks, to `after`) into `work`. The value goes in at
|
|
747
|
+
* the first place `work` lacks the objects on the path of one of the kind
|
|
748
|
+
* `after` holds (the passage created or replaced it): there, the hooks' whole
|
|
749
|
+
* value of it is written. Into an array, elements removed from the end are
|
|
750
|
+
* removed at the same indices and elements added are appended; changes at
|
|
751
|
+
* indices the array lacks are dropped (it was resized, so its indices do not
|
|
752
|
+
* line up with the live ones).
|
|
734
753
|
*/
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
before: Record<string, unknown>,
|
|
754
|
+
function writeHookChange(
|
|
755
|
+
work: Record<string, unknown>,
|
|
738
756
|
after: Record<string, unknown>,
|
|
739
|
-
|
|
757
|
+
change: PathChange,
|
|
758
|
+
own: <T>(value: T) => T,
|
|
740
759
|
): void {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
) {
|
|
749
|
-
|
|
760
|
+
const { path } = change;
|
|
761
|
+
let holder: Record<string, unknown> = work;
|
|
762
|
+
let depth = 0;
|
|
763
|
+
for (; depth < path.length - 1; depth++) {
|
|
764
|
+
const key = path[depth]!;
|
|
765
|
+
const child = hasOwn(holder, key) ? holder[key] : undefined;
|
|
766
|
+
const held = getByPath(after, path.slice(0, depth + 1));
|
|
767
|
+
if (!mergesWith(child, held, true)) {
|
|
768
|
+
writeKey(holder, key, false, own(held));
|
|
769
|
+
return;
|
|
750
770
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
771
|
+
holder = child as Record<string, unknown>;
|
|
772
|
+
}
|
|
773
|
+
const key = path[depth]!;
|
|
774
|
+
if (change.deleted) writeKey(holder, key, true, undefined);
|
|
775
|
+
else writeKey(holder, key, false, own(change.value), change.appended);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function writeKey(
|
|
779
|
+
holder: Record<string, unknown>,
|
|
780
|
+
key: string,
|
|
781
|
+
deleted: boolean,
|
|
782
|
+
value: unknown,
|
|
783
|
+
appended?: boolean,
|
|
784
|
+
): void {
|
|
785
|
+
if (Array.isArray(holder)) {
|
|
786
|
+
const index = Number(key);
|
|
787
|
+
if (deleted) holder.length = Math.min(holder.length, index);
|
|
788
|
+
else if (appended) holder.push(value);
|
|
789
|
+
else if (index < holder.length) holder[index] = value;
|
|
790
|
+
} else if (deleted) {
|
|
791
|
+
delete holder[key];
|
|
792
|
+
} else {
|
|
793
|
+
setOwn(holder, key, value);
|
|
794
|
+
}
|
|
759
795
|
}
|
|
760
796
|
|
|
761
797
|
/** Restore the PRNG from a snapshot, or reset it without one. */
|
package/src/story-api.ts
CHANGED
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
} from './saves/save-manager';
|
|
22
22
|
import { getBackendType } from './saves/storage';
|
|
23
23
|
import { registerClass } from './class-registry';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
frozenCopy,
|
|
26
|
+
getActiveMutationScope,
|
|
27
|
+
mutateState,
|
|
28
|
+
} from './execute-mutation';
|
|
25
29
|
import { getByPath, setByPath } from './utils/object-path';
|
|
26
30
|
import { changedNames, checkVariableName, ownValue } from './utils/namespace';
|
|
27
31
|
import { historyQueries } from './expression';
|
|
@@ -298,6 +302,16 @@ function createStoryAPI(): StoryAPI {
|
|
|
298
302
|
// while mutation code runs ({do}, ctx.mutate, watcher run actions), it
|
|
299
303
|
// follows the code's own pending writes (program order, #215): see
|
|
300
304
|
// routeStoreUpdate.
|
|
305
|
+
if (entries.some(([k]) => k.includes('.'))) {
|
|
306
|
+
// A write below a variable goes through the commit mutation code
|
|
307
|
+
// uses (nested in the running code, if any: it continues from the
|
|
308
|
+
// code's pending writes, and they are committed first): an update of a draft would give the written path new
|
|
309
|
+
// objects and leave the other references to the old ones (#295).
|
|
310
|
+
mutateState((work, adopt) => {
|
|
311
|
+
for (const [k, v] of entries) setOne(work, k, adopt(v));
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
301
315
|
useStoryStore.getState().updateVariables((draft) => {
|
|
302
316
|
for (const [k, v] of entries) setOne(draft, k, v);
|
|
303
317
|
});
|
package/src/structural.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface DeepCloneOptions {
|
|
|
42
42
|
* is the copy already made. The map is filled as values are copied.
|
|
43
43
|
*/
|
|
44
44
|
seen?: Map<object, object>;
|
|
45
|
+
/**
|
|
46
|
+
* Called with each key a Map is given in the copy and the key it was copied
|
|
47
|
+
* from; the key it returns is the one the copy takes (for a store draft,
|
|
48
|
+
* which Immer does not finalize in a Map key).
|
|
49
|
+
*/
|
|
50
|
+
mapKey?: (copy: object, original: object) => object;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
type TypedArrayCtor = new (
|
|
@@ -133,7 +139,14 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
133
139
|
if (val instanceof Map) {
|
|
134
140
|
const copy = keep(obj, new Map());
|
|
135
141
|
for (const [k, v] of val) {
|
|
136
|
-
|
|
142
|
+
const key = clone(k);
|
|
143
|
+
const settle = options.mapKey;
|
|
144
|
+
copy.set(
|
|
145
|
+
settle && isObjectValue(key) && isObjectValue(k)
|
|
146
|
+
? settle(key, k)
|
|
147
|
+
: key,
|
|
148
|
+
clone(v),
|
|
149
|
+
);
|
|
137
150
|
}
|
|
138
151
|
return copy;
|
|
139
152
|
}
|
|
@@ -533,6 +546,8 @@ export type PathChange =
|
|
|
533
546
|
path: string[];
|
|
534
547
|
deleted: false;
|
|
535
548
|
value: unknown;
|
|
549
|
+
/** Set for an element an array gained beyond the length it had. */
|
|
550
|
+
appended?: true;
|
|
536
551
|
/**
|
|
537
552
|
* Set where the value is the same content as before and only which
|
|
538
553
|
* object the path refers to changed (see aliasChanges), so a store
|
|
@@ -545,11 +560,13 @@ export type PathChange =
|
|
|
545
560
|
/**
|
|
546
561
|
* The property paths where `after` differs from `before`, two objects of
|
|
547
562
|
* one class: objects that merge (see isMergeable) are compared property by
|
|
548
|
-
* property, any other value as a whole
|
|
563
|
+
* property, any other value as a whole; with `arrays`, arrays are compared
|
|
564
|
+
* by index too (see keyChanges).
|
|
549
565
|
*/
|
|
550
566
|
export function diffPaths(
|
|
551
567
|
before: Record<string, unknown>,
|
|
552
568
|
after: Record<string, unknown>,
|
|
569
|
+
arrays = false,
|
|
553
570
|
): PathChange[] {
|
|
554
571
|
const changes: PathChange[] = [];
|
|
555
572
|
const ancestors = new Set<object>();
|
|
@@ -561,7 +578,7 @@ export function diffPaths(
|
|
|
561
578
|
// Stop at cycles; shared (non-cyclic) references are visited per path.
|
|
562
579
|
if (ancestors.has(a)) return;
|
|
563
580
|
ancestors.add(a);
|
|
564
|
-
for (const change of keyChanges(b, a,
|
|
581
|
+
for (const change of keyChanges(b, a, arrays)) {
|
|
565
582
|
const at = [...path, change.key];
|
|
566
583
|
if (change.kind === 'nested') {
|
|
567
584
|
walk(change.before, change.after, at);
|
|
@@ -569,7 +586,14 @@ export function diffPaths(
|
|
|
569
586
|
changes.push(
|
|
570
587
|
change.kind === 'deleted'
|
|
571
588
|
? { path: at, deleted: true }
|
|
572
|
-
: {
|
|
589
|
+
: {
|
|
590
|
+
path: at,
|
|
591
|
+
deleted: false,
|
|
592
|
+
value: change.after,
|
|
593
|
+
...(change.kind === 'added' && Array.isArray(a)
|
|
594
|
+
? { appended: true as const }
|
|
595
|
+
: {}),
|
|
596
|
+
},
|
|
573
597
|
);
|
|
574
598
|
}
|
|
575
599
|
}
|
|
@@ -592,27 +616,124 @@ export function underChange(
|
|
|
592
616
|
return false;
|
|
593
617
|
}
|
|
594
618
|
|
|
619
|
+
/** The segment standing for the entry `index` of a Map or Set in a path. */
|
|
620
|
+
const entryKey = (kind: 'k' | 'v' | 's', index: number): string =>
|
|
621
|
+
`\0${kind}${index}`;
|
|
622
|
+
|
|
595
623
|
/**
|
|
596
|
-
*
|
|
597
|
-
* depth-first order. Plain objects
|
|
598
|
-
*
|
|
624
|
+
* Every path each object (or array, or other value) of `root` is at, the
|
|
625
|
+
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
626
|
+
* entered, where first met only (an object held in an array is one object,
|
|
627
|
+
* however it was reached); other values are leaves. A path at or below one
|
|
628
|
+
* of `skip` is left out.
|
|
629
|
+
*
|
|
630
|
+
* With `entries`, the objects held in a Map or Set (as key, value or member)
|
|
631
|
+
* are placed too, after everything else: one met nowhere else is at the path
|
|
632
|
+
* of its Map or Set and the entry's segment (see entryKey), which is no path
|
|
633
|
+
* a value can be read at, so only to tell where an object is from.
|
|
599
634
|
*/
|
|
600
|
-
function
|
|
601
|
-
root:
|
|
635
|
+
function objectPaths(
|
|
636
|
+
root: object,
|
|
602
637
|
skip: ReadonlySet<string> = new Set(),
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
638
|
+
entries = false,
|
|
639
|
+
): Map<object, string[][]> {
|
|
640
|
+
const all = new Map<object, string[][]>();
|
|
641
|
+
const collections: [Map<unknown, unknown> | Set<unknown>, string[]][] = [];
|
|
642
|
+
function place(value: object, at: string[]): void {
|
|
643
|
+
const known = all.get(value);
|
|
644
|
+
if (known) {
|
|
645
|
+
known.push(at);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
all.set(value, [at]);
|
|
649
|
+
if (isMergeable(value) || Array.isArray(value)) {
|
|
650
|
+
walk(value as Record<string, unknown>, at);
|
|
651
|
+
} else if (entries && (value instanceof Map || value instanceof Set)) {
|
|
652
|
+
collections.push([value, at]);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function walk(node: Record<string, unknown>, path: string[]): void {
|
|
606
656
|
for (const key of Object.keys(node)) {
|
|
607
657
|
const value = node[key];
|
|
608
|
-
if (!isObjectValue(value)
|
|
658
|
+
if (!isObjectValue(value)) continue;
|
|
609
659
|
const at = [...path, key];
|
|
610
660
|
if (skip.has(pathKey(at))) continue;
|
|
611
|
-
|
|
612
|
-
|
|
661
|
+
place(value, at);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
walk(root as Record<string, unknown>, []);
|
|
665
|
+
for (const [collection, path] of collections) {
|
|
666
|
+
let index = 0;
|
|
667
|
+
for (const entry of collection.entries()) {
|
|
668
|
+
const [k, v] = entry as [unknown, unknown];
|
|
669
|
+
const parts: [unknown, string][] =
|
|
670
|
+
collection instanceof Set
|
|
671
|
+
? [[k, entryKey('s', index)]]
|
|
672
|
+
: [
|
|
673
|
+
[k, entryKey('k', index)],
|
|
674
|
+
[v, entryKey('v', index)],
|
|
675
|
+
];
|
|
676
|
+
for (const [value, segment] of parts) {
|
|
677
|
+
if (isObjectValue(value) && !all.has(value)) {
|
|
678
|
+
place(value, [...path, segment]);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
index++;
|
|
613
682
|
}
|
|
614
|
-
}
|
|
615
|
-
return
|
|
683
|
+
}
|
|
684
|
+
return all;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Where each object of `root` first appears (see objectPaths). */
|
|
688
|
+
function firstPaths(
|
|
689
|
+
root: object,
|
|
690
|
+
skip?: ReadonlySet<string>,
|
|
691
|
+
entries?: boolean,
|
|
692
|
+
): Map<object, string[]> {
|
|
693
|
+
return new Map(
|
|
694
|
+
[...objectPaths(root, skip, entries)].map(([object, paths]) => [
|
|
695
|
+
object,
|
|
696
|
+
paths[0]!,
|
|
697
|
+
]),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* The objects `root` holds at more than one path (an object two variables
|
|
703
|
+
* refer to, or one that refers to itself), with every path they are at,
|
|
704
|
+
* parents before children.
|
|
705
|
+
*/
|
|
706
|
+
export function sharedPaths(root: object): string[][][] {
|
|
707
|
+
return [...objectPaths(root).values()].filter((paths) => paths.length > 1);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Whether the Map or Set `y` holds, at the position of an entry of `x`, another
|
|
712
|
+
* object than `x` did (as to where its first appearance is, see aliasChanges):
|
|
713
|
+
* `$map.set("k", $a)` over an equal object. Entries are compared by position,
|
|
714
|
+
* and only when both hold as many; other differences are changes of content.
|
|
715
|
+
*/
|
|
716
|
+
function entryAliasChanged(
|
|
717
|
+
x: object,
|
|
718
|
+
y: object,
|
|
719
|
+
earlier: ReadonlyMap<object, string[]>,
|
|
720
|
+
later: ReadonlyMap<object, string[]>,
|
|
721
|
+
): boolean {
|
|
722
|
+
const isSet = x instanceof Set && y instanceof Set;
|
|
723
|
+
if (!isSet && !(x instanceof Map && y instanceof Map)) return false;
|
|
724
|
+
if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
|
|
725
|
+
if (x.size !== y.size) return false;
|
|
726
|
+
const moved = (a: unknown, b: unknown): boolean =>
|
|
727
|
+
isObjectValue(a) &&
|
|
728
|
+
isObjectValue(b) &&
|
|
729
|
+
pathKey(earlier.get(a)!) !== pathKey(later.get(b)!);
|
|
730
|
+
const ys = [...(y as Map<unknown, unknown>).entries()];
|
|
731
|
+
let index = 0;
|
|
732
|
+
for (const [k, v] of (x as Map<unknown, unknown>).entries()) {
|
|
733
|
+
const [k2, v2] = ys[index++]!;
|
|
734
|
+
if (moved(k, k2) || (!isSet && moved(v, v2))) return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
616
737
|
}
|
|
617
738
|
|
|
618
739
|
/**
|
|
@@ -627,8 +748,8 @@ export function aliasChanges(
|
|
|
627
748
|
before: Record<string, unknown>,
|
|
628
749
|
after: Record<string, unknown>,
|
|
629
750
|
): PathChange[] {
|
|
630
|
-
const earlier = firstPaths(before);
|
|
631
|
-
const later = firstPaths(after);
|
|
751
|
+
const earlier = firstPaths(before, undefined, true);
|
|
752
|
+
const later = firstPaths(after, undefined, true);
|
|
632
753
|
const changes: PathChange[] = [];
|
|
633
754
|
(function walk(
|
|
634
755
|
b: Record<string, unknown>,
|
|
@@ -646,31 +767,63 @@ export function aliasChanges(
|
|
|
646
767
|
const k = pathKey(at);
|
|
647
768
|
if (pathKey(wasAt) !== pathKey(isAt)) {
|
|
648
769
|
changes.push({ path: at, deleted: false, value: y, alias: true });
|
|
770
|
+
} else if (
|
|
771
|
+
pathKey(isAt) === k &&
|
|
772
|
+
entryAliasChanged(x, y, earlier, later)
|
|
773
|
+
) {
|
|
774
|
+
changes.push({ path: at, deleted: false, value: y, alias: true });
|
|
649
775
|
}
|
|
650
776
|
// Only paths that are first appearances are entered: elsewhere the
|
|
651
777
|
// contents are those of the object met first.
|
|
652
778
|
if (
|
|
653
|
-
isMergeable(x) &&
|
|
654
|
-
mergesWith(x, y,
|
|
779
|
+
(isMergeable(x) || Array.isArray(x)) &&
|
|
780
|
+
mergesWith(x, y, true) &&
|
|
655
781
|
pathKey(isAt) === k &&
|
|
656
782
|
pathKey(wasAt) === k
|
|
657
783
|
) {
|
|
658
|
-
walk(x, y as Record<string, unknown>, at);
|
|
784
|
+
walk(x as Record<string, unknown>, y as Record<string, unknown>, at);
|
|
659
785
|
}
|
|
660
786
|
}
|
|
661
787
|
})(before, after, []);
|
|
662
788
|
return changes;
|
|
663
789
|
}
|
|
664
790
|
|
|
791
|
+
/**
|
|
792
|
+
* The changes from `before` to `after`, as paths below them: the property
|
|
793
|
+
* paths that differ (see diffPaths), and those that hold equal content but
|
|
794
|
+
* another object than they did (see aliasChanges), those first. Compared as
|
|
795
|
+
* one object, so an object that two of their keys share is one object,
|
|
796
|
+
* whichever of them it is reached through.
|
|
797
|
+
*/
|
|
798
|
+
export function changesBetween(
|
|
799
|
+
before: Record<string, unknown>,
|
|
800
|
+
after: Record<string, unknown>,
|
|
801
|
+
arrays = false,
|
|
802
|
+
): PathChange[] {
|
|
803
|
+
const written = diffPaths(before, after, arrays);
|
|
804
|
+
const keys = new Set(written.map((c) => pathKey(c.path)));
|
|
805
|
+
const aliases: PathChange[] = [];
|
|
806
|
+
for (const change of aliasChanges(before, after)) {
|
|
807
|
+
// Written whole by a change at it or above it
|
|
808
|
+
if (underChange(change.path, keys)) continue;
|
|
809
|
+
keys.add(pathKey(change.path));
|
|
810
|
+
aliases.push(change);
|
|
811
|
+
}
|
|
812
|
+
return [...aliases, ...written];
|
|
813
|
+
}
|
|
814
|
+
|
|
665
815
|
/**
|
|
666
816
|
* Where the objects of `source` are, other than at or below the paths of
|
|
667
817
|
* `changes` (which write something new there), for `existingObjects`.
|
|
668
818
|
*/
|
|
669
819
|
export function locateObjects(
|
|
670
|
-
source:
|
|
820
|
+
source: object,
|
|
671
821
|
changes: readonly PathChange[],
|
|
672
822
|
): Map<object, string[]> {
|
|
673
|
-
return firstPaths(
|
|
823
|
+
return firstPaths(
|
|
824
|
+
source as Record<string, unknown>,
|
|
825
|
+
new Set(changes.map((c) => pathKey(c.path))),
|
|
826
|
+
);
|
|
674
827
|
}
|
|
675
828
|
|
|
676
829
|
/**
|
|
@@ -682,7 +835,7 @@ export function locateObjects(
|
|
|
682
835
|
*/
|
|
683
836
|
export function existingObjects(
|
|
684
837
|
paths: ReadonlyMap<object, string[]>,
|
|
685
|
-
target:
|
|
838
|
+
target: object,
|
|
686
839
|
): Map<object, object> {
|
|
687
840
|
return new (class extends Map<object, object> {
|
|
688
841
|
override has(obj: object): boolean {
|
|
@@ -703,10 +856,7 @@ export function existingObjects(
|
|
|
703
856
|
}
|
|
704
857
|
|
|
705
858
|
/** Whether `target` already holds what `change` would write. */
|
|
706
|
-
export function isApplied(
|
|
707
|
-
target: Record<string, unknown>,
|
|
708
|
-
change: PathChange,
|
|
709
|
-
): boolean {
|
|
859
|
+
export function isApplied(target: object, change: PathChange): boolean {
|
|
710
860
|
const parent = getByPath(target, change.path.slice(0, -1));
|
|
711
861
|
if (parent === null || typeof parent !== 'object') return change.deleted;
|
|
712
862
|
const key = change.path[change.path.length - 1]!;
|
|
@@ -717,12 +867,10 @@ export function isApplied(
|
|
|
717
867
|
);
|
|
718
868
|
}
|
|
719
869
|
|
|
720
|
-
export function applyChange(
|
|
721
|
-
target
|
|
722
|
-
change
|
|
723
|
-
|
|
724
|
-
if (change.deleted) deleteByPath(target, change.path);
|
|
725
|
-
else setByPath(target, change.path, change.value);
|
|
870
|
+
export function applyChange(target: object, change: PathChange): void {
|
|
871
|
+
const root = target as Record<string, unknown>;
|
|
872
|
+
if (change.deleted) deleteByPath(root, change.path);
|
|
873
|
+
else setByPath(root, change.path, change.value);
|
|
726
874
|
}
|
|
727
875
|
|
|
728
876
|
/**
|
package/src/utils/object-path.ts
CHANGED
|
@@ -9,10 +9,7 @@ import { atomicName } from './value-kinds';
|
|
|
9
9
|
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
10
10
|
* (class getters, `size` of a Map) are read.
|
|
11
11
|
*/
|
|
12
|
-
export function getByPath(
|
|
13
|
-
obj: Record<string, unknown>,
|
|
14
|
-
segments: readonly string[],
|
|
15
|
-
): unknown {
|
|
12
|
+
export function getByPath(obj: object, segments: readonly string[]): unknown {
|
|
16
13
|
let current: unknown = obj;
|
|
17
14
|
for (const seg of segments) {
|
|
18
15
|
if (current == null || typeof current !== 'object') return undefined;
|
|
@@ -38,7 +35,8 @@ const builtinName = atomicName;
|
|
|
38
35
|
*/
|
|
39
36
|
export function shallowCopy(value: object): Record<string, unknown> {
|
|
40
37
|
const copy = Array.isArray(value)
|
|
41
|
-
?
|
|
38
|
+
? // Keep the length: trailing holes are not among the keys
|
|
39
|
+
new Array<unknown>(value.length)
|
|
42
40
|
: (Object.create(Object.getPrototypeOf(value) as object | null) as object);
|
|
43
41
|
// Define rather than assign (Object.assign), so that a "__proto__" key
|
|
44
42
|
// stays a key instead of replacing the copy's prototype
|