@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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +318 -116
- package/package.json +1 -1
- package/src/components/macros/Include.tsx +7 -2
- package/src/define-macro.ts +6 -3
- package/src/execute-mutation.ts +192 -123
- package/src/saves/save-manager.ts +6 -6
- package/src/saves/storage.ts +24 -1
- package/src/store.ts +71 -35
- package/src/story-api.ts +15 -1
- package/src/structural.ts +183 -35
- package/src/utils/object-path.ts +3 -5
package/dist/pkg/headless.js
CHANGED
|
@@ -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
|
|
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) ?
|
|
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)
|
|
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,
|
|
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
|
-
*
|
|
13697
|
-
* depth-first order. Plain objects
|
|
13698
|
-
*
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
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)
|
|
13748
|
+
if (!isObjectValue(value)) continue;
|
|
13706
13749
|
const at = [...path, key];
|
|
13707
13750
|
if (skip.has(pathKey(at))) continue;
|
|
13708
|
-
|
|
13709
|
-
|
|
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
|
-
}
|
|
13712
|
-
return
|
|
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 (
|
|
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
|
-
|
|
13784
|
-
|
|
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
|
|
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
|
|
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 =
|
|
14161
|
-
var SLOT_KEY_PREFIX =
|
|
14162
|
-
var SLOT_INDEX_KEY_PREFIX =
|
|
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,
|
|
14601
|
-
const root = change.path
|
|
14602
|
-
for (const scope of scopes) for (const copy of [scope.work
|
|
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.
|
|
14828
|
+
const changes = NAMESPACES.flatMap((ns) => {
|
|
14711
14829
|
const own = patches.filter((p) => p.path[0] === ns);
|
|
14712
|
-
return
|
|
14830
|
+
return own.length ? writtenPaths(view[ns], next[ns], own).map((change) => ({
|
|
14831
|
+
...change,
|
|
14832
|
+
path: [ns, ...change.path]
|
|
14833
|
+
})) : [];
|
|
14713
14834
|
});
|
|
14714
|
-
const
|
|
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
|
|
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
|
|
14728
|
-
const root = change.path
|
|
14849
|
+
for (const change of changes) {
|
|
14850
|
+
const [ns, root] = change.path;
|
|
14729
14851
|
try {
|
|
14730
|
-
applyChange(namespaces
|
|
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,
|
|
14857
|
+
mirror(namespaces, change, activeScopes, cloners);
|
|
14736
14858
|
}
|
|
14737
14859
|
};
|
|
14738
14860
|
}
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
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:
|
|
14916
|
+
changes: changesOf(scope.base, scope.work)
|
|
14764
14917
|
}));
|
|
14765
|
-
const changed = all.filter(({ changes }) => changes.
|
|
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 =
|
|
14777
|
-
const own =
|
|
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 =
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
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
|
|
14808
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
15598
|
-
*
|
|
15599
|
-
*
|
|
15600
|
-
*
|
|
15601
|
-
*
|
|
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
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
}
|
|
15625
|
-
|
|
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
|
-
|
|
17200
|
-
setByPath(
|
|
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));
|