@rohal12/spindle 0.59.2 → 0.59.4

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,
@@ -13693,23 +13710,42 @@ function underChange(path, keys) {
13693
13710
  return false;
13694
13711
  }
13695
13712
  /**
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.
13713
+ * Every path each object (or array, or other value) of `root` is at, the
13714
+ * first appearance first, in depth-first order. Plain objects and arrays are
13715
+ * entered, where first met only (an object held in an array is one object,
13716
+ * however it was reached); other values are leaves. A path at or below one
13717
+ * of `skip` is left out.
13699
13718
  */
13700
- function firstPaths(root, skip = /* @__PURE__ */ new Set()) {
13701
- const found = /* @__PURE__ */ new Map();
13719
+ function objectPaths(root, skip = /* @__PURE__ */ new Set()) {
13720
+ const all = /* @__PURE__ */ new Map();
13702
13721
  (function walk(node, path) {
13703
13722
  for (const key of Object.keys(node)) {
13704
13723
  const value = node[key];
13705
- if (!isObjectValue(value) || found.has(value)) continue;
13724
+ if (!isObjectValue(value)) continue;
13706
13725
  const at = [...path, key];
13707
13726
  if (skip.has(pathKey(at))) continue;
13708
- found.set(value, at);
13709
- if (isMergeable(value)) walk(value, at);
13727
+ const known = all.get(value);
13728
+ if (known) {
13729
+ known.push(at);
13730
+ continue;
13731
+ }
13732
+ all.set(value, [at]);
13733
+ if (isMergeable(value) || Array.isArray(value)) walk(value, at);
13710
13734
  }
13711
13735
  })(root, []);
13712
- return found;
13736
+ return all;
13737
+ }
13738
+ /** Where each object of `root` first appears (see objectPaths). */
13739
+ function firstPaths(root, skip) {
13740
+ return new Map([...objectPaths(root, skip)].map(([object, paths]) => [object, paths[0]]));
13741
+ }
13742
+ /**
13743
+ * The objects `root` holds at more than one path (an object two variables
13744
+ * refer to, or one that refers to itself), with every path they are at,
13745
+ * parents before children.
13746
+ */
13747
+ function sharedPaths(root) {
13748
+ return [...objectPaths(root).values()].filter((paths) => paths.length > 1);
13713
13749
  }
13714
13750
  /**
13715
13751
  * The paths whose object is another one in the references of `after` than in
@@ -13739,7 +13775,7 @@ function aliasChanges(before, after) {
13739
13775
  value: y,
13740
13776
  alias: true
13741
13777
  });
13742
- if (isMergeable(x) && mergesWith(x, y, false) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
13778
+ if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
13743
13779
  }
13744
13780
  })(before, after, []);
13745
13781
  return changes;
@@ -13780,8 +13816,9 @@ function isApplied(target, change) {
13780
13816
  return !change.deleted && deepEqual(parent[key], change.value);
13781
13817
  }
13782
13818
  function applyChange(target, change) {
13783
- if (change.deleted) deleteByPath(target, change.path);
13784
- else setByPath(target, change.path, change.value);
13819
+ const root = target;
13820
+ if (change.deleted) deleteByPath(root, change.path);
13821
+ else setByPath(root, change.path, change.value);
13785
13822
  }
13786
13823
  /**
13787
13824
  * Write how `after` differs from `before` (see keyChanges; arrays merge by
@@ -13931,7 +13968,7 @@ async function startNewPlaythroughNow(ifid, id = randomUUID()) {
13931
13968
  }
13932
13969
  var PLAYTHROUGH_LABEL = /^Playthrough (\d+)$/;
13933
13970
  function playthroughCountKey(ifid) {
13934
- return `playthroughCount.${ifid}`;
13971
+ return `${META_PREFIXES.playthroughCount}${ifid}`;
13935
13972
  }
13936
13973
  /**
13937
13974
  * The number of the next playthrough the story starts: one past the highest
@@ -13950,7 +13987,7 @@ async function nextPlaythroughNumber(ifid) {
13950
13987
  return highest + 1;
13951
13988
  }
13952
13989
  function currentPlaythroughKey(ifid) {
13953
- return `currentPlaythroughId.${ifid}`;
13990
+ return `${META_PREFIXES.currentPlaythrough}${ifid}`;
13954
13991
  }
13955
13992
  /**
13956
13993
  * Make `id` the story's current playthrough: the playthrough of a save the
@@ -14157,9 +14194,9 @@ var getSavesGrouped = queued(async function getSavesGroupedNow(ifid) {
14157
14194
  });
14158
14195
  return result;
14159
14196
  });
14160
- var AUTOSAVE_KEY_PREFIX = "autosave.";
14161
- var SLOT_KEY_PREFIX = "slot.";
14162
- var SLOT_INDEX_KEY_PREFIX = "slotIndex.";
14197
+ var AUTOSAVE_KEY_PREFIX = META_PREFIXES.autosave;
14198
+ var SLOT_KEY_PREFIX = META_PREFIXES.slot;
14199
+ var SLOT_INDEX_KEY_PREFIX = META_PREFIXES.slotIndex;
14163
14200
  /**
14164
14201
  * Whether `slot` names a slot. The default (autosave) slot is addressed by
14165
14202
  * omitting `slot` or by `''`, the identifier its SaveInfo reports.
@@ -14597,17 +14634,17 @@ function frozenCopy(value) {
14597
14634
  * path (the code replaced or deleted an intermediate object), its root is
14598
14635
  * replaced with the store's root as written.
14599
14636
  */
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 {
14637
+ function mirror(draft, change, scopes = activeScopes, cloner = clonerOf) {
14638
+ const [ns, root] = change.path;
14639
+ for (const scope of scopes) for (const copy of [scope.work, scope.base]) try {
14603
14640
  applyChange(copy, change.deleted ? change : {
14604
14641
  ...change,
14605
14642
  value: cloner(copy)(change.value)
14606
14643
  });
14607
14644
  } catch {
14608
14645
  const stored = draft[ns][root];
14609
- if (hasOwn(draft[ns], root)) copy[root] = cloneValue(isDraft(stored) ? current(stored) : stored);
14610
- else delete copy[root];
14646
+ if (hasOwn(draft[ns], root)) copy[ns][root] = cloneValue(isDraft(stored) ? current(stored) : stored);
14647
+ else delete copy[ns][root];
14611
14648
  }
14612
14649
  }
14613
14650
  /** A cloner of a copy of its own (no references shared with others). */
@@ -14707,16 +14744,19 @@ function routeStoreUpdate(recipe) {
14707
14744
  transient: inner.work.transient
14708
14745
  };
14709
14746
  const [next, patches] = scratch.produceWithPatches(view, recipe);
14710
- const changes = NAMESPACES.map((ns) => {
14747
+ const changes = NAMESPACES.flatMap((ns) => {
14711
14748
  const own = patches.filter((p) => p.path[0] === ns);
14712
- return [ns, own.length ? writtenPaths(view[ns], next[ns], own) : []];
14749
+ return own.length ? writtenPaths(view[ns], next[ns], own).map((change) => ({
14750
+ ...change,
14751
+ path: [ns, ...change.path]
14752
+ })) : [];
14713
14753
  });
14714
14754
  const cloners = clonersFor();
14715
14755
  const owned = (change, target) => change.deleted ? change : {
14716
14756
  ...change,
14717
14757
  value: cloners(target)(change.value)
14718
14758
  };
14719
- for (const [ns, list] of changes) for (const change of list) applyChange(inner.work[ns], owned(change, inner.work[ns]));
14759
+ for (const change of changes) applyChange(inner.work, owned(change, inner.work));
14720
14760
  return (draft) => {
14721
14761
  const target = draft;
14722
14762
  for (const key of Object.keys(next)) {
@@ -14724,23 +14764,48 @@ function routeStoreUpdate(recipe) {
14724
14764
  if (!NAMESPACE_KEYS$1.has(key) && value !== view[key]) target[key] = value;
14725
14765
  }
14726
14766
  const namespaces = draft;
14727
- for (const [ns, list] of changes) for (const change of list) {
14728
- const root = change.path[0];
14767
+ for (const change of changes) {
14768
+ const [ns, root] = change.path;
14729
14769
  try {
14730
- applyChange(namespaces[ns], owned(change, namespaces[ns]));
14770
+ applyChange(namespaces, owned(change, namespaces));
14731
14771
  } catch {
14732
14772
  if (hasOwn(inner.work[ns], root)) namespaces[ns][root] = cloneValue(inner.work[ns][root]);
14733
14773
  else delete namespaces[ns][root];
14734
14774
  }
14735
- mirror(namespaces, ns, change, activeScopes, cloners);
14775
+ mirror(namespaces, change, activeScopes, cloners);
14736
14776
  }
14737
14777
  };
14738
14778
  }
14739
- var cloneNamespaces = (from) => ({
14740
- variables: deepClone(from.variables),
14741
- temporary: deepClone(from.temporary),
14742
- transient: deepClone(from.transient)
14743
- });
14779
+ /**
14780
+ * Make the objects `work` holds at several paths one object in `draft`
14781
+ * again where `changes` wrote below one of them: a write through one path
14782
+ * of a draft gives that path a new object, and the other paths would go on
14783
+ * referring to the old one (`$a.n = 2` with `$b` the same object as `$a`).
14784
+ */
14785
+ function relink(draft, work, changes) {
14786
+ const written = changes.map((c) => pathKey(c.path).slice(0, -1));
14787
+ for (const [first, ...others] of sharedPaths(work)) {
14788
+ const prefixes = [first, ...others].map((p) => pathKey(p).slice(0, -1));
14789
+ if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
14790
+ try {
14791
+ const object = getByPath(draft, first);
14792
+ if (object === null || typeof object !== "object") continue;
14793
+ for (const path of others) setByPath(draft, path, object);
14794
+ } catch {}
14795
+ }
14796
+ }
14797
+ /**
14798
+ * Copies of the namespaces that keep their references to each other: an
14799
+ * object held in two of them (`%copy = $a`) is one object in the copies too.
14800
+ */
14801
+ function cloneNamespaces(from) {
14802
+ const seen = /* @__PURE__ */ new Map();
14803
+ return {
14804
+ variables: deepClone(from.variables, { seen }),
14805
+ temporary: deepClone(from.temporary, { seen }),
14806
+ transient: deepClone(from.transient, { seen })
14807
+ };
14808
+ }
14744
14809
  /**
14745
14810
  * Commit the property paths each mutation's code changed (work vs base) on
14746
14811
  * top of the current store state, so writes made elsewhere during execution
@@ -14760,9 +14825,9 @@ function commitScopes(commits) {
14760
14825
  const all = commits.map(({ scope, keepRunning }) => ({
14761
14826
  scope,
14762
14827
  keepRunning,
14763
- changes: NAMESPACES.map((ns) => [ns, changesOf(scope.base[ns], scope.work[ns])])
14828
+ changes: changesOf(scope.base, scope.work)
14764
14829
  }));
14765
- const changed = all.filter(({ changes }) => changes.some(([, list]) => list.length > 0));
14830
+ const changed = all.filter(({ changes }) => changes.length > 0);
14766
14831
  if (changed.length === 0) return;
14767
14832
  for (const { scope, keepRunning } of changed) if (keepRunning) scope.base = cloneNamespaces(scope.work);
14768
14833
  committing = true;
@@ -14773,46 +14838,50 @@ function commitScopes(commits) {
14773
14838
  }
14774
14839
  function commitRecipe(draft) {
14775
14840
  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))]));
14841
+ const paths = locateObjects(scope.work, changes);
14842
+ const own = clonerFor(draft, paths);
14778
14843
  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);
14844
+ const enclosingCloners = clonersFor(() => paths);
14845
+ const replaced = /* @__PURE__ */ new Set();
14846
+ for (const change of changes) {
14847
+ const [ns, root] = change.path;
14848
+ const rootKey = pathKey([ns, root]);
14849
+ if (replaced.has(rootKey)) continue;
14850
+ try {
14851
+ if (change.deleted) {
14852
+ if (!isApplied(draft, change)) applyChange(draft, change);
14853
+ continue;
14799
14854
  }
14855
+ const value = change.alias ? own(change.value) : void 0;
14856
+ if (change.alias ? Object.is(getByPath(draft, change.path), value) : isApplied(draft, change)) continue;
14857
+ applyChange(draft, {
14858
+ ...change,
14859
+ value: change.alias ? value : own(change.value)
14860
+ });
14861
+ } catch {
14862
+ draft[ns][root] = own(scope.work[ns][root]);
14863
+ replaced.add(rootKey);
14800
14864
  }
14801
- for (const change of list) mirror(draft, ns, change, enclosing, enclosingCloners.get(ns));
14802
14865
  }
14866
+ relink(draft, scope.work, changes);
14867
+ for (const change of changes) mirror(draft, change, enclosing, enclosingCloners);
14803
14868
  });
14804
14869
  }
14805
14870
  }
14806
14871
  /**
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.
14872
+ * The changes of the code to the namespaces, as paths below them (`['variables',
14873
+ * 'a']`): the property paths that differ (see diffPaths), and those that hold
14874
+ * equal content but another object than they did (see aliasChanges), those
14875
+ * first. Compared as one object, so an object the namespaces share is one
14876
+ * object, whichever of them it is reached through.
14810
14877
  */
14811
14878
  function changesOf(base, work) {
14812
- const written = diffPaths(base, work);
14879
+ const before = base;
14880
+ const after = work;
14881
+ const written = diffPaths(before, after);
14813
14882
  const keys = new Set(written.map((c) => pathKey(c.path)));
14814
14883
  const aliases = [];
14815
- for (const change of aliasChanges(base, work)) {
14884
+ for (const change of aliasChanges(before, after)) {
14816
14885
  if (underChange(change.path, keys)) continue;
14817
14886
  keys.add(pathKey(change.path));
14818
14887
  aliases.push(change);
@@ -14853,14 +14922,38 @@ function runWithCommittedMutations(action) {
14853
14922
  for (const scope of suspended) resync(scope);
14854
14923
  }
14855
14924
  }
14856
- function executeMutation(code, mergedLocals, scopeUpdate) {
14925
+ /**
14926
+ * Run `run` on working copies of the story state and commit what it changed
14927
+ * to the store, as mutation code does (see commitScopes): the references
14928
+ * between values are kept, which a store update made on a draft does not do.
14929
+ * Nothing is committed when `run` throws.
14930
+ */
14931
+ function mutateState(run) {
14932
+ commitScopes(running());
14933
+ const scope = startScope();
14934
+ try {
14935
+ run(scope.work);
14936
+ } finally {
14937
+ activeScopes.pop();
14938
+ }
14939
+ commitScopes([...running(), {
14940
+ scope,
14941
+ keepRunning: false
14942
+ }]);
14943
+ }
14944
+ /** Start a mutation, from the pending state of the one it runs inside. */
14945
+ function startScope() {
14857
14946
  const start = getActiveMutationScope() ?? useStoryStore.getState();
14858
14947
  const scope = {
14859
14948
  work: cloneNamespaces(start),
14860
14949
  base: cloneNamespaces(start)
14861
14950
  };
14862
- const localsClone = asNamespace(deepClone(mergedLocals, { keepUnregistered: true }));
14863
14951
  activeScopes.push(scope);
14952
+ return scope;
14953
+ }
14954
+ function executeMutation(code, mergedLocals, scopeUpdate) {
14955
+ const localsClone = asNamespace(deepClone(mergedLocals, { keepUnregistered: true }));
14956
+ const scope = startScope();
14864
14957
  try {
14865
14958
  execute(code, scope.work.variables, scope.work.temporary, localsClone, scope.work.transient);
14866
14959
  } finally {
@@ -17338,6 +17431,12 @@ function createStoryAPI() {
17338
17431
  const { isTransient, key } = parseName(name);
17339
17432
  warnIfUndeclared(isTransient, key);
17340
17433
  }
17434
+ if (entries.some(([k]) => k.includes("."))) {
17435
+ mutateState((work) => {
17436
+ for (const [k, v] of entries) setOne(work, k, v);
17437
+ });
17438
+ return;
17439
+ }
17341
17440
  useStoryStore.getState().updateVariables((draft) => {
17342
17441
  for (const [k, v] of entries) setOne(draft, k, v);
17343
17442
  });
@@ -19729,13 +19828,16 @@ defineMacro({
19729
19828
  }
19730
19829
  const passage = storyData?.passages.get(passageName);
19731
19830
  const ast = ctx.hooks.useMemo(() => passage ? parseMarkup(passage.content) : null, [passage]);
19831
+ const included = !failure && !!passage;
19832
+ ctx.hooks.useEffect(() => {
19833
+ if (included) useStoryStore.getState().trackRender(passageName);
19834
+ }, [included, passageName]);
19732
19835
  if (!storyData) return null;
19733
19836
  if (failure) return /* @__PURE__ */ jsx(MacroError, {
19734
19837
  macro: "include",
19735
19838
  error: failure.error
19736
19839
  });
19737
19840
  if (!passage || !ast) return null;
19738
- useStoryStore.getState().trackRender(passageName);
19739
19841
  const nobr = passage.tags.includes("nobr");
19740
19842
  const content = inline ? ctx.renderInlineNodes(ast) : ctx.renderNodes(ast, nobr ? { nobr: true } : void 0);
19741
19843
  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.2",
3
+ "version": "0.59.4",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -38,6 +38,13 @@ defineMacro({
38
38
  [passage],
39
39
  );
40
40
 
41
+ // Count the inclusion when its content mounts or the passage changes,
42
+ // not on every render (an unrelated variable update re-renders it)
43
+ const included = !failure && !!passage;
44
+ ctx.hooks.useEffect(() => {
45
+ if (included) useStoryStore.getState().trackRender(passageName);
46
+ }, [included, passageName]);
47
+
41
48
  if (!storyData) return null;
42
49
  if (failure) {
43
50
  return (
@@ -49,8 +56,6 @@ defineMacro({
49
56
  }
50
57
  if (!passage || !ast) return null;
51
58
 
52
- useStoryStore.getState().trackRender(passageName);
53
-
54
59
  const nobr = passage.tags.includes('nobr');
55
60
  const content = inline
56
61
  ? ctx.renderInlineNodes(ast)