@orkestrel/database 0.0.13 → 0.0.15

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 CHANGED
@@ -1,11 +1,15 @@
1
1
  # @orkestrel/database
2
2
 
3
- A typed database abstraction for the `@orkestrel` line one public
4
- `Database` over internal table/query engines and pluggable storage drivers at
5
- the seams. Consumers use `TableInterface` and `QueryInterface`. Built to sit beside
6
- `@orkestrel/contract` (validation) and `@orkestrel/emitter` (observable
7
- lifecycle), reusing both directly. `TableInterface.cursor()` exposes the
8
- `CursorInterface` contract for serial bulk mutation.
3
+ > One typed database API for keyed rows, fluent queries, cursors, and
4
+ > whole-store transactions, running unchanged over an in-memory map, a JSON
5
+ > file, SQLite, or IndexedDB.
6
+
7
+ Declare your tables with the `createDatabase` function, hold each
8
+ `TableInterface` it hands back, and reach rows by key or through the fluent
9
+ `query()` builder. `TableInterface.cursor()` opens the `CursorInterface`
10
+ contract for serial bulk mutation. Built on `@orkestrel/contract` for
11
+ validation and `@orkestrel/emitter` for the observation surface, reusing both
12
+ directly. Part of the `@orkestrel` line.
9
13
 
10
14
  ## Install
11
15
 
@@ -1,19 +1,16 @@
1
- import { ColumnStorage } from '@orkestrel/database';
2
- import { Condition } from '@orkestrel/database';
1
+ import type { ColumnStorage } from '@orkestrel/database';
2
+ import type { Condition } from '@orkestrel/database';
3
3
  import { DatabaseError } from '@orkestrel/database';
4
- import { DriverInterface } from '@orkestrel/database';
5
- import { DriverInterface as DriverInterface_2 } from '@orkestrel/database';
6
- import { DriverMetadata } from '@orkestrel/database';
7
- import { IndexedDBError } from '@orkestrel/indexeddb';
8
- import { Key } from '@orkestrel/database';
9
- import { MigrationInput } from '@orkestrel/database';
10
- import { OperationOptions } from '@orkestrel/database';
11
- import { QueryInput } from '@orkestrel/database';
12
- import { QueryInput as QueryInput_2 } from '@orkestrel/database';
13
- import { Row } from '@orkestrel/database';
14
- import { StoreDefinition } from '@orkestrel/indexeddb';
15
- import { TableSchema } from '@orkestrel/database';
16
- import { TableSchema as TableSchema_2 } from '@orkestrel/database';
4
+ import type { DriverInterface } from '@orkestrel/database';
5
+ import type { DriverMetadata } from '@orkestrel/database';
6
+ import type { IndexedDBError } from '@orkestrel/indexeddb';
7
+ import type { Key } from '@orkestrel/database';
8
+ import type { MigrationInput } from '@orkestrel/database';
9
+ import type { OperationOptions } from '@orkestrel/database';
10
+ import type { QueryInput } from '@orkestrel/database';
11
+ import type { Row } from '@orkestrel/database';
12
+ import type { StoreDefinition } from '@orkestrel/indexeddb';
13
+ import type { TableSchema } from '@orkestrel/database';
17
14
 
18
15
  /**
19
16
  * Translates one {@link Condition} to the `IDBKeyRange` it maps to, when its
@@ -24,14 +21,14 @@ import { TableSchema as TableSchema_2 } from '@orkestrel/database';
24
21
  * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
25
22
  * translate to a key range that a typed (string/number) column can back with an
26
23
  * IndexedDB store/index read — see {@link selectPlan} for the caveats that
27
- * decide WHICH of `below`/`to` may drive a SECONDARY-index read versus the
24
+ * decide which of `below`/`to` may drive a secondary-index read versus the
28
25
  * primary store only (a column-type / absent-row concern, not a range-shape
29
26
  * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
30
27
  * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
31
28
  * `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
32
29
  * guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
33
30
  * boolean) that is not a usable key. `between` additionally guards against a
34
- * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
31
+ * reversed pair (`first > second`): native `IDBKeyRange.bound` throws a raw
35
32
  * `DataError` `DOMException` for a lower bound above the upper bound, so a
36
33
  * reversed pair returns `undefined` here (falls back to a full scan, which the
37
34
  * engine then correctly resolves to an empty result) rather than letting a
@@ -47,7 +44,7 @@ import { TableSchema as TableSchema_2 } from '@orkestrel/database';
47
44
  export declare function conditionToRange(condition: Condition): IDBKeyRange | undefined;
48
45
 
49
46
  /**
50
- * Creates a persistent IndexedDB {@link DriverInterface} for the core database layer.
47
+ * Creates a persistent IndexedDB {@link DriverInterface} for a browser database name.
51
48
  *
52
49
  * @remarks
53
50
  * Pass it to `createDatabase` from `@orkestrel/database` to run the typed database
@@ -74,7 +71,7 @@ export declare function conditionToRange(condition: Condition): IDBKeyRange | un
74
71
  * await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to IndexedDB
75
72
  * ```
76
73
  */
