@rohal12/spindle 0.59.5 → 0.59.7

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.
@@ -12575,10 +12575,18 @@ var defineData = (target, key, value) => {
12575
12575
  });
12576
12576
  };
12577
12577
  /**
12578
+ * A reducer extracts string keys, so devalue never sees the symbol keys of
12579
+ * the original object: refuse them here, as it does for a plain object.
12580
+ */
12581
+ function refuseSymbolKeys(value) {
12582
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError("spindle: Cannot save an object with symbol keys");
12583
+ }
12584
+ /**
12578
12585
  * Own enumerable keys of `value`, as a plain object; for an error also its
12579
12586
  * message, cause and (AggregateError) errors, which are not enumerable.
12580
12587
  */
12581
12588
  function ownData(value) {
12589
+ refuseSymbolKeys(value);
12582
12590
  const data = {};
12583
12591
  if (value instanceof Error) {
12584
12592
  for (const key of ERROR_HIDDEN_KEYS$1) if (hasOwn(value, key)) defineData(data, key, value[key]);
@@ -12605,6 +12613,7 @@ function reducers() {
12605
12613
  if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
12606
12614
  const keys = Object.keys(v);
12607
12615
  if (!keys.some(isEscapedOneCharKey)) return false;
12616
+ refuseSymbolKeys(v);
12608
12617
  if (keys.includes("__proto__")) throw new TypeError("spindle: Cannot save a property named \"__proto__\"");
12609
12618
  return keys.flatMap((k) => [k, v[k]]);
12610
12619
  };
@@ -13244,13 +13253,14 @@ function atomicName(value) {
13244
13253
  * Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
13245
13254
  * are not story state: unless an object holds one as its own property, it
13246
13255
  * reads as missing, as setByPath() treats it. Other inherited properties
13247
- * (class getters, `size` of a Map) are read.
13256
+ * (class getters, `size` of a Map) are read, and so are those of a string,
13257
+ * number or boolean (`length` of a string), as JavaScript boxes them.
13248
13258
  */
13249
13259
  function getByPath(obj, segments) {
13250
13260
  let current = obj;
13251
13261
  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;
13262
+ if (current == null) return void 0;
13263
+ if (seg in Object.prototype && !hasOwn(Object(current), seg)) return;
13254
13264
  current = current[seg];
13255
13265
  }
13256
13266
  return current;
@@ -13407,14 +13417,21 @@ function deepClone(value, options = {}) {
13407
13417
  if (isTemporal(val)) return val;
13408
13418
  if (val instanceof Error) {
13409
13419
  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
- });
13420
+ for (const key of [...ERROR_HIDDEN_KEYS, "stack"]) if (hasOwn(val, key)) {
13421
+ const held = val[key];
13422
+ Object.defineProperty(copy, key, {
13423
+ value: settled(clone(held), held),
13424
+ writable: true,
13425
+ configurable: true
13426
+ });
13427
+ }
13415
13428
  return copyKeys(val, copy);
13416
13429
  }
13417
13430
  }
13431
+ function settled(copy, original) {
13432
+ const settle = options.settle;
13433
+ return settle && isObjectValue(copy) && isObjectValue(original) ? settle(copy, original) : copy;
13434
+ }
13418
13435
  function clone(val) {
13419
13436
  if (val === null || typeof val !== "object") return val;
13420
13437
  const obj = val;
@@ -13426,16 +13443,12 @@ function deepClone(value, options = {}) {
13426
13443
  }
13427
13444
  if (val instanceof Map) {
13428
13445
  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
- }
13446
+ for (const [k, v] of val) copy.set(settled(clone(k), k), clone(v));
13434
13447
  return copy;
13435
13448
  }
13436
13449
  if (val instanceof Set) {
13437
13450
  const copy = keep(obj, /* @__PURE__ */ new Set());
13438
- for (const v of val) copy.add(clone(v));
13451
+ for (const v of val) copy.add(settled(clone(v), v));
13439
13452
  return copy;
13440
13453
  }
13441
13454
  const builtin = cloneBuiltin(obj);
@@ -13492,6 +13505,13 @@ function strictPairs(ab = /* @__PURE__ */ new Map(), ba = /* @__PURE__ */ new Ma
13492
13505
  return pairs;
13493
13506
  }
13494
13507
  /**
13508
+ * Whether `a` and `b` are equal as deepEqual() has it and also share
13509
+ * references at the same places (a save of one loads as the other).
13510
+ */
13511
+ function deepEqualStrict(a, b) {
13512
+ return equal(a, b, strictPairs());
13513
+ }
13514
+ /**
13495
13515
  * `curr` with each part that equals (see deepEqualStrict) the part of
13496
13516
  * `prev` at the same place replaced by `prev`'s: the history moments of a
13497
13517
  * save then share what did not change between them, which a save stores
@@ -13635,7 +13655,9 @@ function mergesWith(a, b, arrays) {
13635
13655
  * is none either. Values that merge are reported as 'nested', to be
13636
13656
  * compared key by key; they may still be equal. With `arrays`, arrays
13637
13657
  * 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.
13658
+ * elements one has beyond the other's length are added or deleted. An
13659
+ * index that holds no element (a hole) is no element, not an undefined one,
13660
+ * and holes the length gained at the end are the change of `length`, by how many.
13639
13661
  */
13640
13662
  function keyChanges(before, after, arrays) {
13641
13663
  const changes = [];
@@ -13656,16 +13678,41 @@ function keyChanges(before, after, arrays) {
13656
13678
  if (Array.isArray(after)) {
13657
13679
  const b = before;
13658
13680
  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
- });
13681
+ for (let i = 0; i < Math.min(b.length, a.length); i++) {
13682
+ const key = String(i);
13683
+ if (i in b && i in a) compare(key, b[i], a[i]);
13684
+ else if (i in b) changes.push({
13685
+ key,
13686
+ kind: "deleted",
13687
+ hole: true
13688
+ });
13689
+ else if (i in a) changes.push({
13690
+ key,
13691
+ kind: "added",
13692
+ after: a[i]
13693
+ });
13694
+ }
13695
+ let next = b.length;
13696
+ for (let i = b.length; i < a.length; i++) {
13697
+ if (!(i in a)) continue;
13698
+ const gap = i - next;
13699
+ changes.push({
13700
+ key: String(i),
13701
+ kind: "added",
13702
+ after: a[i],
13703
+ ...gap ? { gap } : {}
13704
+ });
13705
+ next = i + 1;
13706
+ }
13665
13707
  for (let i = a.length; i < b.length; i++) changes.push({
13666
13708
  key: String(i),
13667
13709
  kind: "deleted"
13668
13710
  });
13711
+ if (a.length > next) changes.push({
13712
+ key: "length",
13713
+ kind: "holes",
13714
+ after: a.length - next
13715
+ });
13669
13716
  return changes;
13670
13717
  }
13671
13718
  for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
@@ -13689,22 +13736,39 @@ function keyChanges(before, after, arrays) {
13689
13736
  function diffPaths(before, after, arrays = false) {
13690
13737
  const changes = [];
13691
13738
  const ancestors = /* @__PURE__ */ new Set();
13739
+ const unchanged = /* @__PURE__ */ new WeakMap();
13740
+ let cuts = 0;
13692
13741
  (function walk(b, a, path) {
13693
- if (ancestors.has(a)) return;
13742
+ if (ancestors.has(a)) {
13743
+ cuts++;
13744
+ return;
13745
+ }
13746
+ if (unchanged.get(a)?.has(b)) return;
13694
13747
  ancestors.add(a);
13748
+ const found = changes.length;
13749
+ const cutsBefore = cuts;
13695
13750
  for (const change of keyChanges(b, a, arrays)) {
13696
13751
  const at = [...path, change.key];
13697
13752
  if (change.kind === "nested") walk(change.before, change.after, at);
13698
13753
  else changes.push(change.kind === "deleted" ? {
13699
13754
  path: at,
13700
- deleted: true
13755
+ deleted: true,
13756
+ ...change.hole ? { hole: true } : {}
13701
13757
  } : {
13702
13758
  path: at,
13703
13759
  deleted: false,
13704
13760
  value: change.after,
13705
- ...change.kind === "added" && Array.isArray(a) ? { appended: true } : {}
13761
+ ...change.kind === "added" && Array.isArray(a) && Number(change.key) >= b.length ? {
13762
+ appended: true,
13763
+ ...change.gap ? { gap: change.gap } : {}
13764
+ } : {}
13706
13765
  });
13707
13766
  }
13767
+ if (changes.length === found && cuts === cutsBefore) {
13768
+ const seen = unchanged.get(a) ?? /* @__PURE__ */ new Set();
13769
+ seen.add(b);
13770
+ unchanged.set(a, seen);
13771
+ }
13708
13772
  ancestors.delete(a);
13709
13773
  })(before, after, []);
13710
13774
  return changes;
@@ -13715,8 +13779,64 @@ function underChange(path, keys) {
13715
13779
  for (let n = 1; n <= path.length; n++) if (keys.has(pathKey(path.slice(0, n)))) return true;
13716
13780
  return false;
13717
13781
  }
13718
- /** The segment standing for the entry `index` of a Map or Set in a path. */
13782
+ /**
13783
+ * The segment standing for the entry `index` of a Map or Set in a path, or
13784
+ * for the `cause` or `errors` of an Error ('c', 'e'), or for the buffer a
13785
+ * typed array or DataView views ('b').
13786
+ */
13719
13787
  var entryKey = (kind, index) => `\0${kind}${index}`;
13788
+ var isEntryKey = (segment) => segment.startsWith("\0");
13789
+ /**
13790
+ * What a Map, Set, Error or view holds in place of properties (keys and
13791
+ * values, members, `cause` and `errors`, the buffer), each with the segment standing for it in
13792
+ * a path (see entryKey); undefined for any other value.
13793
+ */
13794
+ function entryChildren(value) {
13795
+ const children = [];
13796
+ if (value instanceof Map) {
13797
+ let index = 0;
13798
+ for (const [k, v] of value) {
13799
+ children.push([entryKey("k", index), k], [entryKey("v", index), v]);
13800
+ index++;
13801
+ }
13802
+ } else if (value instanceof Set) {
13803
+ let index = 0;
13804
+ for (const member of value) children.push([entryKey("s", index++), member]);
13805
+ } else if (value instanceof Error) {
13806
+ for (const key of ["cause", "errors"]) if (hasOwn(value, key)) children.push([entryKey(key === "cause" ? "c" : "e", 0), value[key]]);
13807
+ } else if (ArrayBuffer.isView(value)) children.push([entryKey("b", 0), value.buffer]);
13808
+ else return;
13809
+ return children;
13810
+ }
13811
+ /**
13812
+ * The value at `path` below `root`, where a path may go through the entries
13813
+ * of Maps, Sets and Errors (see objectPaths) as well as properties.
13814
+ */
13815
+ function getByEntryPath(root, path) {
13816
+ let current = root;
13817
+ let start = 0;
13818
+ for (let i = 0; i < path.length; i++) {
13819
+ if (!isEntryKey(path[i])) continue;
13820
+ current = getByPath(current, path.slice(start, i));
13821
+ if (!isObjectValue(current)) return void 0;
13822
+ current = entryChildren(current)?.find(([at]) => at === path[i])?.[1];
13823
+ start = i + 1;
13824
+ }
13825
+ return start === path.length ? current : getByPath(current, path.slice(start));
13826
+ }
13827
+ /**
13828
+ * Write `value` at the path of an object a Map holds as a value (see
13829
+ * objectPaths); other entries are not replaced, as a key or member cannot
13830
+ * change without the entry moving. Returns whether it wrote.
13831
+ */
13832
+ function setEntryValue(root, path, value) {
13833
+ const holder = getByEntryPath(root, path.slice(0, -1));
13834
+ const match = /^\0v(\d+)$/.exec(path[path.length - 1]);
13835
+ if (!(holder instanceof Map) || !match) return false;
13836
+ const key = [...holder.keys()][Number(match[1])];
13837
+ holder.set(key, value);
13838
+ return true;
13839
+ }
13720
13840
  /**
13721
13841
  * Every path each object (or array, or other value) of `root` is at, the
13722
13842
  * first appearance first, in depth-first order. Plain objects and arrays are
@@ -13727,7 +13847,8 @@ var entryKey = (kind, index) => `\0${kind}${index}`;
13727
13847
  * With `entries`, the objects held in a Map or Set (as key, value or member)
13728
13848
  * are placed too, after everything else: one met nowhere else is at the path
13729
13849
  * 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.
13850
+ * a value can be read at (see getByEntryPath), so only to tell where an
13851
+ * object is from; so are the `cause` and `errors` of an Error.
13731
13852
  */
13732
13853
  function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13733
13854
  const all = /* @__PURE__ */ new Map();
@@ -13740,7 +13861,7 @@ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13740
13861
  }
13741
13862
  all.set(value, [at]);
13742
13863
  if (isMergeable(value) || Array.isArray(value)) walk(value, at);
13743
- else if (entries && (value instanceof Map || value instanceof Set)) collections.push([value, at]);
13864
+ else if (entries && entryChildren(value)) collections.push([value, at]);
13744
13865
  }
13745
13866
  function walk(node, path) {
13746
13867
  for (const key of Object.keys(node)) {
@@ -13752,15 +13873,7 @@ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13752
13873
  }
13753
13874
  }
13754
13875
  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
- }
13876
+ for (const [collection, path] of collections) for (const [segment, value] of entryChildren(collection)) if (isObjectValue(value)) place(value, [...path, segment]);
13764
13877
  return all;
