@rohal12/spindle 0.52.4 → 0.52.5
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 +819 -945
- package/package.json +1 -1
- package/src/class-registry.ts +6 -182
- package/src/execute-mutation.ts +17 -97
- package/src/saves/save-manager.ts +190 -259
- package/src/saves/storage.ts +229 -402
- package/src/store.ts +244 -310
- package/src/structural.ts +388 -0
package/src/saves/storage.ts
CHANGED
|
@@ -1,126 +1,160 @@
|
|
|
1
1
|
import type { SaveRecord, PlaythroughRecord, StorageBackend } from './types';
|
|
2
2
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
//
|
|
4
|
+
// Backend operations, over the tables each backend provides
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* values
|
|
10
|
-
*
|
|
8
|
+
* One kind of record a backend stores, by key: saves, playthroughs or meta
|
|
9
|
+
* values. Each backend implements these few operations; every operation of
|
|
10
|
+
* a StorageBackend is built on them once, by createBackend().
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
async putSave(record: SaveRecord): Promise<void> {
|
|
22
|
-
saves.set(record.meta.id, copy(record));
|
|
23
|
-
},
|
|
12
|
+
interface Table<T> {
|
|
13
|
+
get(key: string): Promise<T | undefined>;
|
|
14
|
+
put(key: string, value: T): Promise<void>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
/** The records whose indexed `field` (see makeTables) holds `value`. */
|
|
17
|
+
find(field: string, value: string): Promise<T[]>;
|
|
18
|
+
/** Every key, in the backend's order. */
|
|
19
|
+
keys(): Promise<string[]>;
|
|
20
|
+
}
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
const record = saves.get(id);
|
|
27
|
-
return record && copy(record);
|
|
28
|
-
},
|
|
22
|
+
type TableName = 'saves' | 'playthroughs' | 'meta';
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
async getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
|
|
35
|
-
return [...saves.values()].filter((s) => s.meta.ifid === ifid).map(copy);
|
|
36
|
-
},
|
|
24
|
+
/** A table's indexed fields, and how to read them from a record. */
|
|
25
|
+
type Indexes<T> = Record<string, (record: T) => string>;
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
for (const id of [...saves.keys()]) {
|
|
40
|
-
const s = saves.get(id);
|
|
41
|
-
if (s && s.meta.ifid === ifid) saves.delete(id);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
async deleteSavesByPlaythrough(playthroughId: string): Promise<string[]> {
|
|
46
|
-
const deleted: string[] = [];
|
|
47
|
-
for (const id of [...saves.keys()]) {
|
|
48
|
-
const s = saves.get(id);
|
|
49
|
-
if (s && s.meta.playthroughId === playthroughId) {
|
|
50
|
-
saves.delete(id);
|
|
51
|
-
deleted.push(id);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return deleted;
|
|
55
|
-
},
|
|
27
|
+
type MakeTable = <T>(name: TableName, indexes: Indexes<T>) => Table<T>;
|
|
56
28
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return [...playthroughs.values()]
|
|
63
|
-
.filter((p) => p.ifid === ifid)
|
|
64
|
-
.map(copy);
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
async deletePlaythroughsByIfid(ifid: string): Promise<void> {
|
|
68
|
-
for (const id of [...playthroughs.keys()]) {
|
|
69
|
-
const p = playthroughs.get(id);
|
|
70
|
-
if (p && p.ifid === ifid) playthroughs.delete(id);
|
|
71
|
-
}
|
|
72
|
-
},
|
|
29
|
+
interface Tables {
|
|
30
|
+
saves: Table<SaveRecord>;
|
|
31
|
+
playthroughs: Table<PlaythroughRecord>;
|
|
32
|
+
meta: Table<unknown>;
|
|
33
|
+
}
|
|
73
34
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
35
|
+
/** A backend's tables, made by its `table` function. */
|
|
36
|
+
function makeTables(table: MakeTable): Tables {
|
|
37
|
+
return {
|
|
38
|
+
saves: table<SaveRecord>('saves', {
|
|
39
|
+
ifid: (r) => r.meta.ifid,
|
|
40
|
+
playthroughId: (r) => r.meta.playthroughId,
|
|
41
|
+
}),
|
|
42
|
+
playthroughs: table<PlaythroughRecord>('playthroughs', {
|
|
43
|
+
ifid: (r) => r.ifid,
|
|
44
|
+
}),
|
|
45
|
+
meta: table('meta', {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
77
48
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
49
|
+
function createBackend(
|
|
50
|
+
type: StorageBackend['type'],
|
|
51
|
+
{ saves, playthroughs, meta }: Tables,
|
|
52
|
+
destroy: () => Promise<void>,
|
|
53
|
+
): StorageBackend {
|
|
54
|
+
/** Delete the records `field` finds, resolving to their keys. */
|
|
55
|
+
async function deleteFound<T>(
|
|
56
|
+
table: Table<T>,
|
|
57
|
+
keyOf: (record: T) => string,
|
|
58
|
+
field: string,
|
|
59
|
+
value: string,
|
|
60
|
+
): Promise<string[]> {
|
|
61
|
+
const keys = (await table.find(field, value)).map(keyOf);
|
|
62
|
+
for (const key of keys) await table.delete(key);
|
|
63
|
+
return keys;
|
|
64
|
+
}
|
|
81
65
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
async function deleteMetaWhere(test: (key: string) => boolean) {
|
|
67
|
+
for (const key of await meta.keys()) {
|
|
68
|
+
if (test(key)) await meta.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
85
71
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
72
|
+
const saveId = (r: SaveRecord) => r.meta.id;
|
|
73
|
+
const playthroughId = (r: PlaythroughRecord) => r.id;
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
75
|
+
return {
|
|
76
|
+
type,
|
|
77
|
+
putSave: (record) => saves.put(record.meta.id, record),
|
|
78
|
+
getSave: (id) => saves.get(id),
|
|
79
|
+
deleteSave: (id) => saves.delete(id),
|
|
80
|
+
getSavesByIfid: (ifid) => saves.find('ifid', ifid),
|
|
81
|
+
deleteSavesByIfid: async (ifid) => {
|
|
82
|
+
await deleteFound(saves, saveId, 'ifid', ifid);
|
|
83
|
+
},
|
|
84
|
+
deleteSavesByPlaythrough: (id) =>
|
|
85
|
+
deleteFound(saves, saveId, 'playthroughId', id),
|
|
86
|
+
|
|
87
|
+
putPlaythrough: (record) => playthroughs.put(record.id, record),
|
|
88
|
+
getPlaythroughsByIfid: (ifid) => playthroughs.find('ifid', ifid),
|
|
89
|
+
deletePlaythroughsByIfid: async (ifid) => {
|
|
90
|
+
await deleteFound(playthroughs, playthroughId, 'ifid', ifid);
|
|
91
|
+
},
|
|
92
|
+
deletePlaythroughById: (id) => playthroughs.delete(id),
|
|
93
|
+
|
|
94
|
+
getMeta: <T>(key: string) => meta.get(key) as Promise<T | undefined>,
|
|
95
|
+
setMeta: (key, value) => meta.put(key, value),
|
|
96
|
+
deleteMeta: (key) => meta.delete(key),
|
|
97
|
+
deleteMetaByPrefix: (prefix) =>
|
|
98
|
+
deleteMetaWhere((key) => key.startsWith(prefix)),
|
|
99
|
+
deleteMetaByIfid: (ifid) => deleteMetaWhere((key) => key.includes(ifid)),
|
|
100
|
+
getAllMetaKeys: () => meta.keys(),
|
|
101
|
+
|
|
102
|
+
destroy,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
},
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Memory backend
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
/**
|
|
111
|
+
* In-memory storage. Like the other backends it stores copies: records and
|
|
112
|
+
* values go in and come out cloned, so changing a record after storing it,
|
|
113
|
+
* or one that was read, changes nothing stored.
|
|
114
|
+
*/
|
|
115
|
+
export function createMemoryBackend(): StorageBackend {
|
|
116
|
+
const copy = <T>(value: T): T => structuredClone(value);
|
|
117
|
+
const maps: Map<string, unknown>[] = [];
|
|
118
|
+
|
|
119
|
+
function table<T>(_name: TableName, indexes: Indexes<T>): Table<T> {
|
|
120
|
+
const rows = new Map<string, T>();
|
|
121
|
+
maps.push(rows);
|
|
122
|
+
return {
|
|
123
|
+
get: async (key) => copy(rows.get(key)),
|
|
124
|
+
put: async (key, value) => {
|
|
125
|
+
rows.set(key, copy(value));
|
|
126
|
+
},
|
|
127
|
+
delete: async (key) => {
|
|
128
|
+
rows.delete(key);
|
|
129
|
+
},
|
|
130
|
+
find: async (field, value) =>
|
|
131
|
+
[...rows.values()]
|
|
132
|
+
.filter((r) => indexes[field]!(r) === value)
|
|
133
|
+
.map(copy),
|
|
134
|
+
keys: async () => [...rows.keys()],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
meta.clear();
|
|
110
|
-
},
|
|
111
|
-
};
|
|
138
|
+
return createBackend('memory', makeTables(table), async () => {
|
|
139
|
+
for (const rows of maps) rows.clear();
|
|
140
|
+
});
|
|
112
141
|
}
|
|
113
142
|
|
|
114
143
|
// ---------------------------------------------------------------------------
|
|
115
144
|
// localStorage backend
|
|
116
145
|
// ---------------------------------------------------------------------------
|
|
117
146
|
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
/** Key prefixes: of the records, and of each index's ID lists. */
|
|
148
|
+
const LS_PREFIXES: Record<TableName, string> = {
|
|
149
|
+
saves: 'spindle.save.',
|
|
150
|
+
playthroughs: 'spindle.pt.',
|
|
151
|
+
meta: 'spindle.meta.',
|
|
152
|
+
};
|
|
153
|
+
const LS_INDEX_PREFIXES: Record<string, string> = {
|
|
154
|
+
'saves.ifid': 'spindle.idx.saves.',
|
|
155
|
+
'saves.playthroughId': 'spindle.idx.saves-pt.',
|
|
156
|
+
'playthroughs.ifid': 'spindle.idx.pt.',
|
|
157
|
+
};
|
|
124
158
|
|
|
125
159
|
function lsGet<T>(key: string): T | undefined {
|
|
126
160
|
const raw = localStorage.getItem(key);
|
|
@@ -152,162 +186,67 @@ function lsIndexAdd(key: string, entry: string): void {
|
|
|
152
186
|
}
|
|
153
187
|
}
|
|
154
188
|
|
|
189
|
+
/** Remove an ID from an index; an index left empty is removed. */
|
|
155
190
|
function lsIndexRemove(key: string, entry: string): void {
|
|
156
191
|
const arr = lsIndex(key).filter((e) => e !== entry);
|
|
157
|
-
lsSet(key, arr);
|
|
192
|
+
if (arr.length > 0) lsSet(key, arr);
|
|
193
|
+
else lsDel(key);
|
|
158
194
|
}
|
|
159
195
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
async putSave(record: SaveRecord): Promise<void> {
|
|
165
|
-
const id = record.meta.id;
|
|
166
|
-
// A save rewritten under another playthrough (an overwrite after a
|
|
167
|
-
// restart) leaves its old playthrough's index
|
|
168
|
-
const previous = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
|
|
169
|
-
if (previous && previous.meta.playthroughId !== record.meta.playthroughId)
|
|
170
|
-
lsIndexRemove(
|
|
171
|
-
`${LS_IDX_SAVES_PT_PREFIX}${previous.meta.playthroughId}`,
|
|
172
|
-
id,
|
|
173
|
-
);
|
|
174
|
-
lsSet(`${LS_SAVE_PREFIX}${id}`, record);
|
|
175
|
-
lsIndexAdd(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
|
|
176
|
-
lsIndexAdd(`${LS_IDX_SAVES_PT_PREFIX}${record.meta.playthroughId}`, id);
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
async getSave(id: string): Promise<SaveRecord | undefined> {
|
|
180
|
-
return lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
async deleteSave(id: string): Promise<void> {
|
|
184
|
-
const record = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
|
|
185
|
-
if (record) {
|
|
186
|
-
lsIndexRemove(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
|
|
187
|
-
lsIndexRemove(
|
|
188
|
-
`${LS_IDX_SAVES_PT_PREFIX}${record.meta.playthroughId}`,
|
|
189
|
-
id,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
lsDel(`${LS_SAVE_PREFIX}${id}`);
|
|
193
|
-
},
|
|
196
|
+
/** The keys of every stored item that start with `prefix`. */
|
|
197
|
+
const lsKeys = (prefix: string): string[] =>
|
|
198
|
+
Object.keys(localStorage).filter((key) => key.startsWith(prefix));
|
|
194
199
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
async deleteSavesByPlaythrough(playthroughId: string): Promise<string[]> {
|
|
221
|
-
const ids = lsIndex(`${LS_IDX_SAVES_PT_PREFIX}${playthroughId}`);
|
|
222
|
-
for (const id of ids) {
|
|
223
|
-
const record = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
|
|
224
|
-
if (record) {
|
|
225
|
-
lsIndexRemove(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
|
|
226
|
-
}
|
|
227
|
-
lsDel(`${LS_SAVE_PREFIX}${id}`);
|
|
228
|
-
}
|
|
229
|
-
lsDel(`${LS_IDX_SAVES_PT_PREFIX}${playthroughId}`);
|
|
230
|
-
return ids;
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
async putPlaythrough(record: PlaythroughRecord): Promise<void> {
|
|
234
|
-
lsSet(`${LS_PT_PREFIX}${record.id}`, record);
|
|
235
|
-
lsIndexAdd(`${LS_IDX_PT_PREFIX}${record.ifid}`, record.id);
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
async getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]> {
|
|
239
|
-
const ids = lsIndex(`${LS_IDX_PT_PREFIX}${ifid}`);
|
|
240
|
-
const results: PlaythroughRecord[] = [];
|
|
241
|
-
for (const id of ids) {
|
|
242
|
-
const record = lsGet<PlaythroughRecord>(`${LS_PT_PREFIX}${id}`);
|
|
243
|
-
if (record) results.push(record);
|
|
244
|
-
}
|
|
245
|
-
return results;
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
async deletePlaythroughsByIfid(ifid: string): Promise<void> {
|
|
249
|
-
const ids = lsIndex(`${LS_IDX_PT_PREFIX}${ifid}`);
|
|
250
|
-
for (const id of ids) {
|
|
251
|
-
lsDel(`${LS_PT_PREFIX}${id}`);
|
|
252
|
-
}
|
|
253
|
-
lsDel(`${LS_IDX_PT_PREFIX}${ifid}`);
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
async deletePlaythroughById(id: string): Promise<void> {
|
|
257
|
-
const record = lsGet<PlaythroughRecord>(`${LS_PT_PREFIX}${id}`);
|
|
258
|
-
if (record) {
|
|
259
|
-
lsIndexRemove(`${LS_IDX_PT_PREFIX}${record.ifid}`, id);
|
|
260
|
-
}
|
|
261
|
-
lsDel(`${LS_PT_PREFIX}${id}`);
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
async getMeta<T = unknown>(key: string): Promise<T | undefined> {
|
|
265
|
-
return lsGet<T>(`${LS_META_PREFIX}${key}`);
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
async setMeta(key: string, value: unknown): Promise<void> {
|
|
269
|
-
lsSet(`${LS_META_PREFIX}${key}`, value);
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
async deleteMeta(key: string): Promise<void> {
|
|
273
|
-
lsDel(`${LS_META_PREFIX}${key}`);
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
async deleteMetaByPrefix(prefix: string): Promise<void> {
|
|
277
|
-
const fullPrefix = `${LS_META_PREFIX}${prefix}`;
|
|
278
|
-
for (const key of Object.keys(localStorage)) {
|
|
279
|
-
if (key.startsWith(fullPrefix)) {
|
|
280
|
-
localStorage.removeItem(key);
|
|
200
|
+
export function createLocalStorageBackend(): StorageBackend {
|
|
201
|
+
function table<T>(name: TableName, fields: Indexes<T>): Table<T> {
|
|
202
|
+
const prefix = LS_PREFIXES[name];
|
|
203
|
+
const indexes = Object.entries(fields).map(([field, read]) => ({
|
|
204
|
+
field,
|
|
205
|
+
read,
|
|
206
|
+
prefix: LS_INDEX_PREFIXES[`${name}.${field}`]!,
|
|
207
|
+
}));
|
|
208
|
+
/** The index lists `record` is in. */
|
|
209
|
+
const listsOf = (record: T) =>
|
|
210
|
+
indexes.map((index) => `${index.prefix}${index.read(record)}`);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
get: async (key) => lsGet<T>(`${prefix}${key}`),
|
|
214
|
+
async put(key, value) {
|
|
215
|
+
// A record rewritten under another index value (a save overwritten
|
|
216
|
+
// after a restart, under the new playthrough) leaves the old list
|
|
217
|
+
const previous = lsGet<T>(`${prefix}${key}`);
|
|
218
|
+
const lists = listsOf(value);
|
|
219
|
+
if (previous !== undefined) {
|
|
220
|
+
for (const list of listsOf(previous)) {
|
|
221
|
+
if (!lists.includes(list)) lsIndexRemove(list, key);
|
|
222
|
+
}
|
|
281
223
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
224
|
+
lsSet(`${prefix}${key}`, value);
|
|
225
|
+
for (const list of lists) lsIndexAdd(list, key);
|
|
226
|
+
},
|
|
227
|
+
async delete(key) {
|
|
228
|
+
const record = lsGet<T>(`${prefix}${key}`);
|
|
229
|
+
if (record !== undefined) {
|
|
230
|
+
for (const list of listsOf(record)) lsIndexRemove(list, key);
|
|
289
231
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
232
|
+
lsDel(`${prefix}${key}`);
|
|
233
|
+
},
|
|
234
|
+
async find(field, value) {
|
|
235
|
+
const index = indexes.find((i) => i.field === field)!;
|
|
236
|
+
const results: T[] = [];
|
|
237
|
+
for (const id of lsIndex(`${index.prefix}${value}`)) {
|
|
238
|
+
const record = lsGet<T>(`${prefix}${id}`);
|
|
239
|
+
if (record !== undefined) results.push(record);
|
|
298
240
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
241
|
+
return results;
|
|
242
|
+
},
|
|
243
|
+
keys: async () => lsKeys(prefix).map((key) => key.slice(prefix.length)),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
302
246
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
localStorage.removeItem(key);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
};
|
|
247
|
+
return createBackend('localstorage', makeTables(table), async () => {
|
|
248
|
+
for (const key of lsKeys('spindle.')) lsDel(key);
|
|
249
|
+
});
|
|
311
250
|
}
|
|
312
251
|
|
|
313
252
|
// ---------------------------------------------------------------------------
|
|
@@ -358,15 +297,6 @@ function createIDBBackend(): StorageBackend {
|
|
|
358
297
|
return dbPromise;
|
|
359
298
|
}
|
|
360
299
|
|
|
361
|
-
function tx(
|
|
362
|
-
storeName: string,
|
|
363
|
-
mode: IDBTransactionMode,
|
|
364
|
-
): Promise<IDBObjectStore> {
|
|
365
|
-
return openDB().then((db) =>
|
|
366
|
-
db.transaction(storeName, mode).objectStore(storeName),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
300
|
function idbReq<T>(req: IDBRequest<T>): Promise<T> {
|
|
371
301
|
return new Promise((resolve, reject) => {
|
|
372
302
|
req.onsuccess = () => resolve(req.result);
|
|
@@ -374,156 +304,53 @@ function createIDBBackend(): StorageBackend {
|
|
|
374
304
|
});
|
|
375
305
|
}
|
|
376
306
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
):
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
307
|
+
/**
|
|
308
|
+
* An object store. Records hold their own key (keyPath); meta values are
|
|
309
|
+
* stored as `{ key, value }` rows.
|
|
310
|
+
*/
|
|
311
|
+
function table<T>(name: TableName): Table<T> {
|
|
312
|
+
const rows = name === 'meta';
|
|
313
|
+
/** Run one request in a transaction of its own. */
|
|
314
|
+
const request = async <R>(
|
|
315
|
+
mode: IDBTransactionMode,
|
|
316
|
+
make: (store: IDBObjectStore) => IDBRequest<R>,
|
|
317
|
+
): Promise<R> => {
|
|
318
|
+
const db = await openDB();
|
|
319
|
+
return idbReq(make(db.transaction(name, mode).objectStore(name)));
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
async get(key) {
|
|
324
|
+
const result = await request('readonly', (s) => s.get(key));
|
|
325
|
+
return (rows ? result?.value : result) as T | undefined;
|
|
326
|
+
},
|
|
327
|
+
async put(key, value) {
|
|
328
|
+
await request('readwrite', (s) => s.put(rows ? { key, value } : value));
|
|
329
|
+
},
|
|
330
|
+
async delete(key) {
|
|
331
|
+
await request('readwrite', (s) => s.delete(key));
|
|
332
|
+
},
|
|
333
|
+
async find(field, value) {
|
|
334
|
+
const result = await request('readonly', (s) =>
|
|
335
|
+
s.index(field).getAll(value),
|
|
336
|
+
);
|
|
337
|
+
return (result ?? []) as T[];
|
|
338
|
+
},
|
|
339
|
+
keys: async () =>
|
|
340
|
+
(await request('readonly', (s) => s.getAllKeys())) as string[],
|
|
341
|
+
};
|
|
386
342
|
}
|
|
387
343
|
|
|
388
|
-
return {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return (
|
|
399
|
-
((await idbReq(store.get(id))) as SaveRecord | undefined) ?? undefined
|
|
400
|
-
);
|
|
401
|
-
},
|
|
402
|
-
|
|
403
|
-
async deleteSave(id: string): Promise<void> {
|
|
404
|
-
const store = await tx('saves', 'readwrite');
|
|
405
|
-
await idbReq(store.delete(id));
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
async getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
|
|
409
|
-
return getAllFromIndex<SaveRecord>('saves', 'ifid', ifid);
|
|
410
|
-
},
|
|
411
|
-
|
|
412
|
-
async deleteSavesByIfid(ifid: string): Promise<void> {
|
|
413
|
-
const records = await getAllFromIndex<SaveRecord>('saves', 'ifid', ifid);
|
|
414
|
-
const store = await tx('saves', 'readwrite');
|
|
415
|
-
for (const r of records) {
|
|
416
|
-
await idbReq(store.delete(r.meta.id));
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
|
|
420
|
-
async deleteSavesByPlaythrough(playthroughId: string): Promise<string[]> {
|
|
421
|
-
const records = await getAllFromIndex<SaveRecord>(
|
|
422
|
-
'saves',
|
|
423
|
-
'playthroughId',
|
|
424
|
-
playthroughId,
|
|
425
|
-
);
|
|
426
|
-
const store = await tx('saves', 'readwrite');
|
|
427
|
-
const ids: string[] = [];
|
|
428
|
-
for (const r of records) {
|
|
429
|
-
await idbReq(store.delete(r.meta.id));
|
|
430
|
-
ids.push(r.meta.id);
|
|
431
|
-
}
|
|
432
|
-
return ids;
|
|
433
|
-
},
|
|
434
|
-
|
|
435
|
-
async putPlaythrough(record: PlaythroughRecord): Promise<void> {
|
|
436
|
-
const store = await tx('playthroughs', 'readwrite');
|
|
437
|
-
await idbReq(store.put(record));
|
|
438
|
-
},
|
|
439
|
-
|
|
440
|
-
async getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]> {
|
|
441
|
-
return getAllFromIndex<PlaythroughRecord>('playthroughs', 'ifid', ifid);
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
async deletePlaythroughsByIfid(ifid: string): Promise<void> {
|
|
445
|
-
const records = await getAllFromIndex<PlaythroughRecord>(
|
|
446
|
-
'playthroughs',
|
|
447
|
-
'ifid',
|
|
448
|
-
ifid,
|
|
449
|
-
);
|
|
450
|
-
const store = await tx('playthroughs', 'readwrite');
|
|
451
|
-
for (const r of records) {
|
|
452
|
-
await idbReq(store.delete(r.id));
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
|
|
456
|
-
async deletePlaythroughById(id: string): Promise<void> {
|
|
457
|
-
const store = await tx('playthroughs', 'readwrite');
|
|
458
|
-
await idbReq(store.delete(id));
|
|
459
|
-
},
|
|
460
|
-
|
|
461
|
-
async getMeta<T = unknown>(key: string): Promise<T | undefined> {
|
|
462
|
-
const store = await tx('meta', 'readonly');
|
|
463
|
-
const row = (await idbReq(store.get(key))) as
|
|
464
|
-
| { key: string; value: T }
|
|
465
|
-
| undefined;
|
|
466
|
-
return row ? row.value : undefined;
|
|
467
|
-
},
|
|
468
|
-
|
|
469
|
-
async setMeta(key: string, value: unknown): Promise<void> {
|
|
470
|
-
const store = await tx('meta', 'readwrite');
|
|
471
|
-
await idbReq(store.put({ key, value }));
|
|
472
|
-
},
|
|
473
|
-
|
|
474
|
-
async deleteMeta(key: string): Promise<void> {
|
|
475
|
-
const store = await tx('meta', 'readwrite');
|
|
476
|
-
await idbReq(store.delete(key));
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
async deleteMetaByPrefix(prefix: string): Promise<void> {
|
|
480
|
-
const store = await tx('meta', 'readwrite');
|
|
481
|
-
const allRows = (await idbReq(store.getAll())) as Array<{
|
|
482
|
-
key: string;
|
|
483
|
-
value: unknown;
|
|
484
|
-
}>;
|
|
485
|
-
for (const row of allRows) {
|
|
486
|
-
if (row.key.startsWith(prefix)) {
|
|
487
|
-
const delStore = await tx('meta', 'readwrite');
|
|
488
|
-
await idbReq(delStore.delete(row.key));
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
},
|
|
492
|
-
|
|
493
|
-
async deleteMetaByIfid(ifid: string): Promise<void> {
|
|
494
|
-
const store = await tx('meta', 'readwrite');
|
|
495
|
-
const allRows = (await idbReq(store.getAll())) as Array<{
|
|
496
|
-
key: string;
|
|
497
|
-
value: unknown;
|
|
498
|
-
}>;
|
|
499
|
-
for (const row of allRows) {
|
|
500
|
-
if (row.key.includes(ifid)) {
|
|
501
|
-
const delStore = await tx('meta', 'readwrite');
|
|
502
|
-
await idbReq(delStore.delete(row.key));
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
},
|
|
506
|
-
|
|
507
|
-
async getAllMetaKeys(): Promise<string[]> {
|
|
508
|
-
const store = await tx('meta', 'readonly');
|
|
509
|
-
const allRows = (await idbReq(store.getAll())) as Array<{
|
|
510
|
-
key: string;
|
|
511
|
-
value: unknown;
|
|
512
|
-
}>;
|
|
513
|
-
return allRows.map((r) => r.key);
|
|
514
|
-
},
|
|
515
|
-
|
|
516
|
-
async destroy(): Promise<void> {
|
|
517
|
-
const db = await openDB();
|
|
518
|
-
db.close();
|
|
519
|
-
dbPromise = null;
|
|
520
|
-
await new Promise<void>((resolve, reject) => {
|
|
521
|
-
const req = indexedDB.deleteDatabase(IDB_DB_NAME);
|
|
522
|
-
req.onsuccess = () => resolve();
|
|
523
|
-
req.onerror = () => reject(req.error);
|
|
524
|
-
});
|
|
525
|
-
},
|
|
526
|
-
};
|
|
344
|
+
return createBackend('indexeddb', makeTables(table), async () => {
|
|
345
|
+
const db = await openDB();
|
|
346
|
+
db.close();
|
|
347
|
+
dbPromise = null;
|
|
348
|
+
await new Promise<void>((resolve, reject) => {
|
|
349
|
+
const req = indexedDB.deleteDatabase(IDB_DB_NAME);
|
|
350
|
+
req.onsuccess = () => resolve();
|
|
351
|
+
req.onerror = () => reject(req.error);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
527
354
|
}
|
|
528
355
|
|
|
529
356
|
// ---------------------------------------------------------------------------
|