@rohal12/spindle 0.59.4 → 0.59.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13244,13 +13244,14 @@ function atomicName(value) {
13244
13244
  * Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
13245
13245
  * are not story state: unless an object holds one as its own property, it
13246
13246
  * reads as missing, as setByPath() treats it. Other inherited properties
13247
- * (class getters, `size` of a Map) are read.
13247
+ * (class getters, `size` of a Map) are read, and so are those of a string,
13248
+ * number or boolean (`length` of a string), as JavaScript boxes them.
13248
13249
  */
13249
13250
  function getByPath(obj, segments) {
13250
13251
  let current = obj;
13251
13252
  for (const seg of segments) {
13252
- if (current == null || typeof current !== "object") return void 0;
13253
- if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
13253
+ if (current == null) return void 0;
13254
+ if (seg in Object.prototype && !hasOwn(Object(current), seg)) return;
13254
13255
  current = current[seg];
13255
13256
  }
13256
13257
  return current;
@@ -13407,14 +13408,21 @@ function deepClone(value, options = {}) {
13407
13408
  if (isTemporal(val)) return val;
13408
13409
  if (val instanceof Error) {
13409
13410
  const copy = keep(val, Object.create(Object.getPrototypeOf(val)));
13410
- for (const key of [...ERROR_HIDDEN_KEYS, "stack"]) if (hasOwn(val, key)) Object.defineProperty(copy, key, {
13411
- value: clone(val[key]),
13412
- writable: true,
13413
- configurable: true
13414
- });
13411
+ for (const key of [...ERROR_HIDDEN_KEYS, "stack"]) if (hasOwn(val, key)) {
13412
+ const held = val[key];
13413
+ Object.defineProperty(copy, key, {
13414
+ value: settled(clone(held), held),
13415
+ writable: true,
13416
+ configurable: true
13417
+ });
13418
+ }
13415
13419
  return copyKeys(val, copy);
13416
13420
  }
13417
13421
  }
13422
+ function settled(copy, original) {
13423
+ const settle = options.settle;
13424
+ return settle && isObjectValue(copy) && isObjectValue(original) ? settle(copy, original) : copy;
13425
+ }
13418
13426
  function clone(val) {
13419
13427
  if (val === null || typeof val !== "object") return val;
13420
13428
  const obj = val;
@@ -13426,12 +13434,12 @@ function deepClone(value, options = {}) {
13426
13434
  }
13427
13435
  if (val instanceof Map) {
13428
13436
  const copy = keep(obj, /* @__PURE__ */ new Map());
13429
- for (const [k, v] of val) copy.set(clone(k), clone(v));
13437
+ for (const [k, v] of val) copy.set(settled(clone(k), k), clone(v));
13430
13438
  return copy;
13431
13439
  }
13432
13440
  if (val instanceof Set) {
13433
13441
  const copy = keep(obj, /* @__PURE__ */ new Set());
13434
- for (const v of val) copy.add(clone(v));
13442
+ for (const v of val) copy.add(settled(clone(v), v));
13435
13443
  return copy;
13436
13444
  }
13437
13445
  const builtin = cloneBuiltin(obj);
@@ -13631,7 +13639,9 @@ function mergesWith(a, b, arrays) {
13631
13639
  * is none either. Values that merge are reported as 'nested', to be
13632
13640
  * compared key by key; they may still be equal. With `arrays`, arrays
13633
13641
  * merge too: elements at the indices both hold are compared, and the
13634
- * elements one has beyond the other's length are added or deleted.
13642
+ * elements one has beyond the other's length are added or deleted. An
13643
+ * index that holds no element (a hole) is no element, not an undefined one,
13644
+ * and holes the length gained at the end are the change of `length`, by how many.
13635
13645
  */
13636
13646
  function keyChanges(before, after, arrays) {
13637
13647
  const changes = [];
@@ -13652,16 +13662,41 @@ function keyChanges(before, after, arrays) {
13652
13662
  if (Array.isArray(after)) {
13653
13663
  const b = before;
13654
13664
  const a = after;
13655
- for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
13656
- for (let i = b.length; i < a.length; i++) changes.push({
13657
- key: String(i),
13658
- kind: "added",
13659
- after: a[i]
13660
- });
13665
+ for (let i = 0; i < Math.min(b.length, a.length); i++) {
13666
+ const key = String(i);
13667
+ if (i in b && i in a) compare(key, b[i], a[i]);
13668
+ else if (i in b) changes.push({
13669
+ key,
13670
+ kind: "deleted",
13671
+ hole: true
13672
+ });
13673
+ else if (i in a) changes.push({
13674
+ key,
13675
+ kind: "added",
13676
+ after: a[i]
13677
+ });
13678
+ }
13679
+ let next = b.length;
13680
+ for (let i = b.length; i < a.length; i++) {
13681
+ if (!(i in a)) continue;
13682
+ const gap = i - next;
13683
+ changes.push({
13684
+ key: String(i),
13685
+ kind: "added",
13686
+ after: a[i],
13687
+ ...gap ? { gap } : {}
13688
+ });
13689
+ next = i + 1;
13690
+ }
13661
13691
  for (let i = a.length; i < b.length; i++) changes.push({
13662
13692
  key: String(i),
13663
13693
  kind: "deleted"
13664
13694
  });
13695
+ if (a.length > next) changes.push({
13696
+ key: "length",
13697
+ kind: "holes",
13698
+ after: a.length - next
13699
+ });
13665
13700
  return changes;
13666
13701
  }
13667
13702
  for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
@@ -13679,26 +13714,45 @@ function keyChanges(before, after, arrays) {
13679
13714
  /**
13680
13715
  * The property paths where `after` differs from `before`, two objects of
13681
13716
  * one class: objects that merge (see isMergeable) are compared property by
13682
- * property, any other value as a whole.
13717
+ * property, any other value as a whole; with `arrays`, arrays are compared
13718
+ * by index too (see keyChanges).
13683
13719
  */
13684
- function diffPaths(before, after) {
13720
+ function diffPaths(before, after, arrays = false) {
13685
13721
  const changes = [];
13686
13722
  const ancestors = /* @__PURE__ */ new Set();
13723
+ const unchanged = /* @__PURE__ */ new WeakMap();
13724
+ let cuts = 0;
13687
13725
  (function walk(b, a, path) {
13688
- if (ancestors.has(a)) return;
13726
+ if (ancestors.has(a)) {
13727
+ cuts++;
13728
+ return;
13729
+ }
13730
+ if (unchanged.get(a)?.has(b)) return;
13689
13731
  ancestors.add(a);
13690
- for (const change of keyChanges(b, a, false)) {
13732
+ const found = changes.length;
13733
+ const cutsBefore = cuts;
13734
+ for (const change of keyChanges(b, a, arrays)) {
13691
13735
  const at = [...path, change.key];
13692
13736
  if (change.kind === "nested") walk(change.before, change.after, at);
13693
13737
  else changes.push(change.kind === "deleted" ? {
13694
13738
  path: at,
13695
- deleted: true
13739
+ deleted: true,
13740
+ ...change.hole ? { hole: true } : {}
13696
13741
  } : {
13697
13742
  path: at,
13698
13743
  deleted: false,
13699
- value: change.after
13744
+ value: change.after,
13745
+ ...change.kind === "added" && Array.isArray(a) && Number(change.key) >= b.length ? {
13746
+ appended: true,
13747
+ ...change.gap ? { gap: change.gap } : {}
13748
+ } : {}
13700
13749
  });
13701
13750
  }
13751
+ if (changes.length === found && cuts === cutsBefore) {
13752
+ const seen = unchanged.get(a) ?? /* @__PURE__ */ new Set();
13753
+ seen.add(b);
13754
+ unchanged.set(a, seen);
13755
+ }
13702
13756
  ancestors.delete(a);
13703
13757
  })(before, after, []);
13704
13758
  return changes;
@@ -13710,34 +13764,103 @@ function underChange(path, keys) {
13710
13764
  return false;
13711
13765
  }
13712
13766
  /**
13767
+ * The segment standing for the entry `index` of a Map or Set in a path, or
13768
+ * for the `cause` or `errors` of an Error ('c', 'e').
13769
+ */
13770
+ var entryKey = (kind, index) => `\0${kind}${index}`;
13771
+ var isEntryKey = (segment) => segment.startsWith("\0");
13772
+ /**
13773
+ * What a Map, Set or Error holds in place of properties (keys and values,
13774
+ * members, `cause` and `errors`), each with the segment standing for it in
13775
+ * a path (see entryKey); undefined for any other value.
13776
+ */
13777
+ function entryChildren(value) {
13778
+ const children = [];
13779
+ if (value instanceof Map) {
13780
+ let index = 0;
13781
+ for (const [k, v] of value) {
13782
+ children.push([entryKey("k", index), k], [entryKey("v", index), v]);
13783
+ index++;
13784
+ }
13785
+ } else if (value instanceof Set) {
13786
+ let index = 0;
13787
+ for (const member of value) children.push([entryKey("s", index++), member]);
13788
+ } else if (value instanceof Error) {
13789
+ for (const key of ["cause", "errors"]) if (hasOwn(value, key)) children.push([entryKey(key === "cause" ? "c" : "e", 0), value[key]]);
13790
+ } else return;
13791
+ return children;
13792
+ }
13793
+ /**
13794
+ * The value at `path` below `root`, where a path may go through the entries
13795
+ * of Maps, Sets and Errors (see objectPaths) as well as properties.
13796
+ */
13797
+ function getByEntryPath(root, path) {
13798
+ let current = root;
13799
+ let start = 0;
13800
+ for (let i = 0; i < path.length; i++) {
13801
+ if (!isEntryKey(path[i])) continue;
13802
+ current = getByPath(current, path.slice(start, i));
13803
+ if (!isObjectValue(current)) return void 0;
13804
+ current = entryChildren(current)?.find(([at]) => at === path[i])?.[1];
13805
+ start = i + 1;
13806
+ }
13807
+ return start === path.length ? current : getByPath(current, path.slice(start));
13808
+ }
13809
+ /**
13810
+ * Write `value` at the path of an object a Map holds as a value (see
13811
+ * objectPaths); other entries are not replaced, as a key or member cannot
13812
+ * change without the entry moving. Returns whether it wrote.
13813
+ */
13814
+ function setEntryValue(root, path, value) {
13815
+ const holder = getByEntryPath(root, path.slice(0, -1));
13816
+ const match = /^\0v(\d+)$/.exec(path[path.length - 1]);
13817
+ if (!(holder instanceof Map) || !match) return false;
13818
+ const key = [...holder.keys()][Number(match[1])];
13819
+ holder.set(key, value);
13820
+ return true;
13821
+ }
13822
+ /**
13713
13823
  * Every path each object (or array, or other value) of `root` is at, the
13714
13824
  * first appearance first, in depth-first order. Plain objects and arrays are
13715
13825
  * entered, where first met only (an object held in an array is one object,
13716
13826
  * however it was reached); other values are leaves. A path at or below one
13717
13827
  * of `skip` is left out.
13828
+ *
13829
+ * With `entries`, the objects held in a Map or Set (as key, value or member)
13830
+ * are placed too, after everything else: one met nowhere else is at the path
13831
+ * of its Map or Set and the entry's segment (see entryKey), which is no path
13832
+ * a value can be read at (see getByEntryPath), so only to tell where an
13833
+ * object is from; so are the `cause` and `errors` of an Error.
13718
13834
  */
13719
- function objectPaths(root, skip = /* @__PURE__ */ new Set()) {
13835
+ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13720
13836
  const all = /* @__PURE__ */ new Map();
13721
- (function walk(node, path) {
13837
+ const collections = [];
13838
+ function place(value, at) {
13839
+ const known = all.get(value);
13840
+ if (known) {
13841
+ known.push(at);
13842
+ return;
13843
+ }
13844
+ all.set(value, [at]);
13845
+ if (isMergeable(value) || Array.isArray(value)) walk(value, at);
13846
+ else if (entries && entryChildren(value)) collections.push([value, at]);
13847
+ }
13848
+ function walk(node, path) {
13722
13849
  for (const key of Object.keys(node)) {
13723
13850
  const value = node[key];
13724
13851
  if (!isObjectValue(value)) continue;
13725
13852
  const at = [...path, key];
13726
13853
  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);
13854
+ place(value, at);
13734
13855
  }
13735
- })(root, []);
13856
+ }
13857
+ walk(root, []);
13858
+ for (const [collection, path] of collections) for (const [segment, value] of entryChildren(collection)) if (isObjectValue(value)) place(value, [...path, segment]);
13736
13859
  return all;
13737
13860
  }
13738
13861
  /** 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]]));
13862
+ function firstPaths(root, skip, entries) {
13863
+ return new Map([...objectPaths(root, skip, entries)].map(([object, paths]) => [object, paths[0]]));
13741
13864
  }
13742
13865
  /**
13743
13866
  * The objects `root` holds at more than one path (an object two variables
@@ -13745,7 +13868,44 @@ function firstPaths(root, skip) {
13745
13868
  * parents before children.
13746
13869
  */
13747
13870
  function sharedPaths(root) {
13748
- return [...objectPaths(root).values()].filter((paths) => paths.length > 1);
13871
+ return [...objectPaths(root, void 0, true).values()].filter((paths) => paths.length > 1);
13872
+ }
13873
+ /**
13874
+ * Whether `y`, which takes the place of `x` at `at` (the first appearance of
13875
+ * both), holds an object elsewhere than `x` did, below an entry of a Map, Set
13876
+ * or Error (see aliasChanges): `$map.set("k", $a)` over an equal object, or
13877
+ * `$map.get("k").child = $a`. Entries are compared by position, and only
13878
+ * where both hold as many; other differences are changes of content. Below
13879
+ * an entry, objects that merge are compared key by key.
13880
+ */
13881
+ function aliasMoved(x, y, at, earlier, later) {
13882
+ const pairs = [];
13883
+ const xs = entryChildren(x);
13884
+ const ys = entryChildren(y);
13885
+ if (xs && ys) {
13886
+ if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
13887
+ if (x instanceof Map || x instanceof Set) {
13888
+ if (x.size !== y.size) return false;
13889
+ }
13890
+ const held = new Map(ys);
13891
+ for (const [segment, a] of xs) if (held.has(segment)) pairs.push([
13892
+ segment,
13893
+ a,
13894
+ held.get(segment)
13895
+ ]);
13896
+ } else if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true)) {
13897
+ for (const key of Object.keys(y)) if (hasOwn(x, key)) pairs.push([
13898
+ key,
13899
+ x[key],
13900
+ y[key]
13901
+ ]);
13902
+ }
13903
+ return pairs.some(([segment, a, b]) => {
13904
+ if (!isObjectValue(a) || !isObjectValue(b)) return false;
13905
+ const here = [...at, segment];
13906
+ const isAt = later.get(b);
13907
+ return pathKey(earlier.get(a)) !== pathKey(isAt) || pathKey(isAt) === pathKey(here) && aliasMoved(a, b, here, earlier, later);
13908
+ });
13749
13909
  }
13750
13910
  /**
13751
13911
  * The paths whose object is another one in the references of `after` than in
@@ -13756,8 +13916,8 @@ function sharedPaths(root) {
13756
13916
  * `after` holds.
13757
13917
  */
13758
13918
  function aliasChanges(before, after) {
13759
- const earlier = firstPaths(before);
13760
- const later = firstPaths(after);
13919
+ const earlier = firstPaths(before, void 0, true);
13920
+ const later = firstPaths(after, void 0, true);
13761
13921
  const changes = [];
13762
13922
  (function walk(b, a, path) {
13763
13923
  for (const key of Object.keys(a)) {
@@ -13775,17 +13935,41 @@ function aliasChanges(before, after) {
13775
13935
  value: y,
13776
13936
  alias: true
13777
13937
  });
13938
+ else if (pathKey(isAt) === k && entryChildren(x) && aliasMoved(x, y, at, earlier, later)) changes.push({
13939
+ path: at,
13940
+ deleted: false,
13941
+ value: y,
13942
+ alias: true
13943
+ });
13778
13944
  if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
13779
13945
  }
13780
13946
  })(before, after, []);
13781
13947
  return changes;
13782
13948
  }
13783
13949
  /**
13950
+ * The changes from `before` to `after`, as paths below them: the property
13951
+ * paths that differ (see diffPaths), and those that hold equal content but
13952
+ * another object than they did (see aliasChanges), those first. Compared as
13953
+ * one object, so an object that two of their keys share is one object,
13954
+ * whichever of them it is reached through.
13955
+ */
13956
+ function changesBetween(before, after, arrays = false) {
13957
+ const written = diffPaths(before, after, arrays);
13958
+ const keys = new Set(written.map((c) => pathKey(c.path)));
13959
+ const aliases = [];
13960
+ for (const change of aliasChanges(before, after)) {
13961
+ if (underChange(change.path, keys)) continue;
13962
+ keys.add(pathKey(change.path));
13963
+ aliases.push(change);
13964
+ }
13965
+ return [...aliases, ...written];
13966
+ }
13967
+ /**
13784
13968
  * Where the objects of `source` are, other than at or below the paths of
13785
13969
  * `changes` (which write something new there), for `existingObjects`.
13786
13970
  */
13787
13971
  function locateObjects(source, changes) {
13788
- return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))));
13972
+ return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))), true);
13789
13973
  }
