@rohal12/spindle 0.34.0 → 0.36.0

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,567 @@
1
+ import type { SaveRecord, PlaythroughRecord, StorageBackend } from './types';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Memory backend
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export function createMemoryBackend(): StorageBackend {
8
+ const saves = new Map<string, SaveRecord>();
9
+ const playthroughs = new Map<string, PlaythroughRecord>();
10
+ const meta = new Map<string, unknown>();
11
+
12
+ return {
13
+ type: 'memory',
14
+
15
+ async putSave(record: SaveRecord): Promise<void> {
16
+ saves.set(record.meta.id, record);
17
+ },
18
+
19
+ async getSave(id: string): Promise<SaveRecord | undefined> {
20
+ return saves.get(id);
21
+ },
22
+
23
+ async deleteSave(id: string): Promise<void> {
24
+ saves.delete(id);
25
+ },
26
+
27
+ async getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
28
+ return [...saves.values()].filter((s) => s.meta.ifid === ifid);
29
+ },
30
+
31
+ async deleteSavesByIfid(ifid: string): Promise<void> {
32
+ for (const id of [...saves.keys()]) {
33
+ const s = saves.get(id);
34
+ if (s && s.meta.ifid === ifid) saves.delete(id);
35
+ }
36
+ },
37
+
38
+ async deleteSavesByPlaythrough(playthroughId: string): Promise<string[]> {
39
+ const deleted: string[] = [];
40
+ for (const id of [...saves.keys()]) {
41
+ const s = saves.get(id);
42
+ if (s && s.meta.playthroughId === playthroughId) {
43
+ saves.delete(id);
44
+ deleted.push(id);
45
+ }
46
+ }
47
+ return deleted;
48
+ },
49
+
50
+ async putPlaythrough(record: PlaythroughRecord): Promise<void> {
51
+ playthroughs.set(record.id, record);
52
+ },
53
+
54
+ async getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]> {
55
+ return [...playthroughs.values()].filter((p) => p.ifid === ifid);
56
+ },
57
+
58
+ async deletePlaythroughsByIfid(ifid: string): Promise<void> {
59
+ for (const id of [...playthroughs.keys()]) {
60
+ const p = playthroughs.get(id);
61
+ if (p && p.ifid === ifid) playthroughs.delete(id);
62
+ }
63
+ },
64
+
65
+ async deletePlaythroughById(id: string): Promise<void> {
66
+ playthroughs.delete(id);
67
+ },
68
+
69
+ async getMeta<T = unknown>(key: string): Promise<T | undefined> {
70
+ return meta.get(key) as T | undefined;
71
+ },
72
+
73
+ async setMeta(key: string, value: unknown): Promise<void> {
74
+ meta.set(key, value);
75
+ },
76
+
77
+ async deleteMeta(key: string): Promise<void> {
78
+ meta.delete(key);
79
+ },
80
+
81
+ async deleteMetaByPrefix(prefix: string): Promise<void> {
82
+ for (const key of [...meta.keys()]) {
83
+ if (key.startsWith(prefix)) meta.delete(key);
84
+ }
85
+ },
86
+
87
+ async deleteMetaByIfid(ifid: string): Promise<void> {
88
+ for (const key of [...meta.keys()]) {
89
+ if (key.includes(ifid)) meta.delete(key);
90
+ }
91
+ },
92
+
93
+ async getAllMetaKeys(): Promise<string[]> {
94
+ return [...meta.keys()];
95
+ },
96
+
97
+ async destroy(): Promise<void> {
98
+ saves.clear();
99
+ playthroughs.clear();
100
+ meta.clear();
101
+ },
102
+ };
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // localStorage backend
107
+ // ---------------------------------------------------------------------------
108
+
109
+ const LS_SAVE_PREFIX = 'spindle.save.';
110
+ const LS_PT_PREFIX = 'spindle.pt.';
111
+ const LS_META_PREFIX = 'spindle.meta.';
112
+ const LS_IDX_SAVES_PREFIX = 'spindle.idx.saves.';
113
+ const LS_IDX_PT_PREFIX = 'spindle.idx.pt.';
114
+ const LS_IDX_SAVES_PT_PREFIX = 'spindle.idx.saves-pt.';
115
+
116
+ function lsGet<T>(key: string): T | undefined {
117
+ const raw = localStorage.getItem(key);
118
+ if (raw === null) return undefined;
119
+ try {
120
+ return JSON.parse(raw) as T;
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ }
125
+
126
+ function lsSet(key: string, value: unknown): void {
127
+ localStorage.setItem(key, JSON.stringify(value));
128
+ }
129
+
130
+ function lsDel(key: string): void {
131
+ localStorage.removeItem(key);
132
+ }
133
+
134
+ function lsIndex(key: string): string[] {
135
+ return lsGet<string[]>(key) ?? [];
136
+ }
137
+
138
+ function lsIndexAdd(key: string, entry: string): void {
139
+ const arr = lsIndex(key);
140
+ if (!arr.includes(entry)) {
141
+ arr.push(entry);
142
+ lsSet(key, arr);
143
+ }
144
+ }
145
+
146
+ function lsIndexRemove(key: string, entry: string): void {
147
+ const arr = lsIndex(key).filter((e) => e !== entry);
148
+ lsSet(key, arr);
149
+ }
150
+
151
+ export function createLocalStorageBackend(): StorageBackend {
152
+ return {
153
+ type: 'localstorage',
154
+
155
+ async putSave(record: SaveRecord): Promise<void> {
156
+ const id = record.meta.id;
157
+ lsSet(`${LS_SAVE_PREFIX}${id}`, record);
158
+ lsIndexAdd(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
159
+ lsIndexAdd(`${LS_IDX_SAVES_PT_PREFIX}${record.meta.playthroughId}`, id);
160
+ },
161
+
162
+ async getSave(id: string): Promise<SaveRecord | undefined> {
163
+ return lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
164
+ },
165
+
166
+ async deleteSave(id: string): Promise<void> {
167
+ const record = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
168
+ if (record) {
169
+ lsIndexRemove(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
170
+ lsIndexRemove(
171
+ `${LS_IDX_SAVES_PT_PREFIX}${record.meta.playthroughId}`,
172
+ id,
173
+ );
174
+ }
175
+ lsDel(`${LS_SAVE_PREFIX}${id}`);
176
+ },
177
+
178
+ async getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
179
+ const ids = lsIndex(`${LS_IDX_SAVES_PREFIX}${ifid}`);
180
+ const results: SaveRecord[] = [];
181
+ for (const id of ids) {
182
+ const record = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
183
+ if (record) results.push(record);
184
+ }
185
+ return results;
186
+ },
187
+
188
+ async deleteSavesByIfid(ifid: string): Promise<void> {
189
+ const ids = lsIndex(`${LS_IDX_SAVES_PREFIX}${ifid}`);
190
+ for (const id of ids) {
191
+ const record = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
192
+ if (record) {
193
+ lsIndexRemove(
194
+ `${LS_IDX_SAVES_PT_PREFIX}${record.meta.playthroughId}`,
195
+ id,
196
+ );
197
+ }
198
+ lsDel(`${LS_SAVE_PREFIX}${id}`);
199
+ }
200
+ lsDel(`${LS_IDX_SAVES_PREFIX}${ifid}`);
201
+ },
202
+
203
+ async deleteSavesByPlaythrough(playthroughId: string): Promise<string[]> {
204
+ const ids = lsIndex(`${LS_IDX_SAVES_PT_PREFIX}${playthroughId}`);
205
+ for (const id of ids) {
206
+ const record = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
207
+ if (record) {
208
+ lsIndexRemove(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
209
+ }
210
+ lsDel(`${LS_SAVE_PREFIX}${id}`);
211
+ }
212
+ lsDel(`${LS_IDX_SAVES_PT_PREFIX}${playthroughId}`);
213
+ return ids;
214
+ },
215
+
216
+ async putPlaythrough(record: PlaythroughRecord): Promise<void> {
217
+ lsSet(`${LS_PT_PREFIX}${record.id}`, record);
218
+ lsIndexAdd(`${LS_IDX_PT_PREFIX}${record.ifid}`, record.id);
219
+ },
220
+
221
+ async getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]> {
222
+ const ids = lsIndex(`${LS_IDX_PT_PREFIX}${ifid}`);
223
+ const results: PlaythroughRecord[] = [];
224
+ for (const id of ids) {
225
+ const record = lsGet<PlaythroughRecord>(`${LS_PT_PREFIX}${id}`);
226
+ if (record) results.push(record);
227
+ }
228
+ return results;
229
+ },
230
+
231
+ async deletePlaythroughsByIfid(ifid: string): Promise<void> {
232
+ const ids = lsIndex(`${LS_IDX_PT_PREFIX}${ifid}`);
233
+ for (const id of ids) {
234
+ lsDel(`${LS_PT_PREFIX}${id}`);
235
+ }
236
+ lsDel(`${LS_IDX_PT_PREFIX}${ifid}`);
237
+ },
238
+
239
+ async deletePlaythroughById(id: string): Promise<void> {
240
+ const record = lsGet<PlaythroughRecord>(`${LS_PT_PREFIX}${id}`);
241
+ if (record) {
242
+ lsIndexRemove(`${LS_IDX_PT_PREFIX}${record.ifid}`, id);
243
+ }
244
+ lsDel(`${LS_PT_PREFIX}${id}`);
245
+ },
246
+
247
+ async getMeta<T = unknown>(key: string): Promise<T | undefined> {
248
+ return lsGet<T>(`${LS_META_PREFIX}${key}`);
249
+ },
250
+
251
+ async setMeta(key: string, value: unknown): Promise<void> {
252
+ lsSet(`${LS_META_PREFIX}${key}`, value);
253
+ },
254
+
255
+ async deleteMeta(key: string): Promise<void> {
256
+ lsDel(`${LS_META_PREFIX}${key}`);
257
+ },
258
+
259
+ async deleteMetaByPrefix(prefix: string): Promise<void> {
260
+ const fullPrefix = `${LS_META_PREFIX}${prefix}`;
261
+ for (const key of Object.keys(localStorage)) {
262
+ if (key.startsWith(fullPrefix)) {
263
+ localStorage.removeItem(key);
264
+ }
265
+ }
266
+ },
267
+
268
+ async deleteMetaByIfid(ifid: string): Promise<void> {
269
+ for (const key of Object.keys(localStorage)) {
270
+ if (key.startsWith(LS_META_PREFIX) && key.includes(ifid)) {
271
+ localStorage.removeItem(key);
272
+ }
273
+ }
274
+ },
275
+
276
+ async getAllMetaKeys(): Promise<string[]> {
277
+ const keys: string[] = [];
278
+ for (const key of Object.keys(localStorage)) {
279
+ if (key.startsWith(LS_META_PREFIX)) {
280
+ keys.push(key.slice(LS_META_PREFIX.length));
281
+ }
282
+ }
283
+ return keys;
284
+ },
285
+
286
+ async destroy(): Promise<void> {
287
+ for (const key of Object.keys(localStorage)) {
288
+ if (key.startsWith('spindle.')) {
289
+ localStorage.removeItem(key);
290
+ }
291
+ }
292
+ },
293
+ };
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // IDB backend
298
+ // ---------------------------------------------------------------------------
299
+
300
+ const IDB_DB_NAME = 'spindle';
301
+ const IDB_DB_VERSION = 1;
302
+
303
+ function createIDBBackend(): StorageBackend {
304
+ let dbPromise: Promise<IDBDatabase> | null = null;
305
+
306
+ function openDB(): Promise<IDBDatabase> {
307
+ if (dbPromise) return dbPromise;
308
+
309
+ dbPromise = new Promise((resolve, reject) => {
310
+ const req = indexedDB.open(IDB_DB_NAME, IDB_DB_VERSION);
311
+
312
+ req.onupgradeneeded = () => {
313
+ const db = req.result;
314
+
315
+ if (!db.objectStoreNames.contains('saves')) {
316
+ const savesStore = db.createObjectStore('saves', {
317
+ keyPath: 'meta.id',
318
+ });
319
+ savesStore.createIndex('ifid', 'meta.ifid', { unique: false });
320
+ savesStore.createIndex('playthroughId', 'meta.playthroughId', {
321
+ unique: false,
322
+ });
323
+ }
324
+
325
+ if (!db.objectStoreNames.contains('playthroughs')) {
326
+ const ptStore = db.createObjectStore('playthroughs', {
327
+ keyPath: 'id',
328
+ });
329
+ ptStore.createIndex('ifid', 'ifid', { unique: false });
330
+ }
331
+
332
+ if (!db.objectStoreNames.contains('meta')) {
333
+ db.createObjectStore('meta', { keyPath: 'key' });
334
+ }
335
+ };
336
+
337
+ req.onsuccess = () => resolve(req.result);
338
+ req.onerror = () => reject(req.error);
339
+ });
340
+
341
+ return dbPromise;
342
+ }
343
+
344
+ function tx(
345
+ storeName: string,
346
+ mode: IDBTransactionMode,
347
+ ): Promise<IDBObjectStore> {
348
+ return openDB().then((db) =>
349
+ db.transaction(storeName, mode).objectStore(storeName),
350
+ );
351
+ }
352
+
353
+ function idbReq<T>(req: IDBRequest<T>): Promise<T> {
354
+ return new Promise((resolve, reject) => {
355
+ req.onsuccess = () => resolve(req.result);
356
+ req.onerror = () => reject(req.error);
357
+ });
358
+ }
359
+
360
+ async function getAllFromIndex<T>(
361
+ storeName: string,
362
+ indexName: string,
363
+ value: string,
364
+ ): Promise<T[]> {
365
+ const store = await tx(storeName, 'readonly');
366
+ const index = store.index(indexName);
367
+ const result = await idbReq(index.getAll(value));
368
+ return (result ?? []) as T[];
369
+ }
370
+
371
+ return {
372
+ type: 'indexeddb',
373
+
374
+ async putSave(record: SaveRecord): Promise<void> {
375
+ const store = await tx('saves', 'readwrite');
376
+ await idbReq(store.put(record));
377
+ },
378
+
379
+ async getSave(id: string): Promise<SaveRecord | undefined> {
380
+ const store = await tx('saves', 'readonly');
381
+ return (
382
+ ((await idbReq(store.get(id))) as SaveRecord | undefined) ?? undefined
383
+ );
384
+ },
385
+
386
+ async deleteSave(id: string): Promise<void> {
387
+ const store = await tx('saves', 'readwrite');
388
+ await idbReq(store.delete(id));
389
+ },
390
+
391
+ async getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
392
+ return getAllFromIndex<SaveRecord>('saves', 'ifid', ifid);
393
+ },
394
+
395
+ async deleteSavesByIfid(ifid: string): Promise<void> {
396
+ const records = await getAllFromIndex<SaveRecord>('saves', 'ifid', ifid);
397
+ const store = await tx('saves', 'readwrite');
398
+ for (const r of records) {
399
+ await idbReq(store.delete(r.meta.id));
400
+ }
401
+ },
402
+
403
+ async deleteSavesByPlaythrough(playthroughId: string): Promise<string[]> {
404
+ const records = await getAllFromIndex<SaveRecord>(
405
+ 'saves',
406
+ 'playthroughId',
407
+ playthroughId,
408
+ );
409
+ const store = await tx('saves', 'readwrite');
410
+ const ids: string[] = [];
411
+ for (const r of records) {
412
+ await idbReq(store.delete(r.meta.id));
413
+ ids.push(r.meta.id);
414
+ }
415
+ return ids;
416
+ },
417
+
418
+ async putPlaythrough(record: PlaythroughRecord): Promise<void> {
419
+ const store = await tx('playthroughs', 'readwrite');
420
+ await idbReq(store.put(record));
421
+ },
422
+
423
+ async getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]> {
424
+ return getAllFromIndex<PlaythroughRecord>('playthroughs', 'ifid', ifid);
425
+ },
426
+
427
+ async deletePlaythroughsByIfid(ifid: string): Promise<void> {
428
+ const records = await getAllFromIndex<PlaythroughRecord>(
429
+ 'playthroughs',
430
+ 'ifid',
431
+ ifid,
432
+ );
433
+ const store = await tx('playthroughs', 'readwrite');
434
+ for (const r of records) {
435
+ await idbReq(store.delete(r.id));
436
+ }
437
+ },
438
+
439
+ async deletePlaythroughById(id: string): Promise<void> {
440
+ const store = await tx('playthroughs', 'readwrite');
441
+ await idbReq(store.delete(id));
442
+ },
443
+
444
+ async getMeta<T = unknown>(key: string): Promise<T | undefined> {
445
+ const store = await tx('meta', 'readonly');
446
+ const row = (await idbReq(store.get(key))) as
447
+ | { key: string; value: T }
448
+ | undefined;
449
+ return row ? row.value : undefined;
450
+ },
451
+
452
+ async setMeta(key: string, value: unknown): Promise<void> {
453
+ const store = await tx('meta', 'readwrite');
454
+ await idbReq(store.put({ key, value }));
455
+ },
456
+
457
+ async deleteMeta(key: string): Promise<void> {
458
+ const store = await tx('meta', 'readwrite');
459
+ await idbReq(store.delete(key));
460
+ },
461
+
462
+ async deleteMetaByPrefix(prefix: string): Promise<void> {
463
+ const store = await tx('meta', 'readwrite');
464
+ const allRows = (await idbReq(store.getAll())) as Array<{
465
+ key: string;
466
+ value: unknown;
467
+ }>;
468
+ for (const row of allRows) {
469
+ if (row.key.startsWith(prefix)) {
470
+ const delStore = await tx('meta', 'readwrite');
471
+ await idbReq(delStore.delete(row.key));
472
+ }
473
+ }
474
+ },
475
+
476
+ async deleteMetaByIfid(ifid: string): Promise<void> {
477
+ const store = await tx('meta', 'readwrite');
478
+ const allRows = (await idbReq(store.getAll())) as Array<{
479
+ key: string;
480
+ value: unknown;
481
+ }>;
482
+ for (const row of allRows) {
483
+ if (row.key.includes(ifid)) {
484
+ const delStore = await tx('meta', 'readwrite');
485
+ await idbReq(delStore.delete(row.key));
486
+ }
487
+ }
488
+ },
489
+
490
+ async getAllMetaKeys(): Promise<string[]> {
491
+ const store = await tx('meta', 'readonly');
492
+ const allRows = (await idbReq(store.getAll())) as Array<{
493
+ key: string;
494
+ value: unknown;
495
+ }>;
496
+ return allRows.map((r) => r.key);
497
+ },
498
+
499
+ async destroy(): Promise<void> {
500
+ const db = await openDB();
501
+ db.close();
502
+ dbPromise = null;
503
+ await new Promise<void>((resolve, reject) => {
504
+ const req = indexedDB.deleteDatabase(IDB_DB_NAME);
505
+ req.onsuccess = () => resolve();
506
+ req.onerror = () => reject(req.error);
507
+ });
508
+ },
509
+ };
510
+ }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Backend detection and singleton
514
+ // ---------------------------------------------------------------------------
515
+
516
+ let _backend: StorageBackend | null = null;
517
+
518
+ async function detectBackend(): Promise<StorageBackend> {
519
+ if (typeof indexedDB !== 'undefined') {
520
+ try {
521
+ await new Promise<void>((resolve, reject) => {
522
+ const req = indexedDB.open('__spindle_probe__', 1);
523
+ req.onsuccess = () => {
524
+ req.result.close();
525
+ indexedDB.deleteDatabase('__spindle_probe__');
526
+ resolve();
527
+ };
528
+ req.onerror = () => reject(req.error);
529
+ });
530
+ return createIDBBackend();
531
+ } catch {
532
+ // fall through
533
+ }
534
+ }
535
+
536
+ if (typeof localStorage !== 'undefined') {
537
+ try {
538
+ const probeKey = '__spindle_probe__';
539
+ localStorage.setItem(probeKey, '1');
540
+ const val = localStorage.getItem(probeKey);
541
+ localStorage.removeItem(probeKey);
542
+ if (val === '1') return createLocalStorageBackend();
543
+ } catch {
544
+ // fall through
545
+ }
546
+ }
547
+
548
+ return createMemoryBackend();
549
+ }
550
+
551
+ export async function getBackend(): Promise<StorageBackend> {
552
+ if (_backend) return _backend;
553
+ _backend = await detectBackend();
554
+ return _backend;
555
+ }
556
+
557
+ export function resetBackend(): void {
558
+ _backend = null;
559
+ }
560
+
561
+ /**
562
+ * Synchronous accessor for the current backend type.
563
+ * Returns 'memory' if the backend hasn't been detected yet.
564
+ */
565
+ export function getBackendType(): 'indexeddb' | 'localstorage' | 'memory' {
566
+ return _backend?.type ?? 'memory';
567
+ }
@@ -27,6 +27,7 @@ export interface SaveMeta {
27
27
  title: string;
28
28
  passage: string;
29
29
  custom: Record<string, unknown>;
30
+ estimatedBytes?: number;
30
31
  }
31
32
 
32
33
  export interface SaveRecord {
@@ -97,3 +98,49 @@ export function isSavePayload(value: unknown): value is SavePayload {
97
98
  if (typeof obj.historyIndex !== 'number') return false;
98
99
  return true;
99
100
  }
101
+
102
+ /** Estimated byte size of a serialized save payload. */
103
+ export function estimatePayloadBytes(payload: SavePayload): number {
104
+ return JSON.stringify(payload).length;
105
+ }
106
+
107
+ export interface StorageInfo {
108
+ saveCount: number;
109
+ playthroughCount: number;
110
+ /** Estimated total bytes across all saves. */
111
+ totalBytes: number;
112
+ backend: 'indexeddb' | 'localstorage' | 'memory';
113
+ }
114
+
115
+ export interface StorageQuota {
116
+ usage: number;
117
+ quota: number;
118
+ /** false if navigator.storage.estimate() is unsupported. */
119
+ estimateSupported: boolean;
120
+ }
121
+
122
+ /** Abstract storage backend for saves, playthroughs, and metadata. */
123
+ export interface StorageBackend {
124
+ putSave(record: SaveRecord): Promise<void>;
125
+ getSave(id: string): Promise<SaveRecord | undefined>;
126
+ deleteSave(id: string): Promise<void>;
127
+ getSavesByIfid(ifid: string): Promise<SaveRecord[]>;
128
+ deleteSavesByIfid(ifid: string): Promise<void>;
129
+ deleteSavesByPlaythrough(playthroughId: string): Promise<string[]>;
130
+
131
+ putPlaythrough(record: PlaythroughRecord): Promise<void>;
132
+ getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]>;
133
+ deletePlaythroughsByIfid(ifid: string): Promise<void>;
134
+ deletePlaythroughById(id: string): Promise<void>;
135
+
136
+ getMeta<T = unknown>(key: string): Promise<T | undefined>;
137
+ setMeta(key: string, value: unknown): Promise<void>;
138
+ deleteMeta(key: string): Promise<void>;
139
+ deleteMetaByPrefix(prefix: string): Promise<void>;
140
+ deleteMetaByIfid(ifid: string): Promise<void>;
141
+ getAllMetaKeys(): Promise<string[]>;
142
+
143
+ destroy(): Promise<void>;
144
+
145
+ readonly type: 'indexeddb' | 'localstorage' | 'memory';
146
+ }
package/src/store.ts CHANGED
@@ -23,6 +23,9 @@ import {
23
23
  deleteSlotSave,
24
24
  saveSession,
25
25
  clearSession,
26
+ clearGameData as smClearGameData,
27
+ clearAllData as smClearAllData,
28
+ deletePlaythroughData as smDeletePlaythroughData,
26
29
  } from './saves/save-manager';
27
30
  import { deepClone, serialize, deserialize } from './class-registry';
28
31
  import {
@@ -231,6 +234,9 @@ export interface StoryState {
231
234
  getSaveInfo: (slot?: string) => Promise<SaveInfo | null>;
232
235
  listSaves: () => Promise<SaveInfo[]>;
233
236
  deleteSave: (slot?: string) => void;
237
+ clearGameData: () => void;
238
+ clearAllData: () => void;
239
+ deletePlaythrough: (playthroughId: string) => void;
234
240
  getSavePayload: () => SavePayload;
235
241
  loadFromPayload: (payload: SavePayload) => void;
236
242
  getHistoryVariables: (index: number) => Record<string, unknown>;
@@ -585,6 +591,54 @@ export const useStoryStore = create<StoryState>()(
585
591
  });
586
592
  },
587
593
 
594
+ clearGameData: () => {
595
+ const { storyData } = get();
596
+ if (!storyData) return;
597
+
598
+ smClearGameData(storyData.ifid)
599
+ .then(() => {
600
+ set((state) => {
601
+ state.knownSaves = {};
602
+ });
603
+ get().restart();
604
+ })
605
+ .catch((err) => {
606
+ console.error('spindle: failed to clear game data', err);
607
+ });
608
+ },
609
+
610
+ clearAllData: () => {
611
+ const { storyData } = get();
612
+ if (!storyData) return;
613
+
614
+ smClearAllData()
615
+ .then(() => {
616
+ set((state) => {
617
+ state.knownSaves = {};
618
+ });
619
+ get().restart();
620
+ })
621
+ .catch((err) => {
622
+ console.error('spindle: failed to clear all data', err);
623
+ });
624
+ },
625
+
626
+ deletePlaythrough: (playthroughId: string) => {
627
+ const { storyData } = get();
628
+ if (!storyData) return;
629
+
630
+ smDeletePlaythroughData(storyData.ifid, playthroughId)
631
+ .then(async () => {
632
+ const known = await populateKnownSaves(storyData.ifid);
633
+ set((state) => {
634
+ state.knownSaves = known;
635
+ });
636
+ })
637
+ .catch((err) => {
638
+ console.error('spindle: failed to delete playthrough', err);
639
+ });
640
+ },
641
+
588
642
  getSavePayload: (): SavePayload => {
589
643
  const {
590
644
  currentPassage,