@rohal12/spindle 0.59.5 → 0.59.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,16 +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) {
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
- }
13437
+ for (const [k, v] of val) copy.set(settled(clone(k), k), clone(v));
13434
13438
  return copy;
13435
13439
  }
13436
13440
  if (val instanceof Set) {
13437
13441
  const copy = keep(obj, /* @__PURE__ */ new Set());
13438
- for (const v of val) copy.add(clone(v));
13442
+ for (const v of val) copy.add(settled(clone(v), v));
13439
13443
  return copy;
13440
13444
  }
13441
13445
  const builtin = cloneBuiltin(obj);
@@ -13635,7 +13639,9 @@ function mergesWith(a, b, arrays) {
13635
13639
  * is none either. Values that merge are reported as 'nested', to be
13636
13640
  * compared key by key; they may still be equal. With `arrays`, arrays
13637
13641
  * merge too: elements at the indices both hold are compared, and the
13638
- * 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.
13639
13645
  */
13640
13646
  function keyChanges(before, after, arrays) {
13641
13647
  const changes = [];
@@ -13656,16 +13662,41 @@ function keyChanges(before, after, arrays) {
13656
13662
  if (Array.isArray(after)) {
13657
13663
  const b = before;
13658
13664
  const a = after;
13659
- for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
13660
- for (let i = b.length; i < a.length; i++) changes.push({
13661
- key: String(i),
13662
- kind: "added",
13663
- after: a[i]
13664
- });
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
+ }
13665
13691
  for (let i = a.length; i < b.length; i++) changes.push({
13666
13692
  key: String(i),
13667
13693
  kind: "deleted"
13668
13694
  });
13695
+ if (a.length > next) changes.push({
13696
+ key: "length",
13697
+ kind: "holes",
13698
+ after: a.length - next
13699
+ });
13669
13700
  return changes;
13670
13701
  }
13671
13702
  for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
@@ -13689,22 +13720,39 @@ function keyChanges(before, after, arrays) {
13689
13720
  function diffPaths(before, after, arrays = false) {
13690
13721
  const changes = [];
13691
13722
  const ancestors = /* @__PURE__ */ new Set();
13723
+ const unchanged = /* @__PURE__ */ new WeakMap();
13724
+ let cuts = 0;
13692
13725
  (function walk(b, a, path) {
13693
- if (ancestors.has(a)) return;
13726
+ if (ancestors.has(a)) {
13727
+ cuts++;
13728
+ return;
13729
+ }
13730
+ if (unchanged.get(a)?.has(b)) return;
13694
13731
  ancestors.add(a);
13732
+ const found = changes.length;
13733
+ const cutsBefore = cuts;
13695
13734
  for (const change of keyChanges(b, a, arrays)) {
13696
13735
  const at = [...path, change.key];
13697
13736
  if (change.kind === "nested") walk(change.before, change.after, at);
13698
13737
  else changes.push(change.kind === "deleted" ? {
13699
13738
  path: at,
13700
- deleted: true
13739
+ deleted: true,
13740
+ ...change.hole ? { hole: true } : {}
13701
13741
  } : {
13702
13742
  path: at,
13703
13743
  deleted: false,
13704
13744
  value: change.after,
13705
- ...change.kind === "added" && Array.isArray(a) ? { appended: true } : {}
13745
+ ...change.kind === "added" && Array.isArray(a) && Number(change.key) >= b.length ? {
13746
+ appended: true,
13747
+ ...change.gap ? { gap: change.gap } : {}
13748
+ } : {}
13706
13749
  });
13707
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
+ }
13708
13756
  ancestors.delete(a);
13709
13757
  })(before, after, []);
13710
13758
  return changes;
@@ -13715,8 +13763,62 @@ function underChange(path, keys) {
13715
13763
  for (let n = 1; n <= path.length; n++) if (keys.has(pathKey(path.slice(0, n)))) return true;
13716
13764
  return false;
13717
13765
  }
13718
- /** The segment standing for the entry `index` of a Map or Set in a path. */
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
+ */
13719
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
+ }
13720
13822
  /**
13721
13823
  * Every path each object (or array, or other value) of `root` is at, the
13722
13824
  * first appearance first, in depth-first order. Plain objects and arrays are
@@ -13727,7 +13829,8 @@ var entryKey = (kind, index) => `\0${kind}${index}`;
13727
13829
  * With `entries`, the objects held in a Map or Set (as key, value or member)
13728
13830
  * are placed too, after everything else: one met nowhere else is at the path
13729
13831
  * 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.
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.
13731
13834
  */
13732
13835
  function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13733
13836
  const all = /* @__PURE__ */ new Map();
@@ -13740,7 +13843,7 @@ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13740
13843
  }
13741
13844
  all.set(value, [at]);
13742
13845
  if (isMergeable(value) || Array.isArray(value)) walk(value, at);
13743
- else if (entries && (value instanceof Map || value instanceof Set)) collections.push([value, at]);
13846
+ else if (entries && entryChildren(value)) collections.push([value, at]);
13744
13847
  }
13745
13848
  function walk(node, path) {
13746
13849
  for (const key of Object.keys(node)) {
@@ -13752,15 +13855,7 @@ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13752
13855
  }
13753
13856
  }
13754
13857
  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
- }
13858
+ for (const [collection, path] of collections) for (const [segment, value] of entryChildren(collection)) if (isObjectValue(value)) place(value, [...path, segment]);
13764
13859
  return all;
