@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.
@@ -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) copy.set(clone(k), clone(v));
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, false)) {
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
- (function walk(node, path) {
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
- const known = all.get(value);
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
- })(root, []);
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 cloners = clonersFor();
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']`): the property paths that differ (see diffPaths), and those that hold
14874
- * equal content but another object than they did (see aliasChanges), those
14875
- * first. Compared as one object, so an object the namespaces share is one
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 (into
15691
- * copies of the objects on those paths, the rest staying shared): a
15692
- * whole variable would bring along what the passage did to the rest of it,
15693
- * which the passage then does again on load (#232). Only the payload's copy
15694
- * changes: the live history keeps the recorded snapshot (#159).
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
- mergeHookWrites(moment.variables, before, after, /* @__PURE__ */ new Set());
15700
- }
15701
- /**
15702
- * Apply the hooks' changes between `before` and `after`, two objects or two
15703
- * arrays, to the snapshot's copy of them, `target` (see mergeKeys: array
15704
- * elements merge by index). A changed value is merged key by key where the
15705
- * snapshot holds the same kind of value. Where it lacks the key or holds
15706
- * another kind (the passage created or replaced it), the hooks' whole value
15707
- * is written.
15708
- */
15709
- function mergeHookWrites(target, before, after, ancestors) {
15710
- ancestors.add(after);
15711
- mergeKeys(target, before, after, (key, b, a) => {
15712
- if (!hasOwn(target, key) || !mergesWith(a, target[key], true) || ancestors.has(a)) return false;
15713
- const copy = shallowCopy(target[key]);
15714
- target[key] = copy;
15715
- mergeHookWrites(copy, b, a, ancestors);
15716
- return true;
15717
- });
15718
- ancestors.delete(after);
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
- useStoryStore.setState((state) => {
17293
- setByPath(state.variables, segments, value, { createMissing: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.59.4",
3
+ "version": "0.59.5",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -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
- useStoryStore.setState((state) => {
232
- setByPath(state.variables, segments, value, {
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
  });
@@ -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
- aliasChanges,
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
- return (value) => deepClone(value, { keepUnregistered: true, seen });
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
- const cloners = clonersFor();
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']`): the property paths that differ (see diffPaths), and those that hold
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
- function changesOf(
493
+ const changesOf = (
479
494
  base: VariableNamespaces,
480
495
  work: VariableNamespaces,
481
- ): PathChange[] {
482
- const before = base as unknown as Record<string, unknown>;
483
- const after = work as unknown as Record<string, unknown>;
484
- const written = diffPaths(before, after);
485
- const keys = new Set(written.map((c) => pathKey(c.path)));
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(run: (work: VariableNamespaces) => void): void {
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
  }