@orkestrel/database 0.0.6 → 0.0.7
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/README.md +13 -35
- package/dist/src/browser/index.d.ts +92 -79
- package/dist/src/browser/index.js +357 -220
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1796 -803
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +545 -706
- package/dist/src/core/index.d.ts +545 -706
- package/dist/src/core/index.js +1775 -790
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1508 -757
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +443 -569
- package/dist/src/server/index.d.ts +443 -569
- package/dist/src/server/index.js +1492 -741
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -10
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { DatabaseError,
|
|
2
|
-
import { createIndexedDBDatabase, isIndexedDBError,
|
|
1
|
+
import { DatabaseError, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, compareValues, equalsValue, extractKey, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
|
|
2
|
+
import { createIndexedDBDatabase, isIndexedDBError, rangeAboveKey, rangeBelowKey, rangeBetweenKeys, rangeExactKey, rangeFromKey, rangeToKey } from "@orkestrel/indexeddb";
|
|
3
3
|
//#region src/browser/constants.ts
|
|
4
|
-
var
|
|
4
|
+
var INDEXABLE_STORAGE = /* @__PURE__ */ new Set([
|
|
5
5
|
"text",
|
|
6
6
|
"integer",
|
|
7
7
|
"real"
|
|
8
8
|
]);
|
|
9
|
-
var
|
|
9
|
+
var METADATA_STORE = "__metadata__";
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/browser/helpers.ts
|
|
12
12
|
/**
|
|
13
13
|
* The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one
|
|
14
|
-
* of the six exact key comparisons over scalar operands
|
|
14
|
+
* of the six exact key comparisons over scalar operands; otherwise
|
|
15
|
+
* `undefined`.
|
|
15
16
|
*
|
|
16
17
|
* @remarks
|
|
17
18
|
* Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
|
|
@@ -26,7 +27,7 @@ var META_STORE = "__meta__";
|
|
|
26
27
|
* boolean) that is not a usable key. `between` additionally guards against a
|
|
27
28
|
* REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
|
|
28
29
|
* `DataError` `DOMException` for a lower bound above the upper bound, so a
|
|
29
|
-
* reversed pair returns `
|
|
30
|
+
* reversed pair returns `undefined` here (falls back to a full scan, which the
|
|
30
31
|
* engine then correctly resolves to an empty result) rather than letting a
|
|
31
32
|
* native exception escape untyped — the same defensive posture as every other
|
|
32
33
|
* backend, which returns empty for a reversed/empty range instead of throwing.
|
|
@@ -35,18 +36,18 @@ var META_STORE = "__meta__";
|
|
|
35
36
|
* to a (possibly lossy) range.
|
|
36
37
|
*
|
|
37
38
|
* @param condition - The condition to translate
|
|
38
|
-
* @returns Its exact key range, or `
|
|
39
|
+
* @returns Its exact key range, or `undefined` when the operator/operands cannot push
|
|
39
40
|
*/
|
|
40
|
-
function
|
|
41
|
+
function conditionToRange(condition) {
|
|
41
42
|
const first = condition.values[0];
|
|
42
43
|
const second = condition.values[1];
|
|
43
44
|
switch (condition.operator) {
|
|
44
|
-
case "equals": return isKey(first) ?
|
|
45
|
-
case "above": return isKey(first) ?
|
|
46
|
-
case "below": return isKey(first) ?
|
|
47
|
-
case "from": return isKey(first) ?
|
|
48
|
-
case "to": return isKey(first) ?
|
|
49
|
-
case "between": return isKey(first) && isKey(second) && compareValues(first, second) <= 0 ?
|
|
45
|
+
case "equals": return isKey(first) ? rangeExactKey(first) : void 0;
|
|
46
|
+
case "above": return isKey(first) ? rangeAboveKey(first) : void 0;
|
|
47
|
+
case "below": return isKey(first) ? rangeBelowKey(first) : void 0;
|
|
48
|
+
case "from": return isKey(first) ? rangeFromKey(first) : void 0;
|
|
49
|
+
case "to": return isKey(first) ? rangeToKey(first) : void 0;
|
|
50
|
+
case "between": return isKey(first) && isKey(second) && compareValues(first, second) <= 0 ? rangeBetweenKeys(first, second) : void 0;
|
|
50
51
|
case "not":
|
|
51
52
|
case "like":
|
|
52
53
|
case "glob":
|
|
@@ -55,14 +56,11 @@ function conditionRange(condition) {
|
|
|
55
56
|
case "any":
|
|
56
57
|
case "none":
|
|
57
58
|
case "absent":
|
|
58
|
-
case "present": return
|
|
59
|
+
case "present": return;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
function isKey(value) {
|
|
62
|
-
return typeof value === "string" || typeof value === "number";
|
|
63
|
-
}
|
|
64
62
|
/**
|
|
65
|
-
* Plan an IndexedDB read for a {@link
|
|
63
|
+
* Plan an IndexedDB read for a {@link QueryInput} — pick the index (or the primary
|
|
66
64
|
* store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
|
|
67
65
|
*
|
|
68
66
|
* @remarks
|
|
@@ -72,9 +70,9 @@ function isKey(value) {
|
|
|
72
70
|
* (a row can match through a later condition the range would exclude), so any `or`
|
|
73
71
|
* forces a full scan. Otherwise it scans the conditions in order and selects the
|
|
74
72
|
* **first** one that is provably range-exact and backed by a key: a comparison
|
|
75
|
-
* operator (`
|
|
76
|
-
* column that is either the table's primary key (read the store directly
|
|
77
|
-
*
|
|
73
|
+
* operator (`conditionToRange`) over a single, orderable (`text`/`integer`/`real`)
|
|
74
|
+
* column that is either the table's primary key (read the store directly with
|
|
75
|
+
* `index` omitted) or has a single-column secondary index (named exactly the column — read
|
|
78
76
|
* that index). A condition whose column is a nested {@link FieldPath} array
|
|
79
77
|
* (descends a json value, not a key), is absent from the schema, is a non-orderable
|
|
80
78
|
* type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
|
|
@@ -100,19 +98,19 @@ function isKey(value) {
|
|
|
100
98
|
* in the total order, so an absent/null-valued row can never satisfy them — the
|
|
101
99
|
* index's silence on such a row is harmless (it was never going to match).
|
|
102
100
|
* **Declared-type trust caveat:** this reasoning holds under the contract that
|
|
103
|
-
* an {@link
|
|
101
|
+
* an {@link INDEXABLE_STORAGE} column, once contract-validated at write time,
|
|
104
102
|
* holds only `string | number | null` (or is absent) — never some other
|
|
105
103
|
* runtime value that could rank differently; a driver bypassing the write
|
|
106
104
|
* contract (writing raw rows directly to the store) could defeat this
|
|
107
105
|
* argument, but that is out of scope for a planner reading validated schema
|
|
108
106
|
* metadata.
|
|
109
107
|
*
|
|
110
|
-
* When no condition qualifies the plan is a full scan (`{
|
|
111
|
-
*
|
|
108
|
+
* When no condition qualifies the plan is a full scan (`{}`) and the engine
|
|
109
|
+
* does everything. The plan is always a SUPERSET of the
|
|
112
110
|
* matching rows — the only correctness contract — so the driver may safely run
|
|
113
111
|
* the exact engine over it.
|
|
114
112
|
*
|
|
115
|
-
* @param
|
|
113
|
+
* @param input - The read specification (its `conditions` drive the plan), or
|
|
116
114
|
* `undefined` for an unconditional read
|
|
117
115
|
* @param schema - The table's schema — its `primary` key and column types
|
|
118
116
|
* @param available - The secondary-index names that physically exist on the store
|
|
@@ -121,37 +119,28 @@ function isKey(value) {
|
|
|
121
119
|
*
|
|
122
120
|
* @example
|
|
123
121
|
* ```ts
|
|
124
|
-
* selectPlan({ conditions: [eq('id', 'u1')] }, schema, []) // {
|
|
122
|
+
* selectPlan({ conditions: [eq('id', 'u1')] }, schema, []) // { range: only('u1') }
|
|
125
123
|
* selectPlan({ conditions: [from('age', 18)] }, schema, ['age']) // { index: 'age', range: from(18) }
|
|
126
|
-
* selectPlan({ conditions: [contains('name', 'a')] }, schema, []) // {
|
|
124
|
+
* selectPlan({ conditions: [contains('name', 'a')] }, schema, []) // {}
|
|
127
125
|
* ```
|
|
128
126
|
*/
|
|
129
|
-
function selectPlan(
|
|
130
|
-
const conditions =
|
|
131
|
-
if (conditions.slice(1).some((condition) => condition.connector === "or")) return {
|
|
132
|
-
index: null,
|
|
133
|
-
range: null
|
|
134
|
-
};
|
|
127
|
+
function selectPlan(input, schema, available) {
|
|
128
|
+
const conditions = input?.conditions ?? [];
|
|
129
|
+
if (conditions.slice(1).some((condition) => condition.connector === "or")) return {};
|
|
135
130
|
for (const condition of conditions) {
|
|
136
131
|
if (typeof condition.column !== "string") continue;
|
|
137
132
|
const column = schema.columns.find((candidate) => candidate.name === condition.column);
|
|
138
|
-
if (column === void 0 || !
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
if (condition.column === schema.primary) return {
|
|
142
|
-
index: null,
|
|
143
|
-
range: keyRange
|
|
144
|
-
};
|
|
133
|
+
if (column === void 0 || !INDEXABLE_STORAGE.has(column.storage)) continue;
|
|
134
|
+
const range = conditionToRange(condition);
|
|
135
|
+
if (range === void 0) continue;
|
|
136
|
+
if (condition.column === schema.primary) return { range };
|
|
145
137
|
if (condition.operator === "below" || condition.operator === "to") continue;
|
|
146
138
|
if (available.includes(condition.column)) return {
|
|
147
139
|
index: condition.column,
|
|
148
|
-
range
|
|
140
|
+
range
|
|
149
141
|
};
|
|
150
142
|
}
|
|
151
|
-
return {
|
|
152
|
-
index: null,
|
|
153
|
-
range: null
|
|
154
|
-
};
|
|
143
|
+
return {};
|
|
155
144
|
}
|
|
156
145
|
/**
|
|
157
146
|
* Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
|
|
@@ -162,17 +151,17 @@ function selectPlan(criteria, schema, available) {
|
|
|
162
151
|
* `CONSTRAINT` (a unique-key violation) is a `CONFLICT` — the same code every
|
|
163
152
|
* other backend uses for a duplicate key. `CLOSED`/`NOT_OPEN`/`INVALID` (the
|
|
164
153
|
* connection is gone, never opened, or the native handle is stale) collapse to
|
|
165
|
-
* `CLOSED`. `QUOTA`
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* `
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
154
|
+
* `CLOSED`. `QUOTA` is a genuine infrastructure fault (`DRIVER`) carrying a
|
|
155
|
+
* machine-readable `context.code` so a caller can branch without parsing the
|
|
156
|
+
* message. A blocked open or versionchange is nonterminal in the backend and
|
|
157
|
+
* remains pending until the competing connection closes, so it never reaches
|
|
158
|
+
* this error mapper. Every other code (`UPGRADE` here — see
|
|
159
|
+
* {@link mapMigrationError} for the `migrate()`-only remapping to `MIGRATION` —
|
|
160
|
+
* `ABORTED`, `NOT_FOUND`, `DATA`, `OPEN`, `INACTIVE`, `READONLY`, `UNKNOWN`) is
|
|
161
|
+
* an unexpected infrastructure fault and maps to `DRIVER` — the driver opens
|
|
162
|
+
* its own readwrite transactions, so a `READONLY` fault can only mean the
|
|
163
|
+
* backend behaved unexpectedly. The original error is always preserved as
|
|
164
|
+
* `context.cause` for diagnostics.
|
|
176
165
|
*
|
|
177
166
|
* @param error - The backend error to translate
|
|
178
167
|
* @returns The portable `DatabaseError`
|
|
@@ -187,11 +176,6 @@ function mapIndexedDBError(error) {
|
|
|
187
176
|
cause: error,
|
|
188
177
|
code: "QUOTA"
|
|
189
178
|
});
|
|
190
|
-
case "BLOCKED": return new DatabaseError("DRIVER", error.message, {
|
|
191
|
-
cause: error,
|
|
192
|
-
code: "BLOCKED",
|
|
193
|
-
retryable: true
|
|
194
|
-
});
|
|
195
179
|
case "UPGRADE":
|
|
196
180
|
case "ABORTED":
|
|
197
181
|
case "NOT_FOUND":
|
|
@@ -246,15 +230,30 @@ function mapMigrationError(error) {
|
|
|
246
230
|
*
|
|
247
231
|
* @example
|
|
248
232
|
* ```ts
|
|
249
|
-
*
|
|
250
|
-
*
|
|
233
|
+
* deriveIndexedDBIndexName(['age']) // 'age'
|
|
234
|
+
* deriveIndexedDBIndexName(['a', 'b']) // '2#1:a1:b'
|
|
251
235
|
* ```
|
|
252
236
|
*/
|
|
253
|
-
function
|
|
237
|
+
function deriveIndexedDBIndexName(columns) {
|
|
254
238
|
const [column] = columns;
|
|
255
239
|
if (columns.length === 1 && column !== void 0) return column;
|
|
256
240
|
return `${columns.length}#${columns.map((part) => `${part.length}:${part}`).join("")}`;
|
|
257
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Project a table schema into the IndexedDB wrapper's store definition.
|
|
244
|
+
*
|
|
245
|
+
* @param schema - Portable table schema
|
|
246
|
+
* @returns Store definition with declared indexes
|
|
247
|
+
*/
|
|
248
|
+
function schemaToStore(schema) {
|
|
249
|
+
return { indexes: schema.indexes.map((columns) => {
|
|
250
|
+
const [column] = columns;
|
|
251
|
+
return {
|
|
252
|
+
name: deriveIndexedDBIndexName(columns),
|
|
253
|
+
path: columns.length === 1 && column !== void 0 ? column : [...columns]
|
|
254
|
+
};
|
|
255
|
+
}) };
|
|
256
|
+
}
|
|
258
257
|
//#endregion
|
|
259
258
|
//#region src/browser/drivers/IndexedDBDriver.ts
|
|
260
259
|
/**
|
|
@@ -267,25 +266,27 @@ function deriveIndexName(columns) {
|
|
|
267
266
|
* / `snapshot`) by delegating to the wrapper's typed store operations — it never
|
|
268
267
|
* touches raw IndexedDB. Rows are stored with **out-of-line keys** (the database
|
|
269
268
|
* passes the key explicitly, `store.set(row, key)`), so each table is declared as a
|
|
270
|
-
* key-path-less store.
|
|
271
|
-
* version), creating
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
269
|
+
* key-path-less store. A fresh database opens in **auto-managed** mode (no fixed
|
|
270
|
+
* version), creating missing declared stores on demand. Once metadata is persisted,
|
|
271
|
+
* bootstrap captures the live stores and version, rejects a missing persisted store,
|
|
272
|
+
* and pins the final open to that version so a competing versionchange cannot
|
|
273
|
+
* silently recreate lost storage. The driver's bulk reads (`scan` / `keys`) use the
|
|
274
|
+
* wrapper's native `getAll` / `getAllKeys`, and `snapshot` rolls back through one
|
|
275
|
+
* atomic wrapper transaction.
|
|
275
276
|
*
|
|
276
|
-
* It also implements the optional native `records` / `
|
|
277
|
-
* (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link
|
|
277
|
+
* It also implements the optional native `records` / `stream` hooks
|
|
278
|
+
* (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
|
|
278
279
|
* key-range pushdown over the primary key or a single-column secondary index,
|
|
279
|
-
* fetching a candidate **superset** that the core engine (`
|
|
280
|
-
* `
|
|
280
|
+
* fetching a candidate **superset** that the core engine (`applyQuery` /
|
|
281
|
+
* `matchesQuery`) then refines — so a native read is byte-identical to a full
|
|
281
282
|
* scan, just cheaper. Pushdown is conservative: only the exact-comparison
|
|
282
283
|
* operators over orderable columns narrow to a range; everything else falls back
|
|
283
284
|
* to a full scan + the engine.
|
|
284
285
|
*
|
|
285
286
|
* @remarks
|
|
286
|
-
* This driver also implements `migrate` / `
|
|
287
|
-
* persist the {@link
|
|
288
|
-
* {@link
|
|
287
|
+
* This driver also implements `migrate` / `metadata` / `stamp`. `metadata` / `stamp`
|
|
288
|
+
* persist the {@link DriverMetadata} in a reserved out-of-line store,
|
|
289
|
+
* {@link METADATA_STORE} (`__metadata__`) — excluded from a whole-store `snapshot`
|
|
289
290
|
* capture, since it is driver bookkeeping, not caller data. `migrate` applies a
|
|
290
291
|
* {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
|
|
291
292
|
* store, creating/dropping an index) is legal only inside a versionchange
|
|
@@ -302,32 +303,67 @@ function deriveIndexName(columns) {
|
|
|
302
303
|
* This unit deliberately OMITS `aggregate` / `transaction`. There is no native
|
|
303
304
|
* `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
|
|
304
305
|
* `records` covers it. `transaction` is impossible here: the wrapper auto-commits
|
|
305
|
-
* an `IDBTransaction`
|
|
306
|
-
*
|
|
307
|
-
*
|
|
306
|
+
* an `IDBTransaction` when control yields outside its request chain, so arbitrary
|
|
307
|
+
* callback awaits cannot remain inside one native transaction. Every atomic
|
|
308
|
+
* multi-operation sequence in this driver
|
|
308
309
|
* (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.
|
|
309
310
|
*/
|
|
310
311
|
var IndexedDBDriver = class {
|
|
311
312
|
#name;
|
|
313
|
+
#identities = /* @__PURE__ */ new Map();
|
|
312
314
|
#schema = /* @__PURE__ */ new Map();
|
|
313
315
|
#database;
|
|
314
316
|
constructor(name) {
|
|
315
317
|
this.#name = name;
|
|
316
318
|
}
|
|
317
319
|
async open(schema) {
|
|
318
|
-
|
|
320
|
+
const owned = normalizeDriverSchema(schema);
|
|
321
|
+
if (owned.some((table) => table.name === "__metadata__")) throw new DatabaseError("VALIDATION", `open: table name '${METADATA_STORE}' is reserved for driver metadata`, { table: METADATA_STORE });
|
|
322
|
+
this.#database?.close();
|
|
323
|
+
this.#database = void 0;
|
|
324
|
+
this.#schema = /* @__PURE__ */ new Map();
|
|
319
325
|
try {
|
|
320
|
-
|
|
326
|
+
const bootstrap = createIndexedDBDatabase({
|
|
327
|
+
name: this.#name,
|
|
328
|
+
stores: { [METADATA_STORE]: {} }
|
|
329
|
+
});
|
|
330
|
+
let persisted;
|
|
331
|
+
let stores = [];
|
|
332
|
+
let version = 0;
|
|
333
|
+
try {
|
|
334
|
+
await bootstrap.connect();
|
|
335
|
+
stores = bootstrap.stores;
|
|
336
|
+
version = bootstrap.version;
|
|
337
|
+
persisted = await this.#load(bootstrap);
|
|
338
|
+
} finally {
|
|
339
|
+
bootstrap.close();
|
|
340
|
+
}
|
|
341
|
+
if (persisted !== void 0) {
|
|
342
|
+
for (const table of persisted.schema) if (!stores.includes(table.name)) throw new DatabaseError("DRIVER", "Stored IndexedDB store is missing", {
|
|
343
|
+
name: this.#name,
|
|
344
|
+
store: table.name,
|
|
345
|
+
aspect: "missing"
|
|
346
|
+
});
|
|
347
|
+
}
|
|
321
348
|
const map = /* @__PURE__ */ new Map();
|
|
322
|
-
for (const table of schema) map.set(table.name, table);
|
|
349
|
+
for (const table of normalizeDriverSchema(persisted?.schema ?? owned)) map.set(table.name, table);
|
|
323
350
|
const database = createIndexedDBDatabase({
|
|
324
351
|
name: this.#name,
|
|
352
|
+
...persisted === void 0 ? {} : { version },
|
|
325
353
|
stores: this.#stores(map)
|
|
326
354
|
});
|
|
327
|
-
|
|
328
|
-
|
|
355
|
+
try {
|
|
356
|
+
await database.connect();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
database.close();
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
const identities = this.#alignIdentities(map);
|
|
329
362
|
this.#schema = map;
|
|
363
|
+
this.#identities = identities;
|
|
364
|
+
this.#database = database;
|
|
330
365
|
} catch (error) {
|
|
366
|
+
this.#identities = /* @__PURE__ */ new Map();
|
|
331
367
|
throw this.#wrap(error);
|
|
332
368
|
}
|
|
333
369
|
}
|
|
@@ -342,26 +378,29 @@ var IndexedDBDriver = class {
|
|
|
342
378
|
throw this.#wrap(error);
|
|
343
379
|
}
|
|
344
380
|
}
|
|
345
|
-
async write(table, key, row) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
381
|
+
async write(table, key, row, options) {
|
|
382
|
+
const bound = bindRowKey(row, this.#table(table).primary, key);
|
|
383
|
+
await this.#mutate(table, options, async (store) => {
|
|
384
|
+
await store.set(bound, key);
|
|
385
|
+
});
|
|
351
386
|
}
|
|
352
|
-
async
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
387
|
+
async insert(table, key, row, options) {
|
|
388
|
+
const bound = bindRowKey(row, this.#table(table).primary, key);
|
|
389
|
+
await this.#mutate(table, options, async (store) => {
|
|
390
|
+
await store.add(bound, key);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async delete(table, key, options) {
|
|
394
|
+
let present = false;
|
|
395
|
+
await this.#mutate(table, options, async (store) => {
|
|
396
|
+
present = await store.has(key);
|
|
356
397
|
await store.remove(key);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
throw this.#wrap(error);
|
|
360
|
-
}
|
|
398
|
+
});
|
|
399
|
+
return present;
|
|
361
400
|
}
|
|
362
401
|
async keys(table) {
|
|
363
402
|
try {
|
|
364
|
-
return (await this.#store(table).keys()).filter(
|
|
403
|
+
return (await this.#store(table).keys()).filter(isKey);
|
|
365
404
|
} catch (error) {
|
|
366
405
|
throw this.#wrap(error);
|
|
367
406
|
}
|
|
@@ -380,42 +419,34 @@ var IndexedDBDriver = class {
|
|
|
380
419
|
throw this.#wrap(error);
|
|
381
420
|
}
|
|
382
421
|
}
|
|
383
|
-
async records(table,
|
|
422
|
+
async records(table, input) {
|
|
423
|
+
validatePage(input);
|
|
384
424
|
try {
|
|
385
425
|
const schema = this.#table(table);
|
|
386
426
|
const store = this.#store(table);
|
|
387
|
-
const plan = selectPlan(
|
|
388
|
-
return
|
|
427
|
+
const plan = selectPlan(input, schema, store.indexes);
|
|
428
|
+
return applyQuery(await this.#candidates(store, schema, plan), input);
|
|
389
429
|
} catch (error) {
|
|
390
430
|
throw this.#wrap(error);
|
|
391
431
|
}
|
|
392
432
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const store = this.#store(table);
|
|
397
|
-
const conditions = criteria.conditions ?? [];
|
|
398
|
-
if (conditions.length === 0) return await store.count();
|
|
399
|
-
const plan = selectPlan(criteria, schema, store.indexes);
|
|
400
|
-
if (conditions.length === 1 && plan.range !== null) return plan.index === null ? await store.count(plan.range) : await store.index(plan.index).count(plan.range);
|
|
401
|
-
return (plan.index === null ? await store.records(plan.range) : await store.index(plan.index).records(plan.range)).reduce((total, row) => matchesCriteria(row, conditions) ? total + 1 : total, 0);
|
|
402
|
-
} catch (error) {
|
|
403
|
-
throw this.#wrap(error);
|
|
404
|
-
}
|
|
433
|
+
stream(table, input) {
|
|
434
|
+
validatePage(input);
|
|
435
|
+
return this.#stream(table, input);
|
|
405
436
|
}
|
|
406
|
-
async
|
|
437
|
+
async *#stream(table, input) {
|
|
407
438
|
try {
|
|
408
439
|
const schema = this.#table(table);
|
|
409
440
|
const store = this.#store(table);
|
|
410
|
-
const plan = selectPlan(
|
|
411
|
-
const conditions =
|
|
412
|
-
const offset =
|
|
413
|
-
const limit =
|
|
441
|
+
const plan = selectPlan(input, schema, store.indexes);
|
|
442
|
+
const conditions = input.conditions ?? [];
|
|
443
|
+
const offset = input.offset ?? 0;
|
|
444
|
+
const limit = input.limit;
|
|
414
445
|
let skipped = 0;
|
|
415
446
|
let yielded = 0;
|
|
416
447
|
for (const row of await this.#candidates(store, schema, plan)) {
|
|
417
448
|
if (limit !== void 0 && yielded >= limit) break;
|
|
418
|
-
if (!
|
|
449
|
+
if (!matchesQuery(row, conditions)) continue;
|
|
419
450
|
if (skipped < offset) {
|
|
420
451
|
skipped += 1;
|
|
421
452
|
continue;
|
|
@@ -430,31 +461,57 @@ var IndexedDBDriver = class {
|
|
|
430
461
|
async snapshot(tables) {
|
|
431
462
|
try {
|
|
432
463
|
const database = this.#require();
|
|
433
|
-
const
|
|
464
|
+
const requested = tables ?? [...this.#schema.keys()];
|
|
465
|
+
const names = [...new Set(requested)].filter((name) => name !== "__metadata__" && this.#schema.has(name) && database.stores.includes(name));
|
|
434
466
|
const captured = /* @__PURE__ */ new Map();
|
|
435
467
|
if (names.length > 0) await database.read(names, async (transaction) => {
|
|
436
468
|
for (const name of names) {
|
|
469
|
+
const schema = this.#schema.get(name);
|
|
470
|
+
const identity = this.#identities.get(name);
|
|
471
|
+
if (schema === void 0 || identity === void 0) continue;
|
|
437
472
|
const store = transaction.store(name);
|
|
438
473
|
captured.set(name, {
|
|
474
|
+
identity,
|
|
439
475
|
keys: await store.keys(),
|
|
440
|
-
rows: await store.records()
|
|
476
|
+
rows: await store.records(),
|
|
477
|
+
schema
|
|
441
478
|
});
|
|
442
479
|
}
|
|
443
480
|
});
|
|
444
481
|
return async () => {
|
|
445
482
|
try {
|
|
446
483
|
const current = this.#require();
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
484
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
485
|
+
for (const [name, snapshot] of captured) {
|
|
486
|
+
const schema = this.#schema.get(name);
|
|
487
|
+
if (schema === void 0 || this.#identities.get(name) !== snapshot.identity || !current.stores.includes(name)) continue;
|
|
488
|
+
const plan = planMigration([snapshot.schema], [schema]);
|
|
489
|
+
const migrated = migrateRows(snapshot.rows, plan.steps);
|
|
490
|
+
if (snapshot.keys.length !== snapshot.rows.length || migrated.length !== snapshot.rows.length) throw new DatabaseError("MIGRATION", "IndexedDB snapshot keys and rows have different cardinality", { table: name });
|
|
491
|
+
const keys = [];
|
|
492
|
+
const rows = [];
|
|
493
|
+
for (const [index, key] of snapshot.keys.entries()) {
|
|
494
|
+
const row = migrated[index];
|
|
495
|
+
if (!isKey(key) || row === void 0) throw new DatabaseError("MIGRATION", "IndexedDB snapshot row has no usable primary key", {
|
|
496
|
+
table: name,
|
|
497
|
+
column: schema.primary,
|
|
498
|
+
index
|
|
499
|
+
});
|
|
500
|
+
keys.push(key);
|
|
501
|
+
rows.push(bindRowKey(row, schema.primary, key));
|
|
502
|
+
}
|
|
503
|
+
replacements.set(name, {
|
|
504
|
+
keys,
|
|
505
|
+
rows
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (replacements.size === 0) return;
|
|
509
|
+
await current.write([...replacements.keys()], async (transaction) => {
|
|
510
|
+
for (const [name, replacement] of replacements) {
|
|
453
511
|
const store = transaction.store(name);
|
|
454
512
|
await store.clear();
|
|
455
|
-
for (
|
|
456
|
-
const row =
|
|
457
|
-
const key = snapshot.keys[index];
|
|
513
|
+
for (const [index, key] of replacement.keys.entries()) {
|
|
514
|
+
const row = replacement.rows[index];
|
|
458
515
|
if (row === void 0 || key === void 0) throw new DatabaseError("DRIVER", "IndexedDB snapshot entry is incomplete", {
|
|
459
516
|
table: name,
|
|
460
517
|
index
|
|
@@ -472,34 +529,37 @@ var IndexedDBDriver = class {
|
|
|
472
529
|
}
|
|
473
530
|
}
|
|
474
531
|
/**
|
|
475
|
-
* Return the persisted {@link
|
|
532
|
+
* Return the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
476
533
|
* never been stamped.
|
|
477
534
|
*
|
|
478
535
|
* @remarks
|
|
479
|
-
* Reads `'
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
536
|
+
* Reads `'metadata'` from the reserved {@link METADATA_STORE} in one readonly
|
|
537
|
+
* transaction that distinguishes key absence from a present `undefined`
|
|
538
|
+
* value. Only absence returns `undefined`; present malformed state fails
|
|
539
|
+
* closed with a payload-safe `DRIVER` error.
|
|
483
540
|
*
|
|
484
|
-
* @returns The last-stamped {@link
|
|
541
|
+
* @returns The last-stamped {@link DriverMetadata}, or `undefined`
|
|
485
542
|
*/
|
|
486
|
-
async
|
|
543
|
+
async metadata() {
|
|
487
544
|
try {
|
|
488
|
-
|
|
489
|
-
if (!isDriverMeta(record)) return void 0;
|
|
490
|
-
return record;
|
|
545
|
+
return await this.#load(this.#require());
|
|
491
546
|
} catch (error) {
|
|
492
547
|
throw this.#wrap(error);
|
|
493
548
|
}
|
|
494
549
|
}
|
|
495
550
|
/**
|
|
496
|
-
* Persist
|
|
551
|
+
* Persist an owned metadata snapshot for a later `metadata()` to return.
|
|
497
552
|
*
|
|
498
|
-
* @param
|
|
553
|
+
* @param metadata - The {@link DriverMetadata} to persist
|
|
499
554
|
*/
|
|
500
|
-
async stamp(
|
|
555
|
+
async stamp(metadata) {
|
|
556
|
+
const database = this.#require();
|
|
557
|
+
const owned = cloneDriverMetadata(metadata);
|
|
501
558
|
try {
|
|
502
|
-
await
|
|
559
|
+
await database.store(METADATA_STORE).set({
|
|
560
|
+
version: owned.version,
|
|
561
|
+
schema: owned.schema
|
|
562
|
+
}, "metadata");
|
|
503
563
|
} catch (error) {
|
|
504
564
|
throw this.#wrap(error);
|
|
505
565
|
}
|
|
@@ -511,7 +571,7 @@ var IndexedDBDriver = class {
|
|
|
511
571
|
* @remarks
|
|
512
572
|
* IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
|
|
513
573
|
* the current connection and opens a FRESH one at `version + 1`, declaring
|
|
514
|
-
* every currently-known store (plus {@link
|
|
574
|
+
* every currently-known store (plus {@link METADATA_STORE}) so nothing is lost,
|
|
515
575
|
* and applying `table.remove` / `index.add` / `index.remove` /
|
|
516
576
|
* `column.remove` inside `upgrade`. Every step's `table` is validated against
|
|
517
577
|
* the driver's own `#schema` BEFORE the reconnect — an unknown-table step
|
|
@@ -524,49 +584,154 @@ var IndexedDBDriver = class {
|
|
|
524
584
|
* `open` tracks, so subsequent pushdown planning and a later `migrate` /
|
|
525
585
|
* `open` see the new shape.
|
|
526
586
|
*
|
|
527
|
-
* @param
|
|
587
|
+
* @param input - The migration plan and optional metadata stamp to apply atomically
|
|
528
588
|
*/
|
|
529
|
-
async migrate(
|
|
530
|
-
for (const step of plan.steps) if (step.operation !== "table.add" && !this.#schema.has(step.table)) throw new DatabaseError("MIGRATION", `migrate: unknown table '${step.table}'`, { table: step.table });
|
|
589
|
+
async migrate(input) {
|
|
531
590
|
const current = this.#require();
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
591
|
+
const owned = cloneMigrationInput(input);
|
|
592
|
+
const projected = projectMigrationSchema([...this.#schema.values()], owned.plan.steps);
|
|
593
|
+
if (owned.metadata !== void 0 && !equalsValue(normalizeDriverSchema(owned.metadata.schema), projected)) throw new DatabaseError("MIGRATION", "Migration metadata schema does not match the plan", {
|
|
594
|
+
projected,
|
|
595
|
+
metadata: owned.metadata.schema
|
|
596
|
+
});
|
|
597
|
+
const schema = new Map(projected.map((table) => [table.name, table]));
|
|
598
|
+
const identities = this.#projectIdentities(this.#identities, owned.plan.steps);
|
|
599
|
+
if (owned.plan.steps.length === 0) {
|
|
600
|
+
const metadata = owned.metadata;
|
|
601
|
+
if (metadata !== void 0) try {
|
|
602
|
+
await current.write(METADATA_STORE, async (transaction) => {
|
|
603
|
+
await transaction.store(METADATA_STORE).set({
|
|
604
|
+
version: metadata.version,
|
|
605
|
+
schema: metadata.schema
|
|
606
|
+
}, "metadata");
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
throw this.#wrap(error);
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
536
613
|
try {
|
|
614
|
+
await current.connect();
|
|
615
|
+
const version = current.version;
|
|
616
|
+
current.close();
|
|
617
|
+
this.#database = void 0;
|
|
618
|
+
const added = new Set([...schema.keys()].filter((name) => !this.#schema.has(name)));
|
|
537
619
|
const database = createIndexedDBDatabase({
|
|
538
620
|
name: this.#name,
|
|
539
621
|
version: version + 1,
|
|
540
622
|
stores: this.#stores(schema),
|
|
541
|
-
upgrade: this.#upgrade.bind(this,
|
|
623
|
+
upgrade: this.#upgrade.bind(this, owned, added)
|
|
542
624
|
});
|
|
543
|
-
|
|
625
|
+
try {
|
|
626
|
+
await database.connect();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
database.close();
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
544
631
|
this.#database = database;
|
|
545
632
|
this.#schema = schema;
|
|
633
|
+
this.#identities = identities;
|
|
546
634
|
} catch (error) {
|
|
547
|
-
|
|
548
|
-
|
|
635
|
+
current.close();
|
|
636
|
+
this.#database = void 0;
|
|
637
|
+
const cause = this.#migrationError(error);
|
|
638
|
+
try {
|
|
639
|
+
await this.#reopen();
|
|
640
|
+
} catch (recoveryError) {
|
|
641
|
+
this.#database = void 0;
|
|
642
|
+
throw new DatabaseError("DRIVER", "IndexedDB migration and recovery failed", {
|
|
643
|
+
cause,
|
|
644
|
+
recovery: this.#recoveryError(recoveryError)
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
throw cause;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async #mutate(table, options, scope) {
|
|
651
|
+
const signal = options?.signal;
|
|
652
|
+
checkAbort(signal);
|
|
653
|
+
const cleanup = new AbortController();
|
|
654
|
+
let aborted = false;
|
|
655
|
+
try {
|
|
656
|
+
await this.#require().write(table, async (transaction) => {
|
|
657
|
+
checkAbort(signal);
|
|
658
|
+
signal?.addEventListener("abort", () => {
|
|
659
|
+
if (!transaction.active) return;
|
|
660
|
+
try {
|
|
661
|
+
transaction.abort();
|
|
662
|
+
aborted = true;
|
|
663
|
+
} catch {}
|
|
664
|
+
}, {
|
|
665
|
+
once: true,
|
|
666
|
+
signal: cleanup.signal
|
|
667
|
+
});
|
|
668
|
+
checkAbort(signal);
|
|
669
|
+
await scope(transaction.store(table));
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (aborted) checkAbort(signal);
|
|
673
|
+
throw this.#wrap(error);
|
|
674
|
+
} finally {
|
|
675
|
+
cleanup.abort();
|
|
549
676
|
}
|
|
550
677
|
}
|
|
551
678
|
#require() {
|
|
552
679
|
if (this.#database === void 0) throw new DatabaseError("CLOSED", `IndexedDB database '${this.#name}' is not open`, { name: this.#name });
|
|
553
680
|
return this.#database;
|
|
554
681
|
}
|
|
682
|
+
async #load(database) {
|
|
683
|
+
let present = false;
|
|
684
|
+
let value;
|
|
685
|
+
await database.read(METADATA_STORE, async (transaction) => {
|
|
686
|
+
const store = transaction.store(METADATA_STORE);
|
|
687
|
+
present = await store.has("metadata");
|
|
688
|
+
if (present) value = await store.get("metadata");
|
|
689
|
+
});
|
|
690
|
+
if (!present) return void 0;
|
|
691
|
+
try {
|
|
692
|
+
return cloneDriverMetadata(value);
|
|
693
|
+
} catch {
|
|
694
|
+
const cause = new DatabaseError("VALIDATION", "Stored IndexedDB metadata failed validation", { path: "metadata" });
|
|
695
|
+
throw new DatabaseError("DRIVER", "Stored IndexedDB metadata is invalid", {
|
|
696
|
+
name: this.#name,
|
|
697
|
+
store: METADATA_STORE,
|
|
698
|
+
key: "metadata",
|
|
699
|
+
cause
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
#migrationError(error) {
|
|
704
|
+
if (isDatabaseError(error)) return error;
|
|
705
|
+
if (isIndexedDBError(error)) return mapMigrationError(error);
|
|
706
|
+
return new DatabaseError("DRIVER", "IndexedDB migration failed", { cause: error });
|
|
707
|
+
}
|
|
708
|
+
#recoveryError(error) {
|
|
709
|
+
if (isDatabaseError(error)) return error;
|
|
710
|
+
if (isIndexedDBError(error)) return mapIndexedDBError(error);
|
|
711
|
+
return new DatabaseError("DRIVER", "IndexedDB recovery failed", { cause: error });
|
|
712
|
+
}
|
|
555
713
|
#wrap(error) {
|
|
556
714
|
return isIndexedDBError(error) ? mapIndexedDBError(error) : error;
|
|
557
715
|
}
|
|
558
716
|
#store(table) {
|
|
559
717
|
return this.#require().store(table);
|
|
560
718
|
}
|
|
719
|
+
#alignIdentities(schema) {
|
|
720
|
+
const aligned = /* @__PURE__ */ new Map();
|
|
721
|
+
for (const table of schema.values()) aligned.set(table.name, this.#identities.get(table.name) ?? {});
|
|
722
|
+
return aligned;
|
|
723
|
+
}
|
|
724
|
+
#projectIdentities(identities, steps) {
|
|
725
|
+
const projected = new Map(identities);
|
|
726
|
+
for (const step of steps) {
|
|
727
|
+
if (step.operation === "table.add") projected.set(step.table.name, {});
|
|
728
|
+
if (step.operation === "table.remove") projected.delete(step.table);
|
|
729
|
+
}
|
|
730
|
+
return projected;
|
|
731
|
+
}
|
|
561
732
|
#stores(schema) {
|
|
562
|
-
const stores = { [
|
|
563
|
-
for (const table of schema.values()) stores[table.name] =
|
|
564
|
-
const [column] = columns;
|
|
565
|
-
return {
|
|
566
|
-
name: deriveIndexName(columns),
|
|
567
|
-
path: columns.length === 1 && column !== void 0 ? column : [...columns]
|
|
568
|
-
};
|
|
569
|
-
}) };
|
|
733
|
+
const stores = { [METADATA_STORE]: {} };
|
|
734
|
+
for (const table of schema.values()) stores[table.name] = schemaToStore(table);
|
|
570
735
|
return stores;
|
|
571
736
|
}
|
|
572
737
|
async #reopen() {
|
|
@@ -574,11 +739,16 @@ var IndexedDBDriver = class {
|
|
|
574
739
|
name: this.#name,
|
|
575
740
|
stores: this.#stores(this.#schema)
|
|
576
741
|
});
|
|
577
|
-
|
|
578
|
-
|
|
742
|
+
try {
|
|
743
|
+
await database.connect();
|
|
744
|
+
this.#database = database;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
database.close();
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
579
749
|
}
|
|
580
750
|
async #candidates(store, schema, plan) {
|
|
581
|
-
if (plan.index ===
|
|
751
|
+
if (plan.index === void 0) return store.records(plan.range);
|
|
582
752
|
const rows = [...await store.index(plan.index).records(plan.range)];
|
|
583
753
|
rows.sort((left, right) => compareValues(extractKey(left, schema.primary), extractKey(right, schema.primary)));
|
|
584
754
|
return rows;
|
|
@@ -588,62 +758,27 @@ var IndexedDBDriver = class {
|
|
|
588
758
|
if (schema === void 0) throw new DatabaseError("NOT_FOUND", `table '${name}' is not declared`, { table: name });
|
|
589
759
|
return schema;
|
|
590
760
|
}
|
|
591
|
-
#
|
|
592
|
-
for (const
|
|
761
|
+
async #upgrade(input, added, context) {
|
|
762
|
+
for (const name of added) if (name !== "__metadata__" && context.stores.includes(name)) context.drop(name);
|
|
763
|
+
for (const step of input.plan.steps) switch (step.operation) {
|
|
593
764
|
case "table.add":
|
|
594
|
-
|
|
765
|
+
context.create(step.table.name, schemaToStore(step.table));
|
|
595
766
|
break;
|
|
596
|
-
case "table.remove":
|
|
597
|
-
schema.delete(step.table);
|
|
598
|
-
break;
|
|
599
|
-
case "column.add": {
|
|
600
|
-
const table = schema.get(step.table);
|
|
601
|
-
if (table !== void 0 && !table.columns.some((column) => column.name === step.column.name)) schema.set(step.table, {
|
|
602
|
-
...table,
|
|
603
|
-
columns: [...table.columns, step.column]
|
|
604
|
-
});
|
|
605
|
-
break;
|
|
606
|
-
}
|
|
607
|
-
case "column.remove": {
|
|
608
|
-
const table = schema.get(step.table);
|
|
609
|
-
if (table !== void 0) schema.set(step.table, {
|
|
610
|
-
...table,
|
|
611
|
-
columns: table.columns.filter((column) => column.name !== step.column)
|
|
612
|
-
});
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
case "index.add": {
|
|
616
|
-
const table = schema.get(step.table);
|
|
617
|
-
if (table !== void 0) schema.set(step.table, {
|
|
618
|
-
...table,
|
|
619
|
-
indexes: [...table.indexes, step.index]
|
|
620
|
-
});
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
case "index.remove": {
|
|
624
|
-
const table = schema.get(step.table);
|
|
625
|
-
if (table !== void 0) schema.set(step.table, {
|
|
626
|
-
...table,
|
|
627
|
-
indexes: table.indexes.filter((index) => !deepEqual(index, step.index))
|
|
628
|
-
});
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
async #upgrade(steps, context) {
|
|
634
|
-
for (const step of steps) switch (step.operation) {
|
|
635
767
|
case "table.remove":
|
|
636
768
|
context.drop(step.table);
|
|
637
769
|
break;
|
|
638
770
|
case "index.add": {
|
|
639
|
-
const name =
|
|
771
|
+
const name = deriveIndexedDBIndexName(step.index);
|
|
640
772
|
const [column] = step.index;
|
|
641
773
|
const path = step.index.length === 1 && column !== void 0 ? column : [...step.index];
|
|
642
|
-
context.
|
|
774
|
+
context.index(step.table, {
|
|
775
|
+
name,
|
|
776
|
+
path
|
|
777
|
+
});
|
|
643
778
|
break;
|
|
644
779
|
}
|
|
645
780
|
case "index.remove":
|
|
646
|
-
context.
|
|
781
|
+
context.deindex(step.table, deriveIndexedDBIndexName(step.index));
|
|
647
782
|
break;
|
|
648
783
|
case "column.remove": {
|
|
649
784
|
let cursor = await context.store(step.table).cursor();
|
|
@@ -655,9 +790,11 @@ var IndexedDBDriver = class {
|
|
|
655
790
|
}
|
|
656
791
|
break;
|
|
657
792
|
}
|
|
658
|
-
case "table.add":
|
|
659
|
-
case "column.add": break;
|
|
660
793
|
}
|
|
794
|
+
if (input.metadata !== void 0) await context.store(METADATA_STORE).set({
|
|
795
|
+
version: input.metadata.version,
|
|
796
|
+
schema: input.metadata.schema
|
|
797
|
+
}, "metadata");
|
|
661
798
|
}
|
|
662
799
|
};
|
|
663
800
|
//#endregion
|
|
@@ -666,9 +803,9 @@ var IndexedDBDriver = class {
|
|
|
666
803
|
* Create a persistent IndexedDB {@link DriverInterface} for the core database layer.
|
|
667
804
|
*
|
|
668
805
|
* @remarks
|
|
669
|
-
* Pass it to `createDatabase` from `@orkestrel/database` to run the
|
|
670
|
-
*
|
|
671
|
-
*
|
|
806
|
+
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed database
|
|
807
|
+
* layer against IndexedDB instead of memory — the `Database` / `Table` / `Query`
|
|
808
|
+
* API is unchanged; only where the bytes live changes. The
|
|
672
809
|
* driver is built on the published `@orkestrel/indexeddb` wrapper in auto-managed
|
|
673
810
|
* mode, so a table added to the `tables` map is created on the next open with no
|
|
674
811
|
* version bump. This unit omits `transaction` / `aggregate` (see
|
|
@@ -694,6 +831,6 @@ function createIndexedDBDriver(name) {
|
|
|
694
831
|
return new IndexedDBDriver(name);
|
|
695
832
|
}
|
|
696
833
|
//#endregion
|
|
697
|
-
export {
|
|
834
|
+
export { INDEXABLE_STORAGE, IndexedDBDriver, METADATA_STORE, conditionToRange, createIndexedDBDriver, deriveIndexedDBIndexName, mapIndexedDBError, mapMigrationError, schemaToStore, selectPlan };
|
|
698
835
|
|
|
699
836
|
//# sourceMappingURL=index.js.map
|