@rohal12/spindle 0.59.4 → 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 +169 -69
- package/package.json +1 -1
- package/src/define-macro.ts +6 -3
- package/src/execute-mutation.ts +43 -27
- package/src/store.ts +71 -35
- package/src/story-api.ts +2 -2
- package/src/structural.ts +139 -17
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
|
@@ -307,8 +307,8 @@ function createStoryAPI(): StoryAPI {
|
|
|
307
307
|
// uses (nested in the running code, if any: it continues from the
|
|
308
308
|
// code's pending writes, and they are committed first): an update of a draft would give the written path new
|
|
309
309
|
// objects and leave the other references to the old ones (#295).
|
|
310
|
-
mutateState((work) => {
|
|
311
|
-
for (const [k, v] of entries) setOne(work, k, v);
|
|
310
|
+
mutateState((work, adopt) => {
|
|
311
|
+
for (const [k, v] of entries) setOne(work, k, adopt(v));
|
|
312
312
|
});
|
|
313
313
|
return;
|
|
314
314
|
}
|
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,35 +616,71 @@ 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
624
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
597
625
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
598
626
|
* entered, where first met only (an object held in an array is one object,
|
|
599
627
|
* however it was reached); other values are leaves. A path at or below one
|
|
600
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.
|
|
601
634
|
*/
|
|
602
635
|
function objectPaths(
|
|
603
636
|
root: object,
|
|
604
637
|
skip: ReadonlySet<string> = new Set(),
|
|
638
|
+
entries = false,
|
|
605
639
|
): Map<object, string[][]> {
|
|
606
640
|
const all = new Map<object, string[][]>();
|
|
607
|
-
|
|
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 {
|
|
608
656
|
for (const key of Object.keys(node)) {
|
|
609
657
|
const value = node[key];
|
|
610
658
|
if (!isObjectValue(value)) continue;
|
|
611
659
|
const at = [...path, key];
|
|
612
660
|
if (skip.has(pathKey(at))) continue;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
+
}
|
|
621
680
|
}
|
|
681
|
+
index++;
|
|
622
682
|
}
|
|
623
|
-
}
|
|
683
|
+
}
|
|
624
684
|
return all;
|
|
625
685
|
}
|
|
626
686
|
|
|
@@ -628,9 +688,13 @@ function objectPaths(
|
|
|
628
688
|
function firstPaths(
|
|
629
689
|
root: object,
|
|
630
690
|
skip?: ReadonlySet<string>,
|
|
691
|
+
entries?: boolean,
|
|
631
692
|
): Map<object, string[]> {
|
|
632
693
|
return new Map(
|
|
633
|
-
[...objectPaths(root, skip)].map(([object, paths]) => [
|
|
694
|
+
[...objectPaths(root, skip, entries)].map(([object, paths]) => [
|
|
695
|
+
object,
|
|
696
|
+
paths[0]!,
|
|
697
|
+
]),
|
|
634
698
|
);
|
|
635
699
|
}
|
|
636
700
|
|
|
@@ -643,6 +707,35 @@ export function sharedPaths(root: object): string[][][] {
|
|
|
643
707
|
return [...objectPaths(root).values()].filter((paths) => paths.length > 1);
|
|
644
708
|
}
|
|
645
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;
|
|
737
|
+
}
|
|
738
|
+
|
|
646
739
|
/**
|
|
647
740
|
* The paths whose object is another one in the references of `after` than in
|
|
648
741
|
* `before`, as to where its first appearance is: `$a = $b` makes `a` another
|
|
@@ -655,8 +748,8 @@ export function aliasChanges(
|
|
|
655
748
|
before: Record<string, unknown>,
|
|
656
749
|
after: Record<string, unknown>,
|
|
657
750
|
): PathChange[] {
|
|
658
|
-
const earlier = firstPaths(before);
|
|
659
|
-
const later = firstPaths(after);
|
|
751
|
+
const earlier = firstPaths(before, undefined, true);
|
|
752
|
+
const later = firstPaths(after, undefined, true);
|
|
660
753
|
const changes: PathChange[] = [];
|
|
661
754
|
(function walk(
|
|
662
755
|
b: Record<string, unknown>,
|
|
@@ -674,6 +767,11 @@ export function aliasChanges(
|
|
|
674
767
|
const k = pathKey(at);
|
|
675
768
|
if (pathKey(wasAt) !== pathKey(isAt)) {
|
|
676
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 });
|
|
677
775
|
}
|
|
678
776
|
// Only paths that are first appearances are entered: elsewhere the
|
|
679
777
|
// contents are those of the object met first.
|
|
@@ -690,6 +788,30 @@ export function aliasChanges(
|
|
|
690
788
|
return changes;
|
|
691
789
|
}
|
|
692
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
|
+
|
|
693
815
|
/**
|
|
694
816
|
* Where the objects of `source` are, other than at or below the paths of
|
|
695
817
|
* `changes` (which write something new there), for `existingObjects`.
|