@rohal12/spindle 0.59.5 → 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 +220 -96
- package/package.json +1 -1
- package/src/components/macros/Unset.tsx +5 -1
- package/src/execute-mutation.ts +56 -18
- package/src/store.ts +19 -8
- package/src/structural.ts +224 -114
- package/src/utils/object-path.ts +6 -3
package/src/store.ts
CHANGED
|
@@ -765,28 +765,39 @@ function writeHookChange(
|
|
|
765
765
|
const child = hasOwn(holder, key) ? holder[key] : undefined;
|
|
766
766
|
const held = getByPath(after, path.slice(0, depth + 1));
|
|
767
767
|
if (!mergesWith(child, held, true)) {
|
|
768
|
-
writeKey(holder, key,
|
|
768
|
+
writeKey(holder, key, own(held));
|
|
769
769
|
return;
|
|
770
770
|
}
|
|
771
771
|
holder = child as Record<string, unknown>;
|
|
772
772
|
}
|
|
773
773
|
const key = path[depth]!;
|
|
774
|
-
if (change.deleted) writeKey(holder, key,
|
|
775
|
-
else writeKey(holder, key,
|
|
774
|
+
if (change.deleted) writeKey(holder, key, undefined, change);
|
|
775
|
+
else writeKey(holder, key, own(change.value), change);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
778
|
function writeKey(
|
|
779
779
|
holder: Record<string, unknown>,
|
|
780
780
|
key: string,
|
|
781
|
-
deleted: boolean,
|
|
782
781
|
value: unknown,
|
|
783
|
-
|
|
782
|
+
change?: PathChange,
|
|
784
783
|
): void {
|
|
784
|
+
const deleted = change?.deleted ?? false;
|
|
785
785
|
if (Array.isArray(holder)) {
|
|
786
786
|
const index = Number(key);
|
|
787
|
-
if (deleted)
|
|
788
|
-
|
|
789
|
-
|
|
787
|
+
if (deleted) {
|
|
788
|
+
// A hole stays where it was; anything else was cut off
|
|
789
|
+
if (change && 'hole' in change) delete holder[index];
|
|
790
|
+
else holder.length = Math.min(holder.length, index);
|
|
791
|
+
} else if (change && 'appended' in change) {
|
|
792
|
+
// After the holes it followed
|
|
793
|
+
holder.length += change.gap ?? 0;
|
|
794
|
+
holder.push(value);
|
|
795
|
+
} else if (key === 'length' && change) {
|
|
796
|
+
// Holes added at the end
|
|
797
|
+
holder.length += value as number;
|
|
798
|
+
} else if (index < holder.length) {
|
|
799
|
+
holder[index] = value;
|
|
800
|
+
}
|
|
790
801
|
} else if (deleted) {
|
|
791
802
|
delete holder[key];
|
|
792
803
|
} else {
|
package/src/structural.ts
CHANGED
|
@@ -43,11 +43,11 @@ export interface DeepCloneOptions {
|
|
|
43
43
|
*/
|
|
44
44
|
seen?: Map<object, object>;
|
|
45
45
|
/**
|
|
46
|
-
* Called with each key
|
|
47
|
-
* from; the
|
|
48
|
-
* which Immer does not finalize
|
|
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
49
|
*/
|
|
50
|
-
|
|
50
|
+
settle?: (copy: object, original: object) => object;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
type TypedArrayCtor = new (
|
|
@@ -109,8 +109,9 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
109
109
|
const copy = keep(val, Object.create(Object.getPrototypeOf(val)));
|
|
110
110
|
for (const key of [...ERROR_HIDDEN_KEYS, 'stack']) {
|
|
111
111
|
if (hasOwn(val, key)) {
|
|
112
|
+
const held = (val as unknown as Record<string, unknown>)[key];
|
|
112
113
|
Object.defineProperty(copy, key, {
|
|
113
|
-
value: clone(
|
|
114
|
+
value: settled(clone(held), held),
|
|
114
115
|
writable: true,
|
|
115
116
|
configurable: true,
|
|
116
117
|
});
|
|
@@ -121,6 +122,13 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
121
122
|
return undefined;
|
|
122
123
|
}
|
|
123
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
|
+
|
|
124
132
|
function clone(val: unknown): unknown {
|
|
125
133
|
if (val === null || typeof val !== 'object') return val;
|
|
126
134
|
|
|
@@ -138,24 +146,13 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
138
146
|
|
|
139
147
|
if (val instanceof Map) {
|
|
140
148
|
const copy = keep(obj, new Map());
|
|
141
|
-
for (const [k, v] of val)
|
|
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
|
-
);
|
|
150
|
-
}
|
|
149
|
+
for (const [k, v] of val) copy.set(settled(clone(k), k), clone(v));
|
|
151
150
|
return copy;
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
if (val instanceof Set) {
|
|
155
154
|
const copy = keep(obj, new Set());
|
|
156
|
-
for (const v of val)
|
|
157
|
-
copy.add(clone(v));
|
|
158
|
-
}
|
|
155
|
+
for (const v of val) copy.add(settled(clone(v), v));
|
|
159
156
|
return copy;
|
|
160
157
|
}
|
|
161
158
|
|
|
@@ -476,8 +473,15 @@ export function mergesWith(
|
|
|
476
473
|
|
|
477
474
|
/** How a value differs from an earlier version at one key. */
|
|
478
475
|
export type KeyChange =
|
|
479
|
-
| {
|
|
480
|
-
|
|
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 }
|
|
481
485
|
| {
|
|
482
486
|
key: string;
|
|
483
487
|
kind: 'nested';
|
|
@@ -493,7 +497,9 @@ export type KeyChange =
|
|
|
493
497
|
* is none either. Values that merge are reported as 'nested', to be
|
|
494
498
|
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
495
499
|
* merge too: elements at the indices both hold are compared, and the
|
|
496
|
-
* 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.
|
|
497
503
|
*/
|
|
498
504
|
export function keyChanges(
|
|
499
505
|
before: Record<string, unknown>,
|
|
@@ -519,14 +525,31 @@ export function keyChanges(
|
|
|
519
525
|
const b = before as unknown as unknown[];
|
|
520
526
|
const a = after as unknown[];
|
|
521
527
|
for (let i = 0; i < Math.min(b.length, a.length); i++) {
|
|
522
|
-
|
|
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] });
|
|
523
533
|
}
|
|
534
|
+
let next = b.length;
|
|
524
535
|
for (let i = b.length; i < a.length; i++) {
|
|
525
|
-
|
|
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;
|
|
526
545
|
}
|
|
527
546
|
for (let i = a.length; i < b.length; i++) {
|
|
528
547
|
changes.push({ key: String(i), kind: 'deleted' });
|
|
529
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
|
+
}
|
|
530
553
|
return changes;
|
|
531
554
|
}
|
|
532
555
|
|
|
@@ -548,6 +571,8 @@ export type PathChange =
|
|
|
548
571
|
value: unknown;
|
|
549
572
|
/** Set for an element an array gained beyond the length it had. */
|
|
550
573
|
appended?: true;
|
|
574
|
+
/** Holes an appended element follows (the array is sparse). */
|
|
575
|
+
gap?: number;
|
|
551
576
|
/**
|
|
552
577
|
* Set where the value is the same content as before and only which
|
|
553
578
|
* object the path refers to changed (see aliasChanges), so a store
|
|
@@ -555,7 +580,12 @@ export type PathChange =
|
|
|
555
580
|
*/
|
|
556
581
|
alias?: true;
|
|
557
582
|
}
|
|
558
|
-
| {
|
|
583
|
+
| {
|
|
584
|
+
path: string[];
|
|
585
|
+
deleted: true;
|
|
586
|
+
/** Set for an element an array lost without shrinking: a hole. */
|
|
587
|
+
hole?: true;
|
|
588
|
+
};
|
|
559
589
|
|
|
560
590
|
/**
|
|
561
591
|
* The property paths where `after` differs from `before`, two objects of
|
|
@@ -570,14 +600,25 @@ export function diffPaths(
|
|
|
570
600
|
): PathChange[] {
|
|
571
601
|
const changes: PathChange[] = [];
|
|
572
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;
|
|
573
607
|
(function walk(
|
|
574
608
|
b: Record<string, unknown>,
|
|
575
609
|
a: Record<string, unknown>,
|
|
576
610
|
path: string[],
|
|
577
611
|
): void {
|
|
578
|
-
// Stop at cycles; shared (non-cyclic) references are visited
|
|
579
|
-
|
|
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;
|
|
580
619
|
ancestors.add(a);
|
|
620
|
+
const found = changes.length;
|
|
621
|
+
const cutsBefore = cuts;
|
|
581
622
|
for (const change of keyChanges(b, a, arrays)) {
|
|
582
623
|
const at = [...path, change.key];
|
|
583
624
|
if (change.kind === 'nested') {
|
|
@@ -585,18 +626,33 @@ export function diffPaths(
|
|
|
585
626
|
} else {
|
|
586
627
|
changes.push(
|
|
587
628
|
change.kind === 'deleted'
|
|
588
|
-
? {
|
|
629
|
+
? {
|
|
630
|
+
path: at,
|
|
631
|
+
deleted: true,
|
|
632
|
+
...(change.hole ? { hole: true } : {}),
|
|
633
|
+
}
|
|
589
634
|
: {
|
|
590
635
|
path: at,
|
|
591
636
|
deleted: false,
|
|
592
637
|
value: change.after,
|
|
593
|
-
...(change.kind === 'added' &&
|
|
594
|
-
|
|
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
|
+
}
|
|
595
645
|
: {}),
|
|
596
646
|
},
|
|
597
647
|
);
|
|
598
648
|
}
|
|
599
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
|
+
}
|
|
600
656
|
ancestors.delete(a);
|
|
601
657
|
})(before, after, []);
|
|
602
658
|
return changes;
|
|
@@ -616,10 +672,84 @@ export function underChange(
|
|
|
616
672
|
return false;
|
|
617
673
|
}
|
|
618
674
|
|
|
619
|
-
/**
|
|
620
|
-
|
|
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 =>
|
|
621
680
|
`\0${kind}${index}`;
|
|
622
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
|
+
|
|
623
753
|
/**
|
|
624
754
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
625
755
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
@@ -630,7 +760,8 @@ const entryKey = (kind: 'k' | 'v' | 's', index: number): string =>
|
|
|
630
760
|
* With `entries`, the objects held in a Map or Set (as key, value or member)
|
|
631
761
|
* are placed too, after everything else: one met nowhere else is at the path
|
|
632
762
|
* 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
|
|
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.
|
|
634
765
|
*/
|
|
635
766
|
function objectPaths(
|
|
636
767
|
root: object,
|
|
@@ -638,7 +769,7 @@ function objectPaths(
|
|
|
638
769
|
entries = false,
|
|
639
770
|
): Map<object, string[][]> {
|
|
640
771
|
const all = new Map<object, string[][]>();
|
|
641
|
-
const collections: [
|
|
772
|
+
const collections: [object, string[]][] = [];
|
|
642
773
|
function place(value: object, at: string[]): void {
|
|
643
774
|
const known = all.get(value);
|
|
644
775
|
if (known) {
|
|
@@ -648,7 +779,7 @@ function objectPaths(
|
|
|
648
779
|
all.set(value, [at]);
|
|
649
780
|
if (isMergeable(value) || Array.isArray(value)) {
|
|
650
781
|
walk(value as Record<string, unknown>, at);
|
|
651
|
-
} else if (entries && (value
|
|
782
|
+
} else if (entries && entryChildren(value)) {
|
|
652
783
|
collections.push([value, at]);
|
|
653
784
|
}
|
|
654
785
|
}
|
|
@@ -663,22 +794,8 @@ function objectPaths(
|
|
|
663
794
|
}
|
|
664
795
|
walk(root as Record<string, unknown>, []);
|
|
665
796
|
for (const [collection, path] of collections) {
|
|
666
|
-
|
|
667
|
-
|
|
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++;
|
|
797
|
+
for (const [segment, value] of entryChildren(collection)!) {
|
|
798
|
+
if (isObjectValue(value)) place(value, [...path, segment]);
|
|
682
799
|
}
|
|
683
800
|
}
|
|
684
801
|
return all;
|
|
@@ -704,36 +821,55 @@ function firstPaths(
|
|
|
704
821
|
* parents before children.
|
|
705
822
|
*/
|
|
706
823
|
export function sharedPaths(root: object): string[][][] {
|
|
707
|
-
return [...objectPaths(root).values()].filter(
|
|
824
|
+
return [...objectPaths(root, undefined, true).values()].filter(
|
|
825
|
+
(paths) => paths.length > 1,
|
|
826
|
+
);
|
|
708
827
|
}
|
|
709
828
|
|
|
710
829
|
/**
|
|
711
|
-
* Whether the
|
|
712
|
-
* object than `x` did
|
|
713
|
-
* `$map.set("k", $a)` over an equal object
|
|
714
|
-
*
|
|
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.
|
|
715
836
|
*/
|
|
716
|
-
function
|
|
837
|
+
function aliasMoved(
|
|
717
838
|
x: object,
|
|
718
839
|
y: object,
|
|
840
|
+
at: string[],
|
|
719
841
|
earlier: ReadonlyMap<object, string[]>,
|
|
720
842
|
later: ReadonlyMap<object, string[]>,
|
|
721
843
|
): boolean {
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
+
}
|
|
735
862
|
}
|
|
736
|
-
return
|
|
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
|
+
});
|
|
737
873
|
}
|
|
738
874
|
|
|
739
875
|
/**
|
|
@@ -769,7 +905,8 @@ export function aliasChanges(
|
|
|
769
905
|
changes.push({ path: at, deleted: false, value: y, alias: true });
|
|
770
906
|
} else if (
|
|
771
907
|
pathKey(isAt) === k &&
|
|
772
|
-
|
|
908
|
+
entryChildren(x) &&
|
|
909
|
+
aliasMoved(x, y, at, earlier, later)
|
|
773
910
|
) {
|
|
774
911
|
changes.push({ path: at, deleted: false, value: y, alias: true });
|
|
775
912
|
}
|
|
@@ -823,6 +960,7 @@ export function locateObjects(
|
|
|
823
960
|
return firstPaths(
|
|
824
961
|
source as Record<string, unknown>,
|
|
825
962
|
new Set(changes.map((c) => pathKey(c.path))),
|
|
963
|
+
true,
|
|
826
964
|
);
|
|
827
965
|
}
|
|
828
966
|
|
|
@@ -842,7 +980,7 @@ export function existingObjects(
|
|
|
842
980
|
if (super.has(obj)) return true;
|
|
843
981
|
const path = paths.get(obj);
|
|
844
982
|
if (!path) return false;
|
|
845
|
-
const found =
|
|
983
|
+
const found = getByEntryPath(target, path);
|
|
846
984
|
const like =
|
|
847
985
|
isObjectValue(found) &&
|
|
848
986
|
(Array.isArray(found)
|
|
@@ -867,50 +1005,22 @@ export function isApplied(target: object, change: PathChange): boolean {
|
|
|
867
1005
|
);
|
|
868
1006
|
}
|
|
869
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
|
+
|
|
870
1022
|
export function applyChange(target: object, change: PathChange): void {
|
|
871
1023
|
const root = target as Record<string, unknown>;
|
|
872
1024
|
if (change.deleted) deleteByPath(root, change.path);
|
|
873
1025
|
else setByPath(root, change.path, change.value);
|
|
874
1026
|
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
878
|
-
* index) into `target`, another version of the same object or array.
|
|
879
|
-
* Values that changed are written whole, as deep copies; for values that
|
|
880
|
-
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
881
|
-
* whether it did. Into an array, elements removed from the end are removed
|
|
882
|
-
* at the same indices and elements added are appended, and changes at
|
|
883
|
-
* indices `target` lacks are dropped: where `target` was resized, its
|
|
884
|
-
* indices do not line up with `before`'s.
|
|
885
|
-
*/
|
|
886
|
-
export function mergeKeys(
|
|
887
|
-
target: Record<string, unknown>,
|
|
888
|
-
before: Record<string, unknown>,
|
|
889
|
-
after: Record<string, unknown>,
|
|
890
|
-
mergeNested?: (
|
|
891
|
-
key: string,
|
|
892
|
-
before: Record<string, unknown>,
|
|
893
|
-
after: Record<string, unknown>,
|
|
894
|
-
) => boolean,
|
|
895
|
-
): void {
|
|
896
|
-
const list = Array.isArray(target) ? (target as unknown[]) : undefined;
|
|
897
|
-
for (const change of keyChanges(before, after, true)) {
|
|
898
|
-
const { key } = change;
|
|
899
|
-
if (change.kind === 'deleted') {
|
|
900
|
-
if (list) list.length = Math.min(list.length, Number(key));
|
|
901
|
-
else delete target[key];
|
|
902
|
-
} else if (list && change.kind === 'added') {
|
|
903
|
-
list.push(deepClone(change.after));
|
|
904
|
-
} else if (list && Number(key) >= list.length) {
|
|
905
|
-
continue;
|
|
906
|
-
} else if (
|
|
907
|
-
change.kind !== 'nested' ||
|
|
908
|
-
!(
|
|
909
|
-
mergeNested?.(key, change.before, change.after) ||
|
|
910
|
-
deepEqual(change.before, change.after)
|
|
911
|
-
)
|
|
912
|
-
) {
|
|
913
|
-
target[key] = deepClone(change.after);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
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;
|