13765
13860
  }
13766
13861
  /** Where each object of `root` first appears (see objectPaths). */
@@ -13773,27 +13868,44 @@ function firstPaths(root, skip, entries) {
13773
13868
  * parents before children.
13774
13869
  */
13775
13870
  function sharedPaths(root) {
13776
- return [...objectPaths(root).values()].filter((paths) => paths.length > 1);
13777
- }
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;
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
+ ]);
13795
13902
  }
13796
- return false;
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
+ });
13797
13909
  }
13798
13910
  /**
13799
13911
  * The paths whose object is another one in the references of `after` than in
@@ -13823,7 +13935,7 @@ function aliasChanges(before, after) {
13823
13935
  value: y,
13824
13936
  alias: true
13825
13937
  });
13826
- else if (pathKey(isAt) === k && entryAliasChanged(x, y, earlier, later)) changes.push({
13938
+ else if (pathKey(isAt) === k && entryChildren(x) && aliasMoved(x, y, at, earlier, later)) changes.push({
13827
13939
  path: at,
13828
13940
  deleted: false,
13829
13941
  value: y,
@@ -13857,7 +13969,7 @@ function changesBetween(before, after, arrays = false) {
13857
13969
  * `changes` (which write something new there), for `existingObjects`.
13858
13970
  */
13859
13971
  function locateObjects(source, changes) {
13860
- return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))));
13972
+ return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))), true);
13861
13973
  }
13862
13974
  /**
13863
13975
  * A map for `deepClone`'s `seen`: an object of the source `paths` locate
@@ -13872,7 +13984,7 @@ function existingObjects(paths, target) {
13872
13984
  if (super.has(obj)) return true;
13873
13985
  const path = paths.get(obj);
13874
13986
  if (!path) return false;
13875
- const found = getByPath(target, path);
13987
+ const found = getByEntryPath(target, path);
13876
13988
  if (!(isObjectValue(found) && (Array.isArray(found) ? Array.isArray(obj) : Object.getPrototypeOf(found) === Object.getPrototypeOf(obj)))) return false;
13877
13989
  super.set(obj, found);
13878
13990
  return true;
@@ -13887,32 +13999,20 @@ function isApplied(target, change) {
13887
13999
  if (!hasOwn(parent, key)) return change.deleted;
13888
14000
  return !change.deleted && deepEqual(parent[key], change.value);
13889
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
+ }
13890
14011
  function applyChange(target, change) {
13891
14012
  const root = target;
13892
14013
  if (change.deleted) deleteByPath(root, change.path);
13893
14014
  else setByPath(root, change.path, change.value);
13894
14015
  }
13895
- /**
13896
- * Write how `after` differs from `before` (see keyChanges; arrays merge by
13897
- * index) into `target`, another version of the same object or array.
13898
- * Values that changed are written whole, as deep copies; for values that
13899
- * merge, `mergeNested` may merge them into `target` instead, returning
13900
- * whether it did. Into an array, elements removed from the end are removed
13901
- * at the same indices and elements added are appended, and changes at
13902
- * indices `target` lacks are dropped: where `target` was resized, its
13903
- * indices do not line up with `before`'s.
13904
- */
13905
- function mergeKeys(target, before, after, mergeNested) {
13906
- const list = Array.isArray(target) ? target : void 0;
13907
- for (const change of keyChanges(before, after, true)) {
13908
- const { key } = change;
13909
- if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
13910
- else delete target[key];
13911
- else if (list && change.kind === "added") list.push(deepClone(change.after));
13912
- else if (list && Number(key) >= list.length) continue;
13913
- else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
13914
- }
13915
- }
13916
14016
  //#endregion
