@orkestrel/database 0.0.5 → 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 +100 -87
- package/dist/src/browser/index.js +369 -219
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +2389 -1244
- 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 +2368 -1231
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1620 -827
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +466 -571
- package/dist/src/server/index.d.ts +466 -571
- package/dist/src/server/index.js +1603 -811
- package/dist/src/server/index.js.map +1 -1
- package/package.json +21 -19
|
@@ -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,13 +230,29 @@ 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
|
|
254
|
-
|
|
255
|
-
|
|
237
|
+
function deriveIndexedDBIndexName(columns) {
|
|
238
|
+
const [column] = columns;
|
|
239
|
+
if (columns.length === 1 && column !== void 0) return column;
|
|
240
|
+
return `${columns.length}#${columns.map((part) => `${part.length}:${part}`).join("")}`;
|
|
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
256
|
}
|
|
257
257
|
//#endregion
|
|
258
258
|
//#region src/browser/drivers/IndexedDBDriver.ts
|
|
@@ -266,25 +266,27 @@ function deriveIndexName(columns) {
|
|
|
266
266
|
* / `snapshot`) by delegating to the wrapper's typed store operations — it never
|
|
267
267
|
* touches raw IndexedDB. Rows are stored with **out-of-line keys** (the database
|
|
268
268
|
* passes the key explicitly, `store.set(row, key)`), so each table is declared as a
|
|
269
|
-
* key-path-less store.
|
|
270
|
-
* version), creating
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
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.
|
|
274
276
|
*
|
|
275
|
-
* It also implements the optional native `records` / `
|
|
276
|
-
* (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
|
|
277
279
|
* key-range pushdown over the primary key or a single-column secondary index,
|
|
278
|
-
* fetching a candidate **superset** that the core engine (`
|
|
279
|
-
* `
|
|
280
|
+
* fetching a candidate **superset** that the core engine (`applyQuery` /
|
|
281
|
+
* `matchesQuery`) then refines — so a native read is byte-identical to a full
|
|
280
282
|
* scan, just cheaper. Pushdown is conservative: only the exact-comparison
|
|
281
283
|
* operators over orderable columns narrow to a range; everything else falls back
|
|
282
284
|
* to a full scan + the engine.
|
|
283
285
|
*
|
|
284
286
|
* @remarks
|
|
285
|
-
* This driver also implements `migrate` / `
|
|
286
|
-
* persist the {@link
|
|
287
|
-
* {@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`
|
|
288
290
|
* capture, since it is driver bookkeeping, not caller data. `migrate` applies a
|
|
289
291
|
* {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
|
|
290
292
|
* store, creating/dropping an index) is legal only inside a versionchange
|
|
@@ -301,32 +303,67 @@ function deriveIndexName(columns) {
|
|
|
301
303
|
* This unit deliberately OMITS `aggregate` / `transaction`. There is no native
|
|
302
304
|
* `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
|
|
303
305
|
* `records` covers it. `transaction` is impossible here: the wrapper auto-commits
|
|
304
|
-
* an `IDBTransaction`
|
|
305
|
-
*
|
|
306
|
-
*
|
|
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
|
|
307
309
|
* (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.
|
|
308
310
|
*/
|
|
309
311
|
var IndexedDBDriver = class {
|
|
310
312
|
#name;
|
|
313
|
+
#identities = /* @__PURE__ */ new Map();
|
|
311
314
|
#schema = /* @__PURE__ */ new Map();
|
|
312
315
|
#database;
|
|
313
316
|
constructor(name) {
|
|
314
317
|
this.#name = name;
|
|
315
318
|
}
|
|
316
319
|
async open(schema) {
|
|
317
|
-
|
|
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();
|
|
318
325
|
try {
|
|
319
|
-
|
|
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
|
+
}
|
|
320
348
|
const map = /* @__PURE__ */ new Map();
|
|
321
|
-
for (const table of schema) map.set(table.name, table);
|
|
349
|
+
for (const table of normalizeDriverSchema(persisted?.schema ?? owned)) map.set(table.name, table);
|
|
322
350
|
const database = createIndexedDBDatabase({
|
|
323
351
|
name: this.#name,
|
|
352
|
+
...persisted === void 0 ? {} : { version },
|
|
324
353
|
stores: this.#stores(map)
|
|
325
354
|
});
|
|
326
|
-
|
|
327
|
-
|
|
355
|
+
try {
|
|
356
|
+
await database.connect();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
database.close();
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
const identities = this.#alignIdentities(map);
|
|
328
362
|
this.#schema = map;
|
|
363
|
+
this.#identities = identities;
|
|
364
|
+
this.#database = database;
|
|
329
365
|
} catch (error) {
|
|
366
|
+
this.#identities = /* @__PURE__ */ new Map();
|
|
330
367
|
throw this.#wrap(error);
|
|
331
368
|
}
|
|
332
369
|
}
|
|
@@ -341,26 +378,29 @@ var IndexedDBDriver = class {
|
|
|
341
378
|
throw this.#wrap(error);
|
|
342
379
|
}
|
|
343
380
|
}
|
|
344
|
-
async write(table, key, row) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
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
|
+
});
|
|
350
386
|
}
|
|
351
|
-
async
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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);
|
|
355
397
|
await store.remove(key);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
throw this.#wrap(error);
|
|
359
|
-
}
|
|
398
|
+
});
|
|
399
|
+
return present;
|
|
360
400
|
}
|
|
361
401
|
async keys(table) {
|
|
362
402
|
try {
|
|
363
|
-
return (await this.#store(table).keys()).filter(
|
|
403
|
+
return (await this.#store(table).keys()).filter(isKey);
|
|
364
404
|
} catch (error) {
|
|
365
405
|
throw this.#wrap(error);
|
|
366
406
|
}
|
|
@@ -379,42 +419,34 @@ var IndexedDBDriver = class {
|
|
|
379
419
|
throw this.#wrap(error);
|
|
380
420
|
}
|
|
381
421
|
}
|
|
382
|
-
async records(table,
|
|
422
|
+
async records(table, input) {
|
|
423
|
+
validatePage(input);
|
|
383
424
|
try {
|
|
384
425
|
const schema = this.#table(table);
|
|
385
426
|
const store = this.#store(table);
|
|
386
|
-
const plan = selectPlan(
|
|
387
|
-
return
|
|
427
|
+
const plan = selectPlan(input, schema, store.indexes);
|
|
428
|
+
return applyQuery(await this.#candidates(store, schema, plan), input);
|
|
388
429
|
} catch (error) {
|
|
389
430
|
throw this.#wrap(error);
|
|
390
431
|
}
|
|
391
432
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const store = this.#store(table);
|
|
396
|
-
const conditions = criteria.conditions ?? [];
|
|
397
|
-
if (conditions.length === 0) return await store.count();
|
|
398
|
-
const plan = selectPlan(criteria, schema, store.indexes);
|
|
399
|
-
if (conditions.length === 1 && plan.range !== null) return plan.index === null ? await store.count(plan.range) : await store.index(plan.index).count(plan.range);
|
|
400
|
-
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);
|
|
401
|
-
} catch (error) {
|
|
402
|
-
throw this.#wrap(error);
|
|
403
|
-
}
|
|
433
|
+
stream(table, input) {
|
|
434
|
+
validatePage(input);
|
|
435
|
+
return this.#stream(table, input);
|
|
404
436
|
}
|
|
405
|
-
async
|
|
437
|
+
async *#stream(table, input) {
|
|
406
438
|
try {
|
|
407
439
|
const schema = this.#table(table);
|
|
408
440
|
const store = this.#store(table);
|
|
409
|
-
const plan = selectPlan(
|
|
410
|
-
const conditions =
|
|
411
|
-
const offset =
|
|
412
|
-
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;
|
|
413
445
|
let skipped = 0;
|
|
414
446
|
let yielded = 0;
|
|
415
447
|
for (const row of await this.#candidates(store, schema, plan)) {
|
|
416
448
|
if (limit !== void 0 && yielded >= limit) break;
|
|
417
|
-
if (!
|
|
449
|
+
if (!matchesQuery(row, conditions)) continue;
|
|
418
450
|
if (skipped < offset) {
|
|
419
451
|
skipped += 1;
|
|
420
452
|
continue;
|
|
@@ -429,29 +461,63 @@ var IndexedDBDriver = class {
|
|
|
429
461
|
async snapshot(tables) {
|
|
430
462
|
try {
|
|
431
463
|
const database = this.#require();
|
|
432
|
-
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));
|
|
433
466
|
const captured = /* @__PURE__ */ new Map();
|
|
434
467
|
if (names.length > 0) await database.read(names, async (transaction) => {
|
|
435
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;
|
|
436
472
|
const store = transaction.store(name);
|
|
437
473
|
captured.set(name, {
|
|
474
|
+
identity,
|
|
438
475
|
keys: await store.keys(),
|
|
439
|
-
rows: await store.records()
|
|
476
|
+
rows: await store.records(),
|
|
477
|
+
schema
|
|
440
478
|
});
|
|
441
479
|
}
|
|
442
480
|
});
|
|
443
481
|
return async () => {
|
|
444
482
|
try {
|
|
445
483
|
const current = this.#require();
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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) {
|
|
452
511
|
const store = transaction.store(name);
|
|
453
512
|
await store.clear();
|
|
454
|
-
for (
|
|
513
|
+
for (const [index, key] of replacement.keys.entries()) {
|
|
514
|
+
const row = replacement.rows[index];
|
|
515
|
+
if (row === void 0 || key === void 0) throw new DatabaseError("DRIVER", "IndexedDB snapshot entry is incomplete", {
|
|
516
|
+
table: name,
|
|
517
|
+
index
|
|
518
|
+
});
|
|
519
|
+
await store.set(row, key);
|
|
520
|
+
}
|
|
455
521
|
}
|
|
456
522
|
});
|
|
457
523
|
} catch (error) {
|
|
@@ -463,34 +529,37 @@ var IndexedDBDriver = class {
|
|
|
463
529
|
}
|
|
464
530
|
}
|
|
465
531
|
/**
|
|
466
|
-
* Return the persisted {@link
|
|
532
|
+
* Return the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
467
533
|
* never been stamped.
|
|
468
534
|
*
|
|
469
535
|
* @remarks
|
|
470
|
-
* Reads `'
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
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.
|
|
474
540
|
*
|
|
475
|
-
* @returns The last-stamped {@link
|
|
541
|
+
* @returns The last-stamped {@link DriverMetadata}, or `undefined`
|
|
476
542
|
*/
|
|
477
|
-
async
|
|
543
|
+
async metadata() {
|
|
478
544
|
try {
|
|
479
|
-
|
|
480
|
-
if (!isDriverMeta(record)) return void 0;
|
|
481
|
-
return record;
|
|
545
|
+
return await this.#load(this.#require());
|
|
482
546
|
} catch (error) {
|
|
483
547
|
throw this.#wrap(error);
|
|
484
548
|
}
|
|
485
549
|
}
|
|
486
550
|
/**
|
|
487
|
-
* Persist
|
|
551
|
+
* Persist an owned metadata snapshot for a later `metadata()` to return.
|
|
488
552
|
*
|
|
489
|
-
* @param
|
|
553
|
+
* @param metadata - The {@link DriverMetadata} to persist
|
|
490
554
|
*/
|
|
491
|
-
async stamp(
|
|
555
|
+
async stamp(metadata) {
|
|
556
|
+
const database = this.#require();
|
|
557
|
+
const owned = cloneDriverMetadata(metadata);
|
|
492
558
|
try {
|
|
493
|
-
await
|
|
559
|
+
await database.store(METADATA_STORE).set({
|
|
560
|
+
version: owned.version,
|
|
561
|
+
schema: owned.schema
|
|
562
|
+
}, "metadata");
|
|
494
563
|
} catch (error) {
|
|
495
564
|
throw this.#wrap(error);
|
|
496
565
|
}
|
|
@@ -502,7 +571,7 @@ var IndexedDBDriver = class {
|
|
|
502
571
|
* @remarks
|
|
503
572
|
* IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
|
|
504
573
|
* the current connection and opens a FRESH one at `version + 1`, declaring
|
|
505
|
-
* every currently-known store (plus {@link
|
|
574
|
+
* every currently-known store (plus {@link METADATA_STORE}) so nothing is lost,
|
|
506
575
|
* and applying `table.remove` / `index.add` / `index.remove` /
|
|
507
576
|
* `column.remove` inside `upgrade`. Every step's `table` is validated against
|
|
508
577
|
* the driver's own `#schema` BEFORE the reconnect — an unknown-table step
|
|
@@ -515,46 +584,154 @@ var IndexedDBDriver = class {
|
|
|
515
584
|
* `open` tracks, so subsequent pushdown planning and a later `migrate` /
|
|
516
585
|
* `open` see the new shape.
|
|
517
586
|
*
|
|
518
|
-
* @param
|
|
587
|
+
* @param input - The migration plan and optional metadata stamp to apply atomically
|
|
519
588
|
*/
|
|
520
|
-
async migrate(
|
|
521
|
-
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) {
|
|
522
590
|
const current = this.#require();
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
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)));
|
|
528
619
|
const database = createIndexedDBDatabase({
|
|
529
620
|
name: this.#name,
|
|
530
621
|
version: version + 1,
|
|
531
622
|
stores: this.#stores(schema),
|
|
532
|
-
upgrade:
|
|
623
|
+
upgrade: this.#upgrade.bind(this, owned, added)
|
|
533
624
|
});
|
|
534
|
-
|
|
625
|
+
try {
|
|
626
|
+
await database.connect();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
database.close();
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
535
631
|
this.#database = database;
|
|
536
632
|
this.#schema = schema;
|
|
633
|
+
this.#identities = identities;
|
|
537
634
|
} catch (error) {
|
|
538
|
-
|
|
539
|
-
|
|
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();
|
|
540
676
|
}
|
|
541
677
|
}
|
|
542
678
|
#require() {
|
|
543
679
|
if (this.#database === void 0) throw new DatabaseError("CLOSED", `IndexedDB database '${this.#name}' is not open`, { name: this.#name });
|
|
544
680
|
return this.#database;
|
|
545
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
|
+
}
|
|
546
713
|
#wrap(error) {
|
|
547
714
|
return isIndexedDBError(error) ? mapIndexedDBError(error) : error;
|
|
548
715
|
}
|
|
549
716
|
#store(table) {
|
|
550
717
|
return this.#require().store(table);
|
|
551
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
|
+
}
|
|
552
732
|
#stores(schema) {
|
|
553
|
-
const stores = { [
|
|
554
|
-
for (const table of schema.values()) stores[table.name] =
|
|
555
|
-
name: deriveIndexName(columns),
|
|
556
|
-
path: columns.length === 1 ? columns[0] : [...columns]
|
|
557
|
-
})) };
|
|
733
|
+
const stores = { [METADATA_STORE]: {} };
|
|
734
|
+
for (const table of schema.values()) stores[table.name] = schemaToStore(table);
|
|
558
735
|
return stores;
|
|
559
736
|
}
|
|
560
737
|
async #reopen() {
|
|
@@ -562,11 +739,16 @@ var IndexedDBDriver = class {
|
|
|
562
739
|
name: this.#name,
|
|
563
740
|
stores: this.#stores(this.#schema)
|
|
564
741
|
});
|
|
565
|
-
|
|
566
|
-
|
|
742
|
+
try {
|
|
743
|
+
await database.connect();
|
|
744
|
+
this.#database = database;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
database.close();
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
567
749
|
}
|
|
568
750
|
async #candidates(store, schema, plan) {
|
|
569
|
-
if (plan.index ===
|
|
751
|
+
if (plan.index === void 0) return store.records(plan.range);
|
|
570
752
|
const rows = [...await store.index(plan.index).records(plan.range)];
|
|
571
753
|
rows.sort((left, right) => compareValues(extractKey(left, schema.primary), extractKey(right, schema.primary)));
|
|
572
754
|
return rows;
|
|
@@ -576,75 +758,43 @@ var IndexedDBDriver = class {
|
|
|
576
758
|
if (schema === void 0) throw new DatabaseError("NOT_FOUND", `table '${name}' is not declared`, { table: name });
|
|
577
759
|
return schema;
|
|
578
760
|
}
|
|
579
|
-
#
|
|
580
|
-
const
|
|
581
|
-
for (const step of steps) switch (step.operation) {
|
|
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) {
|
|
582
764
|
case "table.add":
|
|
583
|
-
|
|
584
|
-
break;
|
|
585
|
-
case "table.remove":
|
|
586
|
-
schema.delete(step.table);
|
|
587
|
-
break;
|
|
588
|
-
case "column.add": {
|
|
589
|
-
const table = schema.get(step.table);
|
|
590
|
-
if (table !== void 0 && !table.columns.some((column) => column.name === step.column.name)) schema.set(step.table, {
|
|
591
|
-
...table,
|
|
592
|
-
columns: [...table.columns, step.column]
|
|
593
|
-
});
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
case "column.remove": {
|
|
597
|
-
const table = schema.get(step.table);
|
|
598
|
-
if (table !== void 0) schema.set(step.table, {
|
|
599
|
-
...table,
|
|
600
|
-
columns: table.columns.filter((column) => column.name !== step.column)
|
|
601
|
-
});
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
case "index.add": {
|
|
605
|
-
const table = schema.get(step.table);
|
|
606
|
-
if (table !== void 0) schema.set(step.table, {
|
|
607
|
-
...table,
|
|
608
|
-
indexes: [...table.indexes, step.index]
|
|
609
|
-
});
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case "index.remove": {
|
|
613
|
-
const table = schema.get(step.table);
|
|
614
|
-
if (table !== void 0) schema.set(step.table, {
|
|
615
|
-
...table,
|
|
616
|
-
indexes: table.indexes.filter((index) => !sameIndex(index, step.index))
|
|
617
|
-
});
|
|
765
|
+
context.create(step.table.name, schemaToStore(step.table));
|
|
618
766
|
break;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
async #upgrade(context, steps) {
|
|
623
|
-
for (const step of steps) switch (step.operation) {
|
|
624
767
|
case "table.remove":
|
|
625
768
|
context.drop(step.table);
|
|
626
769
|
break;
|
|
627
770
|
case "index.add": {
|
|
628
|
-
const name =
|
|
629
|
-
const
|
|
630
|
-
|
|
771
|
+
const name = deriveIndexedDBIndexName(step.index);
|
|
772
|
+
const [column] = step.index;
|
|
773
|
+
const path = step.index.length === 1 && column !== void 0 ? column : [...step.index];
|
|
774
|
+
context.index(step.table, {
|
|
775
|
+
name,
|
|
776
|
+
path
|
|
777
|
+
});
|
|
631
778
|
break;
|
|
632
779
|
}
|
|
633
780
|
case "index.remove":
|
|
634
|
-
context.
|
|
781
|
+
context.deindex(step.table, deriveIndexedDBIndexName(step.index));
|
|
635
782
|
break;
|
|
636
783
|
case "column.remove": {
|
|
637
784
|
let cursor = await context.store(step.table).cursor();
|
|
638
785
|
while (cursor !== null) {
|
|
639
786
|
const [migrated] = migrateRows([cursor.value], [step]);
|
|
787
|
+
if (migrated === void 0) throw new DatabaseError("MIGRATION", "migrate: transformed row is missing", { table: step.table });
|
|
640
788
|
await cursor.update(migrated);
|
|
641
789
|
cursor = await cursor.continue();
|
|
642
790
|
}
|
|
643
791
|
break;
|
|
644
792
|
}
|
|
645
|
-
case "table.add":
|
|
646
|
-
case "column.add": break;
|
|
647
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");
|
|
648
798
|
}
|
|
649
799
|
};
|
|
650
800
|
//#endregion
|
|
@@ -653,9 +803,9 @@ var IndexedDBDriver = class {
|
|
|
653
803
|
* Create a persistent IndexedDB {@link DriverInterface} for the core database layer.
|
|
654
804
|
*
|
|
655
805
|
* @remarks
|
|
656
|
-
* Pass it to `createDatabase` from `@orkestrel/database` to run the
|
|
657
|
-
*
|
|
658
|
-
*
|
|
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
|
|
659
809
|
* driver is built on the published `@orkestrel/indexeddb` wrapper in auto-managed
|
|
660
810
|
* mode, so a table added to the `tables` map is created on the next open with no
|
|
661
811
|
* version bump. This unit omits `transaction` / `aggregate` (see
|
|
@@ -681,6 +831,6 @@ function createIndexedDBDriver(name) {
|
|
|
681
831
|
return new IndexedDBDriver(name);
|
|
682
832
|
}
|
|
683
833
|
//#endregion
|
|
684
|
-
export {
|
|
834
|
+
export { INDEXABLE_STORAGE, IndexedDBDriver, METADATA_STORE, conditionToRange, createIndexedDBDriver, deriveIndexedDBIndexName, mapIndexedDBError, mapMigrationError, schemaToStore, selectPlan };
|
|
685
835
|
|
|
686
836
|
//# sourceMappingURL=index.js.map
|