@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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +277 -113
- package/package.json +1 -1
- package/src/class-registry.ts +12 -0
- package/src/components/App.tsx +3 -5
- package/src/components/macros/Computed.tsx +3 -2
- package/src/components/macros/PassageDisplay.tsx +4 -3
- package/src/components/macros/SaveManager.tsx +3 -5
- package/src/components/macros/Unset.tsx +5 -1
- package/src/execute-mutation.ts +59 -21
- package/src/index.tsx +22 -7
- package/src/saves/save-manager.ts +9 -2
- package/src/store.ts +19 -8
- package/src/structural.ts +231 -115
- package/src/utils/object-path.ts +6 -3
package/dist/pkg/headless.js
CHANGED
|
@@ -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
|
|
13253
|
-
if (seg in Object.prototype && !hasOwn(current, seg)) return
|
|
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))
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
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++)
|
|
13660
|
-
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
|
|
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))
|
|
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)
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
13780
|
-
* object than `x` did
|
|
13781
|
-
* `$map.set("k", $a)` over an equal object
|
|
13782
|
-
*
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
const
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
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
|
|
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 &&
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
14998
|
+
const object = getByEntryPath(draft, first);
|
|
14874
14999
|
if (object === null || typeof object !== "object") continue;
|
|
14875
|
-
for (const path of others)
|
|
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, {
|
|
14887
|
-
|
|
14888
|
-
|
|
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).
|
|
14973
|
-
* the code
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
15809
|
-
else writeKey(holder, key,
|
|
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,
|
|
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)
|
|
15815
|
-
else
|
|
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
|
|
16615
|
-
value:
|
|
16771
|
+
return /* @__PURE__ */ jsx(NobrContext.Provider, {
|
|
16772
|
+
value: nobr,
|
|
16616
16773
|
children: content
|
|
16617
|
-
})
|
|
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 (!
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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);
|