13790
13974
  /**
13791
13975
  * A map for `deepClone`'s `seen`: an object of the source `paths` locate
@@ -13800,7 +13984,7 @@ function existingObjects(paths, target) {
13800
13984
  if (super.has(obj)) return true;
13801
13985
  const path = paths.get(obj);
13802
13986
  if (!path) return false;
13803
- const found = getByPath(target, path);
13987
+ const found = getByEntryPath(target, path);
13804
13988
  if (!(isObjectValue(found) && (Array.isArray(found) ? Array.isArray(obj) : Object.getPrototypeOf(found) === Object.getPrototypeOf(obj)))) return false;
13805
13989
  super.set(obj, found);
13806
13990
  return true;
@@ -13815,32 +13999,20 @@ function isApplied(target, change) {
13815
13999
  if (!hasOwn(parent, key)) return change.deleted;
13816
14000
  return !change.deleted && deepEqual(parent[key], change.value);
13817
14001
  }
14002
+ /**
14003
+ * Make `path` below `root` hold `object` (an object that `sharedPaths` found
14004
+ * at several paths): a property is written like setByPath() does, a Map's
14005
+ * value replaced; an entry that is a key or member is left as it is.
14006
+ */
14007
+ function relinkPath(root, path, object) {
14008
+ if (path.some(isEntryKey)) setEntryValue(root, path, object);
14009
+ else setByPath(root, path, object);
14010
+ }
13818
14011
  function applyChange(target, change) {
13819
14012
  const root = target;
13820
14013
  if (change.deleted) deleteByPath(root, change.path);
13821
14014
  else setByPath(root, change.path, change.value);
13822
14015
  }
