@rohal12/spindle 0.59.15 → 0.59.16
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/README.md +1 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +281 -87
- package/dist/pkg/story-variables.js +36 -3
- package/package.json +1 -1
- package/src/components/macros/For.tsx +16 -9
- package/src/components/macros/Watch.tsx +24 -4
- package/src/components/macros/detached-body.tsx +17 -6
- package/src/define-macro.ts +11 -2
- package/src/runtime-errors.ts +4 -8
- package/src/saves/save-manager.ts +63 -0
- package/src/store.ts +42 -8
- package/src/story-api.ts +4 -3
- package/src/story-variables.ts +15 -3
- package/src/triggers.ts +17 -0
- package/src/utils/control-edits.ts +81 -30
- package/src/utils/listeners.ts +23 -0
- package/src/utils/object-path.ts +18 -6
- package/src/utils/stable-key.ts +73 -44
- package/types/index.d.ts +4 -1
- package/types/tooling.d.ts +5 -1
package/dist/pkg/headless.js
CHANGED
|
@@ -10286,13 +10286,35 @@ function parseDelay(str) {
|
|
|
10286
10286
|
return parseFloat(trimmed);
|
|
10287
10287
|
}
|
|
10288
10288
|
//#endregion
|
|
10289
|
+
//#region src/utils/listeners.ts
|
|
10290
|
+
/**
|
|
10291
|
+
* A set of listeners to call when something happens: `subscribe` adds one and
|
|
10292
|
+
* returns the function that removes it, `notify` calls each one. A listener
|
|
10293
|
+
* removed or added while they are called is called or not as the set was
|
|
10294
|
+
* when `notify` started.
|
|
10295
|
+
*/
|
|
10296
|
+
function createListeners() {
|
|
10297
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
10298
|
+
return {
|
|
10299
|
+
subscribe(listener) {
|
|
10300
|
+
listeners.add(listener);
|
|
10301
|
+
return () => {
|
|
10302
|
+
listeners.delete(listener);
|
|
10303
|
+
};
|
|
10304
|
+
},
|
|
10305
|
+
notify() {
|
|
10306
|
+
for (const listener of [...listeners]) listener();
|
|
10307
|
+
}
|
|
10308
|
+
};
|
|
10309
|
+
}
|
|
10310
|
+
//#endregion
|
|
10289
10311
|
//#region src/runtime-errors.ts
|
|
10290
10312
|
var errors = [];
|
|
10291
10313
|
var nextId = 1;
|
|
10292
|
-
var listeners$1 =
|
|
10314
|
+
var listeners$1 = createListeners();
|
|
10293
10315
|
function update(next) {
|
|
10294
10316
|
errors = next;
|
|
10295
|
-
|
|
10317
|
+
listeners$1.notify();
|
|
10296
10318
|
}
|
|
10297
10319
|
/**
|
|
10298
10320
|
* Show `error` on the page, after `context` (what failed). The same error
|
|
@@ -10328,12 +10350,7 @@ function getRuntimeErrors() {
|
|
|
10328
10350
|
return errors;
|
|
10329
10351
|
}
|
|
10330
10352
|
/** Call `listener` whenever the shown errors change; returns unsubscribe. */
|
|
10331
|
-
|
|
10332
|
-
listeners$1.add(listener);
|
|
10333
|
-
return () => {
|
|
10334
|
-
listeners$1.delete(listener);
|
|
10335
|
-
};
|
|
10336
|
-
}
|
|
10353
|
+
var subscribeRuntimeErrors = listeners$1.subscribe;
|
|
10337
10354
|
//#endregion
|
|
10338
10355
|
//#region src/components/macros/arg-utils.ts
|
|
10339
10356
|
/**
|
|
@@ -11054,7 +11071,7 @@ function getPRNGPull() {
|
|
|
11054
11071
|
* (see src/tooling.ts).
|
|
11055
11072
|
*/
|
|
11056
11073
|
var TRANSFORM_CACHE_MAX = 500;
|
|
11057
|
-
var NAMESPACES$
|
|
11074
|
+
var NAMESPACES$2 = {
|
|
11058
11075
|
$: "variables",
|
|
11059
11076
|
_: "temporary",
|
|
11060
11077
|
"@": "locals",
|
|
@@ -11087,7 +11104,7 @@ function transform(expr, goal = "expression") {
|
|
|
11087
11104
|
result += expr.slice(at, ref.start);
|
|
11088
11105
|
if (ref.shorthand) result += `${ref.name}: `;
|
|
11089
11106
|
else if (IDENT_END_RE.test(result.slice(-2))) result += " ";
|
|
11090
|
-
result += `${NAMESPACES$
|
|
11107
|
+
result += `${NAMESPACES$2[ref.sigil]}["${ref.name}"]`;
|
|
11091
11108
|
at = ref.end;
|
|
11092
11109
|
}
|
|
11093
11110
|
result += expr.slice(at);
|
|
@@ -14437,10 +14454,12 @@ function shallowCopy(value) {
|
|
|
14437
14454
|
* written in place.
|
|
14438
14455
|
*
|
|
14439
14456
|
* The path goes through own properties of plain objects, class instances
|
|
14440
|
-
* and arrays (by index)
|
|
14441
|
-
*
|
|
14442
|
-
*
|
|
14443
|
-
*
|
|
14457
|
+
* and arrays (by index), and, outside a draft, any key of an instance of a
|
|
14458
|
+
* registered subclass of a built-in (#408); anything else throws a
|
|
14459
|
+
* TypeError rather than writing where no clone or save would see it: an
|
|
14460
|
+
* inherited property such as a method counts as missing, and Map, Set,
|
|
14461
|
+
* Date and RegExp values and other keys of arrays are refused, as is a
|
|
14462
|
+
* "__proto__" segment.
|
|
14444
14463
|
*/
|
|
14445
14464
|
function setByPath(root, segments, value, options = {}) {
|
|
14446
14465
|
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
@@ -14475,8 +14494,15 @@ function deleteByPath(root, segments) {
|
|
|
14475
14494
|
function checkSegments(segments) {
|
|
14476
14495
|
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
14477
14496
|
}
|
|
14478
|
-
/**
|
|
14479
|
-
|
|
14497
|
+
/**
|
|
14498
|
+
* Throw unless `holder` can take `key` as story state (see setByPath). An
|
|
14499
|
+
* instance of a registered subclass of a built-in keeps its own fields
|
|
14500
|
+
* through clones and saves, so it takes any key (#408) when written in
|
|
14501
|
+
* place: a draft of one, or one a draft shares, is refused as other
|
|
14502
|
+
* built-ins are, and the caller writes the whole value instead.
|
|
14503
|
+
*/
|
|
14504
|
+
function checkHolder(holder, key, segments, depth, inPlace) {
|
|
14505
|
+
if (inPlace && registeredClassName(holder) !== void 0) return;
|
|
14480
14506
|
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
14481
14507
|
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
14482
14508
|
}
|
|
@@ -14492,7 +14518,7 @@ function walkToParent(root, segments, createMissing) {
|
|
|
14492
14518
|
let current = root;
|
|
14493
14519
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
14494
14520
|
const seg = segments[i];
|
|
14495
|
-
checkHolder(current, seg, segments, i);
|
|
14521
|
+
checkHolder(current, seg, segments, i, !copyOnWrite);
|
|
14496
14522
|
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
14497
14523
|
if (next == null || typeof next !== "object") {
|
|
14498
14524
|
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
@@ -14504,7 +14530,7 @@ function walkToParent(root, segments, createMissing) {
|
|
|
14504
14530
|
}
|
|
14505
14531
|
current = next;
|
|
14506
14532
|
}
|
|
14507
|
-
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
14533
|
+
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1, !copyOnWrite);
|
|
14508
14534
|
return current;
|
|
14509
14535
|
}
|
|
14510
14536
|
//#endregion
|
|
@@ -15659,11 +15685,13 @@ async function slotRecord(ifid, slot) {
|
|
|
15659
15685
|
async function fillSlot(ifid, slot, saveId) {
|
|
15660
15686
|
await (await getBackend()).setMeta(slotMetaKey(ifid, slot), saveId);
|
|
15661
15687
|
if (isNamedSlot(slot)) await updateSlotIndex(ifid, (slots) => slots.includes(slot) ? slots : [...slots, slot]);
|
|
15688
|
+
announceSlots(ifid);
|
|
15662
15689
|
}
|
|
15663
15690
|
/** Clear a slot: its pointer, and its entry in the slot index. */
|
|
15664
15691
|
async function emptySlot(ifid, slot) {
|
|
15665
15692
|
await (await getBackend()).deleteMeta(slotMetaKey(ifid, slot));
|
|
15666
15693
|
if (isNamedSlot(slot)) await updateSlotIndex(ifid, (slots) => slots.filter((s) => s !== slot));
|
|
15694
|
+
announceSlots(ifid);
|
|
15667
15695
|
}
|
|
15668
15696
|
/** Clear every slot holding a save whose ID passes `test`. */
|
|
15669
15697
|
async function emptySlotsHolding(ifid, test) {
|
|
@@ -15672,6 +15700,46 @@ async function emptySlotsHolding(ifid, test) {
|
|
|
15672
15700
|
if (id && test(id)) await emptySlot(ifid, slot);
|
|
15673
15701
|
}
|
|
15674
15702
|
}
|
|
15703
|
+
var SLOTS_CHANNEL = "spindle.slots";
|
|
15704
|
+
/** Every story's slots: what clearing all data announces. */
|
|
15705
|
+
var ALL_STORIES = "*";
|
|
15706
|
+
var slotsChannel;
|
|
15707
|
+
function getSlotsChannel() {
|
|
15708
|
+
if (slotsChannel === void 0) try {
|
|
15709
|
+
slotsChannel = new BroadcastChannel(SLOTS_CHANNEL);
|
|
15710
|
+
slotsChannel.unref?.();
|
|
15711
|
+
} catch {
|
|
15712
|
+
slotsChannel = null;
|
|
15713
|
+
}
|
|
15714
|
+
return slotsChannel;
|
|
15715
|
+
}
|
|
15716
|
+
/** Tell the story's other tabs that its slots changed. */
|
|
15717
|
+
function announceSlots(ifid) {
|
|
15718
|
+
try {
|
|
15719
|
+
getSlotsChannel()?.postMessage(ifid);
|
|
15720
|
+
} catch {}
|
|
15721
|
+
}
|
|
15722
|
+
/**
|
|
15723
|
+
* Call `onChange` when another tab may have filled or emptied the story's
|
|
15724
|
+
* slots: when it says so, and when this tab is shown again. Returns the
|
|
15725
|
+
* function that stops it.
|
|
15726
|
+
*/
|
|
15727
|
+
function watchSlotChanges(ifid, onChange) {
|
|
15728
|
+
const channel = getSlotsChannel();
|
|
15729
|
+
const onMessage = (event) => {
|
|
15730
|
+
if (event.data === ifid || event.data === ALL_STORIES) onChange();
|
|
15731
|
+
};
|
|
15732
|
+
const onShown = () => {
|
|
15733
|
+
if (document.visibilityState === "visible") onChange();
|
|
15734
|
+
};
|
|
15735
|
+
const doc = typeof document === "undefined" ? void 0 : document;
|
|
15736
|
+
channel?.addEventListener("message", onMessage);
|
|
15737
|
+
doc?.addEventListener("visibilitychange", onShown);
|
|
15738
|
+
return () => {
|
|
15739
|
+
channel?.removeEventListener("message", onMessage);
|
|
15740
|
+
doc?.removeEventListener("visibilitychange", onShown);
|
|
15741
|
+
};
|
|
15742
|
+
}
|
|
15675
15743
|
/** The `custom` keys that say which slot a save lives in. */
|
|
15676
15744
|
var slotCustom = (slot) => ({
|
|
15677
15745
|
isAutosave: !isNamedSlot(slot),
|
|
@@ -15925,6 +15993,7 @@ function clearGameData(ifid) {
|
|
|
15925
15993
|
await backend.deletePlaythroughsByIfid(ifid);
|
|
15926
15994
|
await backend.deleteMetaByIfid(ifid);
|
|
15927
15995
|
clearSession(ifid);
|
|
15996
|
+
announceSlots(ifid);
|
|
15928
15997
|
});
|
|
15929
15998
|
}
|
|
15930
15999
|
var clearAllData = queued(clearAllDataNow);
|
|
@@ -15939,6 +16008,7 @@ async function clearAllDataNow() {
|
|
|
15939
16008
|
}
|
|
15940
16009
|
for (const key of toRemove) sessionStorage.removeItem(key);
|
|
15941
16010
|
} catch {}
|
|
16011
|
+
announceSlots(ALL_STORIES);
|
|
15942
16012
|
}
|
|
15943
16013
|
/**
|
|
15944
16014
|
* Delete a playthrough with its saves, emptying the slots that held them.
|
|
@@ -16023,7 +16093,7 @@ function initializeStory(session) {
|
|
|
16023
16093
|
}
|
|
16024
16094
|
//#endregion
|
|
16025
16095
|
//#region src/execute-mutation.ts
|
|
16026
|
-
var NAMESPACES = [
|
|
16096
|
+
var NAMESPACES$1 = [
|
|
16027
16097
|
"variables",
|
|
16028
16098
|
"temporary",
|
|
16029
16099
|
"transient"
|
|
@@ -16146,7 +16216,7 @@ function mirror(draft, change, scopes = activeScopes, cloner = clonerOf) {
|
|
|
16146
16216
|
}
|
|
16147
16217
|
/** A cloner of a copy of its own (no references shared with others). */
|
|
16148
16218
|
var clonerOf = (target) => clonerFor(target);
|
|
16149
|
-
var NAMESPACE_KEYS$1 = new Set(NAMESPACES);
|
|
16219
|
+
var NAMESPACE_KEYS$1 = new Set(NAMESPACES$1);
|
|
16150
16220
|
/**
|
|
16151
16221
|
* Runs store updates on the running code's working copies. They stay the
|
|
16152
16222
|
* code's own, so nothing may freeze them.
|
|
@@ -16241,7 +16311,7 @@ function routeStoreUpdate(recipe) {
|
|
|
16241
16311
|
transient: inner.work.transient
|
|
16242
16312
|
};
|
|
16243
16313
|
const [next, patches] = scratch.produceWithPatches(view, recipe);
|
|
16244
|
-
const changes = NAMESPACES.flatMap((ns) => {
|
|
16314
|
+
const changes = NAMESPACES$1.flatMap((ns) => {
|
|
16245
16315
|
const own = patches.filter((p) => p.path[0] === ns);
|
|
16246
16316
|
return own.length ? writtenPaths(view[ns], next[ns], own).map((change) => ({
|
|
16247
16317
|
...change,
|
|
@@ -16659,8 +16729,21 @@ function reinitTriggerState() {
|
|
|
16659
16729
|
*/
|
|
16660
16730
|
function resetTriggers() {
|
|
16661
16731
|
triggers = [];
|
|
16732
|
+
resets++;
|
|
16662
16733
|
closeAllOpenDialogs();
|
|
16734
|
+
resetListeners.notify();
|
|
16735
|
+
}
|
|
16736
|
+
var resets = 0;
|
|
16737
|
+
var resetListeners = createListeners();
|
|
16738
|
+
/** How many times resetTriggers has forgotten every watcher. */
|
|
16739
|
+
function triggerResets() {
|
|
16740
|
+
return resets;
|
|
16663
16741
|
}
|
|
16742
|
+
/**
|
|
16743
|
+
* Call `listener` after each resetTriggers (a restart): a {watch} that stays
|
|
16744
|
+
* mounted, in the story interface, registers its watcher again (#402).
|
|
16745
|
+
*/
|
|
16746
|
+
var subscribeTriggerResets = resetListeners.subscribe;
|
|
16664
16747
|
function subscribeTriggerDialogs(cb) {
|
|
16665
16748
|
dialogNotify = cb;
|
|
16666
16749
|
if (dialogQueue.length > 0) cb();
|
|
@@ -17148,8 +17231,21 @@ var latestStateApplied = 0;
|
|
|
17148
17231
|
var slotLoadApplying = null;
|
|
17149
17232
|
/** A replacement of the game state applied at its call. */
|
|
17150
17233
|
function replaceStateNow() {
|
|
17151
|
-
|
|
17234
|
+
applyReplacement(++stateReplacementsIssued);
|
|
17235
|
+
}
|
|
17236
|
+
var replacementListeners = createListeners();
|
|
17237
|
+
/** Record that replacement `n` is applied, and tell the listeners. */
|
|
17238
|
+
function applyReplacement(n) {
|
|
17239
|
+
latestStateApplied = n;
|
|
17240
|
+
replacementListeners.notify();
|
|
17152
17241
|
}
|
|
17242
|
+
/**
|
|
17243
|
+
* Call `listener` whenever a boot, restart or load replaces the game state:
|
|
17244
|
+
* work the replaced game started that would write into the new one later (a
|
|
17245
|
+
* click body's timer in the story interface, which stays mounted) is to
|
|
17246
|
+
* stop (#401).
|
|
17247
|
+
*/
|
|
17248
|
+
var subscribeStateReplacement = replacementListeners.subscribe;
|
|
17153
17249
|
var runtimeUnsubs = [];
|
|
17154
17250
|
var inRuntimePhase = false;
|
|
17155
17251
|
/**
|
|
@@ -17381,6 +17477,16 @@ function recordKnownSave(set, slot, known) {
|
|
|
17381
17477
|
}
|
|
17382
17478
|
});
|
|
17383
17479
|
}
|
|
17480
|
+
/** Look up which slots hold a save again, as an operation queued now. */
|
|
17481
|
+
function refreshKnownSaves(set, ifid) {
|
|
17482
|
+
return populateKnownSaves(ifid).then((known) => {
|
|
17483
|
+
set((state) => {
|
|
17484
|
+
state.knownSaves = known;
|
|
17485
|
+
});
|
|
17486
|
+
});
|
|
17487
|
+
}
|
|
17488
|
+
/** Stops following the slot changes of other tabs (see init). */
|
|
17489
|
+
var stopWatchingSlots;
|
|
17384
17490
|
/**
|
|
17385
17491
|
* Queue the clearing of saved data, then restart now: the new playthrough
|
|
17386
17492
|
* is stored after it, and operations issued from here on belong to the new
|
|
@@ -17462,6 +17568,10 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
17462
17568
|
console.error("spindle: failed to init save system", err);
|
|
17463
17569
|
return "";
|
|
17464
17570
|
}));
|
|
17571
|
+
stopWatchingSlots?.();
|
|
17572
|
+
stopWatchingSlots = watchSlotChanges(storyData.ifid, () => {
|
|
17573
|
+
refreshKnownSaves(set, storyData.ifid).catch(() => {});
|
|
17574
|
+
});
|
|
17465
17575
|
},
|
|
17466
17576
|
navigate: (passageName) => {
|
|
17467
17577
|
if (navigationTriggerPhase) {
|
|
@@ -17681,12 +17791,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
17681
17791
|
});
|
|
17682
17792
|
if (playthroughId !== "" && playthroughId === current) switchToPlaythrough(replacementId, deletion);
|
|
17683
17793
|
else if (current === "") switchToLookedUpPlaythrough(deletion.then((replaced) => replaced ? replacementId : established, () => established));
|
|
17684
|
-
return reported(deletion.then(
|
|
17685
|
-
const known = await populateKnownSaves(storyData.ifid);
|
|
17686
|
-
set((state) => {
|
|
17687
|
-
state.knownSaves = known;
|
|
17688
|
-
});
|
|
17689
|
-
}), "spindle: failed to delete playthrough");
|
|
17794
|
+
return reported(deletion.then(() => refreshKnownSaves(set, storyData.ifid)), "spindle: failed to delete playthrough");
|
|
17690
17795
|
},
|
|
17691
17796
|
getSavePayload: () => {
|
|
17692
17797
|
const { currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
@@ -17723,7 +17828,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
17723
17828
|
return;
|
|
17724
17829
|
}
|
|
17725
17830
|
assertPassagesExist(get().storyData, payload);
|
|
17726
|
-
|
|
17831
|
+
applyReplacement(replacement);
|
|
17727
17832
|
emit("beforeload", slot);
|
|
17728
17833
|
const ifid = get().storyData?.ifid;
|
|
17729
17834
|
if (playthroughId && ifid) switchToLoadedPlaythrough(ifid, playthroughId);
|
|
@@ -18399,37 +18504,85 @@ var settings = {
|
|
|
18399
18504
|
//#endregion
|
|
18400
18505
|
//#region src/utils/control-edits.ts
|
|
18401
18506
|
/**
|
|
18402
|
-
* Records the edits
|
|
18403
|
-
*
|
|
18404
|
-
*
|
|
18405
|
-
*
|
|
18507
|
+
* Records the edits made to the items of a list from inside a {for}
|
|
18508
|
+
* iteration, which {for} tells apart from a list that is replaced:
|
|
18509
|
+
*
|
|
18510
|
+
* - a reader editing a variable through a control (`{textbox}`,
|
|
18511
|
+
* `{checkbox}`, ...): typing into a control inside an iteration changes
|
|
18512
|
+
* its item, but must not remount the iteration and drop the control's
|
|
18513
|
+
* focus (#369);
|
|
18514
|
+
* - mutation code an iteration runs as it renders (`{set $party[@i].hp +=
|
|
18515
|
+
* 1}`): remounting would run the iteration's mount-only macros, and so the
|
|
18516
|
+
* write, again for every write it makes (#400).
|
|
18517
|
+
*
|
|
18406
18518
|
* An edit is attributed to the variable path it wrote, so a replacement made
|
|
18407
|
-
* elsewhere (a watcher reacting to the edit
|
|
18519
|
+
* elsewhere (a watcher reacting to the edit, a button outside the loop)
|
|
18520
|
+
* still remounts.
|
|
18408
18521
|
*/
|
|
18409
18522
|
var version = 0;
|
|
18410
|
-
var MAX_LOG =
|
|
18523
|
+
var MAX_LOG = 4096;
|
|
18524
|
+
/** Edits in the order made: paths below the namespaces (`['variables', 'a']`). */
|
|
18411
18525
|
var log = [];
|
|
18412
|
-
function
|
|
18526
|
+
function note(path) {
|
|
18413
18527
|
log.push({
|
|
18414
18528
|
version: ++version,
|
|
18415
|
-
|
|
18529
|
+
path
|
|
18416
18530
|
});
|
|
18417
18531
|
if (log.length > MAX_LOG) log.shift();
|
|
18418
18532
|
}
|
|
18533
|
+
/** Note an edit a control made to the story variable at `segments`. */
|
|
18534
|
+
function noteControlEdit(segments) {
|
|
18535
|
+
note(["variables", ...segments]);
|
|
18536
|
+
}
|
|
18537
|
+
/**
|
|
18538
|
+
* Whether mutation code runs inside a {for} iteration: {for} provides true
|
|
18539
|
+
* around its iterations, and the writes of such code are noted as edits of
|
|
18540
|
+
* the items they reach (see runAsItemEdit).
|
|
18541
|
+
*/
|
|
18542
|
+
var ItemEditContext = createContext(false);
|
|
18543
|
+
var NAMESPACES = [
|
|
18544
|
+
"variables",
|
|
18545
|
+
"temporary",
|
|
18546
|
+
"transient"
|
|
18547
|
+
];
|
|
18548
|
+
/** Run `run`, noting each variable path it changes as an edit. */
|
|
18549
|
+
function runAsItemEdit(run) {
|
|
18550
|
+
const before = readState();
|
|
18551
|
+
try {
|
|
18552
|
+
run();
|
|
18553
|
+
} finally {
|
|
18554
|
+
const after = readState();
|
|
18555
|
+
for (const ns of NAMESPACES) {
|
|
18556
|
+
if (before[ns] === after[ns]) continue;
|
|
18557
|
+
for (const { path } of diffPaths(before[ns], after[ns], true)) note([ns, ...path]);
|
|
18558
|
+
}
|
|
18559
|
+
}
|
|
18560
|
+
}
|
|
18419
18561
|
function controlEditVersion() {
|
|
18420
18562
|
return version;
|
|
18421
18563
|
}
|
|
18422
18564
|
/**
|
|
18423
|
-
*
|
|
18424
|
-
* `list`: the path leads through the item (compared by reference
|
|
18425
|
-
* `
|
|
18426
|
-
*/
|
|
18427
|
-
function
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18565
|
+
* A test of whether an edit made since `since` wrote inside `item`, element
|
|
18566
|
+
* `index` of `list`: the path leads through the item (compared by reference
|
|
18567
|
+
* in `state`), or, for a primitive item, ends at its slot in the list.
|
|
18568
|
+
*/
|
|
18569
|
+
function editedItems(since, state) {
|
|
18570
|
+
const through = /* @__PURE__ */ new Set();
|
|
18571
|
+
const ends = /* @__PURE__ */ new Map();
|
|
18572
|
+
for (let i = log.length - 1; i >= 0 && log[i].version > since; i--) {
|
|
18573
|
+
const { path } = log[i];
|
|
18574
|
+
let node = state;
|
|
18575
|
+
path.forEach((seg, n) => {
|
|
18576
|
+
if (n === path.length - 1 && node !== null && typeof node === "object") {
|
|
18577
|
+
const keys = ends.get(node) ?? /* @__PURE__ */ new Set();
|
|
18578
|
+
keys.add(seg);
|
|
18579
|
+
ends.set(node, keys);
|
|
18580
|
+
}
|
|
18581
|
+
node = node !== null && typeof node === "object" ? getByPath(node, [seg]) : void 0;
|
|
18582
|
+
if (node !== null && typeof node === "object") through.add(node);
|
|
18583
|
+
});
|
|
18584
|
+
}
|
|
18585
|
+
return (list, item, index) => typeof item === "object" && item !== null ? through.has(item) : ends.get(list)?.has(String(index)) === true;
|
|
18433
18586
|
}
|
|
18434
18587
|
//#endregion
|
|
18435
18588
|
//#region src/action-registry.ts
|
|
@@ -18626,6 +18779,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
18626
18779
|
id = resolve(id);
|
|
18627
18780
|
}
|
|
18628
18781
|
const { update, getValues } = useContext(LocalsUpdateContext);
|
|
18782
|
+
const inIteration = useContext(ItemEditContext);
|
|
18629
18783
|
const renderOptions = useRenderOptions();
|
|
18630
18784
|
const renderNodes$1 = (nodes, options) => renderNodes(nodes, {
|
|
18631
18785
|
...renderOptions,
|
|
@@ -18656,7 +18810,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
18656
18810
|
id,
|
|
18657
18811
|
resolve,
|
|
18658
18812
|
cls: macroClass(config.name, className),
|
|
18659
|
-
mutate: (code) => executeMutation(code, getValues(), update),
|
|
18813
|
+
mutate: inIteration ? (code) => runAsItemEdit(() => executeMutation(code, getValues(), update)) : (code) => executeMutation(code, getValues(), update),
|
|
18660
18814
|
update,
|
|
18661
18815
|
getValues,
|
|
18662
18816
|
wrap: (content) => wrapContent(className, id, content)
|
|
@@ -18962,7 +19116,7 @@ function createStoryAPI() {
|
|
|
18962
19116
|
},
|
|
18963
19117
|
deferRender() {
|
|
18964
19118
|
useStoryStore.getState().deferRender();
|
|
18965
|
-
readyPromise
|
|
19119
|
+
readyPromise ??= new Promise((resolve) => {
|
|
18966
19120
|
readyResolve = resolve;
|
|
18967
19121
|
});
|
|
18968
19122
|
},
|
|
@@ -19521,16 +19675,25 @@ var PRIMITIVE_SAMPLES = {
|
|
|
19521
19675
|
number: Object(0),
|
|
19522
19676
|
boolean: Object(false)
|
|
19523
19677
|
};
|
|
19524
|
-
|
|
19678
|
+
/**
|
|
19679
|
+
* The schema of a default value. An object met again (a backreference, a
|
|
19680
|
+
* shared subtree) has the schema made for it the first time, so a cyclic
|
|
19681
|
+
* default gives a schema that refers to itself instead of recursing forever.
|
|
19682
|
+
*/
|
|
19683
|
+
function inferSchema(value, seen = /* @__PURE__ */ new Map()) {
|
|
19525
19684
|
if (Array.isArray(value)) return { type: "array" };
|
|
19526
19685
|
if (value === null) return { type: "null" };
|
|
19527
19686
|
if (typeof value === "object") {
|
|
19687
|
+
const known = seen.get(value);
|
|
19688
|
+
if (known) return known;
|
|
19528
19689
|
const fields = /* @__PURE__ */ new Map();
|
|
19529
|
-
|
|
19530
|
-
return {
|
|
19690
|
+
const schema = {
|
|
19531
19691
|
type: "object",
|
|
19532
19692
|
fields
|
|
19533
19693
|
};
|
|
19694
|
+
seen.set(value, schema);
|
|
19695
|
+
for (const [key, val] of Object.entries(value)) fields.set(key, inferSchema(val, seen));
|
|
19696
|
+
return schema;
|
|
19534
19697
|
}
|
|
19535
19698
|
const jsType = typeof value;
|
|
19536
19699
|
if (!VALID_VAR_TYPES.has(jsType)) throw new Error(`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`);
|
|
@@ -20028,23 +20191,32 @@ defineMacro({
|
|
|
20028
20191
|
* Content-derived string key for a value, used to remount components when
|
|
20029
20192
|
* the value's contents change (e.g. {for} iterations, #45).
|
|
20030
20193
|
*
|
|
20031
|
-
* JSON-compatible values (strings, finite numbers, booleans, null,
|
|
20032
|
-
* plain objects of those)
|
|
20033
|
-
* values get distinct, unambiguous markers so their contents
|
|
20034
|
-
* too: Map and Set entries (nested at any depth), Date,
|
|
20035
|
-
* undefined, NaN/±Infinity, symbols (by description),
|
|
20036
|
-
* and registered class instances
|
|
20037
|
-
*
|
|
20038
|
-
*
|
|
20194
|
+
* A tree of JSON-compatible values (strings, finite numbers, booleans, null,
|
|
20195
|
+
* arrays and plain objects of those) produces exactly its `JSON.stringify`
|
|
20196
|
+
* output. Other values get distinct, unambiguous markers so their contents
|
|
20197
|
+
* are reflected too: Map and Set entries (nested at any depth), Date,
|
|
20198
|
+
* RegExp, BigInt, undefined, NaN/±Infinity, symbols (by description),
|
|
20199
|
+
* functions (by name) and registered class instances, with the class name
|
|
20200
|
+
* and, for a subclass of a built-in, its own fields (#407). A hole in an
|
|
20201
|
+
* array reads as undefined.
|
|
20202
|
+
*
|
|
20203
|
+
* An object met again, through a cycle or a second reference to it, is a
|
|
20204
|
+
* marker naming where it was first met rather than another copy of its
|
|
20205
|
+
* contents, so the key grows with the objects a value holds, not with the
|
|
20206
|
+
* paths to them (#406). The function never throws.
|
|
20039
20207
|
*/
|
|
20040
20208
|
function stableKey(value) {
|
|
20041
20209
|
try {
|
|
20042
|
-
return keyOf(value,
|
|
20210
|
+
return keyOf(value, /* @__PURE__ */ new Map());
|
|
20043
20211
|
} catch {
|
|
20044
20212
|
return "<unkeyable>";
|
|
20045
20213
|
}
|
|
20046
20214
|
}
|
|
20047
|
-
|
|
20215
|
+
/**
|
|
20216
|
+
* The key of `val`. `seen` numbers the objects keyed so far, in the order
|
|
20217
|
+
* they were first met.
|
|
20218
|
+
*/
|
|
20219
|
+
function keyOf(val, seen) {
|
|
20048
20220
|
switch (typeof val) {
|
|
20049
20221
|
case "string": return JSON.stringify(val);
|
|
20050
20222
|
case "number": return Number.isFinite(val) ? JSON.stringify(val) : String(val);
|
|
@@ -20056,22 +20228,28 @@ function keyOf(val, ancestors) {
|
|
|
20056
20228
|
}
|
|
20057
20229
|
if (val === null) return "null";
|
|
20058
20230
|
const obj = val;
|
|
20059
|
-
const
|
|
20060
|
-
if (
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
const className = registeredClassName(obj);
|
|
20071
|
-
return className === void 0 ? `{${body}}` : `Class(${JSON.stringify(className)}){${body}}`;
|
|
20072
|
-
} finally {
|
|
20073
|
-
ancestors.pop();
|
|
20231
|
+
const first = seen.get(obj);
|
|
20232
|
+
if (first !== void 0) return `<ref ${first}>`;
|
|
20233
|
+
const className = registeredClassName(obj);
|
|
20234
|
+
const named = (key) => className === void 0 ? key : `Class(${JSON.stringify(className)})${key}`;
|
|
20235
|
+
const withFields = (key) => {
|
|
20236
|
+
const keys = className === void 0 ? [] : extraKeys(obj);
|
|
20237
|
+
return named(keys.length ? `${key}${fieldsOf(obj, keys, seen)}` : key);
|
|
20238
|
+
};
|
|
20239
|
+
if (val instanceof Date || val instanceof RegExp) {
|
|
20240
|
+
if (className !== void 0 && extraKeys(obj).length) seen.set(obj, seen.size);
|
|
20241
|
+
return withFields(val instanceof Date ? `Date(${Date.prototype.getTime.call(val)})` : `RegExp(${RegExp.prototype.toString.call(val)}@${val.lastIndex})`);
|
|
20074
20242
|
}
|
|
20243
|
+
seen.set(obj, seen.size);
|
|
20244
|
+
if (Array.isArray(val)) return withFields(`[${Array.from(val, (v) => keyOf(v, seen)).join(",")}]`);
|
|
20245
|
+
if (val instanceof Map) return withFields(`Map[${Array.from(mapEntries(val), ([k, v]) => `[${keyOf(k, seen)},${keyOf(v, seen)}]`).join(",")}]`);
|
|
20246
|
+
if (val instanceof Set) return withFields(`Set[${Array.from(setMembers(val), (v) => keyOf(v, seen)).join(",")}]`);
|
|
20247
|
+
return named(fieldsOf(obj, Object.keys(obj), seen));
|
|
20248
|
+
}
|
|
20249
|
+
/** `{"key":value,...}` for the `keys` of `obj`. */
|
|
20250
|
+
function fieldsOf(obj, keys, seen) {
|
|
20251
|
+
const record = obj;
|
|
20252
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${keyOf(record[k], seen)}`).join(",")}}`;
|
|
20075
20253
|
}
|
|
20076
20254
|
//#endregion
|
|
20077
20255
|
//#region src/components/macros/For.tsx
|
|
@@ -20153,12 +20331,11 @@ defineMacro({
|
|
|
20153
20331
|
});
|
|
20154
20332
|
}
|
|
20155
20333
|
const { itemVar, indexVar } = loop;
|
|
20156
|
-
const
|
|
20157
|
-
const { variables } = readState();
|
|
20334
|
+
const edited = editedItems(keys.edits, readState());
|
|
20158
20335
|
keys.edits = controlEditVersion();
|
|
20159
20336
|
const generations = list.map((item, i) => {
|
|
20160
20337
|
const contents = stableKey(item);
|
|
20161
|
-
if (keys.generations[i] === void 0 || contents !== keys.contents[i] && !
|
|
20338
|
+
if (keys.generations[i] === void 0 || contents !== keys.contents[i] && !edited(list, item, i)) keys.generations[i] = keys.next++;
|
|
20162
20339
|
keys.contents[i] = contents;
|
|
20163
20340
|
return keys.generations[i];
|
|
20164
20341
|
});
|
|
@@ -20171,7 +20348,10 @@ defineMacro({
|
|
|
20171
20348
|
indexValue: i,
|
|
20172
20349
|
children
|
|
20173
20350
|
}, `${i}-${generations[i]}`));
|
|
20174
|
-
return ctx.wrap(
|
|
20351
|
+
return ctx.wrap(/* @__PURE__ */ jsx(ItemEditContext.Provider, {
|
|
20352
|
+
value: true,
|
|
20353
|
+
children: content
|
|
20354
|
+
}));
|
|
20175
20355
|
},
|
|
20176
20356
|
text({ rawArgs, children = [] }, ctx) {
|
|
20177
20357
|
const { itemVar, indexVar, listExpr } = loopArgs(rawArgs, ctx.args);
|
|
@@ -20262,7 +20442,9 @@ var frozenAlways = () => true;
|
|
|
20262
20442
|
* once it has rendered, so it runs once per click. It stays mounted
|
|
20263
20443
|
* until the owning macro unmounts (its passage changes, say), so work it
|
|
20264
20444
|
* starts in an effect, such as the timer of a {timed} or {repeat}, can
|
|
20265
|
-
* finish; the passage leaving cancels it
|
|
20445
|
+
* finish; the passage leaving cancels it, and so does a restart or a load
|
|
20446
|
+
* replacing the game, which a control of the story interface outlives
|
|
20447
|
+
* (#401).
|
|
20266
20448
|
*
|
|
20267
20449
|
* A detached render starts with no contexts, so the hook captures the ones
|
|
20268
20450
|
* the owning macro sees and provides them again: the locals scope (read
|
|
@@ -20284,8 +20466,17 @@ function useDetachedBody() {
|
|
|
20284
20466
|
const repeat = useContext(RepeatContext);
|
|
20285
20467
|
const closeDialog = useContext(DialogCloseContext);
|
|
20286
20468
|
const mounted = useRef([]);
|
|
20287
|
-
useEffect(() =>
|
|
20288
|
-
|
|
20469
|
+
useEffect(() => {
|
|
20470
|
+
const unsubscribe = subscribeStateReplacement(() => {
|
|
20471
|
+
const replaced = mounted.current.splice(0);
|
|
20472
|
+
queueMicrotask(() => {
|
|
20473
|
+
for (const container of replaced) render(null, container);
|
|
20474
|
+
});
|
|
20475
|
+
});
|
|
20476
|
+
return () => {
|
|
20477
|
+
unsubscribe();
|
|
20478
|
+
for (const container of mounted.current.splice(0)) render(null, container);
|
|
20479
|
+
};
|
|
20289
20480
|
}, []);
|
|
20290
20481
|
const run = (children) => {
|
|
20291
20482
|
const locals = liveLocalsView(updater.getValues);
|
|
@@ -21949,9 +22140,12 @@ defineMacro({
|
|
|
21949
22140
|
const { hooks } = ctx;
|
|
21950
22141
|
const { condition, options } = ctx.args;
|
|
21951
22142
|
if (condition === void 0) return null;
|
|
21952
|
-
const
|
|
21953
|
-
|
|
21954
|
-
|
|
22143
|
+
const frozen = hooks.useContext(FrozenStateContext);
|
|
22144
|
+
const [, rerender] = hooks.useState(0);
|
|
22145
|
+
hooks.useEffect(() => subscribeTriggerResets(() => rerender((n) => n + 1)), []);
|
|
22146
|
+
const registered = hooks.useRef(null);
|
|
22147
|
+
if (registered.current === null || registered.current !== triggerResets() && !frozen?.(useStoryStore.getState())) {
|
|
22148
|
+
registered.current = triggerResets();
|
|
21955
22149
|
addMacroTrigger(condition, options);
|
|
21956
22150
|
}
|
|
21957
22151
|
return null;
|