@rohal12/spindle 0.50.1 → 0.51.1

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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Zustand's `create()` for Preact.
3
+ *
4
+ * Zustand's default entry (`zustand` / `zustand/react`) imports `react`, which
5
+ * Spindle doesn't depend on. Bundlers that alias `react` to `preact/compat`
6
+ * hide that, but the published headless entry keeps Zustand external and runs
7
+ * under plain Node resolution, where `react` doesn't exist. Building on the
8
+ * vanilla store and Preact's own hooks keeps `react` out of the module graph.
9
+ */
10
+ import { useSyncExternalStore } from 'preact/compat';
11
+ import { useCallback } from 'preact/hooks';
12
+ import {
13
+ createStore,
14
+ type ExtractState,
15
+ type Mutate,
16
+ type StateCreator,
17
+ type StoreApi,
18
+ type StoreMutatorIdentifier,
19
+ } from 'zustand/vanilla';
20
+
21
+ /** A store API that is also a selector hook, like Zustand's bound stores. */
22
+ export type UseBoundStore<S extends StoreApi<unknown>> = S & {
23
+ (): ExtractState<S>;
24
+ <U>(selector: (state: ExtractState<S>) => U): U;
25
+ };
26
+
27
+ const identity = <T>(value: T): T => value;
28
+
29
+ function bindPreact<S extends StoreApi<unknown>>(api: S): UseBoundStore<S> {
30
+ function useBoundStore<U>(
31
+ selector: (state: ExtractState<S>) => U = identity as (
32
+ state: ExtractState<S>,
33
+ ) => U,
34
+ ): U {
35
+ const getSnapshot = useCallback(
36
+ () => selector(api.getState() as ExtractState<S>),
37
+ [selector],
38
+ );
39
+ return useSyncExternalStore(api.subscribe, getSnapshot);
40
+ }
41
+ return Object.assign(useBoundStore, api) as UseBoundStore<S>;
42
+ }
43
+
44
+ /** Curried `create<State>()(initializer)`, as in Zustand. */
45
+ export function create<T>() {
46
+ return <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
47
+ initializer: StateCreator<T, [], Mos>,
48
+ ): UseBoundStore<Mutate<StoreApi<T>, Mos>> =>
49
+ bindPreact(createStore<T>()(initializer));
50
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  SaveExport,
8
8
  StorageInfo,
9
9
  } from './types';
10
- import { isSavePayload } from './types';
10
+ import { isSaveExport, isSavePayload } from './types';
11
11
  import { getBackend, resetBackend } from './storage';
12
12
  import { deepClone, serialize, deserialize } from '../class-registry';
13
13
  import { emit } from '../event-emitter';