13765
13878
  }
13766
13879
  /** Where each object of `root` first appears (see objectPaths). */
@@ -13773,27 +13886,44 @@ function firstPaths(root, skip, entries) {
13773
13886
  * parents before children.
13774
13887
  */
13775
13888
  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;
13889
+ return [...objectPaths(root, void 0, true).values()].filter((paths) => paths.length > 1);
13890
+ }
13891
+ /**
13892
+ * Whether `y`, which takes the place of `x` at `at` (the first appearance of
13893
+ * both), holds an object elsewhere than `x` did, below an entry of a Map, Set
13894
+ * or Error (see aliasChanges): `$map.set("k", $a)` over an equal object, or
13895
+ * `$map.get("k").child = $a`. Entries are compared by position, and only
13896
+ * where both hold as many; other differences are changes of content. Below
13897
+ * an entry, objects that merge are compared key by key.
13898
+ */
13899
+ function aliasMoved(x, y, at, earlier, later) {
13900
+ const pairs = [];
13901
+ const xs = entryChildren(x);
13902
+ const ys = entryChildren(y);
13903
+ if (xs && ys) {
13904
+ if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
13905
+ if (x instanceof Map || x instanceof Set) {
13906
+ if (x.size !== y.size) return false;
13907
+ }
13908
+ const held = new Map(ys);
13909
+ for (const [segment, a] of xs) if (held.has(segment)) pairs.push([
13910
+ segment,
13911
+ a,
13912
+ held.get(segment)
13913
+ ]);
13914
+ } else if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true)) {
13915
+ for (const key of Object.keys(y)) if (hasOwn(x, key)) pairs.push([
13916
+ key,
13917
+ x[key],
13918
+ y[key]
13919
+ ]);
13795
13920
  }
