@orkestrel/indexeddb 0.0.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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/src/browser/IndexedDBCursor.d.ts +28 -0
- package/dist/src/browser/IndexedDBDatabase.d.ts +27 -0
- package/dist/src/browser/IndexedDBIndex.d.ts +31 -0
- package/dist/src/browser/IndexedDBStore.d.ts +41 -0
- package/dist/src/browser/IndexedDBTransaction.d.ts +24 -0
- package/dist/src/browser/IndexedDBTransactionStore.d.ts +34 -0
- package/dist/src/browser/constants.d.ts +10 -0
- package/dist/src/browser/errors.d.ts +30 -0
- package/dist/src/browser/factories.d.ts +30 -0
- package/dist/src/browser/helpers.d.ts +130 -0
- package/dist/src/browser/index.d.ts +11 -0
- package/dist/src/browser/index.js +939 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/types.d.ts +290 -0
- package/package.json +77 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
//#region src/browser/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Native `DOMException.name` → our {@link IndexedDBErrorCode}.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The mapping the request boundary's `wrapError` reads to translate a raw
|
|
7
|
+
* IndexedDB fault into a typed {@link IndexedDBError} code; an unmapped name
|
|
8
|
+
* falls back to `UNKNOWN`. Frozen plain data (AGENTS §5).
|
|
9
|
+
*/
|
|
10
|
+
var ERROR_CODES = Object.freeze({
|
|
11
|
+
ConstraintError: "CONSTRAINT",
|
|
12
|
+
QuotaExceededError: "QUOTA",
|
|
13
|
+
AbortError: "ABORTED",
|
|
14
|
+
NotFoundError: "NOT_FOUND",
|
|
15
|
+
DataError: "DATA",
|
|
16
|
+
VersionError: "UPGRADE",
|
|
17
|
+
TransactionInactiveError: "INACTIVE",
|
|
18
|
+
InvalidStateError: "INVALID"
|
|
19
|
+
});
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/browser/errors.ts
|
|
22
|
+
/**
|
|
23
|
+
* An error thrown by the IndexedDB wrapper.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Carries an {@link IndexedDBErrorCode} and the originating native error as the
|
|
27
|
+
* standard `cause`. Construct it directly for wrapper-lifecycle faults; the
|
|
28
|
+
* internal `wrapError` maps a native `DOMException` to the right code at the
|
|
29
|
+
* request boundary. Narrow a caught value with `instanceof IndexedDBError`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* try {
|
|
34
|
+
* await store.add(row)
|
|
35
|
+
* } catch (error) {
|
|
36
|
+
* if (error instanceof IndexedDBError && error.code === 'CONSTRAINT') await store.set(row)
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
var IndexedDBError = class extends Error {
|
|
41
|
+
code;
|
|
42
|
+
constructor(code, message, cause) {
|
|
43
|
+
super(message, { cause });
|
|
44
|
+
this.name = "IndexedDBError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Whether a value is an {@link IndexedDBError}.
|
|
50
|
+
*
|
|
51
|
+
* @param value - The value to test
|
|
52
|
+
* @returns `true` when `value` is an `IndexedDBError`
|
|
53
|
+
*/
|
|
54
|
+
function isIndexedDBError(value) {
|
|
55
|
+
return value instanceof IndexedDBError;
|
|
56
|
+
}
|
|
57
|
+
Object.freeze([
|
|
58
|
+
"null",
|
|
59
|
+
"boolean",
|
|
60
|
+
"object",
|
|
61
|
+
"array",
|
|
62
|
+
"number",
|
|
63
|
+
"integer",
|
|
64
|
+
"string"
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Determine whether a value is a non-null object.
|
|
68
|
+
*
|
|
69
|
+
* @remarks
|
|
70
|
+
* `true` for arrays, class instances, plain objects, `Map`, `Set`, etc. — use
|
|
71
|
+
* {@link isRecord} when you need a plain-record check.
|
|
72
|
+
*/
|
|
73
|
+
function isObject(value) {
|
|
74
|
+
return typeof value === "object" && value !== null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Determine whether a value is a plain record (object literal or null-prototype),
|
|
78
|
+
* not an array or class instance.
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* Use instead of {@link isObject} to distinguish a plain `{}` /
|
|
82
|
+
* `Object.create(null)` from arrays, `Date`, `Map`, etc. The prototype-chain
|
|
83
|
+
* test is realm-agnostic: rather than comparing against the current realm's
|
|
84
|
+
* `Object.prototype` (which a plain object from another `vm.Context`, iframe,
|
|
85
|
+
* or worker would fail), it accepts any value whose prototype is `null`, OR
|
|
86
|
+
* whose prototype's own prototype is `null` — the shape every plain object
|
|
87
|
+
* has in every realm, since `Object.prototype` itself always sits one step
|
|
88
|
+
* above `null`. Arrays and class instances are still rejected: an array's
|
|
89
|
+
* prototype chain runs through `Array.prototype` before `null`, and a class
|
|
90
|
+
* instance's runs through the class's own prototype. The whole body runs
|
|
91
|
+
* inside `attempt` (AGENTS §14) so a revoked `Proxy` or a hostile
|
|
92
|
+
* `getPrototypeOf` trap cannot escape as a thrown error.
|
|
93
|
+
*/
|
|
94
|
+
function isRecord(value) {
|
|
95
|
+
const outcome = attempt(() => {
|
|
96
|
+
if (!isObject(value) || isArray(value)) return false;
|
|
97
|
+
const prototype = Object.getPrototypeOf(value);
|
|
98
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
99
|
+
});
|
|
100
|
+
return outcome.success && outcome.value;
|
|
101
|
+
}
|
|
102
|
+
/** Determine whether a value is an array. */
|
|
103
|
+
function isArray(value) {
|
|
104
|
+
return Array.isArray(value);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Invoke a callback and capture its outcome as a {@link Result}, never letting
|
|
108
|
+
* a throw escape.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* The single sanctioned never-throw boundary for the guards (AGENTS §14). The
|
|
112
|
+
* `whereOf`, `lazyOf`, and `transformOf` combinators invoke caller-supplied
|
|
113
|
+
* callbacks *inside* a guard body, yet a guard must NEVER throw — it returns a
|
|
114
|
+
* `boolean`. This converts a throwing callback into a `Failure` so the
|
|
115
|
+
* surrounding guard can treat it as a non-match instead of propagating the
|
|
116
|
+
* exception, written once and shared rather than copy-pasted as ad-hoc
|
|
117
|
+
* `try`/`catch`.
|
|
118
|
+
*
|
|
119
|
+
* @param callback - The callback to invoke with no arguments
|
|
120
|
+
* @returns A `Success` carrying the return value, or a `Failure` carrying the
|
|
121
|
+
* thrown reason normalised to an `Error`
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const outcome = attempt(() => predicate(value))
|
|
126
|
+
* return outcome.success && outcome.value
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
function attempt(callback) {
|
|
130
|
+
try {
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
value: callback()
|
|
134
|
+
};
|
|
135
|
+
} catch (reason) {
|
|
136
|
+
if (reason instanceof Error) return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: reason
|
|
139
|
+
};
|
|
140
|
+
let message = "Unknown thrown value";
|
|
141
|
+
try {
|
|
142
|
+
message = String(reason);
|
|
143
|
+
} catch {}
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: new Error(message)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/browser/helpers.ts
|
|
152
|
+
/**
|
|
153
|
+
* Whether IndexedDB is available in this environment.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* Gate IndexedDB code with this and fall back to another storage strategy where
|
|
157
|
+
* it is absent (a non-browser runtime, a privacy mode that disables storage).
|
|
158
|
+
* The entry probe, checked before reaching for the rest of this module.
|
|
159
|
+
*
|
|
160
|
+
* @returns `true` when `globalThis.indexedDB` exists
|
|
161
|
+
*/
|
|
162
|
+
function isIndexedDBSupported() {
|
|
163
|
+
return typeof globalThis.indexedDB !== "undefined";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve an `IDBRequest` to its result, rejecting with an {@link IndexedDBError}.
|
|
167
|
+
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* The single bridge from IndexedDB's event-based requests to Promises. Issue the
|
|
170
|
+
* request, then `await` this — within an implicit transaction, issue every request
|
|
171
|
+
* for that transaction before the first `await`, so they share it.
|
|
172
|
+
*
|
|
173
|
+
* @param request - The pending request
|
|
174
|
+
* @returns Its `result` on success
|
|
175
|
+
*/
|
|
176
|
+
function promisifyRequest(request) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
request.onsuccess = () => resolve(request.result);
|
|
179
|
+
request.onerror = () => reject(wrapError(request.error));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Resolve once an `IDBTransaction` commits, rejecting if it errors or aborts.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* Await this after issuing the writes of a `readwrite` transaction to guarantee
|
|
187
|
+
* they are durable before continuing.
|
|
188
|
+
*
|
|
189
|
+
* @param transaction - The transaction to await
|
|
190
|
+
*/
|
|
191
|
+
function promisifyTransaction(transaction) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
transaction.addEventListener("complete", () => resolve());
|
|
194
|
+
transaction.addEventListener("error", () => reject(wrapError(transaction.error)));
|
|
195
|
+
transaction.addEventListener("abort", () => reject(wrapError(transaction.error)));
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Run a synchronous native IndexedDB call, wrapping a thrown `DOMException`
|
|
200
|
+
* into a typed {@link IndexedDBError}.
|
|
201
|
+
*
|
|
202
|
+
* @remarks
|
|
203
|
+
* Native IndexedDB throws SYNCHRONOUSLY (not through a request's `onerror`)
|
|
204
|
+
* from calls like `database.transaction(...)` or an inactive/closed store's
|
|
205
|
+
* `get` / `put` / `openCursor` — `TransactionInactiveError` /
|
|
206
|
+
* `InvalidStateError` never reach {@link promisifyRequest}'s `onerror`
|
|
207
|
+
* bridge. Every request-issuing call site wraps its native invocation in this
|
|
208
|
+
* so those faults surface as the same typed `IndexedDBError` as an
|
|
209
|
+
* asynchronous one.
|
|
210
|
+
*
|
|
211
|
+
* @param action - The synchronous native call to run
|
|
212
|
+
* @returns Its return value
|
|
213
|
+
*/
|
|
214
|
+
function guardSync(action) {
|
|
215
|
+
try {
|
|
216
|
+
return action();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof DOMException) throw wrapError(error);
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Read one record by key from a store or index, narrowing it to a {@link Row}.
|
|
224
|
+
*
|
|
225
|
+
* @remarks
|
|
226
|
+
* The shared point-read of every store-like class (`IndexedDBStore`,
|
|
227
|
+
* `IndexedDBIndex`, `IndexedDBTransactionStore`): issue the native `get`, then
|
|
228
|
+
* narrow the structured clone with `isRecord` — a non-record (or a miss) reads as
|
|
229
|
+
* `undefined`, never an unchecked cast. On an index, `source.get` returns the
|
|
230
|
+
* first record for the index key.
|
|
231
|
+
*
|
|
232
|
+
* @param source - The object store or index to read from
|
|
233
|
+
* @param key - The key (a primary key for a store, an index key for an index)
|
|
234
|
+
* @returns The record, or `undefined` on a miss
|
|
235
|
+
*/
|
|
236
|
+
async function readRecord(source, key) {
|
|
237
|
+
const value = await promisifyRequest(guardSync(() => source.get(key)));
|
|
238
|
+
return isRecord(value) ? value : void 0;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Read many records from a store or index over an optional key range.
|
|
242
|
+
*
|
|
243
|
+
* @remarks
|
|
244
|
+
* The shared bulk read of every store-like class: issue the native `getAll` over
|
|
245
|
+
* an optional `query` (a key range or a single key) and `count` cap, then keep
|
|
246
|
+
* only the records with `isRecord` — the same boundary narrowing as
|
|
247
|
+
* {@link readRecord}, applied across the batch.
|
|
248
|
+
*
|
|
249
|
+
* @param source - The object store or index to read from
|
|
250
|
+
* @param query - A key range or single key to restrict the read, or `null` for all
|
|
251
|
+
* @param count - The maximum number of records to read
|
|
252
|
+
* @returns The matching records
|
|
253
|
+
*/
|
|
254
|
+
async function readRecords(source, query, count) {
|
|
255
|
+
return (await promisifyRequest(guardSync(() => source.getAll(query ?? void 0, count)))).filter(isRecord);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Whether a key is present in a store or index.
|
|
259
|
+
*
|
|
260
|
+
* @remarks
|
|
261
|
+
* The shared presence test of every store-like class: a native `count` of the
|
|
262
|
+
* key, true when at least one record matches — cheaper than reading the record
|
|
263
|
+
* when only existence matters.
|
|
264
|
+
*
|
|
265
|
+
* @param source - The object store or index to test
|
|
266
|
+
* @param key - The key to look for
|
|
267
|
+
* @returns `true` when at least one record has the key
|
|
268
|
+
*/
|
|
269
|
+
async function hasKey(source, key) {
|
|
270
|
+
return await promisifyRequest(guardSync(() => source.count(key))) > 0;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Key-range builders — the wrapper's filter vocabulary.
|
|
274
|
+
*
|
|
275
|
+
* @remarks
|
|
276
|
+
* Each returns an `IDBKeyRange` to pass to `records` / `keys` / `count` / `cursor`,
|
|
277
|
+
* so a read is index-backed (O(log n)) rather than a full scan. `only` (exact),
|
|
278
|
+
* `above` / `from` (greater than, exclusive / inclusive), `below` / `to` (less
|
|
279
|
+
* than, exclusive / inclusive), `between` (bounded), and `prefix` (string
|
|
280
|
+
* starts-with).
|
|
281
|
+
*/
|
|
282
|
+
var range = {
|
|
283
|
+
only(value) {
|
|
284
|
+
return IDBKeyRange.only(value);
|
|
285
|
+
},
|
|
286
|
+
above(value) {
|
|
287
|
+
return IDBKeyRange.lowerBound(value, true);
|
|
288
|
+
},
|
|
289
|
+
from(value) {
|
|
290
|
+
return IDBKeyRange.lowerBound(value, false);
|
|
291
|
+
},
|
|
292
|
+
below(value) {
|
|
293
|
+
return IDBKeyRange.upperBound(value, true);
|
|
294
|
+
},
|
|
295
|
+
to(value) {
|
|
296
|
+
return IDBKeyRange.upperBound(value, false);
|
|
297
|
+
},
|
|
298
|
+
between(lower, upper, options) {
|
|
299
|
+
return IDBKeyRange.bound(lower, upper, options?.lowerOpen ?? false, options?.upperOpen ?? false);
|
|
300
|
+
},
|
|
301
|
+
prefix(value) {
|
|
302
|
+
return IDBKeyRange.bound(value, value + "", false, false);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Map a native IndexedDB `DOMException` to a typed {@link IndexedDBError}.
|
|
307
|
+
*
|
|
308
|
+
* @remarks
|
|
309
|
+
* The boundary the two Promise bridges ({@link promisifyRequest} /
|
|
310
|
+
* {@link promisifyTransaction}) share: it reads {@link ERROR_CODES} to pick the
|
|
311
|
+
* machine-readable code for the native `name`, falling back to `UNKNOWN` for an
|
|
312
|
+
* unmapped name or a `null` error.
|
|
313
|
+
*
|
|
314
|
+
* @param error - The native error, or `null` when none is attached
|
|
315
|
+
* @returns The wrapped, typed error
|
|
316
|
+
*/
|
|
317
|
+
function wrapError(error) {
|
|
318
|
+
if (error === null) return new IndexedDBError("UNKNOWN", "Unknown IndexedDB error");
|
|
319
|
+
return new IndexedDBError(ERROR_CODES[error.name] ?? "UNKNOWN", error.message || `IndexedDB error: ${error.name}`, error);
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/browser/IndexedDBCursor.ts
|
|
323
|
+
/**
|
|
324
|
+
* A promisified value cursor over an object store or index.
|
|
325
|
+
*
|
|
326
|
+
* @remarks
|
|
327
|
+
* Wraps `IDBCursorWithValue` and the request that drives it. The position
|
|
328
|
+
* (`key` / `primary` / `value`) is snapshot at construction because IndexedDB
|
|
329
|
+
* mutates the live cursor object in place on each advance. `continue` / `seek` /
|
|
330
|
+
* `advance` re-arm the shared request and resolve to the next position (a fresh
|
|
331
|
+
* `IndexedDBCursor`) or `null` at the end. `update` / `delete` act on the current
|
|
332
|
+
* record — they require the cursor's transaction to be `readwrite` (a `store`
|
|
333
|
+
* cursor), so they reject on an `index` cursor's read-only transaction.
|
|
334
|
+
*/
|
|
335
|
+
var IndexedDBCursor = class IndexedDBCursor {
|
|
336
|
+
#cursor;
|
|
337
|
+
#request;
|
|
338
|
+
#key;
|
|
339
|
+
#primary;
|
|
340
|
+
#value;
|
|
341
|
+
#direction;
|
|
342
|
+
constructor(cursor, request) {
|
|
343
|
+
this.#cursor = cursor;
|
|
344
|
+
this.#request = request;
|
|
345
|
+
this.#key = cursor.key;
|
|
346
|
+
this.#primary = cursor.primaryKey;
|
|
347
|
+
this.#value = isRecord(cursor.value) ? cursor.value : {};
|
|
348
|
+
this.#direction = cursor.direction;
|
|
349
|
+
}
|
|
350
|
+
get cursor() {
|
|
351
|
+
return this.#cursor;
|
|
352
|
+
}
|
|
353
|
+
get source() {
|
|
354
|
+
return this.#cursor.source;
|
|
355
|
+
}
|
|
356
|
+
get key() {
|
|
357
|
+
return this.#key;
|
|
358
|
+
}
|
|
359
|
+
get primary() {
|
|
360
|
+
return this.#primary;
|
|
361
|
+
}
|
|
362
|
+
get value() {
|
|
363
|
+
return this.#value;
|
|
364
|
+
}
|
|
365
|
+
get direction() {
|
|
366
|
+
return this.#direction;
|
|
367
|
+
}
|
|
368
|
+
async continue(key) {
|
|
369
|
+
guardSync(() => {
|
|
370
|
+
if (key === void 0) this.#cursor.continue();
|
|
371
|
+
else this.#cursor.continue(key);
|
|
372
|
+
});
|
|
373
|
+
return this.#advance();
|
|
374
|
+
}
|
|
375
|
+
async seek(key, primary) {
|
|
376
|
+
guardSync(() => this.#cursor.continuePrimaryKey(key, primary));
|
|
377
|
+
return this.#advance();
|
|
378
|
+
}
|
|
379
|
+
async advance(count) {
|
|
380
|
+
guardSync(() => this.#cursor.advance(count));
|
|
381
|
+
return this.#advance();
|
|
382
|
+
}
|
|
383
|
+
async update(value) {
|
|
384
|
+
const key = await promisifyRequest(guardSync(() => this.#cursor.update(value)));
|
|
385
|
+
this.#value = value;
|
|
386
|
+
return key;
|
|
387
|
+
}
|
|
388
|
+
async delete() {
|
|
389
|
+
await promisifyRequest(guardSync(() => this.#cursor.delete()));
|
|
390
|
+
}
|
|
391
|
+
async #advance() {
|
|
392
|
+
const next = await promisifyRequest(this.#request);
|
|
393
|
+
return next ? new IndexedDBCursor(next, this.#request) : null;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/browser/IndexedDBIndex.ts
|
|
398
|
+
/**
|
|
399
|
+
* A secondary index on a store — a read-only view keyed by an indexed path.
|
|
400
|
+
*
|
|
401
|
+
* @remarks
|
|
402
|
+
* Reached through `store.index(name)`. Each call opens its own `readonly`
|
|
403
|
+
* transaction. `get` / `resolve` fetch the first record for an index key
|
|
404
|
+
* (`resolve` throws `NOT_FOUND`); `records` reads the matching records and `keys`
|
|
405
|
+
* their **primary** keys; `primary` maps an index key to one primary key; `count`
|
|
406
|
+
* / `has` test presence; `cursor` streams matches. Reads batch by their array
|
|
407
|
+
* overload (AGENTS §9.2).
|
|
408
|
+
*/
|
|
409
|
+
var IndexedDBIndex = class {
|
|
410
|
+
#store;
|
|
411
|
+
#name;
|
|
412
|
+
#definition;
|
|
413
|
+
#connect;
|
|
414
|
+
constructor(store, name, definition, connect) {
|
|
415
|
+
this.#store = store;
|
|
416
|
+
this.#name = name;
|
|
417
|
+
this.#definition = definition;
|
|
418
|
+
this.#connect = connect;
|
|
419
|
+
}
|
|
420
|
+
get name() {
|
|
421
|
+
return this.#name;
|
|
422
|
+
}
|
|
423
|
+
get path() {
|
|
424
|
+
return this.#definition.path;
|
|
425
|
+
}
|
|
426
|
+
get unique() {
|
|
427
|
+
return this.#definition.unique ?? false;
|
|
428
|
+
}
|
|
429
|
+
get multiple() {
|
|
430
|
+
return this.#definition.multiple ?? false;
|
|
431
|
+
}
|
|
432
|
+
async get(keyOrKeys) {
|
|
433
|
+
const index = await this.#index();
|
|
434
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => readRecord(index, key)));
|
|
435
|
+
return readRecord(index, keyOrKeys);
|
|
436
|
+
}
|
|
437
|
+
async resolve(keyOrKeys) {
|
|
438
|
+
const index = await this.#index();
|
|
439
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => this.#resolve(index, key)));
|
|
440
|
+
return this.#resolve(index, keyOrKeys);
|
|
441
|
+
}
|
|
442
|
+
async records(query, count) {
|
|
443
|
+
return readRecords(await this.#index(), query, count);
|
|
444
|
+
}
|
|
445
|
+
async keys(query, count) {
|
|
446
|
+
const index = await this.#index();
|
|
447
|
+
return promisifyRequest(guardSync(() => index.getAllKeys(query ?? void 0, count)));
|
|
448
|
+
}
|
|
449
|
+
async primary(key) {
|
|
450
|
+
const index = await this.#index();
|
|
451
|
+
return promisifyRequest(guardSync(() => index.getKey(key)));
|
|
452
|
+
}
|
|
453
|
+
async has(keyOrKeys) {
|
|
454
|
+
const index = await this.#index();
|
|
455
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => hasKey(index, key)));
|
|
456
|
+
return hasKey(index, keyOrKeys);
|
|
457
|
+
}
|
|
458
|
+
async count(query) {
|
|
459
|
+
const index = await this.#index();
|
|
460
|
+
return promisifyRequest(guardSync(() => index.count(query ?? void 0)));
|
|
461
|
+
}
|
|
462
|
+
async cursor(options) {
|
|
463
|
+
const index = await this.#index();
|
|
464
|
+
const request = guardSync(() => index.openCursor(options?.query ?? null, options?.direction ?? "next"));
|
|
465
|
+
const cursor = await promisifyRequest(request);
|
|
466
|
+
return cursor ? new IndexedDBCursor(cursor, request) : null;
|
|
467
|
+
}
|
|
468
|
+
async #index() {
|
|
469
|
+
const database = await this.#connect();
|
|
470
|
+
return guardSync(() => database.transaction([this.#store], "readonly").objectStore(this.#store).index(this.#name));
|
|
471
|
+
}
|
|
472
|
+
async #resolve(index, key) {
|
|
473
|
+
const value = await readRecord(index, key);
|
|
474
|
+
if (value === void 0) throw new IndexedDBError("NOT_FOUND", `No record in index '${this.#name}' of store '${this.#store}' for key ${String(key)}`);
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/browser/IndexedDBStore.ts
|
|
480
|
+
/**
|
|
481
|
+
* An object store — the full keyed CRUD surface plus index, count, and cursor
|
|
482
|
+
* access.
|
|
483
|
+
*
|
|
484
|
+
* @remarks
|
|
485
|
+
* Reached through `database.store(name)`. Each call runs in its own implicit
|
|
486
|
+
* transaction (`readonly` for reads, `readwrite` for writes), awaiting completion
|
|
487
|
+
* so writes are durable on return; for atomic multi-operation work use the
|
|
488
|
+
* database's `read` / `write`. The keyed verbs batch by their array overload — and
|
|
489
|
+
* those overloads are declared first, because an array is itself both a record and
|
|
490
|
+
* a compound `IDBValidKey`, so the array signature must take precedence to read as
|
|
491
|
+
* a batch (AGENTS §9.2). Pass `range.only([…])` to `records` / `count` to act on a
|
|
492
|
+
* single compound key.
|
|
493
|
+
*/
|
|
494
|
+
var IndexedDBStore = class {
|
|
495
|
+
#name;
|
|
496
|
+
#definition;
|
|
497
|
+
#connect;
|
|
498
|
+
constructor(name, definition, connect) {
|
|
499
|
+
this.#name = name;
|
|
500
|
+
this.#definition = definition;
|
|
501
|
+
this.#connect = connect;
|
|
502
|
+
}
|
|
503
|
+
get name() {
|
|
504
|
+
return this.#name;
|
|
505
|
+
}
|
|
506
|
+
get path() {
|
|
507
|
+
return this.#definition.path ?? null;
|
|
508
|
+
}
|
|
509
|
+
get indexes() {
|
|
510
|
+
return (this.#definition.indexes ?? []).map((index) => index.name);
|
|
511
|
+
}
|
|
512
|
+
get increment() {
|
|
513
|
+
return this.#definition.increment ?? false;
|
|
514
|
+
}
|
|
515
|
+
async get(keyOrKeys) {
|
|
516
|
+
const store = await this.#store("readonly");
|
|
517
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => readRecord(store, key)));
|
|
518
|
+
return readRecord(store, keyOrKeys);
|
|
519
|
+
}
|
|
520
|
+
async resolve(keyOrKeys) {
|
|
521
|
+
const store = await this.#store("readonly");
|
|
522
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => this.#resolve(store, key)));
|
|
523
|
+
return this.#resolve(store, keyOrKeys);
|
|
524
|
+
}
|
|
525
|
+
async records(query, count) {
|
|
526
|
+
return readRecords(await this.#store("readonly"), query, count);
|
|
527
|
+
}
|
|
528
|
+
async keys(query, count) {
|
|
529
|
+
const store = await this.#store("readonly");
|
|
530
|
+
return promisifyRequest(guardSync(() => store.getAllKeys(query ?? void 0, count)));
|
|
531
|
+
}
|
|
532
|
+
async has(keyOrKeys) {
|
|
533
|
+
const store = await this.#store("readonly");
|
|
534
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => hasKey(store, key)));
|
|
535
|
+
return hasKey(store, keyOrKeys);
|
|
536
|
+
}
|
|
537
|
+
async count(query) {
|
|
538
|
+
const store = await this.#store("readonly");
|
|
539
|
+
return promisifyRequest(guardSync(() => store.count(query ?? void 0)));
|
|
540
|
+
}
|
|
541
|
+
async set(valueOrValues, key) {
|
|
542
|
+
const store = await this.#store("readwrite");
|
|
543
|
+
if (isArray(valueOrValues)) {
|
|
544
|
+
const keys = await Promise.all(valueOrValues.map((value) => promisifyRequest(guardSync(() => store.put(value)))));
|
|
545
|
+
await promisifyTransaction(store.transaction);
|
|
546
|
+
return keys;
|
|
547
|
+
}
|
|
548
|
+
const written = await promisifyRequest(guardSync(() => key === void 0 ? store.put(valueOrValues) : store.put(valueOrValues, key)));
|
|
549
|
+
await promisifyTransaction(store.transaction);
|
|
550
|
+
return written;
|
|
551
|
+
}
|
|
552
|
+
async add(valueOrValues, key) {
|
|
553
|
+
const store = await this.#store("readwrite");
|
|
554
|
+
if (isArray(valueOrValues)) {
|
|
555
|
+
const keys = await Promise.all(valueOrValues.map((value) => promisifyRequest(guardSync(() => store.add(value)))));
|
|
556
|
+
await promisifyTransaction(store.transaction);
|
|
557
|
+
return keys;
|
|
558
|
+
}
|
|
559
|
+
const written = await promisifyRequest(guardSync(() => key === void 0 ? store.add(valueOrValues) : store.add(valueOrValues, key)));
|
|
560
|
+
await promisifyTransaction(store.transaction);
|
|
561
|
+
return written;
|
|
562
|
+
}
|
|
563
|
+
async remove(keyOrKeys) {
|
|
564
|
+
const store = await this.#store("readwrite");
|
|
565
|
+
if (isArray(keyOrKeys)) await Promise.all(keyOrKeys.map((key) => promisifyRequest(guardSync(() => store.delete(key)))));
|
|
566
|
+
else await promisifyRequest(guardSync(() => store.delete(keyOrKeys)));
|
|
567
|
+
await promisifyTransaction(store.transaction);
|
|
568
|
+
}
|
|
569
|
+
async clear() {
|
|
570
|
+
const store = await this.#store("readwrite");
|
|
571
|
+
await promisifyRequest(guardSync(() => store.clear()));
|
|
572
|
+
await promisifyTransaction(store.transaction);
|
|
573
|
+
}
|
|
574
|
+
index(name) {
|
|
575
|
+
const definition = (this.#definition.indexes ?? []).find((index) => index.name === name);
|
|
576
|
+
if (definition === void 0) throw new IndexedDBError("NOT_FOUND", `Index '${name}' is not declared on store '${this.#name}'`);
|
|
577
|
+
return new IndexedDBIndex(this.#name, name, definition, this.#connect);
|
|
578
|
+
}
|
|
579
|
+
async cursor(options) {
|
|
580
|
+
const store = await this.#store("readwrite");
|
|
581
|
+
const request = guardSync(() => store.openCursor(options?.query ?? null, options?.direction ?? "next"));
|
|
582
|
+
const cursor = await promisifyRequest(request);
|
|
583
|
+
return cursor ? new IndexedDBCursor(cursor, request) : null;
|
|
584
|
+
}
|
|
585
|
+
async #store(mode) {
|
|
586
|
+
const database = await this.#connect();
|
|
587
|
+
return guardSync(() => database.transaction([this.#name], mode).objectStore(this.#name));
|
|
588
|
+
}
|
|
589
|
+
async #resolve(store, key) {
|
|
590
|
+
const value = await readRecord(store, key);
|
|
591
|
+
if (value === void 0) throw new IndexedDBError("NOT_FOUND", `No record in store '${this.#name}' for key ${String(key)}`);
|
|
592
|
+
return value;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/browser/IndexedDBTransactionStore.ts
|
|
597
|
+
/**
|
|
598
|
+
* An object store bound to an explicit transaction.
|
|
599
|
+
*
|
|
600
|
+
* @remarks
|
|
601
|
+
* The same CRUD surface as a standalone store, but every call runs in the owning
|
|
602
|
+
* transaction (opened by the database's `read` / `write`) rather than its own — so
|
|
603
|
+
* a sequence of reads and writes commits atomically when the scope resolves. It
|
|
604
|
+
* does not await transaction completion per call (the scope does that once) and
|
|
605
|
+
* omits `index`; keep your awaited operations on IndexedDB requests only, so the
|
|
606
|
+
* transaction stays active across them.
|
|
607
|
+
*/
|
|
608
|
+
var IndexedDBTransactionStore = class {
|
|
609
|
+
#store;
|
|
610
|
+
#name;
|
|
611
|
+
constructor(store) {
|
|
612
|
+
this.#store = store;
|
|
613
|
+
this.#name = store.name;
|
|
614
|
+
}
|
|
615
|
+
get store() {
|
|
616
|
+
return this.#store;
|
|
617
|
+
}
|
|
618
|
+
async get(keyOrKeys) {
|
|
619
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => readRecord(this.#store, key)));
|
|
620
|
+
return readRecord(this.#store, keyOrKeys);
|
|
621
|
+
}
|
|
622
|
+
async resolve(keyOrKeys) {
|
|
623
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => this.#resolve(key)));
|
|
624
|
+
return this.#resolve(keyOrKeys);
|
|
625
|
+
}
|
|
626
|
+
async records(query, count) {
|
|
627
|
+
return readRecords(this.#store, query, count);
|
|
628
|
+
}
|
|
629
|
+
async keys(query, count) {
|
|
630
|
+
return promisifyRequest(guardSync(() => this.#store.getAllKeys(query ?? void 0, count)));
|
|
631
|
+
}
|
|
632
|
+
async has(keyOrKeys) {
|
|
633
|
+
if (isArray(keyOrKeys)) return Promise.all(keyOrKeys.map((key) => hasKey(this.#store, key)));
|
|
634
|
+
return hasKey(this.#store, keyOrKeys);
|
|
635
|
+
}
|
|
636
|
+
async count(query) {
|
|
637
|
+
return promisifyRequest(guardSync(() => this.#store.count(query ?? void 0)));
|
|
638
|
+
}
|
|
639
|
+
async set(valueOrValues, key) {
|
|
640
|
+
if (isArray(valueOrValues)) return Promise.all(valueOrValues.map((value) => promisifyRequest(guardSync(() => this.#store.put(value)))));
|
|
641
|
+
return promisifyRequest(guardSync(() => key === void 0 ? this.#store.put(valueOrValues) : this.#store.put(valueOrValues, key)));
|
|
642
|
+
}
|
|
643
|
+
async add(valueOrValues, key) {
|
|
644
|
+
if (isArray(valueOrValues)) return Promise.all(valueOrValues.map((value) => promisifyRequest(guardSync(() => this.#store.add(value)))));
|
|
645
|
+
return promisifyRequest(guardSync(() => key === void 0 ? this.#store.add(valueOrValues) : this.#store.add(valueOrValues, key)));
|
|
646
|
+
}
|
|
647
|
+
async remove(keyOrKeys) {
|
|
648
|
+
if (isArray(keyOrKeys)) {
|
|
649
|
+
await Promise.all(keyOrKeys.map((key) => promisifyRequest(guardSync(() => this.#store.delete(key)))));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
await promisifyRequest(guardSync(() => this.#store.delete(keyOrKeys)));
|
|
653
|
+
}
|
|
654
|
+
async clear() {
|
|
655
|
+
await promisifyRequest(guardSync(() => this.#store.clear()));
|
|
656
|
+
}
|
|
657
|
+
async cursor(options) {
|
|
658
|
+
const request = guardSync(() => this.#store.openCursor(options?.query ?? null, options?.direction ?? "next"));
|
|
659
|
+
const cursor = await promisifyRequest(request);
|
|
660
|
+
return cursor ? new IndexedDBCursor(cursor, request) : null;
|
|
661
|
+
}
|
|
662
|
+
async #resolve(key) {
|
|
663
|
+
const value = await readRecord(this.#store, key);
|
|
664
|
+
if (value === void 0) throw new IndexedDBError("NOT_FOUND", `No record in store '${this.#name}' for key ${String(key)}`);
|
|
665
|
+
return value;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/browser/IndexedDBTransaction.ts
|
|
670
|
+
/**
|
|
671
|
+
* An explicit transaction over one or more stores.
|
|
672
|
+
*
|
|
673
|
+
* @remarks
|
|
674
|
+
* Wraps `IDBTransaction` with state tracking and typed, scope-bound store access.
|
|
675
|
+
* Constructed by the database's `read` / `write`, which await its completion (or
|
|
676
|
+
* roll it back on a throw). `store` reaches a store within the transaction's scope;
|
|
677
|
+
* `abort` rolls back; `commit` flushes early (the scope's completion commits
|
|
678
|
+
* otherwise). `active` is true until commit or abort; `finished` is its complement.
|
|
679
|
+
*/
|
|
680
|
+
var IndexedDBTransaction = class {
|
|
681
|
+
#transaction;
|
|
682
|
+
#stores;
|
|
683
|
+
#active = true;
|
|
684
|
+
#finished = false;
|
|
685
|
+
constructor(transaction) {
|
|
686
|
+
this.#transaction = transaction;
|
|
687
|
+
this.#stores = Array.from(transaction.objectStoreNames);
|
|
688
|
+
const settle = () => {
|
|
689
|
+
this.#active = false;
|
|
690
|
+
this.#finished = true;
|
|
691
|
+
};
|
|
692
|
+
transaction.addEventListener("complete", settle);
|
|
693
|
+
transaction.addEventListener("abort", settle);
|
|
694
|
+
transaction.addEventListener("error", settle);
|
|
695
|
+
}
|
|
696
|
+
get transaction() {
|
|
697
|
+
return this.#transaction;
|
|
698
|
+
}
|
|
699
|
+
get mode() {
|
|
700
|
+
return this.#transaction.mode;
|
|
701
|
+
}
|
|
702
|
+
get stores() {
|
|
703
|
+
return this.#stores;
|
|
704
|
+
}
|
|
705
|
+
get active() {
|
|
706
|
+
return this.#active;
|
|
707
|
+
}
|
|
708
|
+
get finished() {
|
|
709
|
+
return this.#finished;
|
|
710
|
+
}
|
|
711
|
+
get error() {
|
|
712
|
+
return this.#transaction.error;
|
|
713
|
+
}
|
|
714
|
+
store(name) {
|
|
715
|
+
if (!this.#stores.includes(name)) throw new IndexedDBError("NOT_FOUND", `Store '${name}' is outside this transaction's scope (${this.#stores.join(", ")})`);
|
|
716
|
+
if (!this.#active) throw new IndexedDBError("ABORTED", `Transaction over ${this.#stores.join(", ")} is no longer active`);
|
|
717
|
+
return new IndexedDBTransactionStore(this.#transaction.objectStore(name));
|
|
718
|
+
}
|
|
719
|
+
abort() {
|
|
720
|
+
if (this.#finished) throw new IndexedDBError("ABORTED", "Cannot abort an already-finished transaction");
|
|
721
|
+
this.#transaction.abort();
|
|
722
|
+
this.#active = false;
|
|
723
|
+
this.#finished = true;
|
|
724
|
+
}
|
|
725
|
+
commit() {
|
|
726
|
+
if (this.#finished) throw new IndexedDBError("ABORTED", "Cannot commit an already-finished transaction");
|
|
727
|
+
this.#transaction.commit();
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/browser/IndexedDBDatabase.ts
|
|
732
|
+
/**
|
|
733
|
+
* A browser-native IndexedDB database — a typed, Promise-based handle.
|
|
734
|
+
*
|
|
735
|
+
* @remarks
|
|
736
|
+
* Connects lazily on first use (`connect`, also awaited internally by every store
|
|
737
|
+
* operation), creating any missing stores from their definitions inside
|
|
738
|
+
* `onupgradeneeded`. `store` reaches a typed store; `read` / `write` run an atomic
|
|
739
|
+
* scope over one or more stores, committing on resolve and rolling back on a throw;
|
|
740
|
+
* `close` releases the connection and `drop` deletes the database. Schema changes
|
|
741
|
+
* beyond creating new stores (dropping stores, altering indexes) are deferred.
|
|
742
|
+
*/
|
|
743
|
+
var IndexedDBDatabase = class {
|
|
744
|
+
#name;
|
|
745
|
+
#version;
|
|
746
|
+
#stores;
|
|
747
|
+
#upgrade;
|
|
748
|
+
#database;
|
|
749
|
+
#opening;
|
|
750
|
+
#closed = false;
|
|
751
|
+
constructor(options) {
|
|
752
|
+
if (options.name.length === 0) throw new IndexedDBError("OPEN", "Database name must be a non-empty string");
|
|
753
|
+
if (options.version !== void 0 && (!Number.isInteger(options.version) || options.version < 1)) throw new IndexedDBError("OPEN", `Database version must be a positive integer, got ${String(options.version)}`);
|
|
754
|
+
this.#name = options.name;
|
|
755
|
+
this.#version = options.version;
|
|
756
|
+
this.#stores = options.stores;
|
|
757
|
+
this.#upgrade = options.upgrade;
|
|
758
|
+
}
|
|
759
|
+
get database() {
|
|
760
|
+
if (this.#database === void 0) throw new IndexedDBError("NOT_OPEN", `Database '${this.#name}' is not open — call connect() first`);
|
|
761
|
+
return this.#database;
|
|
762
|
+
}
|
|
763
|
+
get name() {
|
|
764
|
+
return this.#name;
|
|
765
|
+
}
|
|
766
|
+
get version() {
|
|
767
|
+
return this.#database?.version ?? this.#version ?? 0;
|
|
768
|
+
}
|
|
769
|
+
get stores() {
|
|
770
|
+
if (this.#database !== void 0) return Array.from(this.#database.objectStoreNames);
|
|
771
|
+
return Object.keys(this.#stores);
|
|
772
|
+
}
|
|
773
|
+
get open() {
|
|
774
|
+
return this.#database !== void 0 && !this.#closed;
|
|
775
|
+
}
|
|
776
|
+
connect() {
|
|
777
|
+
if (this.#closed) throw new IndexedDBError("CLOSED", `Database '${this.#name}' has been closed`);
|
|
778
|
+
if (this.#database !== void 0) return Promise.resolve(this.#database);
|
|
779
|
+
if (this.#opening !== void 0) return this.#opening;
|
|
780
|
+
this.#opening = this.#open().catch((error) => {
|
|
781
|
+
this.#opening = void 0;
|
|
782
|
+
throw error;
|
|
783
|
+
});
|
|
784
|
+
return this.#opening;
|
|
785
|
+
}
|
|
786
|
+
store(name) {
|
|
787
|
+
const definition = this.#stores[name];
|
|
788
|
+
if (definition === void 0) throw new IndexedDBError("NOT_FOUND", `Store '${name}' is not declared on database '${this.#name}'`);
|
|
789
|
+
return new IndexedDBStore(name, definition, () => this.connect());
|
|
790
|
+
}
|
|
791
|
+
read(stores, scope) {
|
|
792
|
+
return this.#run("readonly", stores, scope);
|
|
793
|
+
}
|
|
794
|
+
write(stores, scope) {
|
|
795
|
+
return this.#run("readwrite", stores, scope);
|
|
796
|
+
}
|
|
797
|
+
close() {
|
|
798
|
+
this.#database?.close();
|
|
799
|
+
this.#database = void 0;
|
|
800
|
+
this.#opening = void 0;
|
|
801
|
+
this.#closed = true;
|
|
802
|
+
}
|
|
803
|
+
async drop() {
|
|
804
|
+
this.close();
|
|
805
|
+
return new Promise((resolve, reject) => {
|
|
806
|
+
const request = globalThis.indexedDB.deleteDatabase(this.#name);
|
|
807
|
+
request.onsuccess = () => resolve();
|
|
808
|
+
request.onerror = () => reject(new IndexedDBError("UNKNOWN", `Failed to delete database '${this.#name}'`, request.error));
|
|
809
|
+
request.onblocked = () => reject(new IndexedDBError("BLOCKED", `Deletion of '${this.#name}' is blocked by another connection`));
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async #run(mode, stores, scope) {
|
|
813
|
+
const database = await this.connect();
|
|
814
|
+
const names = isArray(stores) ? [...stores] : [stores];
|
|
815
|
+
const native = guardSync(() => database.transaction(names, mode));
|
|
816
|
+
const tx = new IndexedDBTransaction(native);
|
|
817
|
+
try {
|
|
818
|
+
await scope(tx);
|
|
819
|
+
await promisifyTransaction(native);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (tx.active) try {
|
|
822
|
+
tx.abort();
|
|
823
|
+
} catch {}
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async #open() {
|
|
828
|
+
let database = await this.#request(this.#version);
|
|
829
|
+
if (this.#version === void 0) {
|
|
830
|
+
if (this.#missing(database).length > 0) {
|
|
831
|
+
const next = database.version + 1;
|
|
832
|
+
database.close();
|
|
833
|
+
database = await this.#request(next);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
database.onclose = () => {
|
|
837
|
+
this.#database = void 0;
|
|
838
|
+
};
|
|
839
|
+
database.onversionchange = () => {
|
|
840
|
+
database.close();
|
|
841
|
+
this.#database = void 0;
|
|
842
|
+
this.#opening = void 0;
|
|
843
|
+
};
|
|
844
|
+
this.#database = database;
|
|
845
|
+
return database;
|
|
846
|
+
}
|
|
847
|
+
#request(version) {
|
|
848
|
+
return new Promise((resolve, reject) => {
|
|
849
|
+
let upgradeError;
|
|
850
|
+
const request = version === void 0 ? globalThis.indexedDB.open(this.#name) : globalThis.indexedDB.open(this.#name, version);
|
|
851
|
+
request.onupgradeneeded = (event) => {
|
|
852
|
+
const database = request.result;
|
|
853
|
+
for (const [name, definition] of Object.entries(this.#stores)) if (!database.objectStoreNames.contains(name)) this.#createStore(database, name, definition);
|
|
854
|
+
if (this.#upgrade !== void 0) {
|
|
855
|
+
const transaction = request.transaction;
|
|
856
|
+
if (transaction !== null) {
|
|
857
|
+
const result = this.#upgrade(this.#context(database, transaction, event));
|
|
858
|
+
if (result !== void 0) result.catch((error) => {
|
|
859
|
+
upgradeError = error;
|
|
860
|
+
try {
|
|
861
|
+
transaction.abort();
|
|
862
|
+
} catch {}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
request.onsuccess = () => resolve(request.result);
|
|
868
|
+
request.onerror = () => reject(upgradeError !== void 0 ? new IndexedDBError("UPGRADE", `Upgrade of '${this.#name}' failed`, upgradeError) : new IndexedDBError("OPEN", `Failed to open database '${this.#name}'`, request.error));
|
|
869
|
+
request.onblocked = () => reject(new IndexedDBError("BLOCKED", `Open of '${this.#name}' is blocked by another connection`));
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
#missing(database) {
|
|
873
|
+
return Object.keys(this.#stores).filter((name) => !database.objectStoreNames.contains(name));
|
|
874
|
+
}
|
|
875
|
+
#context(database, transaction, event) {
|
|
876
|
+
return {
|
|
877
|
+
transaction,
|
|
878
|
+
old: event.oldVersion,
|
|
879
|
+
version: event.newVersion ?? database.version,
|
|
880
|
+
stores: Array.from(database.objectStoreNames),
|
|
881
|
+
create: (name, definition) => {
|
|
882
|
+
this.#createStore(database, name, definition);
|
|
883
|
+
},
|
|
884
|
+
drop: (name) => {
|
|
885
|
+
database.deleteObjectStore(name);
|
|
886
|
+
},
|
|
887
|
+
store: (name) => new IndexedDBTransactionStore(transaction.objectStore(name))
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
#createStore(database, name, definition) {
|
|
891
|
+
const options = { autoIncrement: definition.increment ?? false };
|
|
892
|
+
if (definition.path !== void 0) options.keyPath = typeof definition.path === "string" ? definition.path : [...definition.path];
|
|
893
|
+
const store = database.createObjectStore(name, options);
|
|
894
|
+
for (const index of definition.indexes ?? []) {
|
|
895
|
+
const keyPath = typeof index.path === "string" ? index.path : [...index.path];
|
|
896
|
+
store.createIndex(index.name, keyPath, {
|
|
897
|
+
unique: index.unique ?? false,
|
|
898
|
+
multiEntry: index.multiple ?? false
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/browser/factories.ts
|
|
905
|
+
/**
|
|
906
|
+
* Create a browser-native IndexedDB database over a store schema.
|
|
907
|
+
*
|
|
908
|
+
* @remarks
|
|
909
|
+
* The `const` type parameter captures the literal store names, so `db.store(name)`
|
|
910
|
+
* and `db.read` / `db.write` are checked against the declared stores. Stores are
|
|
911
|
+
* created from their definitions the first time the database opens at a new
|
|
912
|
+
* `version`; omit `version` for auto-managed mode, where the database bumps its
|
|
913
|
+
* own version once to create any declared store the stored schema is missing.
|
|
914
|
+
*
|
|
915
|
+
* @param options - The database `name`, `version`, and `stores` schema
|
|
916
|
+
* @returns A typed {@link IndexedDBDatabaseInterface}
|
|
917
|
+
*
|
|
918
|
+
* @example
|
|
919
|
+
* ```ts
|
|
920
|
+
* import { createIndexedDBDatabase, range } from '@src/browser'
|
|
921
|
+
*
|
|
922
|
+
* const db = createIndexedDBDatabase({
|
|
923
|
+
* name: 'app',
|
|
924
|
+
* version: 1,
|
|
925
|
+
* stores: {
|
|
926
|
+
* users: { path: 'id', indexes: [{ name: 'byAge', path: 'age' }] },
|
|
927
|
+
* },
|
|
928
|
+
* })
|
|
929
|
+
* await db.store('users').set({ id: 'u1', name: 'Ada', age: 36 })
|
|
930
|
+
* await db.store('users').index('byAge').records(range.from(18)) // adults, index-backed
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
function createIndexedDBDatabase(options) {
|
|
934
|
+
return new IndexedDBDatabase(options);
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
export { ERROR_CODES, IndexedDBCursor, IndexedDBDatabase, IndexedDBError, IndexedDBIndex, IndexedDBStore, IndexedDBTransaction, IndexedDBTransactionStore, createIndexedDBDatabase, guardSync, hasKey, isIndexedDBError, isIndexedDBSupported, promisifyRequest, promisifyTransaction, range, readRecord, readRecords, wrapError };
|
|
938
|
+
|
|
939
|
+
//# sourceMappingURL=index.js.map
|