77
- export declare function createIndexedDBDriver(name: string): DriverInterface_2;
74
+ export declare function createIndexedDBDriver(name: string): DriverInterface;
78
75
 
79
76
  /**
80
77
  * Derives an IndexedDB index name for a declared column group — a bare column
@@ -83,14 +80,14 @@ export declare function createIndexedDBDriver(name: string): DriverInterface_2;
83
80
  *
84
81
  * @remarks
85
82
  * Naming a compound index by joining its columns with `_` (`['a', 'b'] →
86
- * 'a_b'`) collides with a single-column index over a column LITERALLY named
83
+ * 'a_b'`) collides with a single-column index over a column literally named
87
84
  * `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),
88
85
  * which either throws a native `ConstraintError` from a duplicate
89
86
  * `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based
90
- * lookup match the wrong index. A single-column index keeps the BARE column
87
+ * lookup match the wrong index. A single-column index keeps the bare column
91
88
  * name — {@link selectPlan} matches `available.includes(condition.column)` by
92
89
  * that exact name, so a single-column index must stay named after its column
93
- * verbatim. A compound index instead encodes each column as a LENGTH-PREFIXED
90
+ * verbatim. A compound index instead encodes each column as a length-prefixed
94
91
  * segment (`'2#1:a1:b'`), so the boundary between columns is self-describing
95
92
  * and cannot be reconstructed by any other column list — including one
96
93
  * containing a column that happens to look like an encoded segment.
@@ -155,28 +152,28 @@ export declare const INDEXABLE_STORAGE: readonly ColumnStorage[];
155
152
  * {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
156
153
  * store, creating/dropping an index) is legal only inside a versionchange
157
154
  * transaction (`onupgradeneeded`), so `migrate` closes the current connection
158
- * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the
155
+ * and opens a fresh one at `version + 1` with an `upgrade` hook that walks the
159
156
  * plan's steps — dropping stores, adding/removing indexes on the raw
160
157
  * `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
161
158
  * (the one step needing to touch existing data; `column.add` is a no-op — this
162
159
  * driver stores whatever a row carries, so there is nothing to backfill). A
163
- * step referencing an unknown table is validated BEFORE the reconnect, so a
160
+ * step referencing an unknown table is validated before the reconnect, so a
164
161
  * `MIGRATION` `DatabaseError` never wastes a version bump.
165
162
  *
166
163
  * @remarks
167
- * This unit deliberately OMITS `aggregate` / `transaction`. There is no native
164
+ * This unit deliberately omits `aggregate` / `transaction`. There is no native
168
165
  * `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
169
166
  * `records` covers it. `transaction` is impossible here: the wrapper
170
167
  * auto-commits an `IDBTransaction` when control yields outside its request
171
168
  * chain, so arbitrary callback awaits cannot remain inside one native
172
169
  * transaction. Every atomic multi-operation sequence in this driver
173
- * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)`
170
+ * (`snapshot`'s rollback) instead runs entirely inside one `db.write(...)`
174
171
  * scope.
175
172
  */