13796
- return false;
13921
+ return pairs.some(([segment, a, b]) => {
13922
+ if (!isObjectValue(a) || !isObjectValue(b)) return false;
13923
+ const here = [...at, segment];
13924
+ const isAt = later.get(b);
13925
+ return pathKey(earlier.get(a)) !== pathKey(isAt) || pathKey(isAt) === pathKey(here) && aliasMoved(a, b, here, earlier, later);
13926
+ });
13797
13927
  }
13798
13928
  /**
13799
13929
  * The paths whose object is another one in the references of `after` than in
@@ -13823,7 +13953,7 @@ function aliasChanges(before, after) {
13823
13953
  value: y,
13824
13954
  alias: true
13825
13955
  });
13826
- else if (pathKey(isAt) === k && entryAliasChanged(x, y, earlier, later)) changes.push({
13956
+ else if (pathKey(isAt) === k && entryChildren(x) && aliasMoved(x, y, at, earlier, later)) changes.push({
13827
13957
  path: at,
13828
13958
  deleted: false,
13829
13959
  value: y,
@@ -13857,7 +13987,7 @@ function changesBetween(before, after, arrays = false) {
13857
13987
  * `changes` (which write something new there), for `existingObjects`.
13858
13988
  */
13859
13989
  function locateObjects(source, changes) {
13860
- return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))));
13990
+ return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))), true);
13861
13991
  }