13823
- /**
13824
- * Write how `after` differs from `before` (see keyChanges; arrays merge by
13825
- * index) into `target`, another version of the same object or array.
13826
- * Values that changed are written whole, as deep copies; for values that
13827
- * merge, `mergeNested` may merge them into `target` instead, returning
13828
- * whether it did. Into an array, elements removed from the end are removed
13829
- * at the same indices and elements added are appended, and changes at
13830
- * indices `target` lacks are dropped: where `target` was resized, its
13831
- * indices do not line up with `before`'s.
13832
- */
13833
- function mergeKeys(target, before, after, mergeNested) {
13834
- const list = Array.isArray(target) ? target : void 0;
13835
- for (const change of keyChanges(before, after, true)) {
13836
- const { key } = change;
13837
- if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
13838
- else delete target[key];
13839
- else if (list && change.kind === "added") list.push(deepClone(change.after));
13840
- else if (list && Number(key) >= list.length) continue;
13841
- else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
13842
- }
13843
- }
13844
14016
  //#endregion
13845
14017
  //#region src/event-emitter.ts
13846
14018
  var VALID_EVENTS = new Set([
@@ -14600,9 +14772,18 @@ var cloneValue = (value) => deepClone(value, { keepUnregistered: true });
14600
14772
  */
14601
14773
  function clonerFor(target, paths) {
14602
14774
  const seen = paths ? existingObjects(paths, target) : /* @__PURE__ */ new Map();
14775
+ const settle = (copy, original) => {
14776
+ const path = paths?.get(original);
14777
+ if (!path || !isDraft(copy)) return copy;
14778
+ const plain = current(copy);
14779
+ if (!path.some(isEntryKey)) setByPath(target, path, plain);
14780
+ seen.set(original, plain);
14781
+ return plain;
14782
+ };
14603
14783
  return (value) => deepClone(value, {
14604
14784
  keepUnregistered: true,
14605
- seen
14785
+ seen,
14786
+ settle
14606
14787
  });
14607
14788
  }
14608
14789
  /** One cloner per target copy, made when first used. */
@@ -14751,7 +14932,8 @@ function routeStoreUpdate(recipe) {
14751
14932
  path: [ns, ...change.path]
14752
14933
  })) : [];