@@ -102,6 +102,10 @@ export async function getCurrentPlaythroughId(
102
102
  * before `getPayload()` captures the state, so data a hook sets is part of the
103
103
  * save; `aftersave` fires once `write` has stored it. Every user-facing save
104
104
  * path goes through here.
105
+ *
106
+ * The hook and capture run synchronously, but an error they throw (a failing
107
+ * hook, an unserializable payload) rejects the returned promise like a failed
108
+ * write rather than escaping to the caller.
105
109
  */
106
110
  export function saveWithHooks<T>(
107
111
  slot: string | undefined,
@@ -109,12 +113,16 @@ export function saveWithHooks<T>(
109
113
  getPayload: () => SavePayload,
110
114
  write: (payload: SavePayload) => Promise<T>,
111
115
  ): Promise<T> {
112
- emit('beforesave', slot, custom);
113
- const payload = getPayload();
114
- return write(payload).then((result) => {
115
- emit('aftersave', slot);
116
- return result;
117
- });
116
+ try {
117
+ emit('beforesave', slot, custom);
118
+ const payload = getPayload();
119
+ return write(payload).then((result) => {
120
+ emit('aftersave', slot);
121
+ return result;
122
+ });
123
+ } catch (err) {
124
+ return Promise.reject(err);
125
+ }
118
126
  }
119
127
 
120
128
  // --- Save CRUD ---
@@ -204,8 +212,26 @@ export async function loadSave(
204
212
  return deserializePayload(record.payload);
205
213
  }
206
214
 
215
+ /**
216
+ * Delete a save record. If the default slot or a named slot holds it, that
217
+ * slot is cleared as well (pointer and slot index), as `deleteSlotSave` would.
218
+ */
207
219
  export async function deleteSaveById(saveId: string): Promise<void> {
208
- await (await getBackend()).deleteSave(saveId);
220
+ const backend = await getBackend();
221
+ const record = await backend.getSave(saveId);
222
+ await backend.deleteSave(saveId);
223
+ if (!record) return;
224
+
225
+ const ifid = record.meta.ifid;
226
+ const slots = [undefined, ...(await getIndexedSlots(ifid))];
227
+ for (const slot of slots) {
228
+ const metaKey = slotMetaKey(ifid, slot);
229
+ await withSlot(metaKey, async () => {
230
+ if ((await backend.getMeta<string>(metaKey)) !== saveId) return;
231
+ await backend.deleteMeta(metaKey);
232
+ await removeFromSlotIndex(ifid, slot);
233
+ });
234
+ }
209
235
  }
210
236
 
211
237
  export async function renameSave(
@@ -314,6 +340,25 @@ function slotMetaKey(ifid: string, slot?: string): string {
314
340
  : `${AUTOSAVE_KEY_PREFIX}${ifid}`;
315
341
  }
316
342
 
343
+ /** Tail of each slot's operation queue, keyed by the slot's meta key. */
344
+ const slotQueues = new Map<string, Promise<unknown>>();
345
+
346
+ /**
347
+ * Run a complete operation on one slot (save, import, delete) after the ones
348
+ * already queued for it. Each operation reads the slot pointer and writes the
349
+ * record and pointer as a unit, so concurrent operations on the same slot
350
+ * apply in call order instead of each creating its own record.
351
+ */
352
+ function withSlot<T>(metaKey: string, op: () => Promise<T>): Promise<T> {
353
+ const run = (slotQueues.get(metaKey) ?? Promise.resolve()).then(op);
354
+ const tail = run.catch(() => {});
355
+ slotQueues.set(metaKey, tail);
356
+ void tail.then(() => {
357
+ if (slotQueues.get(metaKey) === tail) slotQueues.delete(metaKey);
358
+ });
359
+ return run;
360
+ }
361
+
317
362
  /** Tail of the queue that serializes slot index updates. */
318
363
  let slotIndexQueue: Promise<unknown> = Promise.resolve();
319
364
 
@@ -368,32 +413,34 @@ function toSaveInfo(record: SaveRecord, slot?: string): SaveInfo {
368
413
  };
369
414
  }
370
415
 
371
- export async function quickSave(
416
+ export function quickSave(
372
417
  ifid: string,
373
418
  playthroughId: string,
374
419
  payload: SavePayload,
375
420
  slot?: string,
376
421
  custom?: Record<string, unknown>,
377
422
  ): Promise<SaveRecord> {
378
- const backend = await getBackend();
379
423
  const metaKey = slotMetaKey(ifid, slot);
380
- const existingId = await backend.getMeta<string>(metaKey);
424
+ return withSlot(metaKey, async () => {
425
+ const backend = await getBackend();
426
+ const existingId = await backend.getMeta<string>(metaKey);
381
427
 
382
- if (existingId) {
383
- const updated = await overwriteSave(existingId, payload, custom);
384
- if (updated) return updated;
385
- }
428
+ if (existingId) {
429
+ const updated = await overwriteSave(existingId, payload, custom);
430
+ if (updated) return updated;
431
+ }
386
432
 
387
- // Create new save
388
- const record = await createSave(ifid, playthroughId, payload, {
389
- isAutosave: !isNamedSlot(slot),
390
- ...(isNamedSlot(slot) ? { slot } : {}),
391
- ...custom,
392
- });
393
- await backend.setMeta(metaKey, record.meta.id);
394
- await addToSlotIndex(ifid, slot);
433
+ // Create new save
434
+ const record = await createSave(ifid, playthroughId, payload, {
435
+ isAutosave: !isNamedSlot(slot),
436
+ ...(isNamedSlot(slot) ? { slot } : {}),
437
+ ...custom,
438
+ });
439
+ await backend.setMeta(metaKey, record.meta.id);
440
+ await addToSlotIndex(ifid, slot);
395
441
 
396
- return record;
442
+ return record;
443
+ });
397
444
  }
398
445
 
399
446
  export async function hasQuickSave(
@@ -425,7 +472,8 @@ export async function loadQuickSave(
425
472
  export async function populateKnownSaves(
426
473
  ifid: string,
427
474
  ): Promise<Record<string, true>> {
428
- const result: Record<string, true> = {};
475
+ // No prototype, so a slot named '__proto__' is recorded as an entry
476
+ const result = Object.create(null) as Record<string, true>;
429
477
 
430
478
  // Check default autosave
431
479
  if (await hasQuickSave(ifid)) {
@@ -481,20 +529,19 @@ export async function listSlotSaves(ifid: string): Promise<SaveInfo[]> {
481
529
  /**
482
530
  * Delete a save by slot name. Removes from slot index if named.
483
531
  */
484
- export async function deleteSlotSave(
485
- ifid: string,
486
- slot?: string,
487
- ): Promise<void> {
488
- const backend = await getBackend();
532
+ export function deleteSlotSave(ifid: string, slot?: string): Promise<void> {
489
533
  const metaKey = slotMetaKey(ifid, slot);
490
- const existingId = await backend.getMeta<string>(metaKey);
491
- if (!existingId) return;
534
+ return withSlot(metaKey, async () => {
535
+ const backend = await getBackend();
536
+ const existingId = await backend.getMeta<string>(metaKey);
537
+ if (!existingId) return;
492
538
 
493
- await backend.deleteSave(existingId);
494
- await backend.deleteMeta(metaKey);
539
+ await backend.deleteSave(existingId);
540
+ await backend.deleteMeta(metaKey);
495
541
 
496
- // Remove from slot index if named
497
- await removeFromSlotIndex(ifid, slot);
542
+ // Remove from slot index if named
543
+ await removeFromSlotIndex(ifid, slot);
544
+ });
498
545
  }
499
546
 
500
547
  // --- Session Persistence (survives F5, cleared on tab close) ---
@@ -569,6 +616,11 @@ async function prepareImport(
569
616
  `Save is from a different story (expected IFID ${ifid}, got ${data.ifid})`,
570
617
  );
571
618
  }
619
+ // Callers may pass parsed JSON; check the full structure before anything
620
+ // is stored or replaced
621
+ if (!isSaveExport(data)) {
622
+ throw new Error('Invalid save file format');
623
+ }
572
624
 
573
625
  const backend = await getBackend();
574
626
 
@@ -624,32 +676,34 @@ export async function exportSlotSave(
624
676
  * validation as `importSave` (version, IFID) and keeps the playthrough record
625
677
  * and slot index consistent.
626
678
  */
627
- export async function importSlotSave(
679
+ export function importSlotSave(
628
680
  data: SaveExport,
629
681
  ifid: string,
630
682
  slot?: string,
631
683
  ): Promise<SaveInfo> {
632
- const record = await prepareImport(data, ifid);
633
-
634
- // The slot keys in `custom` describe where the save lives, so they follow
635
- // the target slot rather than the slot the save was exported from.
636
- const { slot: _exportedSlot, ...custom } = record.meta.custom ?? {};
637
- record.meta.custom = {
638
- ...custom,
639
- isAutosave: !isNamedSlot(slot),
640
- ...(isNamedSlot(slot) ? { slot } : {}),
641
- };
642
-
643
- const backend = await getBackend();
644
684
  const metaKey = slotMetaKey(ifid, slot);
645
- const previousId = await backend.getMeta<string>(metaKey);
685
+ return withSlot(metaKey, async () => {
686
+ const record = await prepareImport(data, ifid);
687
+
688
+ // The slot keys in `custom` describe where the save lives, so they follow
689
+ // the target slot rather than the slot the save was exported from.
690
+ const { slot: _exportedSlot, ...custom } = record.meta.custom ?? {};
691
+ record.meta.custom = {
692
+ ...custom,
693
+ isAutosave: !isNamedSlot(slot),
694
+ ...(isNamedSlot(slot) ? { slot } : {}),
695
+ };
646
696
 
647
- await backend.putSave(record);
648
- await backend.setMeta(metaKey, record.meta.id);
649
- await addToSlotIndex(ifid, slot);
650
- if (previousId) await backend.deleteSave(previousId);
697
+ const backend = await getBackend();
698
+ const previousId = await backend.getMeta<string>(metaKey);
651
699
 
652
- return toSaveInfo(record, slot);
700
+ await backend.putSave(record);
701
+ await backend.setMeta(metaKey, record.meta.id);
702
+ await addToSlotIndex(ifid, slot);
703
+ if (previousId) await backend.deleteSave(previousId);
704
+
705
+ return toSaveInfo(record, slot);
706
+ });
653
707
  }
654
708
 
655
709
  // --- Storage Management ---
@@ -548,14 +548,28 @@ async function detectBackend(): Promise<StorageBackend> {
548
548
  return createMemoryBackend();
549
549
  }
550
550
 
551
- export async function getBackend(): Promise<StorageBackend> {
552
- if (_backend) return _backend;
553
- _backend = await detectBackend();
554
- return _backend;
551
+ /**
552
+ * Detection in flight. Concurrent first callers share it so they all get the
553
+ * same backend (the memory fallback makes a new, empty store per detection).
554
+ */
555
+ let _backendPromise: Promise<StorageBackend> | null = null;
556
+
557
+ export function getBackend(): Promise<StorageBackend> {
558
+ if (_backend) return Promise.resolve(_backend);
559
+ if (!_backendPromise) {
560
+ const pending = detectBackend().then((backend) => {
561
+ // A reset while detecting discards this result
562
+ if (_backendPromise === pending) _backend = backend;
563
+ return backend;
564
+ });
565
+ _backendPromise = pending;
566
+ }
567
+ return _backendPromise;
555
568
  }
556
569
 
557
570
  export function resetBackend(): void {
558
571
  _backend = null;
572
+ _backendPromise = null;
559
573
  }
560
574
 
561
575
  /**
@@ -78,15 +78,64 @@ export function isSaveExport(value: unknown): value is SaveExport {
78
78
  if (typeof meta.updatedAt !== 'string') return false;
79
79
  if (typeof meta.title !== 'string') return false;
80
80
 
81
- const payload = save.payload as Record<string, unknown>;
82
- if (typeof payload.passage !== 'string') return false;
83
- if (!Array.isArray(payload.history) || payload.history.length === 0)
84
- return false;
85
- if (typeof payload.historyIndex !== 'number') return false;
86
- if (typeof payload.variables !== 'object' || payload.variables === null)
81
+ return isImportablePayload(save.payload);
82
+ }
83
+
84
+ function isRecord(value: unknown): value is Record<string, unknown> {
85
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
86
+ }
87
+
88
+ /** Absent, null, or a `{ seed, pull }` PRNG snapshot. */
89
+ function isOptionalPRNGSnapshot(value: unknown): boolean {
90
+ if (value == null) return true;
91
+ return (
92
+ isRecord(value) &&
93
+ typeof value.seed === 'string' &&
94
+ typeof value.pull === 'number' &&
95
+ Number.isInteger(value.pull) &&
96
+ value.pull >= 0
97
+ );
98
+ }
99
+
100
+ function isSaveHistoryMoment(value: unknown): value is SaveHistoryMoment {
101
+ return (
102
+ isRecord(value) &&
103
+ typeof value.passage === 'string' &&
104
+ isRecord(value.variables) &&
105
+ typeof value.timestamp === 'number' &&
106
+ isOptionalPRNGSnapshot(value.prng)
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Full check of a payload from outside the running story (an imported save):
112
+ * every history moment is well formed, and `historyIndex` is an integer that
113
+ * points at a moment of the payload's passage. Anything that passes can be
114
+ * stored and later loaded.
115
+ */
116
+ function isImportablePayload(value: unknown): value is SavePayload {
117
+ if (!isRecord(value)) return false;
118
+ if (typeof value.passage !== 'string') return false;
119
+ if (!isRecord(value.variables)) return false;
120
+
121
+ const { history, historyIndex } = value;
122
+ if (!Array.isArray(history) || history.length === 0) return false;
123
+ if (!history.every(isSaveHistoryMoment)) return false;
124
+ if (
125
+ typeof historyIndex !== 'number' ||
126
+ !Number.isInteger(historyIndex) ||
127
+ historyIndex < 0 ||
128
+ historyIndex >= history.length
129
+ ) {
87
130
  return false;
131
+ }
132
+ if (history[historyIndex]!.passage !== value.passage) return false;
88
133
 
89
- return true;
134
+ if (value.visitCounts !== undefined && !isRecord(value.visitCounts))
135
+ return false;
136
+ if (value.renderCounts !== undefined && !isRecord(value.renderCounts))
137
+ return false;
138
+ return isOptionalPRNGSnapshot(value.prng);
90
139
  }
91
140
 
92
141
  export function isSavePayload(value: unknown): value is SavePayload {
package/src/store.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { create } from 'zustand';
1
+ import { create } from './preact-store';
2
2
  import { immer } from 'zustand/middleware/immer';
3
3
  import {
4
4
  enablePatches,
@@ -214,8 +214,10 @@ function resetModuleState(base: Record<string, unknown>): void {
214
214
  /**
215
215
  * Record the state StoryInit left behind as the start moment: its variable
216
216
  * snapshot (the history base) and PRNG state. Called by executeStoryInit()
217
- * after the StoryInit passage has rendered; init()/restart() record the
218
- * start moment before StoryInit runs. A no-op once the story has moved on.
217
+ * after the StoryInit passage has rendered, and again after the `storyinit`
218
+ * handlers have run at boot (unless a session was restored) and on restart;
219
+ * init()/restart() record the start moment before StoryInit runs. A no-op
220
+ * once the story has moved on.
219
221
  */
220
222
  export function recordStoryInitState(): void {
221
223
  const { history, historyIndex, variables } = useStoryStore.getState();
@@ -370,12 +372,12 @@ export interface StoryState {
370
372
  updateVariables: (recipe: (draft: VariableNamespaces) => void) => void;
371
373
  trackRender: (passageName: string) => void;
372
374
  restart: () => void;
373
- save: (slot?: string, custom?: Record<string, unknown>) => void;
374
- load: (slot?: string) => void;
375
+ save: (slot?: string, custom?: Record<string, unknown>) => Promise<void>;
376
+ load: (slot?: string) => Promise<void>;
375
377
  hasSave: (slot?: string) => boolean;
376
378
  getSaveInfo: (slot?: string) => Promise<SaveInfo | null>;
377
379
  listSaves: () => Promise<SaveInfo[]>;
378
- deleteSave: (slot?: string) => void;
380
+ deleteSave: (slot?: string) => Promise<void>;
379
381
  exportSave: (slot?: string) => Promise<SaveExport | null>;
380
382
  importSave: (data: unknown, slot?: string) => Promise<SaveInfo>;
381
383
  clearGameData: () => void;
@@ -395,6 +397,17 @@ export interface StoryState {
395
397
  clearDeferredRender: () => void;
396
398
  }
397
399
 
400
+ /**
401
+ * Return `p` marked as handled: a caller that ignores the result of a
402
+ * fire-and-forget operation (whose failure is already logged) gets no
403
+ * unhandled-rejection report, while a caller that awaits it still sees the
404
+ * error.
405
+ */
406
+ function handled<T>(p: Promise<T>): Promise<T> {
407
+ p.catch(() => {});
408
+ return p;
409
+ }
410
+
398
411
  export const useStoryStore = create<StoryState>()(
399
412
  immer((set, get) => ({
400
413
  storyData: null,
@@ -766,6 +779,8 @@ export const useStoryStore = create<StoryState>()(
766
779
  executeStoryInit();
767
780
  clearSession(storyData.ifid);
768
781
  emit('storyinit');
782
+ // The storyinit handlers' changes belong to the start moment too
783
+ recordStoryInitState();
769
784
 
770
785
  // Start a new playthrough on restart
771
786
  startNewPlaythrough(storyData.ifid)
@@ -781,58 +796,71 @@ export const useStoryStore = create<StoryState>()(
781
796
 
782
797
  save: (slot?: string, custom?: Record<string, unknown>) => {
783
798
  const { storyData, playthroughId } = get();
784
- if (!storyData) return;
785
-
786
- saveWithHooks(
787
- slot,
788
- custom,
789
- () => get().getSavePayload(),
790
- async (payload) => {
791
- set((state) => {
792
- state.saveError = null;
793
- });
794
- await quickSave(storyData.ifid, playthroughId, payload, slot, custom);
799
+ if (!storyData) return Promise.resolve();
800
+
801
+ return handled(
802
+ saveWithHooks(
803
+ slot,
804
+ custom,
805
+ () => get().getSavePayload(),
806
+ async (payload) => {
807
+ set((state) => {
808
+ state.saveError = null;
809
+ });
810
+ await quickSave(
811
+ storyData.ifid,
812
+ playthroughId,
813
+ payload,
814
+ slot,
815
+ custom,
816
+ );
817
+ set((state) => {
818
+ state.knownSaves = {
819
+ ...state.knownSaves,
820
+ [slot ?? '']: true,
821
+ };
822
+ });
823
+ },
824
+ ).catch((err) => {
825
+ console.error('spindle: failed to save', err);
795
826
  set((state) => {
796
- state.knownSaves = {
797
- ...state.knownSaves,
798
- [slot ?? '']: true,
799
- };
827
+ state.saveError =
828
+ err instanceof Error ? err.message : 'Failed to save';
800
829
  });
801
- },
802
- ).catch((err) => {
803
- console.error('spindle: failed to save', err);
804
- set((state) => {
805
- state.saveError =
806
- err instanceof Error ? err.message : 'Failed to save';
807
- });
808
- });
830
+ throw err;
831
+ }),
832
+ );
809
833
  },
810
834
 
811
835
  load: (slot?: string) => {
812
836
  const { storyData } = get();
813
- if (!storyData) return;
837
+ if (!storyData) return Promise.resolve();
814
838
 
815
839
  set((state) => {
816
840
  state.loadError = null;
817
841
  });
818
- loadQuickSave(storyData.ifid, slot)
819
- .then((payload) => {
820
- if (!payload) return;
821
- get().loadFromPayload(payload, slot);
822
- })
823
- .catch((err) => {
824
- console.error('spindle: failed to load save', err);
825
- set((state) => {
826
- state.loadError =
827
- err instanceof Error ? err.message : 'Failed to load';
828
- });
829
- });
842
+ return handled(
843
+ loadQuickSave(storyData.ifid, slot)
844
+ .then((payload) => {
845
+ if (!payload) return;
846
+ get().loadFromPayload(payload, slot);
847
+ })
848
+ .catch((err) => {
849
+ console.error('spindle: failed to load save', err);
850
+ set((state) => {
851
+ state.loadError =
852
+ err instanceof Error ? err.message : 'Failed to load';
853
+ });
854
+ throw err;
855
+ }),
856
+ );
830
857
  },
831
858
 
832
859
  hasSave: (slot?: string) => {
833
860
  const { storyData, knownSaves } = get();
834
861
  if (!storyData) return false;
835
- return (slot ?? '') in knownSaves;
862
+ // Own entries only: slot names like 'constructor' are not inherited saves
863
+ return Object.prototype.hasOwnProperty.call(knownSaves, slot ?? '');
836
864
  },
837
865
 
838
866
  getSaveInfo: async (slot?: string): Promise<SaveInfo | null> => {
@@ -849,19 +877,22 @@ export const useStoryStore = create<StoryState>()(
849
877
 
850
878
  deleteSave: (slot?: string) => {
851
879
  const { storyData } = get();
852
- if (!storyData) return;
880
+ if (!storyData) return Promise.resolve();
853
881
 
854
- deleteSlotSave(storyData.ifid, slot)
855
- .then(() => {
856
- set((state) => {
857
- const key = slot ?? '';
858
- const { [key]: _, ...rest } = state.knownSaves;
859
- state.knownSaves = rest as Record<string, true>;
860
- });
861
- })
862
- .catch((err) => {
863
- console.error('spindle: failed to delete save', err);
864
- });
882
+ return handled(
883
+ deleteSlotSave(storyData.ifid, slot)
884
+ .then(() => {
885
+ set((state) => {
886
+ const key = slot ?? '';
887
+ const { [key]: _, ...rest } = state.knownSaves;
888
+ state.knownSaves = rest as Record<string, true>;
889
+ });
890
+ })
891
+ .catch((err) => {
892
+ console.error('spindle: failed to delete save', err);
893
+ throw err;
894
+ }),
895
+ );
865
896
  },
866
897
 
867
898
  exportSave: async (slot?: string): Promise<SaveExport | null> => {
package/src/story-api.ts CHANGED
@@ -155,12 +155,12 @@ export interface StoryAPI {
155
155
  back(): void;
156
156
  forward(): void;
157
157
  restart(): void;
158
- save(slot?: string, custom?: Record<string, unknown>): void;
159
- load(slot?: string): void;
158
+ save(slot?: string, custom?: Record<string, unknown>): Promise<void>;
159
+ load(slot?: string): Promise<void>;
160
160
  hasSave(slot?: string): boolean;
161
161
  getSaveInfo(slot?: string): Promise<SaveInfo | null>;
162
162
  listSaves(): Promise<SaveInfo[]>;
163
- deleteSave(slot?: string): void;
163
+ deleteSave(slot?: string): Promise<void>;
164
164
  exportSave(slot?: string): Promise<SaveExport | null>;
165
165
  importSave(data: unknown, slot?: string): Promise<SaveInfo>;
166
166
  visited(name?: string): number;
@@ -343,12 +343,12 @@ function createStoryAPI(): StoryAPI {
343
343
  useStoryStore.getState().restart();
344
344
  },
345
345
 
346
- save(slot?: string, custom?: Record<string, unknown>): void {
347
- useStoryStore.getState().save(slot, custom);
346
+ save(slot?: string, custom?: Record<string, unknown>): Promise<void> {
347
+ return useStoryStore.getState().save(slot, custom);
348
348
  },
349
349
 
350
- load(slot?: string): void {
351
- useStoryStore.getState().load(slot);
350
+ load(slot?: string): Promise<void> {
351
+ return useStoryStore.getState().load(slot);
352
352
  },
353
353
 
354
354
  hasSave(slot?: string): boolean {
@@ -363,8 +363,8 @@ function createStoryAPI(): StoryAPI {
363
363
  return useStoryStore.getState().listSaves();
364
364
  },
365
365
 
366
- deleteSave(slot?: string): void {
367
- useStoryStore.getState().deleteSave(slot);
366
+ deleteSave(slot?: string): Promise<void> {
367
+ return useStoryStore.getState().deleteSave(slot);
368
368
  },
369
369
 
370
370
  exportSave(slot?: string): Promise<SaveExport | null> {