13862
13992
  /**
13863
13993
  * A map for `deepClone`'s `seen`: an object of the source `paths` locate
@@ -13872,7 +14002,7 @@ function existingObjects(paths, target) {
13872
14002
  if (super.has(obj)) return true;
13873
14003
  const path = paths.get(obj);
13874
14004
  if (!path) return false;
13875
- const found = getByPath(target, path);
14005
+ const found = getByEntryPath(target, path);
13876
14006
  if (!(isObjectValue(found) && (Array.isArray(found) ? Array.isArray(obj) : Object.getPrototypeOf(found) === Object.getPrototypeOf(obj)))) return false;
13877
14007
  super.set(obj, found);
13878
14008
  return true;
@@ -13887,32 +14017,20 @@ function isApplied(target, change) {
13887
14017
  if (!hasOwn(parent, key)) return change.deleted;
13888
14018
  return !change.deleted && deepEqual(parent[key], change.value);
13889
14019
  }
14020
+ /**
14021
+ * Make `path` below `root` hold `object` (an object that `sharedPaths` found
14022
+ * at several paths): a property is written like setByPath() does, a Map's
14023
+ * value replaced; an entry that is a key or member is left as it is.
14024
+ */
14025
+ function relinkPath(root, path, object) {
14026
+ if (path.some(isEntryKey)) setEntryValue(root, path, object);
14027
+ else setByPath(root, path, object);
14028
+ }
13890
14029
  function applyChange(target, change) {
13891
14030
  const root = target;
13892
14031
  if (change.deleted) deleteByPath(root, change.path);
13893
14032
  else setByPath(root, change.path, change.value);
13894
14033
  }
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
14034
  //#endregion
