@rohal12/spindle 0.59.4 → 0.59.6
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 +342 -118
- package/package.json +1 -1
- package/src/components/macros/Unset.tsx +5 -1
- package/src/define-macro.ts +6 -3
- package/src/execute-mutation.ts +93 -39
- package/src/store.ts +82 -35
- package/src/story-api.ts +2 -2
- package/src/structural.ts +309 -77
- package/src/utils/object-path.ts +6 -3
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 Map key and Set member in the copy and the object it
|
|
47
|
+
* was copied from; the object it returns is the one the copy takes (for a
|
|
48
|
+
* store draft, which Immer does not finalize there).
|
|
49
|
+
*/
|
|
50
|
+
settle?: (copy: object, original: object) => object;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
type TypedArrayCtor = new (
|
|
@@ -103,8 +109,9 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
103
109
|
const copy = keep(val, Object.create(Object.getPrototypeOf(val)));
|
|
104
110
|
for (const key of [...ERROR_HIDDEN_KEYS, 'stack']) {
|
|
105
111
|
if (hasOwn(val, key)) {
|
|
112
|
+
const held = (val as unknown as Record<string, unknown>)[key];
|
|
106
113
|
Object.defineProperty(copy, key, {
|
|
107
|
-
value: clone(
|
|
114
|
+
value: settled(clone(held), held),
|
|
108
115
|
writable: true,
|
|
109
116
|
configurable: true,
|
|
110
117
|
});
|
|
@@ -115,6 +122,13 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
115
122
|
return undefined;
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
function settled(copy: unknown, original: unknown): unknown {
|
|
126
|
+
const settle = options.settle;
|
|
127
|
+
return settle && isObjectValue(copy) && isObjectValue(original)
|
|
128
|
+
? settle(copy, original)
|
|
129
|
+
: copy;
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function clone(val: unknown): unknown {
|
|
119
133
|
if (val === null || typeof val !== 'object') return val;
|
|
120
134
|
|
|
@@ -132,17 +146,13 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
132
146
|
|
|
133
147
|
if (val instanceof Map) {
|
|
134
148
|
const copy = keep(obj, new Map());
|
|
135
|
-
for (const [k, v] of val)
|
|
136
|
-
copy.set(clone(k), clone(v));
|
|
137
|
-
}
|
|
149
|
+
for (const [k, v] of val) copy.set(settled(clone(k), k), clone(v));
|
|
138
150
|
return copy;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
if (val instanceof Set) {
|
|
142
154
|
const copy = keep(obj, new Set());
|
|
143
|
-
for (const v of val)
|
|
144
|
-
copy.add(clone(v));
|
|
145
|
-
}
|
|
155
|
+
for (const v of val) copy.add(settled(clone(v), v));
|
|
146
156
|
return copy;
|
|
147
157
|
}
|
|
148
158
|
|
|
@@ -463,8 +473,15 @@ export function mergesWith(
|
|
|
463
473
|
|
|
464
474
|
/** How a value differs from an earlier version at one key. */
|
|
465
475
|
export type KeyChange =
|
|
466
|
-
| {
|
|
467
|
-
|
|
476
|
+
| {
|
|
477
|
+
key: string;
|
|
478
|
+
kind: 'added' | 'changed';
|
|
479
|
+
after: unknown;
|
|
480
|
+
/** Holes between this element an array gained and the one before. */
|
|
481
|
+
gap?: number;
|
|
482
|
+
}
|
|
483
|
+
| { key: string; kind: 'deleted'; hole?: true }
|
|
484
|
+
| { key: string; kind: 'holes'; after: number }
|
|
468
485
|
| {
|
|
469
486
|
key: string;
|
|
470
487
|
kind: 'nested';
|
|
@@ -480,7 +497,9 @@ export type KeyChange =
|
|
|
480
497
|
* is none either. Values that merge are reported as 'nested', to be
|
|
481
498
|
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
482
499
|
* merge too: elements at the indices both hold are compared, and the
|
|
483
|
-
* elements one has beyond the other's length are added or deleted.
|
|
500
|
+
* elements one has beyond the other's length are added or deleted. An
|
|
501
|
+
* index that holds no element (a hole) is no element, not an undefined one,
|
|
502
|
+
* and holes the length gained at the end are the change of `length`, by how many.
|
|
484
503
|
*/
|
|
485
504
|
export function keyChanges(
|
|
486
505
|
before: Record<string, unknown>,
|
|
@@ -506,14 +525,31 @@ export function keyChanges(
|
|
|
506
525
|
const b = before as unknown as unknown[];
|
|
507
526
|
const a = after as unknown[];
|
|
508
527
|
for (let i = 0; i < Math.min(b.length, a.length); i++) {
|
|
509
|
-
|
|
528
|
+
// A hole is no element: not the same as an undefined one
|
|
529
|
+
const key = String(i);
|
|
530
|
+
if (i in b && i in a) compare(key, b[i], a[i]);
|
|
531
|
+
else if (i in b) changes.push({ key, kind: 'deleted', hole: true });
|
|
532
|
+
else if (i in a) changes.push({ key, kind: 'added', after: a[i] });
|
|
510
533
|
}
|
|
534
|
+
let next = b.length;
|
|
511
535
|
for (let i = b.length; i < a.length; i++) {
|
|
512
|
-
|
|
536
|
+
if (!(i in a)) continue;
|
|
537
|
+
const gap = i - next;
|
|
538
|
+
changes.push({
|
|
539
|
+
key: String(i),
|
|
540
|
+
kind: 'added',
|
|
541
|
+
after: a[i],
|
|
542
|
+
...(gap ? { gap } : {}),
|
|
543
|
+
});
|
|
544
|
+
next = i + 1;
|
|
513
545
|
}
|
|
514
546
|
for (let i = a.length; i < b.length; i++) {
|
|
515
547
|
changes.push({ key: String(i), kind: 'deleted' });
|
|
516
548
|
}
|
|
549
|
+
// Holes at the end are in no index, only in the length
|
|
550
|
+
if (a.length > next) {
|
|
551
|
+
changes.push({ key: 'length', kind: 'holes', after: a.length - next });
|
|
552
|
+
}
|
|
517
553
|
return changes;
|
|
518
554
|
}
|
|
519
555
|
|
|
@@ -533,6 +569,10 @@ export type PathChange =
|
|
|
533
569
|
path: string[];
|
|
534
570
|
deleted: false;
|
|
535
571
|
value: unknown;
|
|
572
|
+
/** Set for an element an array gained beyond the length it had. */
|
|
573
|
+
appended?: true;
|
|
574
|
+
/** Holes an appended element follows (the array is sparse). */
|
|
575
|
+
gap?: number;
|
|
536
576
|
/**
|
|
537
577
|
* Set where the value is the same content as before and only which
|
|
538
578
|
* object the path refers to changed (see aliasChanges), so a store
|
|
@@ -540,39 +580,79 @@ export type PathChange =
|
|
|
540
580
|
*/
|
|
541
581
|
alias?: true;
|
|
542
582
|
}
|
|
543
|
-
| {
|
|
583
|
+
| {
|
|
584
|
+
path: string[];
|
|
585
|
+
deleted: true;
|
|
586
|
+
/** Set for an element an array lost without shrinking: a hole. */
|
|
587
|
+
hole?: true;
|
|
588
|
+
};
|
|
544
589
|
|
|
545
590
|
/**
|
|
546
591
|
* The property paths where `after` differs from `before`, two objects of
|
|
547
592
|
* one class: objects that merge (see isMergeable) are compared property by
|
|
548
|
-
* property, any other value as a whole
|
|
593
|
+
* property, any other value as a whole; with `arrays`, arrays are compared
|
|
594
|
+
* by index too (see keyChanges).
|
|
549
595
|
*/
|
|
550
596
|
export function diffPaths(
|
|
551
597
|
before: Record<string, unknown>,
|
|
552
598
|
after: Record<string, unknown>,
|
|
599
|
+
arrays = false,
|
|
553
600
|
): PathChange[] {
|
|
554
601
|
const changes: PathChange[] = [];
|
|
555
602
|
const ancestors = new Set<object>();
|
|
603
|
+
// Pairs found equal, so a graph that shares objects is not walked once per
|
|
604
|
+
// path to them (exponentially many), and how often a walk met a cycle
|
|
605
|
+
const unchanged = new WeakMap<object, Set<object>>();
|
|
606
|
+
let cuts = 0;
|
|
556
607
|
(function walk(
|
|
557
608
|
b: Record<string, unknown>,
|
|
558
609
|
a: Record<string, unknown>,
|
|
559
610
|
path: string[],
|
|
560
611
|
): void {
|
|
561
|
-
// Stop at cycles; shared (non-cyclic) references are visited
|
|
562
|
-
|
|
612
|
+
// Stop at cycles; shared (non-cyclic) references that differ are visited
|
|
613
|
+
// per path, as each is a change there.
|
|
614
|
+
if (ancestors.has(a)) {
|
|
615
|
+
cuts++;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (unchanged.get(a)?.has(b)) return;
|
|
563
619
|
ancestors.add(a);
|
|
564
|
-
|
|
620
|
+
const found = changes.length;
|
|
621
|
+
const cutsBefore = cuts;
|
|
622
|
+
for (const change of keyChanges(b, a, arrays)) {
|
|
565
623
|
const at = [...path, change.key];
|
|
566
624
|
if (change.kind === 'nested') {
|
|
567
625
|
walk(change.before, change.after, at);
|
|
568
626
|
} else {
|
|
569
627
|
changes.push(
|
|
570
628
|
change.kind === 'deleted'
|
|
571
|
-
? {
|
|
572
|
-
|
|
629
|
+
? {
|
|
630
|
+
path: at,
|
|
631
|
+
deleted: true,
|
|
632
|
+
...(change.hole ? { hole: true } : {}),
|
|
633
|
+
}
|
|
634
|
+
: {
|
|
635
|
+
path: at,
|
|
636
|
+
deleted: false,
|
|
637
|
+
value: change.after,
|
|
638
|
+
...(change.kind === 'added' &&
|
|
639
|
+
Array.isArray(a) &&
|
|
640
|
+
Number(change.key) >= (b as unknown as unknown[]).length
|
|
641
|
+
? {
|
|
642
|
+
appended: true as const,
|
|
643
|
+
...(change.gap ? { gap: change.gap } : {}),
|
|
644
|
+
}
|
|
645
|
+
: {}),
|
|
646
|
+
},
|
|
573
647
|
);
|
|
574
648
|
}
|
|
575
649
|
}
|
|
650
|
+
// A walk that met a cycle depends on the ancestors it was reached by
|
|
651
|
+
if (changes.length === found && cuts === cutsBefore) {
|
|
652
|
+
const seen = unchanged.get(a) ?? new Set<object>();
|
|
653
|
+
seen.add(b);
|
|
654
|
+
unchanged.set(a, seen);
|
|
655
|
+
}
|
|
576
656
|
ancestors.delete(a);
|
|
577
657
|
})(before, after, []);
|
|
578
658
|
return changes;
|
|
@@ -592,35 +672,132 @@ export function underChange(
|
|
|
592
672
|
return false;
|
|
593
673
|
}
|
|
594
674
|
|
|
675
|
+
/**
|
|
676
|
+
* The segment standing for the entry `index` of a Map or Set in a path, or
|
|
677
|
+
* for the `cause` or `errors` of an Error ('c', 'e').
|
|
678
|
+
*/
|
|
679
|
+
const entryKey = (kind: 'k' | 'v' | 's' | 'c' | 'e', index: number): string =>
|
|
680
|
+
`\0${kind}${index}`;
|
|
681
|
+
|
|
682
|
+
export const isEntryKey = (segment: string): boolean =>
|
|
683
|
+
segment.startsWith('\0');
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* What a Map, Set or Error holds in place of properties (keys and values,
|
|
687
|
+
* members, `cause` and `errors`), each with the segment standing for it in
|
|
688
|
+
* a path (see entryKey); undefined for any other value.
|
|
689
|
+
*/
|
|
690
|
+
function entryChildren(value: object): [string, unknown][] | undefined {
|
|
691
|
+
const children: [string, unknown][] = [];
|
|
692
|
+
if (value instanceof Map) {
|
|
693
|
+
let index = 0;
|
|
694
|
+
for (const [k, v] of value) {
|
|
695
|
+
children.push([entryKey('k', index), k], [entryKey('v', index), v]);
|
|
696
|
+
index++;
|
|
697
|
+
}
|
|
698
|
+
} else if (value instanceof Set) {
|
|
699
|
+
let index = 0;
|
|
700
|
+
for (const member of value) children.push([entryKey('s', index++), member]);
|
|
701
|
+
} else if (value instanceof Error) {
|
|
702
|
+
for (const key of ['cause', 'errors'] as const) {
|
|
703
|
+
if (hasOwn(value, key)) {
|
|
704
|
+
children.push([
|
|
705
|
+
entryKey(key === 'cause' ? 'c' : 'e', 0),
|
|
706
|
+
(value as any)[key],
|
|
707
|
+
]);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
return children;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* The value at `path` below `root`, where a path may go through the entries
|
|
718
|
+
* of Maps, Sets and Errors (see objectPaths) as well as properties.
|
|
719
|
+
*/
|
|
720
|
+
export function getByEntryPath(root: object, path: readonly string[]): unknown {
|
|
721
|
+
let current: unknown = root;
|
|
722
|
+
let start = 0;
|
|
723
|
+
for (let i = 0; i < path.length; i++) {
|
|
724
|
+
if (!isEntryKey(path[i]!)) continue;
|
|
725
|
+
current = getByPath(current as object, path.slice(start, i));
|
|
726
|
+
if (!isObjectValue(current)) return undefined;
|
|
727
|
+
current = entryChildren(current)?.find(([at]) => at === path[i])?.[1];
|
|
728
|
+
start = i + 1;
|
|
729
|
+
}
|
|
730
|
+
return start === path.length
|
|
731
|
+
? current
|
|
732
|
+
: getByPath(current as object, path.slice(start));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Write `value` at the path of an object a Map holds as a value (see
|
|
737
|
+
* objectPaths); other entries are not replaced, as a key or member cannot
|
|
738
|
+
* change without the entry moving. Returns whether it wrote.
|
|
739
|
+
*/
|
|
740
|
+
function setEntryValue(
|
|
741
|
+
root: object,
|
|
742
|
+
path: readonly string[],
|
|
743
|
+
value: unknown,
|
|
744
|
+
): boolean {
|
|
745
|
+
const holder = getByEntryPath(root, path.slice(0, -1));
|
|
746
|
+
const match = /^\0v(\d+)$/.exec(path[path.length - 1]!);
|
|
747
|
+
if (!(holder instanceof Map) || !match) return false;
|
|
748
|
+
const key = [...holder.keys()][Number(match[1])];
|
|
749
|
+
holder.set(key, value);
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
|
|
595
753
|
/**
|
|
596
754
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
597
755
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
598
756
|
* entered, where first met only (an object held in an array is one object,
|
|
599
757
|
* however it was reached); other values are leaves. A path at or below one
|
|
600
758
|
* of `skip` is left out.
|
|
759
|
+
*
|
|
760
|
+
* With `entries`, the objects held in a Map or Set (as key, value or member)
|
|
761
|
+
* are placed too, after everything else: one met nowhere else is at the path
|
|
762
|
+
* of its Map or Set and the entry's segment (see entryKey), which is no path
|
|
763
|
+
* a value can be read at (see getByEntryPath), so only to tell where an
|
|
764
|
+
* object is from; so are the `cause` and `errors` of an Error.
|
|
601
765
|
*/
|
|
602
766
|
function objectPaths(
|
|
603
767
|
root: object,
|
|
604
768
|
skip: ReadonlySet<string> = new Set(),
|
|
769
|
+
entries = false,
|
|
605
770
|
): Map<object, string[][]> {
|
|
606
771
|
const all = new Map<object, string[][]>();
|
|
607
|
-
|
|
772
|
+
const collections: [object, string[]][] = [];
|
|
773
|
+
function place(value: object, at: string[]): void {
|
|
774
|
+
const known = all.get(value);
|
|
775
|
+
if (known) {
|
|
776
|
+
known.push(at);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
all.set(value, [at]);
|
|
780
|
+
if (isMergeable(value) || Array.isArray(value)) {
|
|
781
|
+
walk(value as Record<string, unknown>, at);
|
|
782
|
+
} else if (entries && entryChildren(value)) {
|
|
783
|
+
collections.push([value, at]);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function walk(node: Record<string, unknown>, path: string[]): void {
|
|
608
787
|
for (const key of Object.keys(node)) {
|
|
609
788
|
const value = node[key];
|
|
610
789
|
if (!isObjectValue(value)) continue;
|
|
611
790
|
const at = [...path, key];
|
|
612
791
|
if (skip.has(pathKey(at))) continue;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
walk(value as Record<string, unknown>, at);
|
|
621
|
-
}
|
|
792
|
+
place(value, at);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
walk(root as Record<string, unknown>, []);
|
|
796
|
+
for (const [collection, path] of collections) {
|
|
797
|
+
for (const [segment, value] of entryChildren(collection)!) {
|
|
798
|
+
if (isObjectValue(value)) place(value, [...path, segment]);
|
|
622
799
|
}
|
|
623
|
-
}
|
|
800
|
+
}
|
|
624
801
|
return all;
|
|
625
802
|
}
|
|
626
803
|
|
|
@@ -628,9 +805,13 @@ function objectPaths(
|
|
|
628
805
|
function firstPaths(
|
|
629
806
|
root: object,
|
|
630
807
|
skip?: ReadonlySet<string>,
|
|
808
|
+
entries?: boolean,
|
|
631
809
|
): Map<object, string[]> {
|
|
632
810
|
return new Map(
|
|
633
|
-
[...objectPaths(root, skip)].map(([object, paths]) => [
|
|
811
|
+
[...objectPaths(root, skip, entries)].map(([object, paths]) => [
|
|
812
|
+
object,
|
|
813
|
+
paths[0]!,
|
|
814
|
+
]),
|
|
634
815
|
);
|
|
635
816
|
}
|
|
636
817
|
|
|
@@ -640,7 +821,55 @@ function firstPaths(
|
|
|
640
821
|
* parents before children.
|
|
641
822
|
*/
|
|
642
823
|
export function sharedPaths(root: object): string[][][] {
|
|
643
|
-
return [...objectPaths(root).values()].filter(
|
|
824
|
+
return [...objectPaths(root, undefined, true).values()].filter(
|
|
825
|
+
(paths) => paths.length > 1,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Whether `y`, which takes the place of `x` at `at` (the first appearance of
|
|
831
|
+
* both), holds an object elsewhere than `x` did, below an entry of a Map, Set
|
|
832
|
+
* or Error (see aliasChanges): `$map.set("k", $a)` over an equal object, or
|
|
833
|
+
* `$map.get("k").child = $a`. Entries are compared by position, and only
|
|
834
|
+
* where both hold as many; other differences are changes of content. Below
|
|
835
|
+
* an entry, objects that merge are compared key by key.
|
|
836
|
+
*/
|
|
837
|
+
function aliasMoved(
|
|
838
|
+
x: object,
|
|
839
|
+
y: object,
|
|
840
|
+
at: string[],
|
|
841
|
+
earlier: ReadonlyMap<object, string[]>,
|
|
842
|
+
later: ReadonlyMap<object, string[]>,
|
|
843
|
+
): boolean {
|
|
844
|
+
const pairs: [string, unknown, unknown][] = [];
|
|
845
|
+
const xs = entryChildren(x);
|
|
846
|
+
const ys = entryChildren(y);
|
|
847
|
+
if (xs && ys) {
|
|
848
|
+
if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
|
|
849
|
+
if (x instanceof Map || x instanceof Set) {
|
|
850
|
+
if (x.size !== (y as Map<unknown, unknown>).size) return false;
|
|
851
|
+
}
|
|
852
|
+
const held = new Map(ys);
|
|
853
|
+
for (const [segment, a] of xs) {
|
|
854
|
+
if (held.has(segment)) pairs.push([segment, a, held.get(segment)]);
|
|
855
|
+
}
|
|
856
|
+
} else if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true)) {
|
|
857
|
+
for (const key of Object.keys(y)) {
|
|
858
|
+
if (hasOwn(x, key)) {
|
|
859
|
+
pairs.push([key, (x as any)[key], (y as any)[key]]);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return pairs.some(([segment, a, b]) => {
|
|
864
|
+
if (!isObjectValue(a) || !isObjectValue(b)) return false;
|
|
865
|
+
const here = [...at, segment];
|
|
866
|
+
const isAt = later.get(b)!;
|
|
867
|
+
return (
|
|
868
|
+
pathKey(earlier.get(a)!) !== pathKey(isAt) ||
|
|
869
|
+
(pathKey(isAt) === pathKey(here) &&
|
|
870
|
+
aliasMoved(a, b, here, earlier, later))
|
|
871
|
+
);
|
|
872
|
+
});
|
|
644
873
|
}
|
|
645
874
|
|
|
646
875
|
/**
|
|
@@ -655,8 +884,8 @@ export function aliasChanges(
|
|
|
655
884
|
before: Record<string, unknown>,
|
|
656
885
|
after: Record<string, unknown>,
|
|
657
886
|
): PathChange[] {
|
|
658
|
-
const earlier = firstPaths(before);
|
|
659
|
-
const later = firstPaths(after);
|
|
887
|
+
const earlier = firstPaths(before, undefined, true);
|
|
888
|
+
const later = firstPaths(after, undefined, true);
|
|
660
889
|
const changes: PathChange[] = [];
|
|
661
890
|
(function walk(
|
|
662
891
|
b: Record<string, unknown>,
|
|
@@ -674,6 +903,12 @@ export function aliasChanges(
|
|
|
674
903
|
const k = pathKey(at);
|
|
675
904
|
if (pathKey(wasAt) !== pathKey(isAt)) {
|
|
676
905
|
changes.push({ path: at, deleted: false, value: y, alias: true });
|
|
906
|
+
} else if (
|
|
907
|
+
pathKey(isAt) === k &&
|
|
908
|
+
entryChildren(x) &&
|
|
909
|
+
aliasMoved(x, y, at, earlier, later)
|
|
910
|
+
) {
|
|
911
|
+
changes.push({ path: at, deleted: false, value: y, alias: true });
|
|
677
912
|
}
|
|
678
913
|
// Only paths that are first appearances are entered: elsewhere the
|
|
679
914
|
// contents are those of the object met first.
|
|
@@ -690,6 +925,30 @@ export function aliasChanges(
|
|
|
690
925
|
return changes;
|
|
691
926
|
}
|
|
692
927
|
|
|
928
|
+
/**
|
|
929
|
+
* The changes from `before` to `after`, as paths below them: the property
|
|
930
|
+
* paths that differ (see diffPaths), and those that hold equal content but
|
|
931
|
+
* another object than they did (see aliasChanges), those first. Compared as
|
|
932
|
+
* one object, so an object that two of their keys share is one object,
|
|
933
|
+
* whichever of them it is reached through.
|
|
934
|
+
*/
|
|
935
|
+
export function changesBetween(
|
|
936
|
+
before: Record<string, unknown>,
|
|
937
|
+
after: Record<string, unknown>,
|
|
938
|
+
arrays = false,
|
|
939
|
+
): PathChange[] {
|
|
940
|
+
const written = diffPaths(before, after, arrays);
|
|
941
|
+
const keys = new Set(written.map((c) => pathKey(c.path)));
|
|
942
|
+
const aliases: PathChange[] = [];
|
|
943
|
+
for (const change of aliasChanges(before, after)) {
|
|
944
|
+
// Written whole by a change at it or above it
|
|
945
|
+
if (underChange(change.path, keys)) continue;
|
|
946
|
+
keys.add(pathKey(change.path));
|
|
947
|
+
aliases.push(change);
|
|
948
|
+
}
|
|
949
|
+
return [...aliases, ...written];
|
|
950
|
+
}
|
|
951
|
+
|
|
693
952
|
/**
|
|
694
953
|
* Where the objects of `source` are, other than at or below the paths of
|
|
695
954
|
* `changes` (which write something new there), for `existingObjects`.
|
|
@@ -701,6 +960,7 @@ export function locateObjects(
|
|
|
701
960
|
return firstPaths(
|
|
702
961
|
source as Record<string, unknown>,
|
|
703
962
|
new Set(changes.map((c) => pathKey(c.path))),
|
|
963
|
+
true,
|
|
704
964
|
);
|
|
705
965
|
}
|
|
706
966
|
|
|
@@ -720,7 +980,7 @@ export function existingObjects(
|
|
|
720
980
|
if (super.has(obj)) return true;
|
|
721
981
|
const path = paths.get(obj);
|
|
722
982
|
if (!path) return false;
|
|
723
|
-
const found =
|
|
983
|
+
const found = getByEntryPath(target, path);
|
|
724
984
|
const like =
|
|
725
985
|
isObjectValue(found) &&
|
|
726
986
|
(Array.isArray(found)
|
|
@@ -745,50 +1005,22 @@ export function isApplied(target: object, change: PathChange): boolean {
|
|
|
745
1005
|
);
|
|
746
1006
|
}
|
|
747
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* Make `path` below `root` hold `object` (an object that `sharedPaths` found
|
|
1010
|
+
* at several paths): a property is written like setByPath() does, a Map's
|
|
1011
|
+
* value replaced; an entry that is a key or member is left as it is.
|
|
1012
|
+
*/
|
|
1013
|
+
export function relinkPath(
|
|
1014
|
+
root: Record<string, unknown>,
|
|
1015
|
+
path: readonly string[],
|
|
1016
|
+
object: object,
|
|
1017
|
+
): void {
|
|
1018
|
+
if (path.some(isEntryKey)) setEntryValue(root, path, object);
|
|
1019
|
+
else setByPath(root, path, object);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
748
1022
|
export function applyChange(target: object, change: PathChange): void {
|
|
749
1023
|
const root = target as Record<string, unknown>;
|
|
750
1024
|
if (change.deleted) deleteByPath(root, change.path);
|
|
751
1025
|
else setByPath(root, change.path, change.value);
|
|
752
1026
|
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
756
|
-
* index) into `target`, another version of the same object or array.
|
|
757
|
-
* Values that changed are written whole, as deep copies; for values that
|
|
758
|
-
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
759
|
-
* whether it did. Into an array, elements removed from the end are removed
|
|
760
|
-
* at the same indices and elements added are appended, and changes at
|
|
761
|
-
* indices `target` lacks are dropped: where `target` was resized, its
|
|
762
|
-
* indices do not line up with `before`'s.
|
|
763
|
-
*/
|
|
764
|
-
export function mergeKeys(
|
|
765
|
-
target: Record<string, unknown>,
|
|
766
|
-
before: Record<string, unknown>,
|
|
767
|
-
after: Record<string, unknown>,
|
|
768
|
-
mergeNested?: (
|
|
769
|
-
key: string,
|
|
770
|
-
before: Record<string, unknown>,
|
|
771
|
-
after: Record<string, unknown>,
|
|
772
|
-
) => boolean,
|
|
773
|
-
): void {
|
|
774
|
-
const list = Array.isArray(target) ? (target as unknown[]) : undefined;
|
|
775
|
-
for (const change of keyChanges(before, after, true)) {
|
|
776
|
-
const { key } = change;
|
|
777
|
-
if (change.kind === 'deleted') {
|
|
778
|
-
if (list) list.length = Math.min(list.length, Number(key));
|
|
779
|
-
else delete target[key];
|
|
780
|
-
} else if (list && change.kind === 'added') {
|
|
781
|
-
list.push(deepClone(change.after));
|
|
782
|
-
} else if (list && Number(key) >= list.length) {
|
|
783
|
-
continue;
|
|
784
|
-
} else if (
|
|
785
|
-
change.kind !== 'nested' ||
|
|
786
|
-
!(
|
|
787
|
-
mergeNested?.(key, change.before, change.after) ||
|
|
788
|
-
deepEqual(change.before, change.after)
|
|
789
|
-
)
|
|
790
|
-
) {
|
|
791
|
-
target[key] = deepClone(change.after);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
package/src/utils/object-path.ts
CHANGED
|
@@ -7,13 +7,16 @@ import { atomicName } from './value-kinds';
|
|
|
7
7
|
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
8
8
|
* are not story state: unless an object holds one as its own property, it
|
|
9
9
|
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
10
|
-
* (class getters, `size` of a Map) are read
|
|
10
|
+
* (class getters, `size` of a Map) are read, and so are those of a string,
|
|
11
|
+
* number or boolean (`length` of a string), as JavaScript boxes them.
|
|
11
12
|
*/
|
|
12
13
|
export function getByPath(obj: object, segments: readonly string[]): unknown {
|
|
13
14
|
let current: unknown = obj;
|
|
14
15
|
for (const seg of segments) {
|
|
15
|
-
if (current == null
|
|
16
|
-
if (seg in Object.prototype && !hasOwn(current, seg))
|
|
16
|
+
if (current == null) return undefined;
|
|
17
|
+
if (seg in Object.prototype && !hasOwn(Object(current), seg)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
17
20
|
current = (current as Record<string, unknown>)[seg];
|
|
18
21
|
}
|
|
19
22
|
return current;
|