@rohal12/spindle 0.35.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.
- package/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/saves/save-manager.ts +128 -44
- package/src/saves/storage.ts +567 -0
- package/src/saves/types.ts +47 -0
- package/src/store.ts +54 -0
- package/src/story-api.ts +85 -2
- package/src/saves/idb.ts +0 -165
package/src/story-api.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { useStoryStore } from './store';
|
|
2
2
|
import type { Passage } from './parser';
|
|
3
3
|
import { settings } from './settings';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
4
|
+
import type {
|
|
5
|
+
SavePayload,
|
|
6
|
+
SaveInfo,
|
|
7
|
+
StorageInfo,
|
|
8
|
+
StorageQuota,
|
|
9
|
+
} from './saves/types';
|
|
10
|
+
import {
|
|
11
|
+
setTitleGenerator,
|
|
12
|
+
getStorageInfo as _getStorageInfo,
|
|
13
|
+
clearGameData as _clearGameData,
|
|
14
|
+
clearAllData as _clearAllData,
|
|
15
|
+
deletePlaythroughData,
|
|
16
|
+
populateKnownSaves,
|
|
17
|
+
} from './saves/save-manager';
|
|
18
|
+
import { getBackendType } from './saves/storage';
|
|
6
19
|
import { registerClass } from './class-registry';
|
|
7
20
|
import { defineMacro } from './define-macro';
|
|
8
21
|
import type { MacroDefinition } from './define-macro';
|
|
@@ -76,6 +89,14 @@ export interface StoryAPI {
|
|
|
76
89
|
readonly saves: {
|
|
77
90
|
setTitleGenerator(fn: (payload: SavePayload) => string): void;
|
|
78
91
|
};
|
|
92
|
+
readonly storage: {
|
|
93
|
+
getInfo(): Promise<StorageInfo>;
|
|
94
|
+
getQuota(): Promise<StorageQuota>;
|
|
95
|
+
clearGameData(): Promise<void>;
|
|
96
|
+
clearAllData(): Promise<void>;
|
|
97
|
+
deletePlaythrough(playthroughId: string): Promise<void>;
|
|
98
|
+
readonly backend: 'indexeddb' | 'localstorage' | 'memory';
|
|
99
|
+
};
|
|
79
100
|
getActions(): StoryAction[];
|
|
80
101
|
performAction(id: string, value?: unknown): void;
|
|
81
102
|
on(event: 'navigate', callback: NavigateCallback): () => void;
|
|
@@ -246,6 +267,68 @@ function createStoryAPI(): StoryAPI {
|
|
|
246
267
|
},
|
|
247
268
|
},
|
|
248
269
|
|
|
270
|
+
storage: {
|
|
271
|
+
async getInfo(): Promise<StorageInfo> {
|
|
272
|
+
const ifid = useStoryStore.getState().storyData?.ifid;
|
|
273
|
+
if (!ifid)
|
|
274
|
+
return {
|
|
275
|
+
saveCount: 0,
|
|
276
|
+
playthroughCount: 0,
|
|
277
|
+
totalBytes: 0,
|
|
278
|
+
backend: getBackendType(),
|
|
279
|
+
};
|
|
280
|
+
return _getStorageInfo(ifid);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async getQuota(): Promise<StorageQuota> {
|
|
284
|
+
if (typeof navigator !== 'undefined' && navigator.storage?.estimate) {
|
|
285
|
+
try {
|
|
286
|
+
const est = await navigator.storage.estimate();
|
|
287
|
+
return {
|
|
288
|
+
usage: est.usage ?? 0,
|
|
289
|
+
quota: est.quota ?? 0,
|
|
290
|
+
estimateSupported: true,
|
|
291
|
+
};
|
|
292
|
+
} catch {
|
|
293
|
+
// fall through
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { usage: 0, quota: 0, estimateSupported: false };
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async clearGameData(): Promise<void> {
|
|
300
|
+
const ifid = useStoryStore.getState().storyData?.ifid;
|
|
301
|
+
if (!ifid) return;
|
|
302
|
+
await _clearGameData(ifid);
|
|
303
|
+
useStoryStore.setState((state) => {
|
|
304
|
+
state.knownSaves = {};
|
|
305
|
+
});
|
|
306
|
+
useStoryStore.getState().restart();
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async clearAllData(): Promise<void> {
|
|
310
|
+
await _clearAllData();
|
|
311
|
+
useStoryStore.setState((state) => {
|
|
312
|
+
state.knownSaves = {};
|
|
313
|
+
});
|
|
314
|
+
useStoryStore.getState().restart();
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
async deletePlaythrough(playthroughId: string): Promise<void> {
|
|
318
|
+
const { storyData } = useStoryStore.getState();
|
|
319
|
+
if (!storyData) return;
|
|
320
|
+
await deletePlaythroughData(storyData.ifid, playthroughId);
|
|
321
|
+
const known = await populateKnownSaves(storyData.ifid);
|
|
322
|
+
useStoryStore.setState((state) => {
|
|
323
|
+
state.knownSaves = known;
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
get backend() {
|
|
328
|
+
return getBackendType();
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
|
|
249
332
|
getActions(): StoryAction[] {
|
|
250
333
|
return getActions();
|
|
251
334
|
},
|
package/src/saves/idb.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import type { SaveRecord, PlaythroughRecord } from './types';
|
|
2
|
-
|
|
3
|
-
const DB_NAME = 'spindle';
|
|
4
|
-
const DB_VERSION = 1;
|
|
5
|
-
|
|
6
|
-
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
7
|
-
let fallbackStore: {
|
|
8
|
-
saves: Map<string, SaveRecord>;
|
|
9
|
-
playthroughs: Map<string, PlaythroughRecord>;
|
|
10
|
-
meta: Map<string, unknown>;
|
|
11
|
-
} | null = null;
|
|
12
|
-
|
|
13
|
-
function useFallback() {
|
|
14
|
-
if (!fallbackStore) {
|
|
15
|
-
fallbackStore = {
|
|
16
|
-
saves: new Map(),
|
|
17
|
-
playthroughs: new Map(),
|
|
18
|
-
meta: new Map(),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
return fallbackStore;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function openDB(): Promise<IDBDatabase> {
|
|
25
|
-
if (dbPromise) return dbPromise;
|
|
26
|
-
|
|
27
|
-
if (typeof indexedDB === 'undefined') {
|
|
28
|
-
return Promise.reject(new Error('IndexedDB unavailable'));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
dbPromise = new Promise((resolve, reject) => {
|
|
32
|
-
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
33
|
-
|
|
34
|
-
req.onupgradeneeded = () => {
|
|
35
|
-
const db = req.result;
|
|
36
|
-
|
|
37
|
-
if (!db.objectStoreNames.contains('saves')) {
|
|
38
|
-
const saves = db.createObjectStore('saves', { keyPath: 'meta.id' });
|
|
39
|
-
saves.createIndex('ifid', 'meta.ifid', { unique: false });
|
|
40
|
-
saves.createIndex('playthroughId', 'meta.playthroughId', {
|
|
41
|
-
unique: false,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!db.objectStoreNames.contains('playthroughs')) {
|
|
46
|
-
const pt = db.createObjectStore('playthroughs', { keyPath: 'id' });
|
|
47
|
-
pt.createIndex('ifid', 'ifid', { unique: false });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!db.objectStoreNames.contains('meta')) {
|
|
51
|
-
db.createObjectStore('meta', { keyPath: 'key' });
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
req.onsuccess = () => resolve(req.result);
|
|
56
|
-
req.onerror = () => reject(req.error);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return dbPromise;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function tx(
|
|
63
|
-
storeName: string,
|
|
64
|
-
mode: IDBTransactionMode,
|
|
65
|
-
): Promise<IDBObjectStore> {
|
|
66
|
-
return openDB().then((db) =>
|
|
67
|
-
db.transaction(storeName, mode).objectStore(storeName),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function idbRequest<T>(req: IDBRequest<T>): Promise<T> {
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
req.onsuccess = () => resolve(req.result);
|
|
74
|
-
req.onerror = () => reject(req.error);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// --- Saves ---
|
|
79
|
-
|
|
80
|
-
export async function putSave(record: SaveRecord): Promise<void> {
|
|
81
|
-
try {
|
|
82
|
-
const store = await tx('saves', 'readwrite');
|
|
83
|
-
await idbRequest(store.put(record));
|
|
84
|
-
} catch {
|
|
85
|
-
useFallback().saves.set(record.meta.id, record);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function getSave(id: string): Promise<SaveRecord | undefined> {
|
|
90
|
-
try {
|
|
91
|
-
const store = await tx('saves', 'readonly');
|
|
92
|
-
return (await idbRequest(store.get(id))) ?? undefined;
|
|
93
|
-
} catch {
|
|
94
|
-
return useFallback().saves.get(id);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function deleteSave(id: string): Promise<void> {
|
|
99
|
-
try {
|
|
100
|
-
const store = await tx('saves', 'readwrite');
|
|
101
|
-
await idbRequest(store.delete(id));
|
|
102
|
-
} catch {
|
|
103
|
-
useFallback().saves.delete(id);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
|
|
108
|
-
try {
|
|
109
|
-
const store = await tx('saves', 'readonly');
|
|
110
|
-
const index = store.index('ifid');
|
|
111
|
-
return (await idbRequest(index.getAll(ifid))) ?? [];
|
|
112
|
-
} catch {
|
|
113
|
-
return [...useFallback().saves.values()].filter(
|
|
114
|
-
(s) => s.meta.ifid === ifid,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// --- Playthroughs ---
|
|
120
|
-
|
|
121
|
-
export async function putPlaythrough(record: PlaythroughRecord): Promise<void> {
|
|
122
|
-
try {
|
|
123
|
-
const store = await tx('playthroughs', 'readwrite');
|
|
124
|
-
await idbRequest(store.put(record));
|
|
125
|
-
} catch {
|
|
126
|
-
useFallback().playthroughs.set(record.id, record);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export async function getPlaythroughsByIfid(
|
|
131
|
-
ifid: string,
|
|
132
|
-
): Promise<PlaythroughRecord[]> {
|
|
133
|
-
try {
|
|
134
|
-
const store = await tx('playthroughs', 'readonly');
|
|
135
|
-
const index = store.index('ifid');
|
|
136
|
-
return (await idbRequest(index.getAll(ifid))) ?? [];
|
|
137
|
-
} catch {
|
|
138
|
-
return [...useFallback().playthroughs.values()].filter(
|
|
139
|
-
(p) => p.ifid === ifid,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// --- Meta (key-value) ---
|
|
145
|
-
|
|
146
|
-
export async function getMeta<T = unknown>(
|
|
147
|
-
key: string,
|
|
148
|
-
): Promise<T | undefined> {
|
|
149
|
-
try {
|
|
150
|
-
const store = await tx('meta', 'readonly');
|
|
151
|
-
const row = await idbRequest(store.get(key));
|
|
152
|
-
return row ? (row as { key: string; value: T }).value : undefined;
|
|
153
|
-
} catch {
|
|
154
|
-
return useFallback().meta.get(key) as T | undefined;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function setMeta(key: string, value: unknown): Promise<void> {
|
|
159
|
-
try {
|
|
160
|
-
const store = await tx('meta', 'readwrite');
|
|
161
|
-
await idbRequest(store.put({ key, value }));
|
|
162
|
-
} catch {
|
|
163
|
-
useFallback().meta.set(key, value);
|
|
164
|
-
}
|
|
165
|
-
}
|