13917
14035
  //#region src/event-emitter.ts
13918
14036
  var VALID_EVENTS = new Set([
@@ -14145,7 +14263,9 @@ function storedRecord(meta, payload) {
14145
14263
  };
14146
14264
  }
14147
14265
  var createSave = queued(createSaveNow);
14148
- async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
14266
+ async function createSaveNow(ifid, playthroughPromise, payload, custom = {}) {
14267
+ const playthroughId = await playthroughPromise;
14268
+ if (!playthroughId) throw new Error("No playthrough");
14149
14269
  const now = (/* @__PURE__ */ new Date()).toISOString();
14150
14270
  const record = storedRecord({
14151
14271
  id: randomUUID(),
@@ -14161,6 +14281,10 @@ async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
14161
14281
  return record;
14162
14282
  }
14163
14283
  /**
14284
+ * A playthrough passed as a promise (see resolvePlaythroughId) is awaited
14285
+ * inside the operation, so the call takes its place in the order of
14286
+ * operations when it is made.
14287
+ *
14164
14288
  * Replace the payload of an existing save, keeping its ID and `createdAt`.
14165
14289
  * Pass the current `playthroughId` when the new payload comes from the
14166
14290
  * running game: the save then holds that playthrough's state, so it is
@@ -14176,7 +14300,8 @@ async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
14176
14300
  * before, so slot keys and metadata not passed again are kept.
14177
14301
  */
14178
14302
  var overwriteSave = queued(overwriteSaveNow);
14179
- async function overwriteSaveNow(saveId, payload, custom, playthroughId) {
14303
+ async function overwriteSaveNow(saveId, payload, custom, playthroughPromise) {
14304
+ const playthroughId = await playthroughPromise;
14180
14305
  const backend = await getBackend();
14181
14306
  const existing = await backend.getSave(saveId);
14182
14307
  if (!existing) return void 0;
@@ -14672,18 +14797,18 @@ var cloneValue = (value) => deepClone(value, { keepUnregistered: true });
14672
14797
  */
14673
14798
  function clonerFor(target, paths) {
14674
14799
  const seen = paths ? existingObjects(paths, target) : /* @__PURE__ */ new Map();
14675
- const mapKey = (copy, original) => {
14800
+ const settle = (copy, original) => {
14676
14801
  const path = paths?.get(original);
14677
14802
  if (!path || !isDraft(copy)) return copy;
14678
14803
  const plain = current(copy);
14679
- setByPath(target, path, plain);
14804
+ if (!path.some(isEntryKey)) setByPath(target, path, plain);
14680
14805
  seen.set(original, plain);
14681
14806
  return plain;
14682
14807
  };
14683
14808
  return (value) => deepClone(value, {
14684
14809
  keepUnregistered: true,
14685
14810
  seen,
14686
- mapKey
14811
+ settle
14687
14812
  });
14688
14813
  }
14689
14814
  /** One cloner per target copy, made when first used. */
@@ -14870,9 +14995,9 @@ function relink(draft, work, changes) {
14870
14995
  const prefixes = [first, ...others].map((p) => pathKey(p).slice(0, -1));
14871
14996
  if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
14872
14997
  try {
14873
- const object = getByPath(draft, first);
14998
+ const object = getByEntryPath(draft, first);
14874
14999
  if (object === null || typeof object !== "object") continue;
14875
- for (const path of others) setByPath(draft, path, object);
15000
+ for (const path of others) relinkPath(draft, path, object);
14876
15001
  } catch {}
14877
15002
  }
14878
15003
  }
@@ -14883,9 +15008,18 @@ function relink(draft, work, changes) {
14883
15008
  function cloneNamespaces(from) {
14884
15009
  const seen = /* @__PURE__ */ new Map();
14885
15010
  return {
14886
- variables: deepClone(from.variables, { seen }),
14887
- temporary: deepClone(from.temporary, { seen }),
14888
- transient: deepClone(from.transient, { seen })
15011
+ variables: deepClone(from.variables, {
15012
+ keepUnregistered: true,
15013
+ seen
15014
+ }),
15015
+ temporary: deepClone(from.temporary, {
15016
+ keepUnregistered: true,
15017
+ seen
15018
+ }),
15019
+ transient: deepClone(from.transient, {
15020
+ keepUnregistered: true,
15021
+ seen
15022
+ })
14889
15023
  };
14890
15024
  }
14891
15025
  /** The three variable namespaces of `state`, as an object to walk. */
@@ -14969,12 +15103,26 @@ var running = () => activeScopes.map((scope) => ({
14969
15103
  /**
14970
15104
  * Bring a suspended mutation's copies up to the store after an action
14971
15105
  * 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.
15106
+ * and restart replace variables). What the action changed (see
15107
+ * changesBetween) is written into the code's working copy, which keeps the
15108
+ * objects the code may hold where nothing changed, and the references among
15109
+ * the values written, and to the objects it holds already, as a commit does.
14974
15110
  */
14975
15111
  function resync(scope) {
14976
15112
  const state = useStoryStore.getState();
14977
- for (const ns of NAMESPACES) mergeKeys(scope.work[ns], scope.work[ns], state[ns]);
15113
+ const now = namespacesOf(state);
15114
+ const changes = changesBetween(scope.base, now);
15115
+ const copy = clonerFor(scope.work, locateObjects(now, changes));
15116
+ for (const change of changes) try {
15117
+ applyChange(scope.work, change.deleted ? change : {
15118
+ ...change,
15119
+ value: copy(change.value)
15120
+ });
15121
+ } catch {
15122
+ const [ns, root] = change.path;
15123
+ if (hasOwn(now[ns], root)) scope.work[ns][root] = copy(now[ns][root]);
15124
+ else delete scope.work[ns][root];
15125
+ }
14978
15126
  scope.base = cloneNamespaces(state);
14979
15127
  }
14980
15128
  /**
@@ -15038,7 +15186,11 @@ function executeMutation(code, mergedLocals, scopeUpdate) {
15038
15186
  scope,
15039
15187
  keepRunning: false
15040
15188
  }]);
15041
- for (const key of Object.keys(localsClone)) if (!deepEqual(localsClone[key], mergedLocals[key])) scopeUpdate(key, localsClone[key]);
15189
+ const changed = new Set(changesBetween(mergedLocals, localsClone, true).map((c) => c.path[0]));
15190
+ for (const group of sharedPaths(localsClone)) if (group.some((path) => changed.has(path[0]))) {
15191
+ for (const path of group) if (localsClone[path[0]] !== mergedLocals[path[0]]) changed.add(path[0]);
15192
+ }
15193
+ for (const key of Object.keys(localsClone)) if (changed.has(key)) scopeUpdate(key, localsClone[key]);
15042
15194
  for (const key of Object.keys(mergedLocals)) if (!hasOwn(localsClone, key)) scopeUpdate(key, void 0);
15043
15195
  }
15044
15196
  //#endregion
@@ -15799,20 +15951,25 @@ function writeHookChange(work, after, change, own) {
15799
15951
  const child = hasOwn(holder, key) ? holder[key] : void 0;
15800
15952
  const held = getByPath(after, path.slice(0, depth + 1));
15801
15953
  if (!mergesWith(child, held, true)) {
15802
- writeKey(holder, key, false, own(held));
15954
+ writeKey(holder, key, own(held));
15803
15955
  return;
15804
15956
  }
15805
15957
  holder = child;
15806
15958
  }
15807
15959
  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);
15960
+ if (change.deleted) writeKey(holder, key, void 0, change);
15961
+ else writeKey(holder, key, own(change.value), change);
15810
15962
  }
15811
- function writeKey(holder, key, deleted, value, appended) {
15963
+ function writeKey(holder, key, value, change) {
15964
+ const deleted = change?.deleted ?? false;
15812
15965
  if (Array.isArray(holder)) {
15813
15966
  const index = Number(key);
15814
- if (deleted) holder.length = Math.min(holder.length, index);
15815
- else if (appended) holder.push(value);
15967
+ if (deleted) if (change && "hole" in change) delete holder[index];
15968
+ else holder.length = Math.min(holder.length, index);
15969
+ else if (change && "appended" in change) {
15970
+ holder.length += change.gap ?? 0;
15971
+ holder.push(value);
15972
+ } else if (key === "length" && change) holder.length += value;
15816
15973
  else if (index < holder.length) holder[index] = value;
15817
15974
  } else if (deleted) delete holder[key];
15818
15975
  else setOwn(holder, key, value);
@@ -16611,10 +16768,10 @@ function App() {
16611
16768
  /* @__PURE__ */ jsx(TriggerDialogHost, {}),
16612
16769
  /* @__PURE__ */ jsx(RuntimeErrors, {})
16613
16770
  ] });
16614
- return nobr ? /* @__PURE__ */ jsx(NobrContext.Provider, {
16615
- value: true,
16771
+ return /* @__PURE__ */ jsx(NobrContext.Provider, {
16772
+ value: nobr,
16616
16773
  children: content
16617
- }) : content;
16774
+ });
16618
16775
  }
16619
16776
  //#endregion
16620
16777
  //#region src/parser.ts
@@ -18656,7 +18813,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
18656
18813
  logMacroError(`computed ${rawArgs}`, err, location);
18657
18814
  return;
18658
18815
  }
18659
- if (!deepEqual(prevRef.current, newValue)) {
18816
+ if (!deepEqualStrict(prevRef.current, newValue)) {
18660
18817
  prevRef.current = newValue;
18661
18818
  if (isLocal) try {
18662
18819
  localsUpdate(name, newValue);
@@ -19592,9 +19749,7 @@ function SaveManagerContent() {
19592
19749
  const playthrough = resolvePlaythroughId();
19593
19750
  try {
19594
19751
  await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
19595
- const ptId = await playthrough;
19596
- if (!ptId) throw new Error("No playthrough");
19597
- return createSave(ifid, ptId, payload);
19752
+ return createSave(ifid, playthrough, payload);
19598
19753
  });
19599
19754
  showStatus("Save created");
19600
19755
  await refresh();
@@ -19606,7 +19761,7 @@ function SaveManagerContent() {
19606
19761
  const playthrough = resolvePlaythroughId();
19607
19762
  try {
19608
19763
  await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
19609
- if (!await overwriteSave(saveId, payload, void 0, await playthrough || void 0)) throw new Error("Save not found");
19764
+ if (!await overwriteSave(saveId, payload, void 0, playthrough)) throw new Error("Save not found");
19610
19765
  });
19611
19766
  showStatus("Save overwritten");
19612
19767
  await refresh();
@@ -19995,7 +20150,8 @@ defineMacro({
19995
20150
  return useRunOnce("unset", rawArgs, () => {
19996
20151
  const state = useStoryStore.getState();
19997
20152
  const name = ctx.args.variable ?? "";
19998
- if (name.startsWith("$")) state.deleteVariable(name.slice(1));
20153
+ if (/^[$_%@][^.]*\./.test(name)) ctx.mutate(`delete ${name}`);
20154
+ else if (name.startsWith("$")) state.deleteVariable(name.slice(1));
19999
20155
  else if (name.startsWith("_")) state.deleteTemporary(name.slice(1));
20000
20156
  else if (name.startsWith("%")) state.deleteTransient(name.slice(1));
20001
20157
  else if (name.startsWith("@")) ctx.update(name.slice(1), void 0);
@@ -20442,13 +20598,13 @@ defineMacro({
20442
20598
  useEffect(() => {
20443
20599
  const store = useStoryStore.getState();
20444
20600
  const { history, historyIndex, transitionConfig } = store;
20445
- const next = store.consumeNextTransition();
20446
20601
  const prevLen = prevHistoryLenRef.current;
20447
20602
  prevHistoryLenRef.current = history.length;
20448
20603
  const isFirstLoad = !displayedPassage;
20449
20604
  const isRestart = history.length === 1 && historyIndex === 0 && prevLen > 1;
20450
20605
  const isLoad = history.length > 1 && historyIndex === history.length - 1 && prevLen > 1 && Math.abs(history.length - prevLen) > 1;
20451
20606
  if (navigationId === displayed.id) return;
20607
+ const next = store.consumeNextTransition();
20452
20608
  cancelTransition();
20453
20609
  const targetPassage = storyData?.passages.get(currentPassage);
20454
20610
  if (!targetPassage) {
@@ -20629,6 +20785,15 @@ function stopWithErrors(errors) {
20629
20785
  throw new Error(`spindle: ${errors.length} validation error(s):\n${errors.join("\n")}`);
20630
20786
  }
20631
20787
  /**
20788
+ * The undeclared variables the passages refer to, with the macros known now:
20789
+ * the input macros that bind a variable (registered by author JS or
20790
+ * StoryInit) and the roles of their parameters decide what is a reference.
20791
+ */
20792
+ function variableErrors(storyData, schema) {
20793
+ const storeVarMacros = getMacroRegistry().filter((m) => m.storeVar).map((m) => m.name);
20794
+ return validatePassages(storyData.passages, schema, storeVarMacros);
20795
+ }
20796
+ /**
20632
20797
  * The markup errors (malformed markup, unknown macros, syntax errors in
20633
20798
  * code) of the passages for which `only` holds, with the macros and widgets
20634
20799
  * known now.
@@ -20677,9 +20842,8 @@ function boot() {
20677
20842
  throw new Error(`spindle: ${msg}`);
20678
20843
  }
20679
20844
  const schema = parseStoryVariables(storyVarsPassage.content);
20680
- const storeVarMacros = getMacroRegistry().filter((m) => m.storeVar).map((m) => m.name);
20681
20845
  for (const name of blockWidgetNames(storyData.passages.values())) registerBlockMacro(name);
20682
- const errors = validatePassages(storyData.passages, schema, storeVarMacros);
20846
+ const errors = variableErrors(storyData, schema);
20683
20847
  errors.push(...markupErrors(storyData, (p) => p.name === "StoryInit"));
20684
20848
  let transientDefaults = {};
20685
20849
  const storyTransientsPassage = storyData.passages.get("StoryTransients");
@@ -20694,7 +20858,7 @@ function boot() {
20694
20858
  useStoryStore.getState().init(storyData, defaults, transientDefaults);
20695
20859
  enterRuntimePhase();
20696
20860
  initializeStory(loadSession(storyData.ifid));
20697
- const markup = markupErrors(storyData, (p) => p.name !== "StoryInit");
20861
+ const markup = [...variableErrors(storyData, schema), ...markupErrors(storyData, (p) => p.name !== "StoryInit")];
20698
20862
  if (markup.length > 0) stopWithErrors(markup);
20699
20863
  for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
20700
20864
  const widgetAST = parseMarkup(passage.content);