@indietabletop/appkit 3.5.0 → 3.6.0-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/lib/ClientContext/ClientContext.tsx +25 -0
- package/lib/InfoPage/index.tsx +46 -0
- package/lib/InfoPage/pages.tsx +36 -0
- package/lib/InfoPage/style.css.ts +36 -0
- package/lib/Letterhead/index.tsx +3 -3
- package/lib/LetterheadForm/index.tsx +1 -1
- package/lib/LoginPage/LoginPage.stories.tsx +107 -0
- package/lib/LoginPage/LoginPage.tsx +204 -0
- package/lib/LoginPage/style.css.ts +17 -0
- package/lib/ModernIDB/Cursor.ts +91 -0
- package/lib/ModernIDB/ModernIDB.ts +337 -0
- package/lib/ModernIDB/ModernIDBError.ts +9 -0
- package/lib/ModernIDB/ObjectStore.ts +195 -0
- package/lib/ModernIDB/ObjectStoreIndex.ts +102 -0
- package/lib/ModernIDB/README.md +9 -0
- package/lib/ModernIDB/Transaction.ts +40 -0
- package/lib/ModernIDB/VersionChangeManager.ts +57 -0
- package/lib/ModernIDB/bindings/factory.tsx +160 -0
- package/lib/ModernIDB/bindings/index.ts +2 -0
- package/lib/ModernIDB/bindings/types.ts +56 -0
- package/lib/ModernIDB/bindings/utils.tsx +32 -0
- package/lib/ModernIDB/index.ts +10 -0
- package/lib/ModernIDB/types.ts +77 -0
- package/lib/ModernIDB/utils.ts +51 -0
- package/lib/ReleaseInfo/index.tsx +29 -0
- package/lib/RulesetResolver.ts +214 -0
- package/lib/SubscribeCard/SubscribeCard.stories.tsx +133 -0
- package/lib/SubscribeCard/SubscribeCard.tsx +107 -0
- package/lib/SubscribeCard/style.css.ts +14 -0
- package/lib/Title/index.tsx +4 -0
- package/lib/append-copy-to-text.ts +1 -1
- package/lib/async-op.ts +8 -0
- package/lib/client.ts +37 -2
- package/lib/copyrightRange.ts +6 -0
- package/lib/groupBy.ts +25 -0
- package/lib/ids.ts +6 -0
- package/lib/index.ts +12 -0
- package/lib/random.ts +12 -0
- package/lib/result/swr.ts +18 -0
- package/lib/structs.ts +10 -0
- package/lib/typeguards.ts +12 -0
- package/lib/types.ts +3 -1
- package/lib/unique.test.ts +22 -0
- package/lib/unique.ts +24 -0
- package/package.json +16 -6
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
3
|
+
|
|
4
|
+
import { ModernIDBError } from "./ModernIDBError.ts";
|
|
5
|
+
import { ObjectStore } from "./ObjectStore.ts";
|
|
6
|
+
import type {
|
|
7
|
+
BlockingHandler,
|
|
8
|
+
ModernIDBSchema,
|
|
9
|
+
ModernIDBState,
|
|
10
|
+
OpenRequestHandlers,
|
|
11
|
+
TransactionMode,
|
|
12
|
+
VersionChangeHandler,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
import { openRequestToPromise, transactionToPromise } from "./utils.ts";
|
|
15
|
+
import { VersionChangeManager } from "./VersionChangeManager.ts";
|
|
16
|
+
|
|
17
|
+
export class TransactionEvent<StoreName extends string> extends CustomEvent<{
|
|
18
|
+
storeNames: StoreName[];
|
|
19
|
+
}> {
|
|
20
|
+
constructor(type: TransactionMode, storeNames: StoreName[]) {
|
|
21
|
+
super(type, { detail: { storeNames } });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type TransactionOptions = IDBTransactionOptions & {
|
|
26
|
+
/**
|
|
27
|
+
* Do not emit events when this transaction completes.
|
|
28
|
+
*/
|
|
29
|
+
noEmit?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type DatabaseProps = {
|
|
33
|
+
name: string;
|
|
34
|
+
version: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class ModernIDB<
|
|
38
|
+
Schema extends ModernIDBSchema,
|
|
39
|
+
IndexNames extends { [K in keyof Schema]?: string },
|
|
40
|
+
> {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly version: number;
|
|
43
|
+
|
|
44
|
+
protected state: ModernIDBState = "closed";
|
|
45
|
+
protected idbDatabase: IDBDatabase | null = null;
|
|
46
|
+
|
|
47
|
+
private eventTarget: EventTarget;
|
|
48
|
+
private onBlocking?: BlockingHandler<Schema, IndexNames>;
|
|
49
|
+
private onUpgrade?: VersionChangeHandler<Schema, IndexNames>;
|
|
50
|
+
private onInit?: VersionChangeHandler<Schema, IndexNames>;
|
|
51
|
+
|
|
52
|
+
constructor(props: DatabaseProps & OpenRequestHandlers<Schema, IndexNames>) {
|
|
53
|
+
this.eventTarget = new EventTarget();
|
|
54
|
+
this.name = props.name;
|
|
55
|
+
this.version = props.version;
|
|
56
|
+
|
|
57
|
+
this.onInit = props.onInit;
|
|
58
|
+
this.onUpgrade = props.onUpgrade;
|
|
59
|
+
this.onBlocking = props.onBlocking;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public addEventListener(
|
|
63
|
+
type: TransactionMode,
|
|
64
|
+
callback: (event: TransactionEvent<string & keyof Schema>) => void,
|
|
65
|
+
options?: boolean | AddEventListenerOptions,
|
|
66
|
+
): void {
|
|
67
|
+
this.eventTarget.addEventListener(
|
|
68
|
+
type,
|
|
69
|
+
|
|
70
|
+
// The EventTarget should only have events dispatched by ModernIDB,
|
|
71
|
+
// meaning that it should be safe to assume that the event will be
|
|
72
|
+
// a TransactionEvent.
|
|
73
|
+
callback as EventListener,
|
|
74
|
+
options,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public removeEventListener(
|
|
79
|
+
type: TransactionMode,
|
|
80
|
+
callback: (event: TransactionEvent<string & keyof Schema>) => void,
|
|
81
|
+
options?: boolean | EventListenerOptions,
|
|
82
|
+
): void {
|
|
83
|
+
this.eventTarget.removeEventListener(
|
|
84
|
+
type,
|
|
85
|
+
|
|
86
|
+
// The EventTarget should only have events dispatched by ModernIDB,
|
|
87
|
+
// meaning that it should be safe to assume that the event will be
|
|
88
|
+
// a TransactionEvent.
|
|
89
|
+
callback as EventListener,
|
|
90
|
+
options,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Opens a connection to IndexedDB.
|
|
96
|
+
*
|
|
97
|
+
* ModernIDB instance must be in a `closed` state, otherwise an
|
|
98
|
+
* `InvalidConnectionStateError` will be thrown.
|
|
99
|
+
*
|
|
100
|
+
* If the database needs to upgrade (i.e. supplied version is higher than
|
|
101
|
+
* the current version), but there are existing connections that don't
|
|
102
|
+
* close on `versionchange` event (the onBlocking handler), an
|
|
103
|
+
* `OpenRequestBlockedError` will be thrown.
|
|
104
|
+
*
|
|
105
|
+
* Additional browser-specific exceptions can also be thrown. Make
|
|
106
|
+
* sure to inspect any error's `name` property to differentiate
|
|
107
|
+
* between the various types of errors that could occur.
|
|
108
|
+
*/
|
|
109
|
+
async open(handlers?: OpenRequestHandlers<Schema, IndexNames>) {
|
|
110
|
+
if (this.state !== "closed") {
|
|
111
|
+
throw new ModernIDBError(
|
|
112
|
+
"InvalidConnectionStateError",
|
|
113
|
+
|
|
114
|
+
`Cannot open connection to database '${this.name}'. Instance must ` +
|
|
115
|
+
`be in a 'closed' state in order to be opened, but current ` +
|
|
116
|
+
`state is '${this.state}'.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.state = "opening";
|
|
121
|
+
|
|
122
|
+
// Override handlers supplied in the constructor
|
|
123
|
+
Object.assign(this, handlers);
|
|
124
|
+
|
|
125
|
+
const handleInit = this.onInit;
|
|
126
|
+
const handleBlocking = this.onBlocking;
|
|
127
|
+
const handleUpgrade = this.onUpgrade;
|
|
128
|
+
|
|
129
|
+
return new Promise<Event>((resolve, reject) => {
|
|
130
|
+
try {
|
|
131
|
+
const request = indexedDB.open(this.name, this.version);
|
|
132
|
+
|
|
133
|
+
if (handleUpgrade || handleInit) {
|
|
134
|
+
request.addEventListener(
|
|
135
|
+
"upgradeneeded",
|
|
136
|
+
(event) => {
|
|
137
|
+
const idbDatabase = request.result;
|
|
138
|
+
const idbTransaction = request.transaction!;
|
|
139
|
+
|
|
140
|
+
const manager = new VersionChangeManager<Schema, IndexNames>({
|
|
141
|
+
idbDatabase,
|
|
142
|
+
idbTransaction,
|
|
143
|
+
event,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (event.oldVersion === 0) {
|
|
148
|
+
handleInit?.({ event, manager, db: this });
|
|
149
|
+
} else {
|
|
150
|
+
handleUpgrade?.({ event, manager, db: this });
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(error);
|
|
154
|
+
idbTransaction.abort();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{ once: true },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
request.addEventListener(
|
|
162
|
+
"blocked",
|
|
163
|
+
() => {
|
|
164
|
+
this.state = "closed";
|
|
165
|
+
reject(
|
|
166
|
+
new ModernIDBError(
|
|
167
|
+
"OpenRequestBlockedError",
|
|
168
|
+
|
|
169
|
+
`ModernIDB connection could not be opened because there ` +
|
|
170
|
+
`is an open connection preventing a 'versionchange' ` +
|
|
171
|
+
`transaction from being created.`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
{ once: true },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
request.addEventListener(
|
|
179
|
+
"success",
|
|
180
|
+
(event) => {
|
|
181
|
+
if (this.state === "opening") {
|
|
182
|
+
const idb = request.result;
|
|
183
|
+
this.idbDatabase = idb;
|
|
184
|
+
|
|
185
|
+
if (handleBlocking) {
|
|
186
|
+
this.idbDatabase.addEventListener("versionchange", (event) => {
|
|
187
|
+
handleBlocking({ event, db: this });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.state = "open";
|
|
192
|
+
resolve(event);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{ once: true },
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
request.addEventListener(
|
|
199
|
+
"error",
|
|
200
|
+
() => {
|
|
201
|
+
this.state = "closed";
|
|
202
|
+
reject(request.error);
|
|
203
|
+
},
|
|
204
|
+
{ once: true },
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
reject(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
close() {
|
|
213
|
+
this.state = "closed";
|
|
214
|
+
|
|
215
|
+
this.idbDatabase?.close();
|
|
216
|
+
this.idbDatabase = null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
isOpen(): this is this & { idbDatabase: IDBDatabase } {
|
|
220
|
+
return !!this.idbDatabase;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private assertOpen(
|
|
224
|
+
hint: string,
|
|
225
|
+
): asserts this is this & { idbDatabase: IDBDatabase } {
|
|
226
|
+
if (!this.isOpen()) {
|
|
227
|
+
throw new ModernIDBError("InvalidConnectionStateError", hint);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
transaction<StoreName extends string & keyof Schema>(
|
|
232
|
+
name: StoreName,
|
|
233
|
+
mode?: TransactionMode,
|
|
234
|
+
options?: TransactionOptions,
|
|
235
|
+
): [
|
|
236
|
+
ObjectStore<
|
|
237
|
+
Schema[StoreName],
|
|
238
|
+
IndexNames[StoreName] extends string ? IndexNames[StoreName] : never
|
|
239
|
+
>,
|
|
240
|
+
Promise<Event>,
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
transaction<StoreNamesArray extends readonly (string & keyof Schema)[] | []>(
|
|
244
|
+
names: StoreNamesArray,
|
|
245
|
+
mode?: TransactionMode,
|
|
246
|
+
options?: TransactionOptions,
|
|
247
|
+
): [
|
|
248
|
+
{
|
|
249
|
+
-readonly [Index in keyof StoreNamesArray]: ObjectStore<
|
|
250
|
+
Schema[StoreNamesArray[Index]],
|
|
251
|
+
IndexNames[StoreNamesArray[Index]] extends string
|
|
252
|
+
? IndexNames[StoreNamesArray[Index]]
|
|
253
|
+
: never
|
|
254
|
+
>;
|
|
255
|
+
},
|
|
256
|
+
Promise<Event>,
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
transaction(
|
|
260
|
+
nameOrNames: string | string[],
|
|
261
|
+
mode?: TransactionMode,
|
|
262
|
+
options?: TransactionOptions,
|
|
263
|
+
) {
|
|
264
|
+
const transactionMode = mode ?? "readonly";
|
|
265
|
+
|
|
266
|
+
this.assertOpen(
|
|
267
|
+
`The "transaction" method can only be called on an instance that is ` +
|
|
268
|
+
`in the "open" state, but current state is: "${this.state}".`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const transaction = this.idbDatabase.transaction(
|
|
272
|
+
nameOrNames,
|
|
273
|
+
transactionMode,
|
|
274
|
+
options,
|
|
275
|
+
);
|
|
276
|
+
const complete = transactionToPromise(transaction);
|
|
277
|
+
|
|
278
|
+
if (!options?.noEmit) {
|
|
279
|
+
// Generally, it is best practice to always attach then/catch handlers to
|
|
280
|
+
// promises. However, in this case, these should be attached in user-code,
|
|
281
|
+
// as it doesn't make sense to try to handle errors from here.
|
|
282
|
+
//
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
284
|
+
complete.then((event) => {
|
|
285
|
+
this.eventTarget.dispatchEvent(
|
|
286
|
+
new TransactionEvent(
|
|
287
|
+
transactionMode,
|
|
288
|
+
Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames],
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
return event;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (Array.isArray(nameOrNames)) {
|
|
296
|
+
const objectStores = nameOrNames.map(
|
|
297
|
+
(name) => new ObjectStore(transaction.objectStore(name)),
|
|
298
|
+
);
|
|
299
|
+
return [objectStores, complete];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [new ObjectStore(transaction.objectStore(nameOrNames)), complete];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async deleteFromStore(
|
|
306
|
+
storeName: string & keyof Schema,
|
|
307
|
+
query: IDBValidKey | IDBKeyRange,
|
|
308
|
+
options?: IDBTransactionOptions,
|
|
309
|
+
) {
|
|
310
|
+
const [store, done] = this.transaction(storeName, "readwrite", options);
|
|
311
|
+
await Promise.all([store.delete(query), done]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getFromStore<StoreName extends string & keyof Schema>(
|
|
315
|
+
storeName: StoreName,
|
|
316
|
+
query: IDBValidKey | IDBKeyRange,
|
|
317
|
+
options?: IDBTransactionOptions,
|
|
318
|
+
) {
|
|
319
|
+
const [store, done] = this.transaction(storeName, "readonly", options);
|
|
320
|
+
const [data] = await Promise.all([store.get(query), done]);
|
|
321
|
+
return data;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async putToStore<StoreName extends string & keyof Schema>(
|
|
325
|
+
storeName: StoreName,
|
|
326
|
+
value: Schema[StoreName],
|
|
327
|
+
key?: IDBValidKey,
|
|
328
|
+
) {
|
|
329
|
+
const [store, done] = this.transaction(storeName, "readwrite");
|
|
330
|
+
const [itemKey] = await Promise.all([store.put(value, key), done]);
|
|
331
|
+
return itemKey;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function deleteDatabase(name: string) {
|
|
336
|
+
return openRequestToPromise(indexedDB.deleteDatabase(name));
|
|
337
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
2
|
+
|
|
3
|
+
import { Cursor, CursorWithValue } from "./Cursor.ts";
|
|
4
|
+
import { ObjectStoreIndex } from "./ObjectStoreIndex.ts";
|
|
5
|
+
import type { KeyPath } from "./types.ts";
|
|
6
|
+
import { requestToAsyncGenerator, requestToPromise } from "./utils.ts";
|
|
7
|
+
|
|
8
|
+
export class ObjectStore<Item, IndexName extends string> {
|
|
9
|
+
readonly idbObjectStore = IDBObjectStore["prototype"];
|
|
10
|
+
|
|
11
|
+
constructor(objectStore: IDBObjectStore) {
|
|
12
|
+
this.idbObjectStore = objectStore;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Adds or updates a record in store with the given value and key.
|
|
17
|
+
*
|
|
18
|
+
* If the store uses in-line keys and key is specified a "DataError" DOMException will be thrown.
|
|
19
|
+
*
|
|
20
|
+
* If put() is used, any existing record with the key will be replaced. If add() is used, and if a record with the key already exists the request will fail, with request's error set to a "ConstraintError" DOMException.
|
|
21
|
+
*
|
|
22
|
+
* If successful, request's result will be the record's key.
|
|
23
|
+
*
|
|
24
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/add)
|
|
25
|
+
*/
|
|
26
|
+
add(value: Item, key?: IDBValidKey): Promise<IDBValidKey> {
|
|
27
|
+
return requestToPromise(this.idbObjectStore.add(value, key));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Deletes all records in store.
|
|
31
|
+
*
|
|
32
|
+
* If successful, request's result will be undefined.
|
|
33
|
+
*
|
|
34
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/clear)
|
|
35
|
+
*/
|
|
36
|
+
clear(): Promise<undefined> {
|
|
37
|
+
return requestToPromise(this.idbObjectStore.clear());
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Retrieves the number of records matching the given key or key range in query.
|
|
41
|
+
*
|
|
42
|
+
* If successful, request's result will be the count.
|
|
43
|
+
*
|
|
44
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/count)
|
|
45
|
+
*/
|
|
46
|
+
count(query?: IDBValidKey | IDBKeyRange): Promise<number> {
|
|
47
|
+
return requestToPromise(this.idbObjectStore.count(query));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Deletes records in store with the given key or in the given key range in query.
|
|
51
|
+
*
|
|
52
|
+
* If successful, request's result will be undefined.
|
|
53
|
+
*
|
|
54
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/delete)
|
|
55
|
+
*/
|
|
56
|
+
delete(query: IDBValidKey | IDBKeyRange): Promise<undefined> {
|
|
57
|
+
return requestToPromise(this.idbObjectStore.delete(query));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Retrieves the value of the first record matching the given key or key range in query.
|
|
61
|
+
*
|
|
62
|
+
* If successful, request's result will be the value, or undefined if there was no matching record.
|
|
63
|
+
*
|
|
64
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/get)
|
|
65
|
+
*/
|
|
66
|
+
get(query: IDBValidKey | IDBKeyRange): Promise<Item | undefined> {
|
|
67
|
+
return requestToPromise(this.idbObjectStore.get(query));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves the values of the records matching the given key or key range in query (up to count if given).
|
|
71
|
+
*
|
|
72
|
+
* If successful, request's result will be an Array of the values.
|
|
73
|
+
*
|
|
74
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/getAll)
|
|
75
|
+
*/
|
|
76
|
+
getAll(
|
|
77
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
78
|
+
count?: number,
|
|
79
|
+
): Promise<Item[]> {
|
|
80
|
+
return requestToPromise(this.idbObjectStore.getAll(query, count));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves the keys of records matching the given key or key range in query (up to count if given).
|
|
84
|
+
*
|
|
85
|
+
* If successful, request's result will be an Array of the keys.
|
|
86
|
+
*
|
|
87
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/getAllKeys)
|
|
88
|
+
*/
|
|
89
|
+
getAllKeys(
|
|
90
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
91
|
+
count?: number,
|
|
92
|
+
): Promise<IDBValidKey[]> {
|
|
93
|
+
return requestToPromise(this.idbObjectStore.getAllKeys(query, count));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Retrieves the key of the first record matching the given key or key range in query.
|
|
97
|
+
*
|
|
98
|
+
* If successful, request's result will be the key, or undefined if there was no matching record.
|
|
99
|
+
*
|
|
100
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/getKey)
|
|
101
|
+
*/
|
|
102
|
+
getKey(query: IDBValidKey | IDBKeyRange): Promise<IDBValidKey | undefined> {
|
|
103
|
+
return requestToPromise(this.idbObjectStore.getKey(query));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/index) */
|
|
107
|
+
index(name: IndexName): ObjectStoreIndex<Item> {
|
|
108
|
+
const idbIndex = this.idbObjectStore.index(name);
|
|
109
|
+
return new ObjectStoreIndex(idbIndex);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Opens a cursor over the records matching query, ordered by direction. If query is null, all records in store are matched.
|
|
114
|
+
*/
|
|
115
|
+
async *openCursor(
|
|
116
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
117
|
+
direction?: IDBCursorDirection,
|
|
118
|
+
): AsyncGenerator<CursorWithValue<Item>, void, undefined> {
|
|
119
|
+
const idbCursorGenerator = requestToAsyncGenerator(
|
|
120
|
+
this.idbObjectStore.openCursor(query, direction),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
for await (const idbCursor of idbCursorGenerator) {
|
|
124
|
+
yield new CursorWithValue<Item>(idbCursor);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Opens a cursor with key only flag set over the records matching query, ordered by direction. If query is null, all records in store are matched.
|
|
130
|
+
*/
|
|
131
|
+
async *openKeyCursor(
|
|
132
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
133
|
+
direction?: IDBCursorDirection,
|
|
134
|
+
): AsyncGenerator<Cursor, void, undefined> {
|
|
135
|
+
const idbCursorGenerator = requestToAsyncGenerator(
|
|
136
|
+
this.idbObjectStore.openCursor(query, direction),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
for await (const idbCursor of idbCursorGenerator) {
|
|
140
|
+
yield new Cursor(idbCursor);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Adds or updates a record in store with the given value and key.
|
|
146
|
+
*
|
|
147
|
+
* If the store uses in-line keys and key is specified a "DataError" DOMException will be thrown.
|
|
148
|
+
*
|
|
149
|
+
* If put() is used, any existing record with the key will be replaced. If add() is used, and if a record with the key already exists the request will fail, with request's error set to a "ConstraintError" DOMException.
|
|
150
|
+
*
|
|
151
|
+
* If successful, request's result will be the record's key.
|
|
152
|
+
*
|
|
153
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/put)
|
|
154
|
+
*/
|
|
155
|
+
put(value: Item, key?: IDBValidKey): Promise<IDBValidKey> {
|
|
156
|
+
return requestToPromise(this.idbObjectStore.put(value, key));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class VersionChangeObjectStore<
|
|
161
|
+
Item,
|
|
162
|
+
IndexName extends string,
|
|
163
|
+
> extends ObjectStore<Item, IndexName> {
|
|
164
|
+
/**
|
|
165
|
+
* Creates a new index in store with the given name, keyPath and options and returns a new IDBIndex. If the keyPath and options define constraints that cannot be satisfied with the data already in store the upgrade transaction will abort with a "ConstraintError" DOMException.
|
|
166
|
+
*
|
|
167
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/createIndex)
|
|
168
|
+
*/
|
|
169
|
+
createIndex(
|
|
170
|
+
name: IndexName,
|
|
171
|
+
keyPath: KeyPath<Item> | KeyPath<Item>[],
|
|
172
|
+
options?: IDBIndexParameters,
|
|
173
|
+
): ObjectStoreIndex<Item> {
|
|
174
|
+
const idbIndex = this.idbObjectStore.createIndex(name, keyPath, options);
|
|
175
|
+
return new ObjectStoreIndex(idbIndex);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Deletes the index in store with the given name.
|
|
180
|
+
*
|
|
181
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/deleteIndex)
|
|
182
|
+
*/
|
|
183
|
+
deleteIndex(name: IndexName): void {
|
|
184
|
+
this.idbObjectStore.deleteIndex(name);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
renameIndex(currentName: string, newName: IndexName) {
|
|
188
|
+
const index = this.idbObjectStore.index(currentName);
|
|
189
|
+
this.idbObjectStore.createIndex(newName, index.keyPath, {
|
|
190
|
+
multiEntry: index.multiEntry,
|
|
191
|
+
unique: index.unique,
|
|
192
|
+
});
|
|
193
|
+
this.idbObjectStore.deleteIndex(currentName);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
2
|
+
|
|
3
|
+
import { IndexCursor, IndexCursorWithValue } from "./Cursor.ts";
|
|
4
|
+
import { requestToAsyncGenerator, requestToPromise } from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
export class ObjectStoreIndex<Item> {
|
|
7
|
+
readonly idbIndex = IDBIndex["prototype"];
|
|
8
|
+
|
|
9
|
+
constructor(idbIndex: IDBIndex) {
|
|
10
|
+
this.idbIndex = idbIndex;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves the number of records matching the given key or key range in query.
|
|
15
|
+
*
|
|
16
|
+
* If successful, request's result will be the count.
|
|
17
|
+
*
|
|
18
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/count)
|
|
19
|
+
*/
|
|
20
|
+
count(query?: IDBValidKey | IDBKeyRange): Promise<number> {
|
|
21
|
+
return requestToPromise(this.idbIndex.count(query));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves the value of the first record matching the given key or key range in query.
|
|
26
|
+
*
|
|
27
|
+
* If successful, request's result will be the value, or undefined if there was no matching record.
|
|
28
|
+
*
|
|
29
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/get)
|
|
30
|
+
*/
|
|
31
|
+
get(query: IDBValidKey | IDBKeyRange): Promise<Item | undefined> {
|
|
32
|
+
return requestToPromise(this.idbIndex.get(query));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves the values of the records matching the given key or key range in query (up to count if given).
|
|
36
|
+
*
|
|
37
|
+
* If successful, request's result will be an Array of the values.
|
|
38
|
+
*
|
|
39
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/getAll)
|
|
40
|
+
*/
|
|
41
|
+
getAll(
|
|
42
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
43
|
+
count?: number,
|
|
44
|
+
): Promise<Item[]> {
|
|
45
|
+
return requestToPromise(this.idbIndex.getAll(query, count));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Retrieves the keys of records matching the given key or key range in query (up to count if given).
|
|
49
|
+
*
|
|
50
|
+
* If successful, request's result will be an Array of the keys.
|
|
51
|
+
*
|
|
52
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/getAllKeys)
|
|
53
|
+
*/
|
|
54
|
+
getAllKeys(
|
|
55
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
56
|
+
count?: number,
|
|
57
|
+
): Promise<IDBValidKey[]> {
|
|
58
|
+
return requestToPromise(this.idbIndex.getAllKeys(query, count));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves the key of the first record matching the given key or key range in query.
|
|
62
|
+
*
|
|
63
|
+
* If successful, request's result will be the key, or undefined if there was no matching record.
|
|
64
|
+
*
|
|
65
|
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/getKey)
|
|
66
|
+
*/
|
|
67
|
+
getKey(query: IDBValidKey | IDBKeyRange): Promise<IDBValidKey | undefined> {
|
|
68
|
+
return requestToPromise(this.idbIndex.getKey(query));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Opens a cursor over the records matching query, ordered by direction. If query is null, all records in store are matched.
|
|
73
|
+
*/
|
|
74
|
+
async *openCursor(
|
|
75
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
76
|
+
direction?: IDBCursorDirection,
|
|
77
|
+
): AsyncGenerator<IndexCursorWithValue<Item>, void, undefined> {
|
|
78
|
+
const idbCursorGenerator = requestToAsyncGenerator(
|
|
79
|
+
this.idbIndex.openCursor(query, direction),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
for await (const idbCursor of idbCursorGenerator) {
|
|
83
|
+
yield new IndexCursorWithValue<Item>(idbCursor);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Opens a cursor with key only flag set over the records matching query, ordered by direction. If query is null, all records in store are matched.
|
|
89
|
+
*/
|
|
90
|
+
async *openKeyCursor(
|
|
91
|
+
query?: IDBValidKey | IDBKeyRange | null,
|
|
92
|
+
direction?: IDBCursorDirection,
|
|
93
|
+
): AsyncGenerator<IndexCursor, void, undefined> {
|
|
94
|
+
const idbCursorGenerator = requestToAsyncGenerator(
|
|
95
|
+
this.idbIndex.openCursor(query, direction),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
for await (const idbCursor of idbCursorGenerator) {
|
|
99
|
+
yield new IndexCursor(idbCursor);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Modern IndexedDB
|
|
2
|
+
|
|
3
|
+
## Todo
|
|
4
|
+
|
|
5
|
+
Include properties for unusual objects in Kaypath type. Eg. Array.length, etc...
|
|
6
|
+
|
|
7
|
+
## Implementation Notes
|
|
8
|
+
|
|
9
|
+
In several places, it could feel like subclassing should have been used in certain cases: ObjectStore could extend IDBObjectStore, ModernIDB could extend EventTarget, etc... However, this could not be done without violating the [Liskov substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle). ModernIDB methods usually return Promises, which are not compatible with IDBRequests. Similarly, the addEventListener/removeEventListener of ModernIDB always dispatch CustomEvents, whereas the base addEventListener/removeEventListener of EventTarget must be able to handle any Event.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ObjectStore, VersionChangeObjectStore } from "./ObjectStore.ts";
|
|
2
|
+
import type { ModernIDBSchema } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export class Transaction<
|
|
5
|
+
Schema extends ModernIDBSchema,
|
|
6
|
+
IndexNames extends {
|
|
7
|
+
[K in keyof Schema]?: string;
|
|
8
|
+
},
|
|
9
|
+
> {
|
|
10
|
+
idbTransaction: IDBTransaction;
|
|
11
|
+
|
|
12
|
+
constructor(props: { transaction: IDBTransaction }) {
|
|
13
|
+
this.idbTransaction = props.transaction;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
objectStore<StoreName extends string & keyof Schema>(
|
|
17
|
+
name: StoreName,
|
|
18
|
+
): ObjectStore<
|
|
19
|
+
Schema[StoreName],
|
|
20
|
+
IndexNames[StoreName] extends string ? IndexNames[StoreName] : never
|
|
21
|
+
> {
|
|
22
|
+
return new ObjectStore(this.idbTransaction.objectStore(name));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class VersionChangeTransaction<
|
|
27
|
+
Schema extends ModernIDBSchema,
|
|
28
|
+
IndexNames extends {
|
|
29
|
+
[K in keyof Schema]?: string;
|
|
30
|
+
},
|
|
31
|
+
> extends Transaction<Schema, IndexNames> {
|
|
32
|
+
override objectStore<StoreName extends string & keyof Schema>(
|
|
33
|
+
name: StoreName,
|
|
34
|
+
): VersionChangeObjectStore<
|
|
35
|
+
Schema[StoreName],
|
|
36
|
+
IndexNames[StoreName] extends string ? IndexNames[StoreName] : never
|
|
37
|
+
> {
|
|
38
|
+
return new VersionChangeObjectStore(this.idbTransaction.objectStore(name));
|
|
39
|
+
}
|
|
40
|
+
}
|