176
173
  export declare class IndexedDBDriver implements DriverInterface {
177
174
  #private;
178
175
  constructor(name: string);
179
- open(schema: readonly TableSchema_2[]): Promise<void>;
176
+ open(schema: readonly TableSchema[]): Promise<void>;
180
177
  close(): Promise<void>;
181
178
  read(table: string, key: Key): Promise<Row | undefined>;
182
179
  write(table: string, key: Key, row: Row, options?: OperationOptions): Promise<void>;
@@ -185,8 +182,8 @@ export declare class IndexedDBDriver implements DriverInterface {
185
182
  keys(table: string): Promise<readonly Key[]>;
186
183
  scan(table: string): AsyncIterable<Row>;
187
184
  clear(table: string): Promise<void>;
188
- records(table: string, input: QueryInput_2): Promise<readonly Row[]>;
189
- stream(table: string, input: QueryInput_2): AsyncIterable<Row>;
185
+ records(table: string, input: QueryInput): Promise<readonly Row[]>;
186
+ stream(table: string, input: QueryInput): AsyncIterable<Row>;
190
187
  snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
191
188
  /**
192
189
  * Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
@@ -213,11 +210,11 @@ export declare class IndexedDBDriver implements DriverInterface {
213
210
  *
214
211
  * @remarks
215
212
  * IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
216
- * the current connection and opens a FRESH one at `version + 1`, declaring
217
- * every currently-known store (plus {@link METADATA_STORE}) so nothing is lost,
218
- * and applying `table.remove` / `index.add` / `index.remove` /
213
+ * the current connection and opens a fresh one at `version + 1`, declaring
214
+ * every store known at that point (plus {@link METADATA_STORE}) so nothing is
215
+ * lost, and applying `table.remove` / `index.add` / `index.remove` /
219
216
  * `column.remove` inside `upgrade`. Every step's `table` is validated against
220
- * the driver's own `#schema` BEFORE the reconnect — an unknown-table step
217
+ * the driver's own `#schema` before the reconnect — an unknown-table step
221
218
  * throws `DatabaseError` `MIGRATION` without ever bumping the version.
222
219
  * `table.add` / `column.add` need no upgrade-time action: `table.add` is
223
220
  * created by the wrapper's built-in create-missing-stores pass (its
@@ -260,7 +257,7 @@ export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
260
257
 
261
258
  /**
262
259
  * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
263
- * for use INSIDE `migrate()` — the one context where `UPGRADE` means the
260
+ * for use inside `migrate()` — the one context where `UPGRADE` means the
264
261
  * migration itself failed, not a generic driver fault.
265
262
  *
266
263
  * @remarks
@@ -277,13 +274,13 @@ export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
277
274
  export declare function mapMigrationError(error: IndexedDBError): DatabaseError;
278
275
 
279
276
  /**
280
- * Names the reserved out-of-line store the {@link IndexedDBDriver} stamps its
281
- * {@link DriverMetadata} into.
277
+ * Names the reserved out-of-line store `__metadata__` the {@link IndexedDBDriver}
278
+ * stamps its {@link DriverMetadata} into.
282
279
  *
283
280
  * @remarks
284
- * Backs the driver's `metadata` / `stamp` hooks. A user table declared with this
285
- * exact name collides with the driver's own bookkeeping, so a caller must avoid
286
- * it; the collision is caught at `open`.
281
+ * Backs the driver's `metadata` / `stamp` hooks. A user table named `__metadata__`
282
+ * collides with the driver's own bookkeeping, so a caller must avoid it; the
283
+ * collision is caught at `open`.
287
284
  */
288
285
  export declare const METADATA_STORE = "__metadata__";
289
286
 
@@ -292,7 +289,8 @@ export declare const METADATA_STORE = "__metadata__";
292
289
  * a read. An omitted `index` selects the primary store; an omitted `range`
293
290
  * performs a full scan. The plan is always a superset of the matching rows;
294
291
  * the core engine refines it to the exact result. An empty plan (`{}`) is a
295
- * primary-store full scan.
292
+ * primary-store full scan. The `@orkestrel/database/browser` entry point exports
293
+ * this type.
296
294
  */
297
295
  export declare interface QueryPlan {
298
296
  readonly index?: string;
@@ -302,6 +300,11 @@ export declare interface QueryPlan {
302
300
  /**
303
301
  * Projects a table schema into the IndexedDB wrapper's store definition.
304
302
  *
303
+ * @remarks
304
+ * The definition is what an ordered versionchange migration creates the store
305
+ * from, carrying each declared index under the name {@link deriveIndexedDBIndexName}
306
+ * derives for its column group.
307
+ *
305
308
  * @param schema - Portable table schema
306
309
  * @returns Store definition with declared indexes
307
310
  */
@@ -312,7 +315,7 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
312
315
  * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
313
316
  *
314
317
  * @remarks
315
- * Pushdown is sound ONLY when every condition is `and`-joined: the engine folds
318
+ * Pushdown is sound only when every condition is `and`-joined: the engine folds
316
319
  * conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
317
320
  * each — narrowing on any one is then a valid superset. A single `or` breaks that
318
321
  * (a row can match through a later condition the range would exclude), so any `or`
@@ -326,21 +329,21 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
326
329
  * type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
327
330
  * non-scalar operand cannot push and is skipped.
328
331
  *
329
- * **`below`/`to` may drive a SECONDARY-index range only when the column has NO
332
+ * **`below`/`to` may drive a secondary-index range only when the column has no
330
333
  * absent/null rows to lose — which this planner cannot verify from the schema
331
- * alone, so it restricts them to the PRIMARY store, where that is always true.**
332
- * The engine's total order (`compareValues`, see `@src/core`) ranks
333
- * `undefined` (absent) and `null` BELOW every number/string, so
334
- * `matchesCondition('below' | 'to', …)` is TRUE for a row whose field is absent
335
- * or `null` — but a secondary IndexedDB index has NO ENTRY for a row whose
334
+ * alone, so it restricts them to the primary store, where that is always true.**
335
+ * The engine's total order (`compareValues`, see `@orkestrel/database`) ranks
336
+ * `undefined` (absent) and `null` below every number/string, so
337
+ * `matchesCondition('below' | 'to', …)` is true for a row whose field is absent
338
+ * or `null` — but a secondary IndexedDB index has no entry for a row whose
336
339
  * indexed field is absent/`null`, so a `below`/`to` range read against that
337
- * index would SILENTLY DROP those rows (they can never be over-fetched, only
340
+ * index would silently drop those rows (they can never be over-fetched, only
338
341
  * missed — the one shape of lossiness this planner must never produce). The
339
- * table's PRIMARY key is exempt: a row's primary-key value is always present
342
+ * table's primary key is exempt: a row's primary-key value is always present
340
343
  * and never `null` (it is the row's identity, enforced at write time), so a
341
344
  * `below`/`to` range against the primary store can never exclude an
342
345
  * absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
343
- * `between` stay index-eligible on ANY orderable column, primary or secondary:
346
+ * `between` stay index-eligible on any orderable column, primary or secondary:
344
347
  * each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
345
348
  * `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
346
349
  * in the total order, so an absent/null-valued row can never satisfy them — the
@@ -354,7 +357,7 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
354
357
  * metadata.
355
358
  *
356
359
  * When no condition qualifies the plan is a full scan (`{}`) and the engine
357
- * does everything. The plan is always a SUPERSET of the
360
+ * does everything. The plan is always a superset of the
358
361
  * matching rows — the only correctness contract — so the driver may safely run
359
362
  * the exact engine over it.
360
363
  *
@@ -1,3 +1,4 @@
1
+ import { isString } from "@orkestrel/contract";
1
2
  import { DatabaseError, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, compareValues, equalsValue, extractKey, findColumn, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
2
3
  import { createIndexedDBDatabase, isIndexedDBError, rangeAboveKey, rangeBelowKey, rangeFromKey, rangeToKey } from "@orkestrel/indexeddb";
3
4
  //#region src/browser/constants.ts
@@ -19,13 +20,13 @@ var INDEXABLE_STORAGE = Object.freeze([
19
20
  "real"
20
21
  ]);
21
22
  /**
22
- * Names the reserved out-of-line store the {@link IndexedDBDriver} stamps its
23
- * {@link DriverMetadata} into.
23
+ * Names the reserved out-of-line store `__metadata__` the {@link IndexedDBDriver}
24
+ * stamps its {@link DriverMetadata} into.
24
25
  *
25
26
  * @remarks
26
- * Backs the driver's `metadata` / `stamp` hooks. A user table declared with this
27
- * exact name collides with the driver's own bookkeeping, so a caller must avoid
28
- * it; the collision is caught at `open`.
27
+ * Backs the driver's `metadata` / `stamp` hooks. A user table named `__metadata__`
28
+ * collides with the driver's own bookkeeping, so a caller must avoid it; the
29
+ * collision is caught at `open`.
29
30
  */
30
31
  var METADATA_STORE = "__metadata__";
31
32
  //#endregion
@@ -39,14 +40,14 @@ var METADATA_STORE = "__metadata__";
39
40
  * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
40
41
  * translate to a key range that a typed (string/number) column can back with an
41
42
  * IndexedDB store/index read — see {@link selectPlan} for the caveats that
42
- * decide WHICH of `below`/`to` may drive a SECONDARY-index read versus the
43
+ * decide which of `below`/`to` may drive a secondary-index read versus the
43
44
  * primary store only (a column-type / absent-row concern, not a range-shape
44
45
  * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
45
46
  * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
46
47
  * `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
47
48
  * guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
48
49
  * boolean) that is not a usable key. `between` additionally guards against a
49
- * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
50
+ * reversed pair (`first > second`): native `IDBKeyRange.bound` throws a raw
50
51
  * `DataError` `DOMException` for a lower bound above the upper bound, so a
51
52
  * reversed pair returns `undefined` here (falls back to a full scan, which the
52
53
  * engine then correctly resolves to an empty result) rather than letting a
@@ -85,7 +86,7 @@ function conditionToRange(condition) {
85
86
  * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
86
87
  *
87
88
  * @remarks
88
- * Pushdown is sound ONLY when every condition is `and`-joined: the engine folds
89
+ * Pushdown is sound only when every condition is `and`-joined: the engine folds
89
90
  * conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
90
91
  * each — narrowing on any one is then a valid superset. A single `or` breaks that
91
92
  * (a row can match through a later condition the range would exclude), so any `or`
@@ -99,21 +100,21 @@ function conditionToRange(condition) {
99
100
  * type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
100
101
  * non-scalar operand cannot push and is skipped.
101
102
  *
102
- * **`below`/`to` may drive a SECONDARY-index range only when the column has NO
103
+ * **`below`/`to` may drive a secondary-index range only when the column has no
103
104
  * absent/null rows to lose — which this planner cannot verify from the schema
104
- * alone, so it restricts them to the PRIMARY store, where that is always true.**
105
+ * alone, so it restricts them to the primary store, where that is always true.**
105
106
  * The engine's total order (`compareValues`, see `@src/core`) ranks
106
- * `undefined` (absent) and `null` BELOW every number/string, so
107
- * `matchesCondition('below' | 'to', …)` is TRUE for a row whose field is absent
108
- * or `null` — but a secondary IndexedDB index has NO ENTRY for a row whose
107
+ * `undefined` (absent) and `null` below every number/string, so
108
+ * `matchesCondition('below' | 'to', …)` is true for a row whose field is absent
109
+ * or `null` — but a secondary IndexedDB index has no entry for a row whose
109
110
  * indexed field is absent/`null`, so a `below`/`to` range read against that
110
- * index would SILENTLY DROP those rows (they can never be over-fetched, only
111
+ * index would silently drop those rows (they can never be over-fetched, only
111
112
  * missed — the one shape of lossiness this planner must never produce). The
112
- * table's PRIMARY key is exempt: a row's primary-key value is always present
113
+ * table's primary key is exempt: a row's primary-key value is always present
113
114
  * and never `null` (it is the row's identity, enforced at write time), so a
114
115
  * `below`/`to` range against the primary store can never exclude an
115
116
  * absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
116
- * `between` stay index-eligible on ANY orderable column, primary or secondary:
117
+ * `between` stay index-eligible on any orderable column, primary or secondary:
117
118
  * each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
118
119
  * `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
119
120
  * in the total order, so an absent/null-valued row can never satisfy them — the
@@ -127,7 +128,7 @@ function conditionToRange(condition) {
127
128
  * metadata.
128
129
  *
129
130
  * When no condition qualifies the plan is a full scan (`{}`) and the engine
130
- * does everything. The plan is always a SUPERSET of the
131
+ * does everything. The plan is always a superset of the
131
132
  * matching rows — the only correctness contract — so the driver may safely run
132
133
  * the exact engine over it.
133
134
  *
@@ -149,7 +150,7 @@ function selectPlan(input, schema, available) {
149
150
  const conditions = input?.conditions ?? [];
150
151
  if (conditions.slice(1).some((condition) => condition.connector === "or")) return {};
151
152
  for (const condition of conditions) {
152
- if (typeof condition.column !== "string") continue;
153
+ if (!isString(condition.column)) continue;
153
154
  const column = findColumn(condition.column, schema);
154
155
  if (column === void 0 || !INDEXABLE_STORAGE.includes(column.storage)) continue;
155
156
  const range = conditionToRange(condition);
@@ -209,7 +210,7 @@ function mapIndexedDBError(error) {
209
210
  }
210
211
  /**
211
212
  * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
212
- * for use INSIDE `migrate()` — the one context where `UPGRADE` means the
213
+ * for use inside `migrate()` — the one context where `UPGRADE` means the
213
214
  * migration itself failed, not a generic driver fault.
214
215
  *
215
216
  * @remarks
@@ -234,14 +235,14 @@ function mapMigrationError(error) {
234
235
  *
235
236
  * @remarks
236
237
  * Naming a compound index by joining its columns with `_` (`['a', 'b'] →
237
- * 'a_b'`) collides with a single-column index over a column LITERALLY named
238
+ * 'a_b'`) collides with a single-column index over a column literally named
238
239
  * `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),
239
240
  * which either throws a native `ConstraintError` from a duplicate
240
241
  * `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based
241
- * lookup match the wrong index. A single-column index keeps the BARE column
242
+ * lookup match the wrong index. A single-column index keeps the bare column
242
243
  * name — {@link selectPlan} matches `available.includes(condition.column)` by
243
244
  * that exact name, so a single-column index must stay named after its column
244
- * verbatim. A compound index instead encodes each column as a LENGTH-PREFIXED
245
+ * verbatim. A compound index instead encodes each column as a length-prefixed
245
246
  * segment (`'2#1:a1:b'`), so the boundary between columns is self-describing
246
247
  * and cannot be reconstructed by any other column list — including one
247
248
  * containing a column that happens to look like an encoded segment.
@@ -263,6 +264,11 @@ function deriveIndexedDBIndexName(columns) {
263
264
  /**
264
265
  * Projects a table schema into the IndexedDB wrapper's store definition.
265
266
  *
267
+ * @remarks
268
+ * The definition is what an ordered versionchange migration creates the store
269
+ * from, carrying each declared index under the name {@link deriveIndexedDBIndexName}
270
+ * derives for its column group.
271
+ *
266
272
  * @param schema - Portable table schema
267
273
  * @returns Store definition with declared indexes
268
274
  */
@@ -312,22 +318,22 @@ function schemaToStore(schema) {
312
318
  * {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
313
319
  * store, creating/dropping an index) is legal only inside a versionchange
314
320
  * transaction (`onupgradeneeded`), so `migrate` closes the current connection
315
- * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the
321
+ * and opens a fresh one at `version + 1` with an `upgrade` hook that walks the
316
322
  * plan's steps — dropping stores, adding/removing indexes on the raw
317
323
  * `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
318
324
  * (the one step needing to touch existing data; `column.add` is a no-op — this
319
325
  * driver stores whatever a row carries, so there is nothing to backfill). A
320
- * step referencing an unknown table is validated BEFORE the reconnect, so a
326
+ * step referencing an unknown table is validated before the reconnect, so a
321
327
  * `MIGRATION` `DatabaseError` never wastes a version bump.
322
328
  *
323
329
  * @remarks
324
- * This unit deliberately OMITS `aggregate` / `transaction`. There is no native
330
+ * This unit deliberately omits `aggregate` / `transaction`. There is no native
325
331
  * `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
326
332
  * `records` covers it. `transaction` is impossible here: the wrapper
327
333
  * auto-commits an `IDBTransaction` when control yields outside its request
328
334
  * chain, so arbitrary callback awaits cannot remain inside one native
329
335
  * transaction. Every atomic multi-operation sequence in this driver
330
- * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)`
336
+ * (`snapshot`'s rollback) instead runs entirely inside one `db.write(...)`
331
337
  * scope.
332
338
  */
333
339
  var IndexedDBDriver = class {
@@ -568,11 +574,11 @@ var IndexedDBDriver = class {
568
574
  *
569
575
  * @remarks
570
576
  * IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
571
- * the current connection and opens a FRESH one at `version + 1`, declaring
572
- * every currently-known store (plus {@link METADATA_STORE}) so nothing is lost,
573
- * and applying `table.remove` / `index.add` / `index.remove` /
577
+ * the current connection and opens a fresh one at `version + 1`, declaring
578
+ * every store known at that point (plus {@link METADATA_STORE}) so nothing is
579
+ * lost, and applying `table.remove` / `index.add` / `index.remove` /
574
580
  * `column.remove` inside `upgrade`. Every step's `table` is validated against
575
- * the driver's own `#schema` BEFORE the reconnect — an unknown-table step
581
+ * the driver's own `#schema` before the reconnect — an unknown-table step
576
582
  * throws `DatabaseError` `MIGRATION` without ever bumping the version.
577
583
  * `table.add` / `column.add` need no upgrade-time action: `table.add` is
578
584
  * created by the wrapper's built-in create-missing-stores pass (its
@@ -824,7 +830,7 @@ var IndexedDBDriver = class {
824
830
  //#endregion
825
831
  //#region src/browser/factories.ts
826
832
  /**
827
- * Creates a persistent IndexedDB {@link DriverInterface} for the core database layer.
833
+ * Creates a persistent IndexedDB {@link DriverInterface} for a browser database name.
828
834
  *
829
835
  * @remarks
830
836
  * Pass it to `createDatabase` from `@orkestrel/database` to run the typed database