14753
14934
  });
14754
- const cloners = clonersFor();
14935
+ const held = locateObjects(namespacesOf(inner.work), changes);
14936
+ const cloners = clonersFor(() => held);
14755
14937
  const owned = (change, target) => change.deleted ? change : {
14756
14938
  ...change,
14757
14939
  value: cloners(target)(change.value)
@@ -14788,9 +14970,9 @@ function relink(draft, work, changes) {
14788
14970
  const prefixes = [first, ...others].map((p) => pathKey(p).slice(0, -1));
14789
14971
  if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
14790
14972
  try {
14791
- const object = getByPath(draft, first);
14973
+ const object = getByEntryPath(draft, first);
14792
14974
  if (object === null || typeof object !== "object") continue;
14793
- for (const path of others) setByPath(draft, path, object);
14975
+ for (const path of others) relinkPath(draft, path, object);
14794
14976
  } catch {}
14795
14977
  }
14796
14978
  }
@@ -14806,6 +14988,12 @@ function cloneNamespaces(from) {
14806
14988
  transient: deepClone(from.transient, { seen })
14807
14989
  };
14808
14990
  }
14991
+ /** The three variable namespaces of `state`, as an object to walk. */
14992
+ var namespacesOf = (state) => ({
14993
+ variables: state.variables,
14994
+ temporary: state.temporary,
14995
+ transient: state.transient
14996
+ });
14809
14997
  /**
14810
14998
  * Commit the property paths each mutation's code changed (work vs base) on
14811
14999
  * top of the current store state, so writes made elsewhere during execution
@@ -14870,24 +15058,9 @@ function commitScopes(commits) {
14870
15058
  }
14871
15059
  /**
14872
15060
  * 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
- }
15061
+ * 'a']`; see changesBetween).
15062
+ */
15063
+ var changesOf = (base, work) => changesBetween(base, work);
14891
15064
  /** Every running mutation, to commit with its code going on. */