13917
14017
  //#region src/event-emitter.ts
13918
14018
  var VALID_EVENTS = new Set([
@@ -14672,18 +14772,18 @@ var cloneValue = (value) => deepClone(value, { keepUnregistered: true });
14672
14772
  */
14673
14773
  function clonerFor(target, paths) {
14674
14774
  const seen = paths ? existingObjects(paths, target) : /* @__PURE__ */ new Map();
14675
- const mapKey = (copy, original) => {
14775
+ const settle = (copy, original) => {
14676
14776
  const path = paths?.get(original);
14677
14777
  if (!path || !isDraft(copy)) return copy;
14678
14778
  const plain = current(copy);
14679
- setByPath(target, path, plain);
14779
+ if (!path.some(isEntryKey)) setByPath(target, path, plain);
14680
14780
  seen.set(original, plain);
14681
14781
  return plain;
14682
14782
  };
14683
14783
  return (value) => deepClone(value, {
14684
14784
  keepUnregistered: true,
14685
14785
  seen,
14686
- mapKey
14786
+ settle
14687
14787
  });
14688
14788
  }
14689
14789
  /** One cloner per target copy, made when first used. */
@@ -14870,9 +14970,9 @@ function relink(draft, work, changes) {
14870
14970
  const prefixes = [first, ...others].map((p) => pathKey(p).slice(0, -1));
14871
14971
  if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
14872
14972
  try {
14873
- const object = getByPath(draft, first);
14973
+ const object = getByEntryPath(draft, first);
14874
14974
  if (object === null || typeof object !== "object") continue;
14875
- for (const path of others) setByPath(draft, path, object);
14975
+ for (const path of others) relinkPath(draft, path, object);
14876
14976
  } catch {}
14877
14977
  }
14878
14978
  }
@@ -14969,12 +15069,26 @@ var running = () => activeScopes.map((scope) => ({
14969
15069
  /**
14970
15070
  * Bring a suspended mutation's copies up to the store after an action
14971
15071
  * replaced or changed state under it (navigation clears temporaries, back
14972
- * and restart replace variables). Roots that still match keep the objects
14973
- * 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.
14974
15076
  */
14975
15077
  function resync(scope) {
14976
15078
  const state = useStoryStore.getState();
14977
- 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
+ }
14978
15092
  scope.base = cloneNamespaces(state);
14979
15093
  }
14980
15094
  /**
@@ -15038,7 +15152,11 @@ function executeMutation(code, mergedLocals, scopeUpdate) {
15038
15152
  scope,
15039
15153
  keepRunning: false
15040
15154
  }]);
15041
- 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]);
15042
15160
  for (const key of Object.keys(mergedLocals)) if (!hasOwn(localsClone, key)) scopeUpdate(key, void 0);
15043
15161
  }
15044
15162
  //#endregion
@@ -15799,20 +15917,25 @@ function writeHookChange(work, after, change, own) {
15799
15917
  const child = hasOwn(holder, key) ? holder[key] : void 0;
15800
15918
  const held = getByPath(after, path.slice(0, depth + 1));
15801
15919
  if (!mergesWith(child, held, true)) {
15802
- writeKey(holder, key, false, own(held));
15920
+ writeKey(holder, key, own(held));
15803
15921
  return;
15804
15922
  }
15805
15923
  holder = child;
15806
15924
  }
15807
15925
  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);
15926
+ if (change.deleted) writeKey(holder, key, void 0, change);
15927
+ else writeKey(holder, key, own(change.value), change);
15810
15928
  }
15811
- function writeKey(holder, key, deleted, value, appended) {
15929
+ function writeKey(holder, key, value, change) {
15930
+ const deleted = change?.deleted ?? false;
15812
15931
  if (Array.isArray(holder)) {
15813
15932
  const index = Number(key);
15814
- if (deleted) holder.length = Math.min(holder.length, index);
15815
- else if (appended) holder.push(value);
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;
15816
15939
  else if (index < holder.length) holder[index] = value;
15817
15940
  } else if (deleted) delete holder[key];
15818
15941
  else setOwn(holder, key, value);
@@ -19995,7 +20118,8 @@ defineMacro({
19995
20118
  return useRunOnce("unset", rawArgs, () => {
19996
20119
  const state = useStoryStore.getState();
19997
20120
  const name = ctx.args.variable ?? "";
19998
- 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));
19999
20123
  else if (name.startsWith("_")) state.deleteTemporary(name.slice(1));
20000
20124
  else if (name.startsWith("%")) state.deleteTransient(name.slice(1));
20001
20125
  else if (name.startsWith("@")) ctx.update(name.slice(1), void 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.59.5",
3
+ "version": "0.59.6",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -10,7 +10,11 @@ defineMacro({
10
10
  const state = useStoryStore.getState();
11
11
  const name = ctx.args.variable ?? '';
12
12
 
13
- if (name.startsWith('$')) {
13
+ if (/^[$_%@][^.]*\./.test(name)) {
14
+ // A field of a variable: delete it as mutation code does, which
15
+ // resolves the path in every namespace
16
+ ctx.mutate(`delete ${name}`);
17
+ } else if (name.startsWith('$')) {
14
18
  state.deleteVariable(name.slice(1));
15
19
  } else if (name.startsWith('_')) {
16
20
  state.deleteTemporary(name.slice(1));
@@ -13,12 +13,13 @@ import {
13
13
  changesBetween,
14
14
  applyChange,
15
15
  deepClone,
16
- deepEqual,
17
16
  existingObjects,
17
+ getByEntryPath,
18
+ isEntryKey,
18
19
  isApplied,
19
20
  isMergeable,
20
21
  locateObjects,
21
- mergeKeys,
22
+ relinkPath,
22
23
  pathKey,
23
24
  sharedPaths,
24
25
  type PathChange,
@@ -94,18 +95,23 @@ function clonerFor(
94
95
  paths?: ReadonlyMap<object, string[]>,
95
96
  ): Cloner {
96
97
  const seen = paths ? existingObjects(paths, target) : new Map();
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 => {
98
+ // Immer does not finalize a draft held as a Map key or Set member, so an
99
+ // existing object met there is put in the draft as the object itself: its
100
+ // base while the code did not change it, which other references to it then
101
+ // agree with.
102
+ const settle = (copy: object, original: object): object => {
101
103
  const path = paths?.get(original);
102
104
  if (!path || !isDraft(copy)) return copy;
103
105
  const plain = currentDraft(copy) as object;
104
- setByPath(target as Record<string, unknown>, path, plain);
106
+ // Where the object is in an entry of a Map or Set the path cannot be
107
+ // written (see getByEntryPath): that entry keeps its own
108
+ if (!path.some(isEntryKey)) {
109
+ setByPath(target as Record<string, unknown>, path, plain);
110
+ }
105
111
  seen.set(original, plain);
106
112
  return plain;
107
113
  };
108
- return (value) => deepClone(value, { keepUnregistered: true, seen, mapKey });
114
+ return (value) => deepClone(value, { keepUnregistered: true, seen, settle });
109
115
  }
110
116
 
111
117
  /** One cloner per target copy, made when first used. */
@@ -358,10 +364,10 @@ function relink(
358
364
  const prefixes = [first!, ...others].map((p) => pathKey(p).slice(0, -1));
359
365
  if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
360
366
  try {
361
- const object = getByPath(draft, first!);
367
+ const object = getByEntryPath(draft, first!);
362
368
  if (object === null || typeof object !== 'object') continue;
363
369
  for (const path of others)
364
- setByPath(draft as unknown as Record<string, unknown>, path, object);
370
+ relinkPath(draft as unknown as Record<string, unknown>, path, object);
365
371
  } catch {
366
372
  // A path the store does not hold: the code's view is not the store's
367
373
  }
@@ -506,14 +512,31 @@ const running = (): Commit[] =>
506
512
  /**
507
513
  * Bring a suspended mutation's copies up to the store after an action
508
514
  * replaced or changed state under it (navigation clears temporaries, back
509
- * and restart replace variables). Roots that still match keep the objects
510
- * the code may hold.
515
+ * and restart replace variables). What the action changed (see
516
+ * changesBetween) is written into the code's working copy, which keeps the
517
+ * objects the code may hold where nothing changed, and the references among
518
+ * the values written, and to the objects it holds already, as a commit does.
511
519
  */
512
520
  function resync(scope: MutationScope): void {
513
521
  const state = useStoryStore.getState();
514
- for (const ns of NAMESPACES) {
515
- // Root by root: a root that differs is replaced with a copy of the store's
516
- mergeKeys(scope.work[ns], scope.work[ns], state[ns]);
522
+ const now = namespacesOf(state);
523
+ const changes = changesBetween(
524
+ scope.base as unknown as Record<string, unknown>,
525
+ now as unknown as Record<string, unknown>,
526
+ );
527
+ const copy = clonerFor(scope.work, locateObjects(now, changes));
528
+ for (const change of changes) {
529
+ try {
530
+ applyChange(
531
+ scope.work,
532
+ change.deleted ? change : { ...change, value: copy(change.value) },
533
+ );
534
+ } catch {
535
+ // A path the working copy does not hold: replace its root instead
536
+ const [ns, root] = change.path as [NamespaceName, string];
537
+ if (hasOwn(now[ns], root)) scope.work[ns][root] = copy(now[ns][root]);
538
+ else delete scope.work[ns][root];
539
+ }
517
540
  }
518
541
  scope.base = cloneNamespaces(state);
519
542
  }
@@ -611,11 +634,26 @@ export function executeMutation(
611
634
  // came before its own
612
635
  commitScopes([...running(), { scope, keepRunning: false }]);
613
636
 
614
- for (const key of Object.keys(localsClone)) {
615
- if (!deepEqual(localsClone[key], mergedLocals[key])) {
616
- scopeUpdate(key, localsClone[key]);
637
+ // Locals the code changed, as content or as the objects they refer to
638
+ // (`@a = @b` over an equal object), and the locals that share an object
639
+ // with one of those: the updater takes each one's value, so they must all
640
+ // be written for the scope to hold one object again
641
+ const changed = new Set(
642
+ changesBetween(mergedLocals, localsClone, true).map((c) => c.path[0]!),
643
+ );
644
+ for (const group of sharedPaths(localsClone)) {
645
+ if (group.some((path) => changed.has(path[0]!))) {
646
+ // Not a value kept by reference (an unregistered class instance)
647
+ for (const path of group) {
648
+ if (localsClone[path[0]!] !== mergedLocals[path[0]!]) {
649
+ changed.add(path[0]!);
650
+ }
651
+ }
617
652
  }
618
653
  }
654
+ for (const key of Object.keys(localsClone)) {
655
+ if (changed.has(key)) scopeUpdate(key, localsClone[key]);
656
+ }
619
657
 
620
658
  // Detect deleted locals
621
659
  for (const key of Object.keys(mergedLocals)) {