@rohal12/spindle 0.59.3 → 0.59.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12897,6 +12897,23 @@ function makeTables(table) {
12897
12897
  meta: table("meta", {})
12898
12898
  };
12899
12899
  }
12900
+ /**
12901
+ * The meta keys a story owns, in one place: save-manager builds them,
12902
+ * deleteMetaByIfid() matches them. A story's key is its prefix followed by
12903
+ * its IFID, except a named slot's pointer, `slot.<name>.<IFID>`.
12904
+ */
12905
+ var META_PREFIXES = {
12906
+ autosave: "autosave.",
12907
+ slot: "slot.",
12908
+ slotIndex: "slotIndex.",
12909
+ playthroughCount: "playthroughCount.",
12910
+ currentPlaythrough: "currentPlaythroughId."
12911
+ };
12912
+ /** Whether the meta `key` belongs to the story `ifid` (and no other). */
12913
+ function isMetaKeyOf(key, ifid) {
12914
+ const { slot, ...others } = META_PREFIXES;
12915
+ return Object.values(others).some((prefix) => key === prefix + ifid) || key.startsWith(slot) && key.endsWith(`.${ifid}`);
12916
+ }
12900
12917
  function createBackend(type, { saves, playthroughs, meta }, destroy) {
12901
12918
  /** Delete the records `field` finds, resolving to their keys. */
12902
12919
  async function deleteFound(table, keyOf, field, value) {
@@ -12929,7 +12946,7 @@ function createBackend(type, { saves, playthroughs, meta }, destroy) {
12929
12946
  setMeta: (key, value) => meta.put(key, value),
12930
12947
  deleteMeta: (key) => meta.delete(key),
12931
12948
  deleteMetaByPrefix: (prefix) => deleteMetaWhere((key) => key.startsWith(prefix)),
12932
- deleteMetaByIfid: (ifid) => deleteMetaWhere((key) => key.includes(ifid)),
12949
+ deleteMetaByIfid: (ifid) => deleteMetaWhere((key) => isMetaKeyOf(key, ifid)),
12933
12950
  getAllMetaKeys: () => meta.keys(),
12934
12951
  destroy
12935
12952
  };
@@ -13250,7 +13267,7 @@ var builtinName = atomicName;
13250
13267
  * stays an instance of its class (with the same own keys deepClone copies).
13251
13268
  */
13252
13269
  function shallowCopy(value) {
13253
- const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
13270
+ const copy = Array.isArray(value) ? new Array(value.length) : Object.create(Object.getPrototypeOf(value));
13254
13271
  for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
13255
13272
  value: value[key],
13256
13273
  enumerable: true,
@@ -13409,7 +13426,11 @@ function deepClone(value, options = {}) {
13409
13426
  }
13410
13427
  if (val instanceof Map) {
13411
13428
  const copy = keep(obj, /* @__PURE__ */ new Map());
13412
- for (const [k, v] of val) copy.set(clone(k), clone(v));
13429
+ for (const [k, v] of val) {
13430
+ const key = clone(k);
13431
+ const settle = options.mapKey;
13432
+ copy.set(settle && isObjectValue(key) && isObjectValue(k) ? settle(key, k) : key, clone(v));
13433
+ }
13413
13434
  return copy;
13414
13435
  }
13415
13436
  if (val instanceof Set) {
@@ -13662,15 +13683,16 @@ function keyChanges(before, after, arrays) {
13662
13683
  /**
13663
13684
  * The property paths where `after` differs from `before`, two objects of
13664
13685
  * one class: objects that merge (see isMergeable) are compared property by
13665
- * property, any other value as a whole.
13686
+ * property, any other value as a whole; with `arrays`, arrays are compared
13687
+ * by index too (see keyChanges).
13666
13688
  */
13667
- function diffPaths(before, after) {
13689
+ function diffPaths(before, after, arrays = false) {
13668
13690
  const changes = [];
13669
13691
  const ancestors = /* @__PURE__ */ new Set();
13670
13692
  (function walk(b, a, path) {
13671
13693
  if (ancestors.has(a)) return;
13672
13694
  ancestors.add(a);
13673
- for (const change of keyChanges(b, a, false)) {
13695
+ for (const change of keyChanges(b, a, arrays)) {
13674
13696
  const at = [...path, change.key];
13675
13697
  if (change.kind === "nested") walk(change.before, change.after, at);
13676
13698
  else changes.push(change.kind === "deleted" ? {
@@ -13679,7 +13701,8 @@ function diffPaths(before, after) {
13679
13701
  } : {
13680
13702
  path: at,
13681
13703
  deleted: false,
13682
- value: change.after
13704
+ value: change.after,
13705
+ ...change.kind === "added" && Array.isArray(a) ? { appended: true } : {}
13683
13706
  });
13684
13707
  }
13685
13708
  ancestors.delete(a);
@@ -13692,24 +13715,85 @@ function underChange(path, keys) {
13692
13715
  for (let n = 1; n <= path.length; n++) if (keys.has(pathKey(path.slice(0, n)))) return true;
13693
13716
  return false;
13694
13717
  }
13718
+ /** The segment standing for the entry `index` of a Map or Set in a path. */
13719
+ var entryKey = (kind, index) => `\0${kind}${index}`;
13695
13720
  /**
13696
- * Where each object (or array, or other value) of `root` first appears, in
13697
- * depth-first order. Plain objects are entered; arrays and other values are
13698
- * leaves. A path at or below one of `skip` is left out.
13699
- */
13700
- function firstPaths(root, skip = /* @__PURE__ */ new Set()) {
13701
- const found = /* @__PURE__ */ new Map();
13702
- (function walk(node, path) {
13721
+ * Every path each object (or array, or other value) of `root` is at, the
13722
+ * first appearance first, in depth-first order. Plain objects and arrays are
13723
+ * entered, where first met only (an object held in an array is one object,
13724
+ * however it was reached); other values are leaves. A path at or below one
13725
+ * of `skip` is left out.
13726
+ *
13727
+ * With `entries`, the objects held in a Map or Set (as key, value or member)
13728
+ * are placed too, after everything else: one met nowhere else is at the path
13729
+ * of its Map or Set and the entry's segment (see entryKey), which is no path
13730
+ * a value can be read at, so only to tell where an object is from.
13731
+ */
13732
+ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
13733
+ const all = /* @__PURE__ */ new Map();
13734
+ const collections = [];
13735
+ function place(value, at) {
13736
+ const known = all.get(value);
13737
+ if (known) {
13738
+ known.push(at);
13739
+ return;
13740
+ }
13741
+ all.set(value, [at]);
13742
+ if (isMergeable(value) || Array.isArray(value)) walk(value, at);
13743
+ else if (entries && (value instanceof Map || value instanceof Set)) collections.push([value, at]);
13744
+ }
13745
+ function walk(node, path) {
13703
13746
  for (const key of Object.keys(node)) {
13704
13747
  const value = node[key];
13705
- if (!isObjectValue(value) || found.has(value)) continue;
13748
+ if (!isObjectValue(value)) continue;
13706
13749
  const at = [...path, key];
13707
13750
  if (skip.has(pathKey(at))) continue;
13708
- found.set(value, at);
13709
- if (isMergeable(value)) walk(value, at);
13751
+ place(value, at);
13752
+ }
13753
+ }
13754
+ walk(root, []);
13755
+ for (const [collection, path] of collections) {
13756
+ let index = 0;
13757
+ for (const entry of collection.entries()) {
13758
+ const [k, v] = entry;
13759
+ const parts = collection instanceof Set ? [[k, entryKey("s", index)]] : [[k, entryKey("k", index)], [v, entryKey("v", index)]];
13760
+ for (const [value, segment] of parts) if (isObjectValue(value) && !all.has(value)) place(value, [...path, segment]);
13761
+ index++;
13710
13762
  }
13711
- })(root, []);
13712
- return found;
13763
+ }
13764
+ return all;
13765
+ }
13766
+ /** Where each object of `root` first appears (see objectPaths). */
13767
+ function firstPaths(root, skip, entries) {
13768
+ return new Map([...objectPaths(root, skip, entries)].map(([object, paths]) => [object, paths[0]]));
13769
+ }
13770
+ /**
13771
+ * The objects `root` holds at more than one path (an object two variables
13772
+ * refer to, or one that refers to itself), with every path they are at,
13773
+ * parents before children.
13774
+ */
13775
+ 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;
13795
+ }
13796
+ return false;
13713
13797
  }
13714
13798
  /**
13715
13799
  * The paths whose object is another one in the references of `after` than in
@@ -13720,8 +13804,8 @@ function firstPaths(root, skip = /* @__PURE__ */ new Set()) {
13720
13804
  * `after` holds.
13721
13805
  */
13722
13806
  function aliasChanges(before, after) {
13723
- const earlier = firstPaths(before);
13724
- const later = firstPaths(after);
13807
+ const earlier = firstPaths(before, void 0, true);
13808
+ const later = firstPaths(after, void 0, true);
13725
13809
  const changes = [];
13726
13810
  (function walk(b, a, path) {
13727
13811
  for (const key of Object.keys(a)) {
@@ -13739,12 +13823,36 @@ function aliasChanges(before, after) {
13739
13823
  value: y,
13740
13824
  alias: true
13741
13825
  });
13742
- if (isMergeable(x) && mergesWith(x, y, false) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
13826
+ else if (pathKey(isAt) === k && entryAliasChanged(x, y, earlier, later)) changes.push({
13827
+ path: at,
13828
+ deleted: false,
13829
+ value: y,
13830
+ alias: true
13831
+ });
13832
+ if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
13743
13833
  }
13744
13834
  })(before, after, []);
13745
13835
  return changes;
13746
13836
  }
13747
13837
  /**
13838
+ * The changes from `before` to `after`, as paths below them: the property
13839
+ * paths that differ (see diffPaths), and those that hold equal content but
13840
+ * another object than they did (see aliasChanges), those first. Compared as
13841
+ * one object, so an object that two of their keys share is one object,
13842
+ * whichever of them it is reached through.
13843
+ */
13844
+ function changesBetween(before, after, arrays = false) {
13845
+ const written = diffPaths(before, after, arrays);
13846
+ const keys = new Set(written.map((c) => pathKey(c.path)));
13847
+ const aliases = [];
13848
+ for (const change of aliasChanges(before, after)) {
13849
+ if (underChange(change.path, keys)) continue;
13850
+ keys.add(pathKey(change.path));
13851
+ aliases.push(change);
13852
+ }
13853
+ return [...aliases, ...written];
13854
+ }
13855
+ /**
13748
13856
  * Where the objects of `source` are, other than at or below the paths of
13749
13857
  * `changes` (which write something new there), for `existingObjects`.
13750
13858
  */
@@ -13780,8 +13888,9 @@ function isApplied(target, change) {
13780
13888
  return !change.deleted && deepEqual(parent[key], change.value);
13781
13889
  }
13782
13890
  function applyChange(target, change) {
13783
- if (change.deleted) deleteByPath(target, change.path);
13784
- else setByPath(target, change.path, change.value);
13891
+ const root = target;
13892
+ if (change.deleted) deleteByPath(root, change.path);
13893
+ else setByPath(root, change.path, change.value);
13785
13894
  }
13786
13895
  /**
13787
13896
  * Write how `after` differs from `before` (see keyChanges; arrays merge by
@@ -13931,7 +14040,7 @@ async function startNewPlaythroughNow(ifid, id = randomUUID()) {
13931
14040
  }
13932
14041
  var PLAYTHROUGH_LABEL = /^Playthrough (\d+)$/;
13933
14042
  function playthroughCountKey(ifid) {
13934
- return `playthroughCount.${ifid}`;
14043
+ return `${META_PREFIXES.playthroughCount}${ifid}`;
13935
14044
  }
13936
14045
  /**
13937
14046
  * The number of the next playthrough the story starts: one past the highest
@@ -13950,7 +14059,7 @@ async function nextPlaythroughNumber(ifid) {
13950
14059
  return highest + 1;
13951
14060
  }
13952
14061
  function currentPlaythroughKey(ifid) {
13953
- return `currentPlaythroughId.${ifid}`;
14062
+ return `${META_PREFIXES.currentPlaythrough}${ifid}`;
13954
14063
  }
13955
14064
  /**
13956
14065
  * Make `id` the story's current playthrough: the playthrough of a save the
@@ -14157,9 +14266,9 @@ var getSavesGrouped = queued(async function getSavesGroupedNow(ifid) {
14157
14266
  });
14158
14267
  return result;
14159
14268
  });
14160
- var AUTOSAVE_KEY_PREFIX = "autosave.";
14161
- var SLOT_KEY_PREFIX = "slot.";
14162
- var SLOT_INDEX_KEY_PREFIX = "slotIndex.";
14269
+ var AUTOSAVE_KEY_PREFIX = META_PREFIXES.autosave;
14270
+ var SLOT_KEY_PREFIX = META_PREFIXES.slot;
14271
+ var SLOT_INDEX_KEY_PREFIX = META_PREFIXES.slotIndex;
14163
14272
  /**
14164
14273
  * Whether `slot` names a slot. The default (autosave) slot is addressed by
14165
14274
  * omitting `slot` or by `''`, the identifier its SaveInfo reports.
@@ -14563,9 +14672,18 @@ var cloneValue = (value) => deepClone(value, { keepUnregistered: true });
14563
14672
  */
14564
14673
  function clonerFor(target, paths) {
14565
14674
  const seen = paths ? existingObjects(paths, target) : /* @__PURE__ */ new Map();
14675
+ const mapKey = (copy, original) => {
14676
+ const path = paths?.get(original);
14677
+ if (!path || !isDraft(copy)) return copy;
14678
+ const plain = current(copy);
14679
+ setByPath(target, path, plain);
14680
+ seen.set(original, plain);
14681
+ return plain;
14682
+ };
14566
14683
  return (value) => deepClone(value, {
14567
14684
  keepUnregistered: true,
14568
- seen
14685
+ seen,
14686
+ mapKey
14569
14687
  });
14570
14688
  }
14571
14689
  /** One cloner per target copy, made when first used. */
@@ -14597,17 +14715,17 @@ function frozenCopy(value) {
14597
14715
  * path (the code replaced or deleted an intermediate object), its root is
14598
14716
  * replaced with the store's root as written.
14599
14717
  */
14600
- function mirror(draft, ns, change, scopes = activeScopes, cloner = clonerOf) {
14601
- const root = change.path[0];
14602
- for (const scope of scopes) for (const copy of [scope.work[ns], scope.base[ns]]) try {
14718
+ function mirror(draft, change, scopes = activeScopes, cloner = clonerOf) {
14719
+ const [ns, root] = change.path;
14720
+ for (const scope of scopes) for (const copy of [scope.work, scope.base]) try {
14603
14721
  applyChange(copy, change.deleted ? change : {
14604
14722
  ...change,
14605
14723
  value: cloner(copy)(change.value)
14606
14724
  });
14607
14725
  } catch {
14608
14726
  const stored = draft[ns][root];
14609
- if (hasOwn(draft[ns], root)) copy[root] = cloneValue(isDraft(stored) ? current(stored) : stored);
14610
- else delete copy[root];
14727
+ if (hasOwn(draft[ns], root)) copy[ns][root] = cloneValue(isDraft(stored) ? current(stored) : stored);
14728
+ else delete copy[ns][root];
14611
14729
  }
14612
14730
  }
14613
14731
  /** A cloner of a copy of its own (no references shared with others). */
@@ -14707,16 +14825,20 @@ function routeStoreUpdate(recipe) {
14707
14825
  transient: inner.work.transient
14708
14826
  };
14709
14827
  const [next, patches] = scratch.produceWithPatches(view, recipe);
14710
- const changes = NAMESPACES.map((ns) => {
14828
+ const changes = NAMESPACES.flatMap((ns) => {
14711
14829
  const own = patches.filter((p) => p.path[0] === ns);
14712
- return [ns, own.length ? writtenPaths(view[ns], next[ns], own) : []];
14830
+ return own.length ? writtenPaths(view[ns], next[ns], own).map((change) => ({
14831
+ ...change,
14832
+ path: [ns, ...change.path]
14833
+ })) : [];
14713
14834
  });
14714
- const cloners = clonersFor();
14835
+ const held = locateObjects(namespacesOf(inner.work), changes);
14836
+ const cloners = clonersFor(() => held);
14715
14837
  const owned = (change, target) => change.deleted ? change : {
14716
14838
  ...change,
14717
14839
  value: cloners(target)(change.value)
14718
14840
  };
14719
- for (const [ns, list] of changes) for (const change of list) applyChange(inner.work[ns], owned(change, inner.work[ns]));
14841
+ for (const change of changes) applyChange(inner.work, owned(change, inner.work));
14720
14842
  return (draft) => {
14721
14843
  const target = draft;
14722
14844
  for (const key of Object.keys(next)) {
@@ -14724,22 +14846,53 @@ function routeStoreUpdate(recipe) {
14724
14846
  if (!NAMESPACE_KEYS$1.has(key) && value !== view[key]) target[key] = value;
14725
14847
  }
14726
14848
  const namespaces = draft;
14727
- for (const [ns, list] of changes) for (const change of list) {
14728
- const root = change.path[0];
14849
+ for (const change of changes) {
14850
+ const [ns, root] = change.path;
14729
14851
  try {
14730
- applyChange(namespaces[ns], owned(change, namespaces[ns]));
14852
+ applyChange(namespaces, owned(change, namespaces));
14731
14853
  } catch {
14732
14854
  if (hasOwn(inner.work[ns], root)) namespaces[ns][root] = cloneValue(inner.work[ns][root]);
14733
14855
  else delete namespaces[ns][root];
14734
14856
  }
14735
- mirror(namespaces, ns, change, activeScopes, cloners);
14857
+ mirror(namespaces, change, activeScopes, cloners);
14736
14858
  }
14737
14859
  };
14738
14860
  }
14739
- var cloneNamespaces = (from) => ({
14740
- variables: deepClone(from.variables),
14741
- temporary: deepClone(from.temporary),
14742
- transient: deepClone(from.transient)
14861
+ /**
14862
+ * Make the objects `work` holds at several paths one object in `draft`
14863
+ * again where `changes` wrote below one of them: a write through one path
14864
+ * of a draft gives that path a new object, and the other paths would go on
14865
+ * referring to the old one (`$a.n = 2` with `$b` the same object as `$a`).
14866
+ */
14867
+ function relink(draft, work, changes) {
14868
+ const written = changes.map((c) => pathKey(c.path).slice(0, -1));
14869
+ for (const [first, ...others] of sharedPaths(work)) {
14870
+ const prefixes = [first, ...others].map((p) => pathKey(p).slice(0, -1));
14871
+ if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
14872
+ try {
14873
+ const object = getByPath(draft, first);
14874
+ if (object === null || typeof object !== "object") continue;
14875
+ for (const path of others) setByPath(draft, path, object);
14876
+ } catch {}
14877
+ }
14878
+ }
14879
+ /**
14880
+ * Copies of the namespaces that keep their references to each other: an
14881
+ * object held in two of them (`%copy = $a`) is one object in the copies too.
14882
+ */
14883
+ function cloneNamespaces(from) {
14884
+ const seen = /* @__PURE__ */ new Map();
14885
+ return {
14886
+ variables: deepClone(from.variables, { seen }),
14887
+ temporary: deepClone(from.temporary, { seen }),
14888
+ transient: deepClone(from.transient, { seen })
14889
+ };
14890
+ }
14891
+ /** The three variable namespaces of `state`, as an object to walk. */
14892
+ var namespacesOf = (state) => ({
14893
+ variables: state.variables,
14894
+ temporary: state.temporary,
14895
+ transient: state.transient
14743
14896
  });
14744
14897
  /**
14745
14898
  * Commit the property paths each mutation's code changed (work vs base) on
@@ -14760,9 +14913,9 @@ function commitScopes(commits) {
14760
14913
  const all = commits.map(({ scope, keepRunning }) => ({
14761
14914
  scope,
14762
14915
  keepRunning,
14763
- changes: NAMESPACES.map((ns) => [ns, changesOf(scope.base[ns], scope.work[ns])])
14916
+ changes: changesOf(scope.base, scope.work)
14764
14917
  }));
14765
- const changed = all.filter(({ changes }) => changes.some(([, list]) => list.length > 0));
14918
+ const changed = all.filter(({ changes }) => changes.length > 0);
14766
14919
  if (changed.length === 0) return;
14767
14920
  for (const { scope, keepRunning } of changed) if (keepRunning) scope.base = cloneNamespaces(scope.work);
14768
14921
  committing = true;
@@ -14773,52 +14926,41 @@ function commitScopes(commits) {
14773
14926
  }
14774
14927
  function commitRecipe(draft) {
14775
14928
  all.forEach(({ scope, changes }, i) => {
14776
- const paths = new Map(changes.map(([ns, list]) => [ns, locateObjects(scope.work[ns], list)]));
14777
- const own = new Map(changes.map(([ns]) => [ns, clonerFor(draft[ns], paths.get(ns))]));
14929
+ const paths = locateObjects(scope.work, changes);
14930
+ const own = clonerFor(draft, paths);
14778
14931
  const enclosing = all.slice(0, i).map((c) => c.scope);
14779
- const enclosingCloners = new Map(changes.map(([ns]) => [ns, clonersFor(() => paths.get(ns))]));
14780
- for (const [ns, list] of changes) {
14781
- const replaced = /* @__PURE__ */ new Set();
14782
- for (const change of list) {
14783
- const root = change.path[0];
14784
- if (replaced.has(root)) continue;
14785
- try {
14786
- if (change.deleted) {
14787
- if (!isApplied(draft[ns], change)) applyChange(draft[ns], change);
14788
- continue;
14789
- }
14790
- const value = change.alias ? own.get(ns)(change.value) : void 0;
14791
- if (change.alias ? Object.is(getByPath(draft[ns], change.path), value) : isApplied(draft[ns], change)) continue;
14792
- applyChange(draft[ns], {
14793
- ...change,
14794
- value: change.alias ? value : own.get(ns)(change.value)
14795
- });
14796
- } catch {
14797
- draft[ns][root] = own.get(ns)(scope.work[ns][root]);
14798
- replaced.add(root);
14932
+ const enclosingCloners = clonersFor(() => paths);
14933
+ const replaced = /* @__PURE__ */ new Set();
14934
+ for (const change of changes) {
14935
+ const [ns, root] = change.path;
14936
+ const rootKey = pathKey([ns, root]);
14937
+ if (replaced.has(rootKey)) continue;
14938
+ try {
14939
+ if (change.deleted) {
14940
+ if (!isApplied(draft, change)) applyChange(draft, change);
14941
+ continue;
14799
14942
  }
14943
+ const value = change.alias ? own(change.value) : void 0;
14944
+ if (change.alias ? Object.is(getByPath(draft, change.path), value) : isApplied(draft, change)) continue;
14945
+ applyChange(draft, {
14946
+ ...change,
14947
+ value: change.alias ? value : own(change.value)
14948
+ });
14949
+ } catch {
14950
+ draft[ns][root] = own(scope.work[ns][root]);
14951
+ replaced.add(rootKey);
14800
14952
  }
14801
- for (const change of list) mirror(draft, ns, change, enclosing, enclosingCloners.get(ns));
14802
14953
  }
14954
+ relink(draft, scope.work, changes);
14955
+ for (const change of changes) mirror(draft, change, enclosing, enclosingCloners);
14803
14956
  });
14804
14957
  }
14805
14958
  }
14806
14959
  /**
14807
- * The changes of the code to a namespace: its property paths that differ
14808
- * (see diffPaths), and those that hold equal content but another object
14809
- * than they did (see aliasChanges), those first.
14960
+ * The changes of the code to the namespaces, as paths below them (`['variables',
14961
+ * 'a']`; see changesBetween).
14810
14962
  */
14811
- function changesOf(base, work) {
14812
- const written = diffPaths(base, work);
14813
- const keys = new Set(written.map((c) => pathKey(c.path)));
14814
- const aliases = [];
14815
- for (const change of aliasChanges(base, work)) {
14816
- if (underChange(change.path, keys)) continue;
14817
- keys.add(pathKey(change.path));
14818
- aliases.push(change);
14819
- }
14820
- return [...aliases, ...written];
14821
- }
14963
+ var changesOf = (base, work) => changesBetween(base, work);
14822
14964
  /** Every running mutation, to commit with its code going on. */
14823
14965
  var running = () => activeScopes.map((scope) => ({
14824
14966
  scope,
@@ -14853,14 +14995,40 @@ function runWithCommittedMutations(action) {
14853
14995
  for (const scope of suspended) resync(scope);
14854
14996
  }
14855
14997
  }
14856
- function executeMutation(code, mergedLocals, scopeUpdate) {
14998
+ /**
14999
+ * Run `run` on working copies of the story state and commit what it changed
15000
+ * to the store, as mutation code does (see commitScopes): the references
15001
+ * between values are kept, which a store update made on a draft does not do.
15002
+ * Nothing is committed when `run` throws.
15003
+ */
15004
+ function mutateState(run) {
15005
+ commitScopes(running());
15006
+ const held = locateObjects(namespacesOf(getActiveMutationScope() ?? useStoryStore.getState()), []);
15007
+ const scope = startScope();
15008
+ const adopt = clonerFor(scope.work, held);
15009
+ try {
15010
+ run(scope.work, adopt);
15011
+ } finally {
15012
+ activeScopes.pop();
15013
+ }
15014
+ commitScopes([...running(), {
15015
+ scope,
15016
+ keepRunning: false
15017
+ }]);
15018
+ }
15019
+ /** Start a mutation, from the pending state of the one it runs inside. */
15020
+ function startScope() {
14857
15021
  const start = getActiveMutationScope() ?? useStoryStore.getState();
14858
15022
  const scope = {
14859
15023
  work: cloneNamespaces(start),
14860
15024
  base: cloneNamespaces(start)
14861
15025
  };
14862
- const localsClone = asNamespace(deepClone(mergedLocals, { keepUnregistered: true }));
14863
15026
  activeScopes.push(scope);
15027
+ return scope;
15028
+ }
15029
+ function executeMutation(code, mergedLocals, scopeUpdate) {
15030
+ const localsClone = asNamespace(deepClone(mergedLocals, { keepUnregistered: true }));
15031
+ const scope = startScope();
14864
15032
  try {
14865
15033
  execute(code, scope.work.variables, scope.work.temporary, localsClone, scope.work.transient);
14866
15034
  } finally {
@@ -15594,35 +15762,60 @@ function loadedEntryMoment(payload) {
15594
15762
  * the `beforesave` hooks) into the payload's snapshot of the saved moment. A
15595
15763
  * load restores that snapshot, the state on entering the passage, and runs
15596
15764
  * the passage again, so data a hook adds to a save would otherwise be lost
15597
- * on load (#227). Only the property paths the hooks changed are written (into
15598
- * copies of the objects on those paths, the rest staying shared): a
15599
- * whole variable would bring along what the passage did to the rest of it,
15600
- * which the passage then does again on load (#232). Only the payload's copy
15601
- * changes: the live history keeps the recorded snapshot (#159).
15765
+ * on load (#227). Only the property paths the hooks changed are written (see
15766
+ * changesBetween): a whole variable would bring along what the passage did
15767
+ * to the rest of it, which the passage then does again on load (#232). The
15768
+ * references the hooks made stay: an object they put in two variables is one
15769
+ * object in the snapshot, and one they made another variable refer to is the
15770
+ * snapshot's own object of it (#302). Only the payload's copy changes: the
15771
+ * live history keeps the recorded snapshot (#159).
15602
15772
  */
15603
15773
  function keepHookWrites(payload, before, after) {
15604
15774
  const moment = payload.history[payload.historyIndex];
15605
15775
  if (!moment) return;
15606
- mergeHookWrites(moment.variables, before, after, /* @__PURE__ */ new Set());
15607
- }
15608
- /**
15609
- * Apply the hooks' changes between `before` and `after`, two objects or two
15610
- * arrays, to the snapshot's copy of them, `target` (see mergeKeys: array
15611
- * elements merge by index). A changed value is merged key by key where the
15612
- * snapshot holds the same kind of value. Where it lacks the key or holds
15613
- * another kind (the passage created or replaced it), the hooks' whole value
15614
- * is written.
15615
- */
15616
- function mergeHookWrites(target, before, after, ancestors) {
15617
- ancestors.add(after);
15618
- mergeKeys(target, before, after, (key, b, a) => {
15619
- if (!hasOwn(target, key) || !mergesWith(a, target[key], true) || ancestors.has(a)) return false;
15620
- const copy = shallowCopy(target[key]);
15621
- target[key] = copy;
15622
- mergeHookWrites(copy, b, a, ancestors);
15623
- return true;
15624
- });
15625
- ancestors.delete(after);
15776
+ const changes = changesBetween(before, after, true);
15777
+ if (changes.length === 0) return;
15778
+ const work = deepClone(moment.variables);
15779
+ const seen = existingObjects(locateObjects(after, changes), work);
15780
+ const own = (value) => deepClone(value, { seen });
15781
+ for (const change of changes) writeHookChange(work, after, change, own);
15782
+ moment.variables = shareEqual(moment.variables, work);
15783
+ }
15784
+ /**
15785
+ * Write `change` (of the hooks, to `after`) into `work`. The value goes in at
15786
+ * the first place `work` lacks the objects on the path of one of the kind
15787
+ * `after` holds (the passage created or replaced it): there, the hooks' whole
15788
+ * value of it is written. Into an array, elements removed from the end are
15789
+ * removed at the same indices and elements added are appended; changes at
15790
+ * indices the array lacks are dropped (it was resized, so its indices do not
15791
+ * line up with the live ones).
15792
+ */
15793
+ function writeHookChange(work, after, change, own) {
15794
+ const { path } = change;
15795
+ let holder = work;
15796
+ let depth = 0;
15797
+ for (; depth < path.length - 1; depth++) {
15798
+ const key = path[depth];
15799
+ const child = hasOwn(holder, key) ? holder[key] : void 0;
15800
+ const held = getByPath(after, path.slice(0, depth + 1));
15801
+ if (!mergesWith(child, held, true)) {
15802
+ writeKey(holder, key, false, own(held));
15803
+ return;
15804
+ }
15805
+ holder = child;
15806
+ }
15807
+ const key = path[depth];
15808
+ if (change.deleted) writeKey(holder, key, true, void 0);
15809
+ else writeKey(holder, key, false, own(change.value), change.appended);
15810
+ }
15811
+ function writeKey(holder, key, deleted, value, appended) {
15812
+ if (Array.isArray(holder)) {
15813
+ const index = Number(key);
15814
+ if (deleted) holder.length = Math.min(holder.length, index);
15815
+ else if (appended) holder.push(value);
15816
+ else if (index < holder.length) holder[index] = value;
15817
+ } else if (deleted) delete holder[key];
15818
+ else setOwn(holder, key, value);
15626
15819
  }
15627
15820
  /** Restore the PRNG from a snapshot, or reset it without one. */
15628
15821
  function restorePRNGFrom(prng) {
@@ -17196,8 +17389,8 @@ function defineMacro(config, source = "builtin") {
17196
17389
  ctx.value = useStoryStore((s) => getByPath(s.variables, segments));
17197
17390
  ctx.getValue = () => getByPath(readState().variables, segments);
17198
17391
  ctx.setValue = (value) => {
17199
- useStoryStore.setState((state) => {
17200
- setByPath(state.variables, segments, value, { createMissing: true });
17392
+ mutateState((work, adopt) => {
17393
+ setByPath(work.variables, segments, adopt(value), { createMissing: true });
17201
17394
  });
17202
17395
  };
17203
17396
  }
@@ -17338,6 +17531,12 @@ function createStoryAPI() {
17338
17531
  const { isTransient, key } = parseName(name);
17339
17532
  warnIfUndeclared(isTransient, key);
17340
17533
  }
17534
+ if (entries.some(([k]) => k.includes("."))) {
17535
+ mutateState((work, adopt) => {
17536
+ for (const [k, v] of entries) setOne(work, k, adopt(v));
17537
+ });
17538
+ return;
17539
+ }
17341
17540
  useStoryStore.getState().updateVariables((draft) => {
17342
17541
  for (const [k, v] of entries) setOne(draft, k, v);
17343
17542
  });
@@ -19729,13 +19928,16 @@ defineMacro({
19729
19928
  }
19730
19929
  const passage = storyData?.passages.get(passageName);
19731
19930
  const ast = ctx.hooks.useMemo(() => passage ? parseMarkup(passage.content) : null, [passage]);
19931
+ const included = !failure && !!passage;
19932
+ ctx.hooks.useEffect(() => {
19933
+ if (included) useStoryStore.getState().trackRender(passageName);
19934
+ }, [included, passageName]);
19732
19935
  if (!storyData) return null;
19733
19936
  if (failure) return /* @__PURE__ */ jsx(MacroError, {
19734
19937
  macro: "include",
19735
19938
  error: failure.error
19736
19939
  });
19737
19940
  if (!passage || !ast) return null;
19738
- useStoryStore.getState().trackRender(passageName);
19739
19941
  const nobr = passage.tags.includes("nobr");
19740
19942
  const content = inline ? ctx.renderInlineNodes(ast) : ctx.renderNodes(ast, nobr ? { nobr: true } : void 0);
19741
19943
  if (nobr) return ctx.wrap(ctx.h(NobrContext.Provider, { value: true }, content));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.59.3",
3
+ "version": "0.59.5",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",