@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/dist/pkg/headless.js
CHANGED
|
@@ -13426,7 +13426,11 @@ function deepClone(value, options = {}) {
|
|
|
13426
13426
|
}
|
|
13427
13427
|
if (val instanceof Map) {
|
|
13428
13428
|
const copy = keep(obj, /* @__PURE__ */ new Map());
|
|
13429
|
-
for (const [k, v] of val)
|
|
13429
|
+
for (const [k, v] of val) {
|
|
13430
|
+
const key = clone(k);
|
|
13431
|
+
const settle = options.mapKey;
|
|
13432
|
+
copy.set(settle && isObjectValue(key) && isObjectValue(k) ? settle(key, k) : key, clone(v));
|
|
13433
|
+
}
|
|
13430
13434
|
return copy;
|
|
13431
13435
|
}
|
|
13432
13436
|
if (val instanceof Set) {
|
|
@@ -13679,15 +13683,16 @@ function keyChanges(before, after, arrays) {
|
|
|
13679
13683
|
/**
|
|
13680
13684
|
* The property paths where `after` differs from `before`, two objects of
|
|
13681
13685
|
* one class: objects that merge (see isMergeable) are compared property by
|
|
13682
|
-
* property, any other value as a whole
|
|
13686
|
+
* property, any other value as a whole; with `arrays`, arrays are compared
|
|
13687
|
+
* by index too (see keyChanges).
|
|
13683
13688
|
*/
|
|
13684
|
-
function diffPaths(before, after) {
|
|
13689
|
+
function diffPaths(before, after, arrays = false) {
|
|
13685
13690
|
const changes = [];
|
|
13686
13691
|
const ancestors = /* @__PURE__ */ new Set();
|
|
13687
13692
|
(function walk(b, a, path) {
|
|
13688
13693
|
if (ancestors.has(a)) return;
|
|
13689
13694
|
ancestors.add(a);
|
|
13690
|
-
for (const change of keyChanges(b, a,
|
|
13695
|
+
for (const change of keyChanges(b, a, arrays)) {
|
|
13691
13696
|
const at = [...path, change.key];
|
|
13692
13697
|
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
13693
13698
|
else changes.push(change.kind === "deleted" ? {
|
|
@@ -13696,7 +13701,8 @@ function diffPaths(before, after) {
|
|
|
13696
13701
|
} : {
|
|
13697
13702
|
path: at,
|
|
13698
13703
|
deleted: false,
|
|
13699
|
-
value: change.after
|
|
13704
|
+
value: change.after,
|
|
13705
|
+
...change.kind === "added" && Array.isArray(a) ? { appended: true } : {}
|
|
13700
13706
|
});
|
|
13701
13707
|
}
|
|
13702
13708
|
ancestors.delete(a);
|
|
@@ -13709,35 +13715,57 @@ function underChange(path, keys) {
|
|
|
13709
13715
|
for (let n = 1; n <= path.length; n++) if (keys.has(pathKey(path.slice(0, n)))) return true;
|
|
13710
13716
|
return false;
|
|
13711
13717
|
}
|
|
13718
|
+
/** The segment standing for the entry `index` of a Map or Set in a path. */
|
|
13719
|
+
var entryKey = (kind, index) => `\0${kind}${index}`;
|
|
13712
13720
|
/**
|
|
13713
13721
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
13714
13722
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
13715
13723
|
* entered, where first met only (an object held in an array is one object,
|
|
13716
13724
|
* however it was reached); other values are leaves. A path at or below one
|
|
13717
13725
|
* of `skip` is left out.
|
|
13726
|
+
*
|
|
13727
|
+
* With `entries`, the objects held in a Map or Set (as key, value or member)
|
|
13728
|
+
* are placed too, after everything else: one met nowhere else is at the path
|
|
13729
|
+
* of its Map or Set and the entry's segment (see entryKey), which is no path
|
|
13730
|
+
* a value can be read at, so only to tell where an object is from.
|
|
13718
13731
|
*/
|
|
13719
|
-
function objectPaths(root, skip = /* @__PURE__ */ new Set()) {
|
|
13732
|
+
function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
|
|
13720
13733
|
const all = /* @__PURE__ */ new Map();
|
|
13721
|
-
|
|
13734
|
+
const collections = [];
|
|
13735
|
+
function place(value, at) {
|
|
13736
|
+
const known = all.get(value);
|
|
13737
|
+
if (known) {
|
|
13738
|
+
known.push(at);
|
|
13739
|
+
return;
|
|
13740
|
+
}
|
|
13741
|
+
all.set(value, [at]);
|
|
13742
|
+
if (isMergeable(value) || Array.isArray(value)) walk(value, at);
|
|
13743
|
+
else if (entries && (value instanceof Map || value instanceof Set)) collections.push([value, at]);
|
|
13744
|
+
}
|
|
13745
|
+
function walk(node, path) {
|
|
13722
13746
|
for (const key of Object.keys(node)) {
|
|
13723
13747
|
const value = node[key];
|
|
13724
13748
|
if (!isObjectValue(value)) continue;
|
|
13725
13749
|
const at = [...path, key];
|
|
13726
13750
|
if (skip.has(pathKey(at))) continue;
|
|
13727
|
-
|
|
13728
|
-
if (known) {
|
|
13729
|
-
known.push(at);
|
|
13730
|
-
continue;
|
|
13731
|
-
}
|
|
13732
|
-
all.set(value, [at]);
|
|
13733
|
-
if (isMergeable(value) || Array.isArray(value)) walk(value, at);
|
|
13751
|
+
place(value, at);
|
|
13734
13752
|
}
|
|
13735
|
-
}
|
|
13753
|
+
}
|
|
13754
|
+
walk(root, []);
|
|
13755
|
+
for (const [collection, path] of collections) {
|
|
13756
|
+
let index = 0;
|
|
13757
|
+
for (const entry of collection.entries()) {
|
|
13758
|
+
const [k, v] = entry;
|
|
13759
|
+
const parts = collection instanceof Set ? [[k, entryKey("s", index)]] : [[k, entryKey("k", index)], [v, entryKey("v", index)]];
|
|
13760
|
+
for (const [value, segment] of parts) if (isObjectValue(value) && !all.has(value)) place(value, [...path, segment]);
|
|
13761
|
+
index++;
|
|
13762
|
+
}
|
|
13763
|
+
}
|
|
13736
13764
|
return all;
|
|
13737
13765
|
}
|
|
13738
13766
|
/** Where each object of `root` first appears (see objectPaths). */
|
|
13739
|
-
function firstPaths(root, skip) {
|
|
13740
|
-
return new Map([...objectPaths(root, skip)].map(([object, paths]) => [object, paths[0]]));
|
|
13767
|
+
function firstPaths(root, skip, entries) {
|
|
13768
|
+
return new Map([...objectPaths(root, skip, entries)].map(([object, paths]) => [object, paths[0]]));
|
|
13741
13769
|
}
|
|
13742
13770
|
/**
|
|
13743
13771
|
* The objects `root` holds at more than one path (an object two variables
|
|
@@ -13748,6 +13776,26 @@ function sharedPaths(root) {
|
|
|
13748
13776
|
return [...objectPaths(root).values()].filter((paths) => paths.length > 1);
|
|
13749
13777
|
}
|
|
13750
13778
|
/**
|
|
13779
|
+
* Whether the Map or Set `y` holds, at the position of an entry of `x`, another
|
|
13780
|
+
* object than `x` did (as to where its first appearance is, see aliasChanges):
|
|
13781
|
+
* `$map.set("k", $a)` over an equal object. Entries are compared by position,
|
|
13782
|
+
* and only when both hold as many; other differences are changes of content.
|
|
13783
|
+
*/
|
|
13784
|
+
function entryAliasChanged(x, y, earlier, later) {
|
|
13785
|
+
const isSet = x instanceof Set && y instanceof Set;
|
|
13786
|
+
if (!isSet && !(x instanceof Map && y instanceof Map)) return false;
|
|
13787
|
+
if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
|
|
13788
|
+
if (x.size !== y.size) return false;
|
|
13789
|
+
const moved = (a, b) => isObjectValue(a) && isObjectValue(b) && pathKey(earlier.get(a)) !== pathKey(later.get(b));
|
|
13790
|
+
const ys = [...y.entries()];
|
|
13791
|
+
let index = 0;
|
|
13792
|
+
for (const [k, v] of x.entries()) {
|
|
13793
|
+
const [k2, v2] = ys[index++];
|
|
13794
|
+
if (moved(k, k2) || !isSet && moved(v, v2)) return true;
|
|
13795
|
+
}
|
|
13796
|
+
return false;
|
|
13797
|
+
}
|
|
13798
|
+
/**
|
|
13751
13799
|
* The paths whose object is another one in the references of `after` than in
|
|
13752
13800
|
* `before`, as to where its first appearance is: `$a = $b` makes `a` another
|
|
13753
13801
|
* name for the object of `b` even where both hold equal content, and a
|
|
@@ -13756,8 +13804,8 @@ function sharedPaths(root) {
|
|
|
13756
13804
|
* `after` holds.
|
|
13757
13805
|
*/
|
|
13758
13806
|
function aliasChanges(before, after) {
|
|
13759
|
-
const earlier = firstPaths(before);
|
|
13760
|
-
const later = firstPaths(after);
|
|
13807
|
+
const earlier = firstPaths(before, void 0, true);
|
|
13808
|
+
const later = firstPaths(after, void 0, true);
|
|
13761
13809
|
const changes = [];
|
|
13762
13810
|
(function walk(b, a, path) {
|
|
13763
13811
|
for (const key of Object.keys(a)) {
|
|
@@ -13775,12 +13823,36 @@ function aliasChanges(before, after) {
|
|
|
13775
13823
|
value: y,
|
|
13776
13824
|
alias: true
|
|
13777
13825
|
});
|
|
13826
|
+
else if (pathKey(isAt) === k && entryAliasChanged(x, y, earlier, later)) changes.push({
|
|
13827
|
+
path: at,
|
|
13828
|
+
deleted: false,
|
|
13829
|
+
value: y,
|
|
13830
|
+
alias: true
|
|
13831
|
+
});
|
|
13778
13832
|
if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
|
|
13779
13833
|
}
|
|
13780
13834
|
})(before, after, []);
|
|
13781
13835
|
return changes;
|
|
13782
13836
|
}
|
|
13783
13837
|
/**
|
|
13838
|
+
* The changes from `before` to `after`, as paths below them: the property
|
|
13839
|
+
* paths that differ (see diffPaths), and those that hold equal content but
|
|
13840
|
+
* another object than they did (see aliasChanges), those first. Compared as
|
|
13841
|
+
* one object, so an object that two of their keys share is one object,
|
|
13842
|
+
* whichever of them it is reached through.
|
|
13843
|
+
*/
|
|
13844
|
+
function changesBetween(before, after, arrays = false) {
|
|
13845
|
+
const written = diffPaths(before, after, arrays);
|
|
13846
|
+
const keys = new Set(written.map((c) => pathKey(c.path)));
|
|
13847
|
+
const aliases = [];
|
|
13848
|
+
for (const change of aliasChanges(before, after)) {
|
|
13849
|
+
if (underChange(change.path, keys)) continue;
|
|
13850
|
+
keys.add(pathKey(change.path));
|
|
13851
|
+
aliases.push(change);
|
|
13852
|
+
}
|
|
13853
|
+
return [...aliases, ...written];
|
|
13854
|
+
}
|
|
13855
|
+
/**
|
|
13784
13856
|
* Where the objects of `source` are, other than at or below the paths of
|
|
13785
13857
|
* `changes` (which write something new there), for `existingObjects`.
|
|
13786
13858
|
*/
|
|
@@ -14600,9 +14672,18 @@ var cloneValue = (value) => deepClone(value, { keepUnregistered: true });
|
|
|
14600
14672
|
*/
|
|
14601
14673
|
function clonerFor(target, paths) {
|
|
14602
14674
|
const seen = paths ? existingObjects(paths, target) : /* @__PURE__ */ new Map();
|
|
14675
|
+
const mapKey = (copy, original) => {
|
|
14676
|
+
const path = paths?.get(original);
|
|
14677
|
+
if (!path || !isDraft(copy)) return copy;
|
|
14678
|
+
const plain = current(copy);
|
|
14679
|
+
setByPath(target, path, plain);
|
|
14680
|
+
seen.set(original, plain);
|
|
14681
|
+
return plain;
|
|
14682
|
+
};
|
|
14603
14683
|
return (value) => deepClone(value, {
|
|
14604
14684
|
keepUnregistered: true,
|
|
14605
|
-
seen
|
|
14685
|
+
seen,
|
|
14686
|
+
mapKey
|
|
14606
14687
|
});
|
|
14607
14688
|
}
|
|
14608
14689
|
/** One cloner per target copy, made when first used. */
|
|
@@ -14751,7 +14832,8 @@ function routeStoreUpdate(recipe) {
|
|
|
14751
14832
|
path: [ns, ...change.path]
|
|
14752
14833
|
})) : [];
|
|
14753
14834
|
});
|
|
14754
|
-
const
|
|
14835
|
+
const held = locateObjects(namespacesOf(inner.work), changes);
|
|
14836
|
+
const cloners = clonersFor(() => held);
|
|
14755
14837
|
const owned = (change, target) => change.deleted ? change : {
|
|
14756
14838
|
...change,
|
|
14757
14839
|
value: cloners(target)(change.value)
|
|
@@ -14806,6 +14888,12 @@ function cloneNamespaces(from) {
|
|
|
14806
14888
|
transient: deepClone(from.transient, { seen })
|
|
14807
14889
|
};
|
|
14808
14890
|
}
|
|
14891
|
+
/** The three variable namespaces of `state`, as an object to walk. */
|
|
14892
|
+
var namespacesOf = (state) => ({
|
|
14893
|
+
variables: state.variables,
|
|
14894
|
+
temporary: state.temporary,
|
|
14895
|
+
transient: state.transient
|
|
14896
|
+
});
|
|
14809
14897
|
/**
|
|
14810
14898
|
* Commit the property paths each mutation's code changed (work vs base) on
|
|
14811
14899
|
* top of the current store state, so writes made elsewhere during execution
|
|
@@ -14870,24 +14958,9 @@ function commitScopes(commits) {
|
|
|
14870
14958
|
}
|
|
14871
14959
|
/**
|
|
14872
14960
|
* The changes of the code to the namespaces, as paths below them (`['variables',
|
|
14873
|
-
* 'a']
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
* object, whichever of them it is reached through.
|
|
14877
|
-
*/
|
|
14878
|
-
function changesOf(base, work) {
|
|
14879
|
-
const before = base;
|
|
14880
|
-
const after = work;
|
|
14881
|
-
const written = diffPaths(before, after);
|
|
14882
|
-
const keys = new Set(written.map((c) => pathKey(c.path)));
|
|
14883
|
-
const aliases = [];
|
|
14884
|
-
for (const change of aliasChanges(before, after)) {
|
|
14885
|
-
if (underChange(change.path, keys)) continue;
|
|
14886
|
-
keys.add(pathKey(change.path));
|
|
14887
|
-
aliases.push(change);
|
|
14888
|
-
}
|
|
14889
|
-
return [...aliases, ...written];
|
|
14890
|
-
}
|
|
14961
|
+
* 'a']`; see changesBetween).
|
|
14962
|
+
*/
|
|
14963
|
+
var changesOf = (base, work) => changesBetween(base, work);
|
|
14891
14964
|
/** Every running mutation, to commit with its code going on. */
|
|
14892
14965
|
var running = () => activeScopes.map((scope) => ({
|
|
14893
14966
|
scope,
|
|
@@ -14930,9 +15003,11 @@ function runWithCommittedMutations(action) {
|
|
|
14930
15003
|
*/
|
|
14931
15004
|
function mutateState(run) {
|
|
14932
15005
|
commitScopes(running());
|
|
15006
|
+
const held = locateObjects(namespacesOf(getActiveMutationScope() ?? useStoryStore.getState()), []);
|
|
14933
15007
|
const scope = startScope();
|
|
15008
|
+
const adopt = clonerFor(scope.work, held);
|
|
14934
15009
|
try {
|
|
14935
|
-
run(scope.work);
|
|
15010
|
+
run(scope.work, adopt);
|
|
14936
15011
|
} finally {
|
|
14937
15012
|
activeScopes.pop();
|
|
14938
15013
|
}
|
|
@@ -15687,35 +15762,60 @@ function loadedEntryMoment(payload) {
|
|
|
15687
15762
|
* the `beforesave` hooks) into the payload's snapshot of the saved moment. A
|
|
15688
15763
|
* load restores that snapshot, the state on entering the passage, and runs
|
|
15689
15764
|
* the passage again, so data a hook adds to a save would otherwise be lost
|
|
15690
|
-
* on load (#227). Only the property paths the hooks changed are written (
|
|
15691
|
-
*
|
|
15692
|
-
*
|
|
15693
|
-
*
|
|
15694
|
-
*
|
|
15765
|
+
* on load (#227). Only the property paths the hooks changed are written (see
|
|
15766
|
+
* changesBetween): a whole variable would bring along what the passage did
|
|
15767
|
+
* to the rest of it, which the passage then does again on load (#232). The
|
|
15768
|
+
* references the hooks made stay: an object they put in two variables is one
|
|
15769
|
+
* object in the snapshot, and one they made another variable refer to is the
|
|
15770
|
+
* snapshot's own object of it (#302). Only the payload's copy changes: the
|
|
15771
|
+
* live history keeps the recorded snapshot (#159).
|
|
15695
15772
|
*/
|
|
15696
15773
|
function keepHookWrites(payload, before, after) {
|
|
15697
15774
|
const moment = payload.history[payload.historyIndex];
|
|
15698
15775
|
if (!moment) return;
|
|
15699
|
-
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
}
|
|
15718
|
-
|
|
15776
|
+
const changes = changesBetween(before, after, true);
|
|
15777
|
+
if (changes.length === 0) return;
|
|
15778
|
+
const work = deepClone(moment.variables);
|
|
15779
|
+
const seen = existingObjects(locateObjects(after, changes), work);
|
|
15780
|
+
const own = (value) => deepClone(value, { seen });
|
|
15781
|
+
for (const change of changes) writeHookChange(work, after, change, own);
|
|
15782
|
+
moment.variables = shareEqual(moment.variables, work);
|
|
15783
|
+
}
|
|
15784
|
+
/**
|
|
15785
|
+
* Write `change` (of the hooks, to `after`) into `work`. The value goes in at
|
|
15786
|
+
* the first place `work` lacks the objects on the path of one of the kind
|
|
15787
|
+
* `after` holds (the passage created or replaced it): there, the hooks' whole
|
|
15788
|
+
* value of it is written. Into an array, elements removed from the end are
|
|
15789
|
+
* removed at the same indices and elements added are appended; changes at
|
|
15790
|
+
* indices the array lacks are dropped (it was resized, so its indices do not
|
|
15791
|
+
* line up with the live ones).
|
|
15792
|
+
*/
|
|
15793
|
+
function writeHookChange(work, after, change, own) {
|
|
15794
|
+
const { path } = change;
|
|
15795
|
+
let holder = work;
|
|
15796
|
+
let depth = 0;
|
|
15797
|
+
for (; depth < path.length - 1; depth++) {
|
|
15798
|
+
const key = path[depth];
|
|
15799
|
+
const child = hasOwn(holder, key) ? holder[key] : void 0;
|
|
15800
|
+
const held = getByPath(after, path.slice(0, depth + 1));
|
|
15801
|
+
if (!mergesWith(child, held, true)) {
|
|
15802
|
+
writeKey(holder, key, false, own(held));
|
|
15803
|
+
return;
|
|
15804
|
+
}
|
|
15805
|
+
holder = child;
|
|
15806
|
+
}
|
|
15807
|
+
const key = path[depth];
|
|
15808
|
+
if (change.deleted) writeKey(holder, key, true, void 0);
|
|
15809
|
+
else writeKey(holder, key, false, own(change.value), change.appended);
|
|
15810
|
+
}
|
|
15811
|
+
function writeKey(holder, key, deleted, value, appended) {
|
|
15812
|
+
if (Array.isArray(holder)) {
|
|
15813
|
+
const index = Number(key);
|
|
15814
|
+
if (deleted) holder.length = Math.min(holder.length, index);
|
|
15815
|
+
else if (appended) holder.push(value);
|
|
15816
|
+
else if (index < holder.length) holder[index] = value;
|
|
15817
|
+
} else if (deleted) delete holder[key];
|
|
15818
|
+
else setOwn(holder, key, value);
|
|
15719
15819
|
}
|
|
15720
15820
|
/** Restore the PRNG from a snapshot, or reset it without one. */
|
|
15721
15821
|
function restorePRNGFrom(prng) {
|
|
@@ -17289,8 +17389,8 @@ function defineMacro(config, source = "builtin") {
|
|
|
17289
17389
|
ctx.value = useStoryStore((s) => getByPath(s.variables, segments));
|
|
17290
17390
|
ctx.getValue = () => getByPath(readState().variables, segments);
|
|
17291
17391
|
ctx.setValue = (value) => {
|
|
17292
|
-
|
|
17293
|
-
setByPath(
|
|
17392
|
+
mutateState((work, adopt) => {
|
|
17393
|
+
setByPath(work.variables, segments, adopt(value), { createMissing: true });
|
|
17294
17394
|
});
|
|
17295
17395
|
};
|
|
17296
17396
|
}
|
|
@@ -17432,8 +17532,8 @@ function createStoryAPI() {
|
|
|
17432
17532
|
warnIfUndeclared(isTransient, key);
|
|
17433
17533
|
}
|
|
17434
17534
|
if (entries.some(([k]) => k.includes("."))) {
|
|
17435
|
-
mutateState((work) => {
|
|
17436
|
-
for (const [k, v] of entries) setOne(work, k, v);
|
|
17535
|
+
mutateState((work, adopt) => {
|
|
17536
|
+
for (const [k, v] of entries) setOne(work, k, adopt(v));
|
|
17437
17537
|
});
|
|
17438
17538
|
return;
|
|
17439
17539
|
}
|
package/package.json
CHANGED
package/src/define-macro.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
renderInlineNodes,
|
|
19
19
|
} from './markup/render';
|
|
20
20
|
import type { ASTNode } from './markup/ast';
|
|
21
|
-
import { executeMutation, readState } from './execute-mutation';
|
|
21
|
+
import { executeMutation, mutateState, readState } from './execute-mutation';
|
|
22
22
|
import { evaluate } from './expression';
|
|
23
23
|
import { useStoryStore } from './store';
|
|
24
24
|
import { getByPath, setByPath } from './utils/object-path';
|
|
@@ -228,8 +228,11 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
|
|
|
228
228
|
// In program order, also when mutation code performs the input
|
|
229
229
|
ctx.getValue = () => getByPath(readState().variables, segments);
|
|
230
230
|
ctx.setValue = (value: unknown) => {
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
// The commit mutation code uses, which keeps the references among
|
|
232
|
+
// variables: a write to a draft copies only the path written, and
|
|
233
|
+
// other variables sharing the object would keep the old one.
|
|
234
|
+
mutateState((work, adopt) => {
|
|
235
|
+
setByPath(work.variables, segments, adopt(value), {
|
|
233
236
|
createMissing: true,
|
|
234
237
|
});
|
|
235
238
|
});
|
package/src/execute-mutation.ts
CHANGED
|
@@ -10,11 +10,10 @@ import { useStoryStore } from './store';
|
|
|
10
10
|
import type { StoryState, VariableNamespaces } from './store';
|
|
11
11
|
import { execute } from './expression';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
changesBetween,
|
|
14
14
|
applyChange,
|
|
15
15
|
deepClone,
|
|
16
16
|
deepEqual,
|
|
17
|
-
diffPaths,
|
|
18
17
|
existingObjects,
|
|
19
18
|
isApplied,
|
|
20
19
|
isMergeable,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
mergeKeys,
|
|
23
22
|
pathKey,
|
|
24
23
|
sharedPaths,
|
|
25
|
-
underChange,
|
|
26
24
|
type PathChange,
|
|
27
25
|
} from './structural';
|
|
28
26
|
import { getByPath, setByPath } from './utils/object-path';
|
|
@@ -96,7 +94,18 @@ function clonerFor(
|
|
|
96
94
|
paths?: ReadonlyMap<object, string[]>,
|
|
97
95
|
): Cloner {
|
|
98
96
|
const seen = paths ? existingObjects(paths, target) : new Map();
|
|
99
|
-
|
|
97
|
+
// Immer does not finalize a draft held as a Map key, so an existing object
|
|
98
|
+
// met there is put in the draft as the object itself: its base while the
|
|
99
|
+
// code did not change it, which other references to it then agree with.
|
|
100
|
+
const mapKey = (copy: object, original: object): object => {
|
|
101
|
+
const path = paths?.get(original);
|
|
102
|
+
if (!path || !isDraft(copy)) return copy;
|
|
103
|
+
const plain = currentDraft(copy) as object;
|
|
104
|
+
setByPath(target as Record<string, unknown>, path, plain);
|
|
105
|
+
seen.set(original, plain);
|
|
106
|
+
return plain;
|
|
107
|
+
};
|
|
108
|
+
return (value) => deepClone(value, { keepUnregistered: true, seen, mapKey });
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
/** One cloner per target copy, made when first used. */
|
|
@@ -292,8 +301,10 @@ export function routeStoreUpdate<S extends VariableNamespaces>(
|
|
|
292
301
|
)
|
|
293
302
|
: [];
|
|
294
303
|
});
|
|
295
|
-
// Each target takes its own copies, which share what the written values do
|
|
296
|
-
|
|
304
|
+
// Each target takes its own copies, which share what the written values do,
|
|
305
|
+
// and an object the code holds elsewhere stays that one (`Story.set('b', $a)`)
|
|
306
|
+
const held = locateObjects(namespacesOf(inner.work), changes);
|
|
307
|
+
const cloners = clonersFor(() => held);
|
|
297
308
|
const owned = (change: PathChange, target: object): PathChange =>
|
|
298
309
|
change.deleted
|
|
299
310
|
? change
|
|
@@ -370,6 +381,13 @@ function cloneNamespaces(from: VariableNamespaces): VariableNamespaces {
|
|
|
370
381
|
};
|
|
371
382
|
}
|
|
372
383
|
|
|
384
|
+
/** The three variable namespaces of `state`, as an object to walk. */
|
|
385
|
+
const namespacesOf = (state: VariableNamespaces): VariableNamespaces => ({
|
|
386
|
+
variables: state.variables,
|
|
387
|
+
temporary: state.temporary,
|
|
388
|
+
transient: state.transient,
|
|
389
|
+
});
|
|
390
|
+
|
|
373
391
|
/** A mutation to commit, and whether its code goes on running after. */
|
|
374
392
|
interface Commit {
|
|
375
393
|
scope: MutationScope;
|
|
@@ -470,28 +488,16 @@ function commitScopes(commits: readonly Commit[]): void {
|
|
|
470
488
|
|
|
471
489
|
/**
|
|
472
490
|
* The changes of the code to the namespaces, as paths below them (`['variables',
|
|
473
|
-
* 'a']
|
|
474
|
-
* equal content but another object than they did (see aliasChanges), those
|
|
475
|
-
* first. Compared as one object, so an object the namespaces share is one
|
|
476
|
-
* object, whichever of them it is reached through.
|
|
491
|
+
* 'a']`; see changesBetween).
|
|
477
492
|
*/
|
|
478
|
-
|
|
493
|
+
const changesOf = (
|
|
479
494
|
base: VariableNamespaces,
|
|
480
495
|
work: VariableNamespaces,
|
|
481
|
-
): PathChange[]
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const aliases: PathChange[] = [];
|
|
487
|
-
for (const change of aliasChanges(before, after)) {
|
|
488
|
-
// Written whole by a change at it or above it
|
|
489
|
-
if (underChange(change.path, keys)) continue;
|
|
490
|
-
keys.add(pathKey(change.path));
|
|
491
|
-
aliases.push(change);
|
|
492
|
-
}
|
|
493
|
-
return [...aliases, ...written];
|
|
494
|
-
}
|
|
496
|
+
): PathChange[] =>
|
|
497
|
+
changesBetween(
|
|
498
|
+
base as unknown as Record<string, unknown>,
|
|
499
|
+
work as unknown as Record<string, unknown>,
|
|
500
|
+
);
|
|
495
501
|
|
|
496
502
|
/** Every running mutation, to commit with its code going on. */
|
|
497
503
|
const running = (): Commit[] =>
|
|
@@ -537,13 +543,23 @@ export function runWithCommittedMutations<T>(action: () => T): T {
|
|
|
537
543
|
* between values are kept, which a store update made on a draft does not do.
|
|
538
544
|
* Nothing is committed when `run` throws.
|
|
539
545
|
*/
|
|
540
|
-
export function mutateState(
|
|
546
|
+
export function mutateState(
|
|
547
|
+
run: (work: VariableNamespaces, adopt: Cloner) => void,
|
|
548
|
+
): void {
|
|
541
549
|
// The writes of the code this runs inside come first, and stay even if
|
|
542
550
|
// `run` throws
|
|
543
551
|
commitScopes(running());
|
|
552
|
+
// Where the objects `run` may be handed are: in the state it continues from
|
|
553
|
+
const held = locateObjects(
|
|
554
|
+
namespacesOf(getActiveMutationScope() ?? useStoryStore.getState()),
|
|
555
|
+
[],
|
|
556
|
+
);
|
|
544
557
|
const scope = startScope();
|
|
558
|
+
// Copies values into the working copy, an object it holds already staying
|
|
559
|
+
// that one, and what the values share staying shared
|
|
560
|
+
const adopt = clonerFor(scope.work, held);
|
|
545
561
|
try {
|
|
546
|
-
run(scope.work);
|
|
562
|
+
run(scope.work, adopt);
|
|
547
563
|
} finally {
|
|
548
564
|
activeScopes.pop();
|
|
549
565
|
}
|