14892
15065
  var running = () => activeScopes.map((scope) => ({
14893
15066
  scope,
@@ -14896,12 +15069,26 @@ var running = () => activeScopes.map((scope) => ({
14896
15069
  /**
14897
15070
  * Bring a suspended mutation's copies up to the store after an action
14898
15071
  * replaced or changed state under it (navigation clears temporaries, back
14899
- * and restart replace variables). Roots that still match keep the objects
14900
- * the code may hold.
15072
+ * and restart replace variables). What the action changed (see
15073
+ * changesBetween) is written into the code's working copy, which keeps the
15074
+ * objects the code may hold where nothing changed, and the references among
15075
+ * the values written, and to the objects it holds already, as a commit does.
14901
15076
  */
14902
15077
  function resync(scope) {
14903
15078
  const state = useStoryStore.getState();
14904
- for (const ns of NAMESPACES) mergeKeys(scope.work[ns], scope.work[ns], state[ns]);
15079
+ const now = namespacesOf(state);
15080
+ const changes = changesBetween(scope.base, now);
15081
+ const copy = clonerFor(scope.work, locateObjects(now, changes));
15082
+ for (const change of changes) try {
15083
+ applyChange(scope.work, change.deleted ? change : {
15084
+ ...change,
15085
+ value: copy(change.value)
15086
+ });
15087
+ } catch {
15088
+ const [ns, root] = change.path;
15089
+ if (hasOwn(now[ns], root)) scope.work[ns][root] = copy(now[ns][root]);
15090
+ else delete scope.work[ns][root];
15091
+ }
14905
15092
  scope.base = cloneNamespaces(state);
14906
15093
  }
14907
15094
  /**
@@ -14930,9 +15117,11 @@ function runWithCommittedMutations(action) {
14930
15117
  */
14931
15118
  function mutateState(run) {
14932
15119
  commitScopes(running());
15120
+ const held = locateObjects(namespacesOf(getActiveMutationScope() ?? useStoryStore.getState()), []);
14933
15121
  const scope = startScope();
15122
+ const adopt = clonerFor(scope.work, held);
14934
15123
  try {
14935
- run(scope.work);
15124
+ run(scope.work, adopt);
14936
15125
  } finally {
14937
15126
  activeScopes.pop();
14938
15127
  }
@@ -14963,7 +15152,11 @@ function executeMutation(code, mergedLocals, scopeUpdate) {
14963
15152
  scope,
14964
15153
  keepRunning: false
14965
15154
  }]);
14966
- for (const key of Object.keys(localsClone)) if (!deepEqual(localsClone[key], mergedLocals[key])) scopeUpdate(key, localsClone[key]);
15155
+ const changed = new Set(changesBetween(mergedLocals, localsClone, true).map((c) => c.path[0]));
15156
+ for (const group of sharedPaths(localsClone)) if (group.some((path) => changed.has(path[0]))) {
15157
+ for (const path of group) if (localsClone[path[0]] !== mergedLocals[path[0]]) changed.add(path[0]);
15158
+ }
15159
+ for (const key of Object.keys(localsClone)) if (changed.has(key)) scopeUpdate(key, localsClone[key]);
14967
15160
  for (const key of Object.keys(mergedLocals)) if (!hasOwn(localsClone, key)) scopeUpdate(key, void 0);
14968
15161
  }
14969
15162
  //#endregion
@@ -15687,35 +15880,65 @@ function loadedEntryMoment(payload) {
15687
15880
  * the `beforesave` hooks) into the payload's snapshot of the saved moment. A
15688
15881
  * load restores that snapshot, the state on entering the passage, and runs
15689
15882
  * 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).
15883
+ * on load (#227). Only the property paths the hooks changed are written (see
15884
+ * changesBetween): a whole variable would bring along what the passage did
15885
+ * to the rest of it, which the passage then does again on load (#232). The
15886
+ * references the hooks made stay: an object they put in two variables is one
15887
+ * object in the snapshot, and one they made another variable refer to is the
15888
+ * snapshot's own object of it (#302). Only the payload's copy changes: the
15889
+ * live history keeps the recorded snapshot (#159).
15695
15890
  */
15696
15891
  function keepHookWrites(payload, before, after) {
15697
15892
  const moment = payload.history[payload.historyIndex];
15698
15893
  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);
15894
+ const changes = changesBetween(before, after, true);
15895
+ if (changes.length === 0) return;
15896
+ const work = deepClone(moment.variables);
15897
+ const seen = existingObjects(locateObjects(after, changes), work);
15898
+ const own = (value) => deepClone(value, { seen });
15899
+ for (const change of changes) writeHookChange(work, after, change, own);
15900
+ moment.variables = shareEqual(moment.variables, work);
15901
+ }
15902
+ /**
15903
+ * Write `change` (of the hooks, to `after`) into `work`. The value goes in at
15904
+ * the first place `work` lacks the objects on the path of one of the kind
15905
+ * `after` holds (the passage created or replaced it): there, the hooks' whole
15906
+ * value of it is written. Into an array, elements removed from the end are
15907
+ * removed at the same indices and elements added are appended; changes at
15908
+ * indices the array lacks are dropped (it was resized, so its indices do not
15909
+ * line up with the live ones).
15910
+ */
15911
+ function writeHookChange(work, after, change, own) {
15912
+ const { path } = change;
15913
+ let holder = work;
15914
+ let depth = 0;
15915
+ for (; depth < path.length - 1; depth++) {
15916
+ const key = path[depth];
15917
+ const child = hasOwn(holder, key) ? holder[key] : void 0;
15918
+ const held = getByPath(after, path.slice(0, depth + 1));
15919
+ if (!mergesWith(child, held, true)) {
15920
+ writeKey(holder, key, own(held));
15921
+ return;
15922
+ }
15923
+ holder = child;
15924
+ }
15925
+ const key = path[depth];
15926
+ if (change.deleted) writeKey(holder, key, void 0, change);
15927
+ else writeKey(holder, key, own(change.value), change);
15928
+ }
15929
+ function writeKey(holder, key, value, change) {
15930
+ const deleted = change?.deleted ?? false;
15931
+ if (Array.isArray(holder)) {
15932
+ const index = Number(key);
15933
+ if (deleted) if (change && "hole" in change) delete holder[index];
15934
+ else holder.length = Math.min(holder.length, index);
15935
+ else if (change && "appended" in change) {
15936
+ holder.length += change.gap ?? 0;
15937
+ holder.push(value);
15938
+ } else if (key === "length" && change) holder.length += value;
15939
+ else if (index < holder.length) holder[index] = value;
15940
+ } else if (deleted) delete holder[key];
15941
+ else setOwn(holder, key, value);
15719
15942
  }
15720
15943
  /** Restore the PRNG from a snapshot, or reset it without one. */
15721
15944
  function restorePRNGFrom(prng) {
@@ -17289,8 +17512,8 @@ function defineMacro(config, source = "builtin") {
17289
17512
  ctx.value = useStoryStore((s) => getByPath(s.variables, segments));
17290
17513
  ctx.getValue = () => getByPath(readState().variables, segments);
17291
17514
  ctx.setValue = (value) => {
17292
- useStoryStore.setState((state) => {
17293
- setByPath(state.variables, segments, value, { createMissing: true });
17515
+ mutateState((work, adopt) => {
17516
+ setByPath(work.variables, segments, adopt(value), { createMissing: true });
17294
17517
  });
17295
17518
  };
17296
17519
  }
@@ -17432,8 +17655,8 @@ function createStoryAPI() {
17432
17655
  warnIfUndeclared(isTransient, key);
17433
17656
  }
17434
17657
  if (entries.some(([k]) => k.includes("."))) {
17435
- mutateState((work) => {
17436
- for (const [k, v] of entries) setOne(work, k, v);
17658
+ mutateState((work, adopt) => {
17659
+ for (const [k, v] of entries) setOne(work, k, adopt(v));
17437
17660
  });
17438
17661
  return;
17439
17662
  }
@@ -19895,7 +20118,8 @@ defineMacro({
19895
20118
  return useRunOnce("unset", rawArgs, () => {
19896
20119
  const state = useStoryStore.getState();
19897
20120
  const name = ctx.args.variable ?? "";
19898
- if (name.startsWith("$")) state.deleteVariable(name.slice(1));
20121
+ if (/^[$_%@][^.]*\./.test(name)) ctx.mutate(`delete ${name}`);
20122
+ else if (name.startsWith("$")) state.deleteVariable(name.slice(1));
19899
20123
  else if (name.startsWith("_")) state.deleteTemporary(name.slice(1));
19900
20124
  else if (name.startsWith("%")) state.deleteTransient(name.slice(1));
19901
20125
  else if (name.startsWith("@")) ctx.update(name.slice(1), void 0);