@orkestrel/database 0.0.5 → 0.0.6

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.
@@ -1,17 +1,17 @@
1
- import { ColumnType } from '../core/index.js';
2
- import { Condition } from '../core/index.js';
3
- import { Criteria } from '../core/index.js';
4
- import { Criteria as Criteria_2 } from '../core/index.js';
5
- import { DatabaseError } from '../core/index.js';
6
- import { DriverInterface } from '../core/index.js';
7
- import { DriverInterface as DriverInterface_2 } from '../core/index.js';
8
- import { DriverMeta } from '../core/index.js';
1
+ import { ColumnType } from '../core/index.ts';
2
+ import { Condition } from '../core/index.ts';
3
+ import { Criteria } from '../core/index.ts';
4
+ import { Criteria as Criteria_2 } from '../../core/index.ts';
5
+ import { DatabaseError } from '../core/index.ts';
6
+ import { DriverInterface } from '../../core/index.ts';
7
+ import { DriverInterface as DriverInterface_2 } from '../core/index.ts';
8
+ import { DriverMeta } from '../../core/index.ts';
9
9
  import { IndexedDBError } from '@orkestrel/indexeddb';
10
- import { Key } from '../core/index.js';
11
- import { Migration } from '../core/index.js';
12
- import { Row } from '../core/index.js';
13
- import { TableSchema } from '../core/index.js';
14
- import { TableSchema as TableSchema_2 } from '../core/index.js';
10
+ import { Key } from '../../core/index.ts';
11
+ import { Migration } from '../../core/index.ts';
12
+ import { Row } from '../../core/index.ts';
13
+ import { TableSchema } from '../core/index.ts';
14
+ import { TableSchema as TableSchema_2 } from '../../core/index.ts';
15
15
 
16
16
  /**
17
17
  * The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one
@@ -1,4 +1,4 @@
1
- import { DatabaseError, applyCriteria, compareValues, extractKey, isDriverMeta, matchesCriteria, migrateRows } from "../core/index.js";
1
+ import { DatabaseError, applyCriteria, compareValues, deepEqual, extractKey, isDriverMeta, matchesCriteria, migrateRows } from "../core/index.js";
2
2
  import { createIndexedDBDatabase, isIndexedDBError, range } from "@orkestrel/indexeddb";
3
3
  //#region src/browser/constants.ts
4
4
  var INDEXABLE_TYPES = /* @__PURE__ */ new Set([
@@ -251,8 +251,9 @@ function mapMigrationError(error) {
251
251
  * ```
252
252
  */
253
253
  function deriveIndexName(columns) {
254
- if (columns.length === 1) return columns[0];
255
- return `${columns.length}#${columns.map((column) => `${column.length}:${column}`).join("")}`;
254
+ const [column] = columns;
255
+ if (columns.length === 1 && column !== void 0) return column;
256
+ return `${columns.length}#${columns.map((part) => `${part.length}:${part}`).join("")}`;
256
257
  }
257
258
  //#endregion
258
259
  //#region src/browser/drivers/IndexedDBDriver.ts
@@ -451,7 +452,15 @@ var IndexedDBDriver = class {
451
452
  if (snapshot === void 0) continue;
452
453
  const store = transaction.store(name);
453
454
  await store.clear();
454
- for (let index = 0; index < snapshot.keys.length; index += 1) await store.set(snapshot.rows[index], snapshot.keys[index]);
455
+ for (let index = 0; index < snapshot.keys.length; index += 1) {
456
+ const row = snapshot.rows[index];
457
+ const key = snapshot.keys[index];
458
+ if (row === void 0 || key === void 0) throw new DatabaseError("DRIVER", "IndexedDB snapshot entry is incomplete", {
459
+ table: name,
460
+ index
461
+ });
462
+ await store.set(row, key);
463
+ }
455
464
  }
456
465
  });
457
466
  } catch (error) {
@@ -529,7 +538,7 @@ var IndexedDBDriver = class {
529
538
  name: this.#name,
530
539
  version: version + 1,
531
540
  stores: this.#stores(schema),
532
- upgrade: (context) => this.#upgrade(context, plan.steps)
541
+ upgrade: this.#upgrade.bind(this, plan.steps)
533
542
  });
534
543
  await database.connect();
535
544
  this.#database = database;
@@ -551,10 +560,13 @@ var IndexedDBDriver = class {
551
560
  }
552
561
  #stores(schema) {
553
562
  const stores = { [META_STORE]: {} };
554
- for (const table of schema.values()) stores[table.name] = { indexes: table.indexes.map((columns) => ({
555
- name: deriveIndexName(columns),
556
- path: columns.length === 1 ? columns[0] : [...columns]
557
- })) };
563
+ for (const table of schema.values()) stores[table.name] = { indexes: table.indexes.map((columns) => {
564
+ const [column] = columns;
565
+ return {
566
+ name: deriveIndexName(columns),
567
+ path: columns.length === 1 && column !== void 0 ? column : [...columns]
568
+ };
569
+ }) };
558
570
  return stores;
559
571
  }
560
572
  async #reopen() {
@@ -577,7 +589,6 @@ var IndexedDBDriver = class {
577
589
  return schema;
578
590
  }
579
591
  #applySteps(schema, steps) {
580
- const sameIndex = (left, right) => left.length === right.length && left.every((column, position) => column === right[position]);
581
592
  for (const step of steps) switch (step.operation) {
582
593
  case "table.add":
583
594
  if (!schema.has(step.table.name)) schema.set(step.table.name, step.table);
@@ -613,20 +624,21 @@ var IndexedDBDriver = class {
613
624
  const table = schema.get(step.table);
614
625
  if (table !== void 0) schema.set(step.table, {
615
626
  ...table,
616
- indexes: table.indexes.filter((index) => !sameIndex(index, step.index))
627
+ indexes: table.indexes.filter((index) => !deepEqual(index, step.index))
617
628
  });
618
629
  break;
619
630
  }
620
631
  }
621
632
  }
622
- async #upgrade(context, steps) {
633
+ async #upgrade(steps, context) {
623
634
  for (const step of steps) switch (step.operation) {
624
635
  case "table.remove":
625
636
  context.drop(step.table);
626
637
  break;
627
638
  case "index.add": {
628
639
  const name = deriveIndexName(step.index);
629
- const path = step.index.length === 1 ? step.index[0] : [...step.index];
640
+ const [column] = step.index;
641
+ const path = step.index.length === 1 && column !== void 0 ? column : [...step.index];
630
642
  context.transaction.objectStore(step.table).createIndex(name, path);
631
643
  break;
632
644
  }
@@ -637,6 +649,7 @@ var IndexedDBDriver = class {
637
649
  let cursor = await context.store(step.table).cursor();
638
650
  while (cursor !== null) {
639
651
  const [migrated] = migrateRows([cursor.value], [step]);
652
+ if (migrated === void 0) throw new DatabaseError("MIGRATION", "migrate: transformed row is missing", { table: step.table });
640
653
  await cursor.update(migrated);
641
654
  cursor = await cursor.continue();
642
655
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#name","#database","#stores","#schema","#wrap","#store","#table","#candidates","#require","#applySteps","#upgrade","#reopen"],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/drivers/IndexedDBDriver.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { ColumnType } from '@src/core'\n\n// The column types that are valid, orderable IndexedDB keys (string / number key\n// space). `boolean` / `json` / `blob` are not valid `IDBValidKey`s and would make\n// a range silently miss rows, so they are never pushed down.\nexport const INDEXABLE_TYPES: ReadonlySet<ColumnType> = new Set<ColumnType>([\n\t'text',\n\t'integer',\n\t'real',\n])\n\n// The reserved out-of-line store the driver stamps its DriverMeta into\n// (`meta` / `stamp`). A user table declared with this exact name would\n// collide with the driver's own bookkeeping — callers must avoid it.\nexport const META_STORE = '__meta__'\n","import type { Condition, Criteria, TableSchema } from '@src/core'\nimport type { IndexedDBError } from '@orkestrel/indexeddb'\nimport type { QueryPlan } from './types.js'\nimport { compareValues, DatabaseError } from '@src/core'\nimport { range } from '@orkestrel/indexeddb'\nimport { INDEXABLE_TYPES } from './constants.js'\n\n// The IndexedDB driver's pushdown planner. A pure function over the portable\n// `Criteria`: it decides which index (or the primary key) a read can narrow on\n// and the `IDBKeyRange` to use, so the driver fetches a candidate SUPERSET rather\n// than every row. The core engine then refines that superset to the exact result\n// — so a plan is only ever allowed to over-fetch, never to drop a matching row.\n// Anything it cannot prove range-exact (a non-comparison operator, a non-orderable\n// column type, a nested path, a non-scalar operand) falls through to a full scan.\n\n/**\n * The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one\n * of the six exact key comparisons over scalar operands — else `null`.\n *\n * @remarks\n * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)\n * translate to a key range that a typed (string/number) column can back with an\n * IndexedDB store/index read — see {@link selectPlan} for the caveats that\n * decide WHICH of `below`/`to` may drive a SECONDARY-index read versus the\n * primary store only (a column-type / absent-row concern, not a range-shape\n * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;\n * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/\n * `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand\n * guard (`typeof` string/number) rejects a non-scalar value (e.g. an array, a\n * boolean) that is not a usable key. `between` additionally guards against a\n * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw\n * `DataError` `DOMException` for a lower bound above the upper bound, so a\n * reversed pair returns `null` here (falls back to a full scan, which the\n * engine then correctly resolves to an empty result) rather than letting a\n * native exception escape untyped — the same defensive posture as every other\n * backend, which returns empty for a reversed/empty range instead of throwing.\n * The switch is exhaustive over every {@link ConditionOperator}, so a new\n * operator forces a deliberate decision here rather than silently defaulting\n * to a (possibly lossy) range.\n *\n * @param condition - The condition to translate\n * @returns Its exact key range, or `null` when the operator/operands cannot push\n */\nexport function conditionRange(condition: Condition): IDBKeyRange | null {\n\tconst first = condition.values[0]\n\tconst second = condition.values[1]\n\tswitch (condition.operator) {\n\t\tcase 'equals':\n\t\t\treturn isKey(first) ? range.only(first) : null\n\t\tcase 'above':\n\t\t\treturn isKey(first) ? range.above(first) : null\n\t\tcase 'below':\n\t\t\treturn isKey(first) ? range.below(first) : null\n\t\tcase 'from':\n\t\t\treturn isKey(first) ? range.from(first) : null\n\t\tcase 'to':\n\t\t\treturn isKey(first) ? range.to(first) : null\n\t\tcase 'between':\n\t\t\t// A reversed pair (`first > second`) has no valid IDBKeyRange — native\n\t\t\t// `bound` throws rather than returning empty. Fall back to a full scan\n\t\t\t// instead of letting that DOMException escape (the engine then yields\n\t\t\t// the correct, empty result over `compareValues(value, first) >= 0 &&\n\t\t\t// compareValues(value, second) <= 0`, which no row can satisfy).\n\t\t\treturn isKey(first) && isKey(second) && compareValues(first, second) <= 0\n\t\t\t\t? range.between(first, second)\n\t\t\t\t: null\n\t\tcase 'not':\n\t\tcase 'like':\n\t\tcase 'glob':\n\t\tcase 'starts':\n\t\tcase 'ends':\n\t\tcase 'any':\n\t\tcase 'none':\n\t\tcase 'absent':\n\t\tcase 'present':\n\t\t\treturn null\n\t}\n}\n\n// A scalar IndexedDB key operand — a string or number (the core `Key` space).\nexport function isKey(value: unknown): value is string | number {\n\treturn typeof value === 'string' || typeof value === 'number'\n}\n\n/**\n * Plan an IndexedDB read for a {@link Criteria} — pick the index (or the primary\n * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.\n *\n * @remarks\n * Pushdown is sound ONLY when every condition is `and`-joined: the engine folds\n * conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of\n * each — narrowing on any one is then a valid superset. A single `or` breaks that\n * (a row can match through a later condition the range would exclude), so any `or`\n * forces a full scan. Otherwise it scans the conditions in order and selects the\n * **first** one that is provably range-exact and backed by a key: a comparison\n * operator (`conditionRange`) over a single, orderable (`text`/`integer`/`real`)\n * column that is either the table's primary key (read the store directly, `index:\n * null`) or has a single-column secondary index (named exactly the column — read\n * that index). A condition whose column is a nested {@link FieldPath} array\n * (descends a json value, not a key), is absent from the schema, is a non-orderable\n * type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a\n * non-scalar operand cannot push and is skipped.\n *\n * **`below`/`to` may drive a SECONDARY-index range only when the column has NO\n * absent/null rows to lose — which this planner cannot verify from the schema\n * alone, so it restricts them to the PRIMARY store, where that is always true.**\n * The engine's total order (`compareValues`, see `@src/core`) ranks\n * `undefined` (absent) and `null` BELOW every number/string, so\n * `matchesCondition('below' | 'to', …)` is TRUE for a row whose field is absent\n * or `null` — but a secondary IndexedDB index has NO ENTRY for a row whose\n * indexed field is absent/`null`, so a `below`/`to` range read against that\n * index would SILENTLY DROP those rows (they can never be over-fetched, only\n * missed — the one shape of lossiness this planner must never produce). The\n * table's PRIMARY key is exempt: a row's primary-key value is always present\n * and never `null` (it is the row's identity, enforced at write time), so a\n * `below`/`to` range against the primary store can never exclude an\n * absent/null-keyed row because no such row exists. `equals`/`above`/`from`/\n * `between` stay index-eligible on ANY orderable column, primary or secondary:\n * each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/\n * `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`\n * in the total order, so an absent/null-valued row can never satisfy them — the\n * index's silence on such a row is harmless (it was never going to match).\n * **Declared-type trust caveat:** this reasoning holds under the contract that\n * an {@link INDEXABLE_TYPES} column, once contract-validated at write time,\n * holds only `string | number | null` (or is absent) — never some other\n * runtime value that could rank differently; a driver bypassing the write\n * contract (writing raw rows directly to the store) could defeat this\n * argument, but that is out of scope for a planner reading validated schema\n * metadata.\n *\n * When no condition qualifies the plan is a full scan (`{ index: null, range:\n * null }`) and the engine does everything. The plan is always a SUPERSET of the\n * matching rows — the only correctness contract — so the driver may safely run\n * the exact engine over it.\n *\n * @param criteria - The read specification (its `conditions` drive the plan), or\n * `undefined` for an unconditional read\n * @param schema - The table's schema — its `primary` key and column types\n * @param available - The secondary-index names that physically exist on the store\n * (`store.indexes`); a single-column index is named exactly its column\n * @returns The index + range to read, narrowing to a superset (never lossy)\n *\n * @example\n * ```ts\n * selectPlan({ conditions: [eq('id', 'u1')] }, schema, []) // { index: null, range: only('u1') }\n * selectPlan({ conditions: [from('age', 18)] }, schema, ['age']) // { index: 'age', range: from(18) }\n * selectPlan({ conditions: [contains('name', 'a')] }, schema, []) // { index: null, range: null }\n * ```\n */\nexport function selectPlan(\n\tcriteria: Criteria | undefined,\n\tschema: TableSchema,\n\tavailable: readonly string[],\n): QueryPlan {\n\tconst conditions = criteria?.conditions ?? []\n\t// A single condition's range is a SUPERSET of the result only when the result\n\t// implies that condition — which holds iff every condition is `and`-joined (the\n\t// fold is `c1 && c2 && … && cn`, so the result is a subset of each). A single\n\t// `or` breaks that (a row can match via a later condition the range excludes),\n\t// so any `or` forces a full scan. The first condition's connector only seeds the\n\t// fold and is ignored (AGENTS — the `Condition.connector` contract).\n\tif (conditions.slice(1).some((condition) => condition.connector === 'or')) {\n\t\treturn { index: null, range: null }\n\t}\n\tfor (const condition of conditions) {\n\t\t// An array column is a nested FieldPath into a json value — not a key.\n\t\tif (typeof condition.column !== 'string') continue\n\t\tconst column = schema.columns.find((candidate) => candidate.name === condition.column)\n\t\tif (column === undefined || !INDEXABLE_TYPES.has(column.type)) continue\n\t\tconst keyRange = conditionRange(condition)\n\t\tif (keyRange === null) continue\n\t\tif (condition.column === schema.primary) return { index: null, range: keyRange }\n\t\t// `below`/`to` can silently drop an absent/null-valued row from a SECONDARY\n\t\t// index (see @remarks) — only the primary store (handled above) is safe.\n\t\t// Keep scanning: a later condition may still qualify.\n\t\tif (condition.operator === 'below' || condition.operator === 'to') continue\n\t\tif (available.includes(condition.column)) return { index: condition.column, range: keyRange }\n\t\t// The column is range-exact but has no usable index — keep looking.\n\t}\n\treturn { index: null, range: null }\n}\n\n/**\n * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy\n * — the default mapping used everywhere except inside `migrate()`.\n *\n * @remarks\n * No backend fault may leak through `DriverInterface` as a raw `IndexedDBError`.\n * `CONSTRAINT` (a unique-key violation) is a `CONFLICT` — the same code every\n * other backend uses for a duplicate key. `CLOSED`/`NOT_OPEN`/`INVALID` (the\n * connection is gone, never opened, or the native handle is stale) collapse to\n * `CLOSED`. `QUOTA` and `BLOCKED` are genuine infrastructure faults (`DRIVER`),\n * carrying a machine-readable `context.code` (`'QUOTA'` / `'BLOCKED'`) so a\n * caller can branch without parsing the message; `BLOCKED` additionally marks\n * `context.retryable: true` — a concurrent connection holding the database open\n * is a transient condition, not a permanent one. Every other code (`UPGRADE`\n * here — see {@link mapMigrationError} for the `migrate()`-only remapping to\n * `MIGRATION` — `ABORTED`, `NOT_FOUND`, `DATA`, `OPEN`, `INACTIVE`, `READONLY`,\n * `UNKNOWN`) is an unexpected infrastructure fault and maps to `DRIVER` — the\n * driver opens its own readwrite transactions, so a `READONLY` fault can only\n * mean the backend behaved unexpectedly. The original error is always\n * preserved as `context.cause` for diagnostics.\n *\n * @param error - The backend error to translate\n * @returns The portable `DatabaseError`\n */\nexport function mapIndexedDBError(error: IndexedDBError): DatabaseError {\n\tswitch (error.code) {\n\t\tcase 'CONSTRAINT':\n\t\t\treturn new DatabaseError('CONFLICT', error.message, { cause: error })\n\t\tcase 'CLOSED':\n\t\tcase 'NOT_OPEN':\n\t\tcase 'INVALID':\n\t\t\treturn new DatabaseError('CLOSED', error.message, { cause: error })\n\t\tcase 'QUOTA':\n\t\t\treturn new DatabaseError('DRIVER', error.message, { cause: error, code: 'QUOTA' })\n\t\tcase 'BLOCKED':\n\t\t\treturn new DatabaseError('DRIVER', error.message, {\n\t\t\t\tcause: error,\n\t\t\t\tcode: 'BLOCKED',\n\t\t\t\tretryable: true,\n\t\t\t})\n\t\tcase 'UPGRADE':\n\t\tcase 'ABORTED':\n\t\tcase 'NOT_FOUND':\n\t\tcase 'DATA':\n\t\tcase 'OPEN':\n\t\tcase 'INACTIVE':\n\t\tcase 'READONLY':\n\t\tcase 'UNKNOWN':\n\t\t\treturn new DatabaseError('DRIVER', error.message, { cause: error })\n\t}\n}\n\n/**\n * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy\n * for use INSIDE `migrate()` — the one context where `UPGRADE` means the\n * migration itself failed, not a generic driver fault.\n *\n * @remarks\n * `migrate()` reconnects at a bumped version inside `onupgradeneeded`; a\n * rejection there (an inapplicable step, a native `ConstraintError` from a\n * duplicate index, …) surfaces as `IndexedDBError` `UPGRADE` and must become a\n * `MIGRATION` `DatabaseError` so a caller can distinguish \"this migration plan\n * failed\" from \"the driver hit an unrelated infrastructure fault\". Every other\n * code defers to {@link mapIndexedDBError} unchanged.\n *\n * @param error - The backend error to translate\n * @returns The portable `DatabaseError`\n */\nexport function mapMigrationError(error: IndexedDBError): DatabaseError {\n\tif (error.code === 'UPGRADE') {\n\t\treturn new DatabaseError('MIGRATION', error.message, { cause: error })\n\t}\n\treturn mapIndexedDBError(error)\n}\n\n/**\n * Derive an IndexedDB index name for a declared column group — a bare column\n * name for a single-column index, a deterministic collision-free encoding for a\n * compound one.\n *\n * @remarks\n * Naming a compound index by joining its columns with `_` (`['a', 'b'] →\n * 'a_b'`) collides with a single-column index over a column LITERALLY named\n * `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),\n * which either throws a native `ConstraintError` from a duplicate\n * `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based\n * lookup match the wrong index. A single-column index keeps the BARE column\n * name — {@link selectPlan} matches `available.includes(condition.column)` by\n * that exact name, so a single-column index must stay named after its column\n * verbatim. A compound index instead encodes each column as a LENGTH-PREFIXED\n * segment (`'2#1:a1:b'`), so the boundary between columns is self-describing\n * and cannot be reconstructed by any other column list — including one\n * containing a column that happens to look like an encoded segment.\n *\n * @param columns - The index's column group, in declared order\n * @returns The index name to pass to `createIndex` / read back from `indexNames`\n *\n * @example\n * ```ts\n * deriveIndexName(['age']) // 'age'\n * deriveIndexName(['a', 'b']) // '2#1:a1:b'\n * ```\n */\nexport function deriveIndexName(columns: readonly string[]): string {\n\tif (columns.length === 1) return columns[0]\n\treturn `${columns.length}#${columns.map((column) => `${column.length}:${column}`).join('')}`\n}\n","import type {\n\tCriteria,\n\tDriverInterface,\n\tDriverMeta,\n\tKey,\n\tMigration,\n\tMigrationStep,\n\tRow,\n\tTableSchema,\n} from '@src/core'\nimport {\n\tapplyCriteria,\n\tcompareValues,\n\tDatabaseError,\n\textractKey,\n\tisDriverMeta,\n\tmatchesCriteria,\n\tmigrateRows,\n} from '@src/core'\nimport type {\n\tIndexedDBDatabaseInterface,\n\tIndexedDBStoreInterface,\n\tIndexedDBUpgradeContext,\n\tStoreDefinition,\n} from '@orkestrel/indexeddb'\nimport { createIndexedDBDatabase, isIndexedDBError } from '@orkestrel/indexeddb'\nimport type { QueryPlan } from '../types.js'\nimport { deriveIndexName, mapIndexedDBError, mapMigrationError, selectPlan } from '../helpers.js'\nimport { META_STORE } from '../constants.js'\n\n/**\n * The IndexedDB {@link DriverInterface} — the persistent browser backend, built on\n * the published `@orkestrel/indexeddb` wrapper.\n *\n * @remarks\n * A thin adapter: it implements the storage primitives the core database layer\n * needs (`open` / `close` / `read` / `write` / `delete` / `keys` / `scan` / `clear`\n * / `snapshot`) by delegating to the wrapper's typed store operations — it never\n * touches raw IndexedDB. Rows are stored with **out-of-line keys** (the database\n * passes the key explicitly, `store.set(row, key)`), so each table is declared as a\n * key-path-less store. The wrapper opens in **auto-managed** mode (no fixed\n * version), creating any missing store on demand, so a table added to the schema is\n * created on the next open with no manual version bump. The driver's bulk reads\n * (`scan` / `keys`) use the wrapper's native `getAll` / `getAllKeys`, and `snapshot`\n * rolls back through one atomic wrapper transaction.\n *\n * It also implements the optional native `records` / `count` / `stream` hooks\n * (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link Criteria} into a\n * key-range pushdown over the primary key or a single-column secondary index,\n * fetching a candidate **superset** that the core engine (`applyCriteria` /\n * `matchesCriteria`) then refines — so a native read is byte-identical to a full\n * scan, just cheaper. Pushdown is conservative: only the exact-comparison\n * operators over orderable columns narrow to a range; everything else falls back\n * to a full scan + the engine.\n *\n * @remarks\n * This driver also implements `migrate` / `meta` / `stamp`. `meta` / `stamp`\n * persist the {@link DriverMeta} in a reserved out-of-line store,\n * {@link META_STORE} (`__meta__`) — excluded from a whole-store `snapshot`\n * capture, since it is driver bookkeeping, not caller data. `migrate` applies a\n * {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a\n * store, creating/dropping an index) is legal only inside a versionchange\n * transaction (`onupgradeneeded`), so `migrate` closes the current connection\n * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the\n * plan's steps — dropping stores, adding/removing indexes on the raw\n * `IDBTransaction`, and rewriting rows for `column.remove` via a cursor walk\n * (the one step needing to touch existing data; `column.add` is a no-op — this\n * driver stores whatever a row carries, so there is nothing to backfill). A\n * step referencing an unknown table is validated BEFORE the reconnect, so a\n * `MIGRATION` `DatabaseError` never wastes a version bump.\n *\n * @remarks\n * This unit deliberately OMITS `aggregate` / `transaction`. There is no native\n * `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed\n * `records` covers it. `transaction` is impossible here: the wrapper auto-commits\n * an `IDBTransaction` the moment control yields to a non-IDB `await`, so a\n * BEGIN-now / commit-or-rollback-later handle spanning arbitrary caller code\n * cannot be built on top of it — every atomic multi-op sequence in this driver\n * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.\n */\nexport class IndexedDBDriver implements DriverInterface {\n\treadonly #name: string\n\t#schema = new Map<string, TableSchema>()\n\t#database: IndexedDBDatabaseInterface | undefined\n\n\tconstructor(name: string) {\n\t\tthis.#name = name\n\t}\n\n\tasync open(schema: readonly TableSchema[]): Promise<void> {\n\t\t// The reserved meta store name may never collide with a caller-declared\n\t\t// table — it would silently corrupt this driver's own `meta`/`stamp`\n\t\t// bookkeeping (AGENTS §12 — a programmer error throws).\n\t\tif (schema.some((table) => table.name === META_STORE)) {\n\t\t\tthrow new DatabaseError(\n\t\t\t\t'VALIDATION',\n\t\t\t\t`open: table name '${META_STORE}' is reserved for driver metadata`,\n\t\t\t\t{ table: META_STORE },\n\t\t\t)\n\t\t}\n\t\ttry {\n\t\t\t// Reconnect cleanly so an auto-managed version bump (to create new\n\t\t\t// stores) is never blocked by this driver's own open handle.\n\t\t\tthis.#database?.close()\n\t\t\t// Build the new schema into a LOCAL map first — never mutate `#schema`\n\t\t\t// in place — so a reopen with a REDUCED schema replaces the map\n\t\t\t// wholesale instead of retaining ghost tables the caller no longer\n\t\t\t// declared.\n\t\t\tconst map = new Map<string, TableSchema>()\n\t\t\tfor (const table of schema) map.set(table.name, table)\n\t\t\tconst database = createIndexedDBDatabase({ name: this.#name, stores: this.#stores(map) })\n\t\t\tawait database.connect()\n\t\t\tthis.#database = database\n\t\t\t// Remember the schema so the native `records` / `count` / `stream` hooks\n\t\t\t// can plan a key-range pushdown (the primary key, column types, secondary\n\t\t\t// indexes).\n\t\t\tthis.#schema = map\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.#database?.close()\n\t\tthis.#database = undefined\n\t}\n\n\tasync read(table: string, key: Key): Promise<Row | undefined> {\n\t\ttry {\n\t\t\treturn await this.#store(table).get(key)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync write(table: string, key: Key, row: Row): Promise<void> {\n\t\ttry {\n\t\t\tawait this.#store(table).set(row, key)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync delete(table: string, key: Key): Promise<boolean> {\n\t\ttry {\n\t\t\tconst store = this.#store(table)\n\t\t\tconst present = await store.has(key)\n\t\t\tawait store.remove(key)\n\t\t\treturn present\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync keys(table: string): Promise<readonly Key[]> {\n\t\ttry {\n\t\t\tconst keys = await this.#store(table).keys()\n\t\t\treturn keys.filter((key): key is Key => typeof key === 'string' || typeof key === 'number')\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync *scan(table: string): AsyncIterable<Row> {\n\t\ttry {\n\t\t\tfor (const row of await this.#store(table).records()) yield row\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync clear(table: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.#store(table).clear()\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync records(table: string, criteria: Criteria): Promise<readonly Row[]> {\n\t\ttry {\n\t\t\tconst schema = this.#table(table)\n\t\t\tconst store = this.#store(table)\n\t\t\tconst plan = selectPlan(criteria, schema, store.indexes)\n\t\t\treturn applyCriteria(await this.#candidates(store, schema, plan), criteria)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t// A single-condition native count is exact — never a superset needing a\n\t// re-filter — because EVERY range `selectPlan` can produce excludes an\n\t// absent/null-valued row by construction: `equals`/`above`/`from`/`between`\n\t// are bounded below by a scalar (out-ranking absent/null in the engine's\n\t// total order, @src/core `compareValues`), and `below`/`to` are restricted by\n\t// `selectPlan` to the PRIMARY store, whose key is never absent/null. So a\n\t// native range count over a single condition already equals the engine's\n\t// `matchesCriteria` count for that condition — no row the range returns can\n\t// fail the condition, and no row the range omits could have passed it.\n\tasync count(table: string, criteria: Criteria): Promise<number> {\n\t\ttry {\n\t\t\tconst schema = this.#table(table)\n\t\t\tconst store = this.#store(table)\n\t\t\tconst conditions = criteria.conditions ?? []\n\t\t\tif (conditions.length === 0) return await store.count()\n\t\t\tconst plan = selectPlan(criteria, schema, store.indexes)\n\t\t\t// A single pushable condition is fully expressed by its range → native count.\n\t\t\tif (conditions.length === 1 && plan.range !== null) {\n\t\t\t\treturn plan.index === null\n\t\t\t\t\t? await store.count(plan.range)\n\t\t\t\t\t: await store.index(plan.index).count(plan.range)\n\t\t\t}\n\t\t\t// Otherwise the range is a superset (or a full scan) → engine filters exactly.\n\t\t\t// Order is irrelevant to a count, so the candidates need no re-sort.\n\t\t\tconst candidates =\n\t\t\t\tplan.index === null\n\t\t\t\t\t? await store.records(plan.range)\n\t\t\t\t\t: await store.index(plan.index).records(plan.range)\n\t\t\treturn candidates.reduce(\n\t\t\t\t(total, row) => (matchesCriteria(row, conditions) ? total + 1 : total),\n\t\t\t\t0,\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync *stream(table: string, criteria: Criteria): AsyncIterable<Row> {\n\t\ttry {\n\t\t\tconst schema = this.#table(table)\n\t\t\tconst store = this.#store(table)\n\t\t\tconst plan = selectPlan(criteria, schema, store.indexes)\n\t\t\tconst conditions = criteria.conditions ?? []\n\t\t\tconst offset = criteria.offset ?? 0\n\t\t\tconst limit = criteria.limit\n\t\t\tlet skipped = 0\n\t\t\tlet yielded = 0\n\t\t\tfor (const row of await this.#candidates(store, schema, plan)) {\n\t\t\t\tif (limit !== undefined && yielded >= limit) break\n\t\t\t\tif (!matchesCriteria(row, conditions)) continue\n\t\t\t\tif (skipped < offset) {\n\t\t\t\t\tskipped += 1\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tyielded += 1\n\t\t\t\tyield row\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync snapshot(tables?: readonly string[]): Promise<() => Promise<void>> {\n\t\ttry {\n\t\t\tconst database = this.#require()\n\t\t\t// A whole-store capture excludes the reserved meta store — it is driver\n\t\t\t// bookkeeping, not caller data, and rolling it back would undo a `stamp`\n\t\t\t// unrelated to the caller's snapshot scope. An explicit `tables` list is\n\t\t\t// caller-scoped already and passes through untouched.\n\t\t\tconst names = tables ?? database.stores.filter((name) => name !== META_STORE)\n\t\t\tconst captured = new Map<\n\t\t\t\tstring,\n\t\t\t\t{ readonly keys: readonly IDBValidKey[]; readonly rows: readonly Row[] }\n\t\t\t>()\n\t\t\tif (names.length > 0) {\n\t\t\t\t// Capture EVERY store inside ONE read transaction, so the snapshot is a\n\t\t\t\t// single point-in-time view — a concurrent writer can never leave the\n\t\t\t\t// capture straddling two different states (each store's keys/records\n\t\t\t\t// call previously ran in its OWN implicit transaction).\n\t\t\t\tawait database.read(names, async (transaction) => {\n\t\t\t\t\tfor (const name of names) {\n\t\t\t\t\t\tconst store = transaction.store(name)\n\t\t\t\t\t\tcaptured.set(name, { keys: await store.keys(), rows: await store.records() })\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst current = this.#require()\n\t\t\t\t\tconst restorable = names.filter((name) => current.stores.includes(name))\n\t\t\t\t\tif (restorable.length === 0) return\n\t\t\t\t\t// Restore every captured store in one transaction, so a rollback is atomic.\n\t\t\t\t\tawait current.write(restorable, async (transaction) => {\n\t\t\t\t\t\tfor (const name of restorable) {\n\t\t\t\t\t\t\tconst snapshot = captured.get(name)\n\t\t\t\t\t\t\tif (snapshot === undefined) continue\n\t\t\t\t\t\t\tconst store = transaction.store(name)\n\t\t\t\t\t\t\tawait store.clear()\n\t\t\t\t\t\t\tfor (let index = 0; index < snapshot.keys.length; index += 1) {\n\t\t\t\t\t\t\t\tawait store.set(snapshot.rows[index], snapshot.keys[index])\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthrow this.#wrap(error)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t/**\n\t * Return the persisted {@link DriverMeta}, or `undefined` when the store has\n\t * never been stamped.\n\t *\n\t * @remarks\n\t * Reads `'meta'` from the reserved {@link META_STORE}, narrowing the\n\t * structured-clone value with the core {@link isDriverMeta} guard (never\n\t * asserted, AGENTS §14) — a missing or malformed record returns `undefined`,\n\t * exactly like a fresh, never-stamped store.\n\t *\n\t * @returns The last-stamped {@link DriverMeta}, or `undefined`\n\t */\n\tasync meta(): Promise<DriverMeta | undefined> {\n\t\ttry {\n\t\t\tconst record = await this.#require().store(META_STORE).get('meta')\n\t\t\tif (!isDriverMeta(record)) return undefined\n\t\t\treturn record\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t/**\n\t * Persist `meta` verbatim for a later `meta()` to return.\n\t *\n\t * @param meta - The {@link DriverMeta} to persist\n\t */\n\tasync stamp(meta: DriverMeta): Promise<void> {\n\t\ttry {\n\t\t\tawait this.#require()\n\t\t\t\t.store(META_STORE)\n\t\t\t\t.set({ ...meta }, 'meta')\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t/**\n\t * Apply a {@link Migration} plan by reconnecting at a bumped version and\n\t * running the plan's steps inside the wrapper's `upgrade` hook.\n\t *\n\t * @remarks\n\t * IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes\n\t * the current connection and opens a FRESH one at `version + 1`, declaring\n\t * every currently-known store (plus {@link META_STORE}) so nothing is lost,\n\t * and applying `table.remove` / `index.add` / `index.remove` /\n\t * `column.remove` inside `upgrade`. Every step's `table` is validated against\n\t * the driver's own `#schema` BEFORE the reconnect — an unknown-table step\n\t * throws `DatabaseError` `MIGRATION` without ever bumping the version.\n\t * `table.add` / `column.add` need no upgrade-time action: `table.add` is\n\t * created by the wrapper's built-in create-missing-stores pass (its\n\t * definition is already in the declared `stores`), and this driver stores\n\t * whatever a row carries — there is nothing to backfill for a new column.\n\t * `#schema` bookkeeping is updated to match the applied plan, mirroring what\n\t * `open` tracks, so subsequent pushdown planning and a later `migrate` /\n\t * `open` see the new shape.\n\t *\n\t * @param plan - The migration plan to apply\n\t */\n\tasync migrate(plan: Migration): Promise<void> {\n\t\tfor (const step of plan.steps) {\n\t\t\tif (step.operation !== 'table.add' && !this.#schema.has(step.table)) {\n\t\t\t\tthrow new DatabaseError('MIGRATION', `migrate: unknown table '${step.table}'`, {\n\t\t\t\t\ttable: step.table,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tconst current = this.#require()\n\t\tconst version = current.version\n\t\t// Project the post-migration shape into a LOCAL copy first — `#schema`\n\t\t// stays untouched until the upgrade actually commits, so a mid-upgrade\n\t\t// failure never leaves the driver's bookkeeping ahead of the real database.\n\t\tconst schema = new Map(this.#schema)\n\t\tthis.#applySteps(schema, plan.steps)\n\t\tcurrent.close()\n\t\ttry {\n\t\t\tconst database = createIndexedDBDatabase({\n\t\t\t\tname: this.#name,\n\t\t\t\tversion: version + 1,\n\t\t\t\tstores: this.#stores(schema),\n\t\t\t\tupgrade: (context) => this.#upgrade(context, plan.steps),\n\t\t\t})\n\t\t\tawait database.connect()\n\t\t\t// Only on success: adopt the connection AND commit the local map.\n\t\t\tthis.#database = database\n\t\t\tthis.#schema = schema\n\t\t} catch (error) {\n\t\t\t// The old connection was closed to allow the versionchange attempt;\n\t\t\t// reconnect at the PRE-migration schema/version so the driver is left\n\t\t\t// usable, with `#schema` (and the real database) unchanged.\n\t\t\tawait this.#reopen()\n\t\t\t// Inside `migrate()`, an `UPGRADE` fault means THIS migration failed —\n\t\t\t// map it to `MIGRATION`, not the generic `DRIVER` every other caller gets\n\t\t\t// (see `mapMigrationError` @remarks).\n\t\t\tthrow isIndexedDBError(error) ? mapMigrationError(error) : error\n\t\t}\n\t}\n\n\t// === Private\n\n\t#require(): IndexedDBDatabaseInterface {\n\t\tif (this.#database === undefined) {\n\t\t\tthrow new DatabaseError('CLOSED', `IndexedDB database '${this.#name}' is not open`, {\n\t\t\t\tname: this.#name,\n\t\t\t})\n\t\t}\n\t\treturn this.#database\n\t}\n\n\t// The shared backend-fault boundary: no `IndexedDBError` may leak through\n\t// `DriverInterface` — every public method's `catch` routes here. A\n\t// `DatabaseError` this driver threw itself (the `CLOSED` gate, the\n\t// `NOT_FOUND` table guard, `migrate`'s own `MIGRATION` validation) passes\n\t// through unchanged; only a genuine backend `IndexedDBError` is remapped.\n\t#wrap(error: unknown): unknown {\n\t\treturn isIndexedDBError(error) ? mapIndexedDBError(error) : error\n\t}\n\n\t#store(table: string) {\n\t\treturn this.#require().store(table)\n\t}\n\n\t// Project a schema map into the wrapper's declared-stores shape — the\n\t// reserved meta store is always declared alongside every table, out-of-line\n\t// (keys are passed explicitly), with the declared secondary indexes becoming\n\t// each store's `createIndex` definitions. Shared by `open`, `migrate`, and\n\t// `#reopen` so the projection never drifts between them.\n\t#stores(schema: ReadonlyMap<string, TableSchema>): Record<string, StoreDefinition> {\n\t\tconst stores: Record<string, StoreDefinition> = { [META_STORE]: {} }\n\t\tfor (const table of schema.values()) {\n\t\t\tstores[table.name] = {\n\t\t\t\tindexes: table.indexes.map((columns) => ({\n\t\t\t\t\tname: deriveIndexName(columns),\n\t\t\t\t\tpath: columns.length === 1 ? columns[0] : [...columns],\n\t\t\t\t})),\n\t\t\t}\n\t\t}\n\t\treturn stores\n\t}\n\n\t// Reconnect at the CURRENT `#schema` with no version bump (auto-managed\n\t// mode, mirroring `open`) — used to restore a working connection after a\n\t// failed `migrate` left the prior connection closed.\n\tasync #reopen(): Promise<void> {\n\t\tconst database = createIndexedDBDatabase({\n\t\t\tname: this.#name,\n\t\t\tstores: this.#stores(this.#schema),\n\t\t})\n\t\tawait database.connect()\n\t\tthis.#database = database\n\t}\n\n\t// The candidate-superset read for a plan. The primary store already returns rows\n\t// in primary-key order — the same order `scan` yields, which `applyCriteria`\n\t// preserves for an unordered query. A secondary index returns them in INDEX-key\n\t// order, so re-sort by the primary key to reproduce scan order; the engine then\n\t// filters / orders / pages exactly, so a native read equals the scan path.\n\tasync #candidates(\n\t\tstore: IndexedDBStoreInterface,\n\t\tschema: TableSchema,\n\t\tplan: QueryPlan,\n\t): Promise<readonly Row[]> {\n\t\tif (plan.index === null) return store.records(plan.range)\n\t\tconst rows = [...(await store.index(plan.index).records(plan.range))]\n\t\trows.sort((left, right) =>\n\t\t\tcompareValues(extractKey(left, schema.primary), extractKey(right, schema.primary)),\n\t\t)\n\t\treturn rows\n\t}\n\n\t#table(name: string): TableSchema {\n\t\tconst schema = this.#schema.get(name)\n\t\tif (schema === undefined) {\n\t\t\tthrow new DatabaseError('NOT_FOUND', `table '${name}' is not declared`, { table: name })\n\t\t}\n\t\treturn schema\n\t}\n\n\t// Mirror a migration plan's steps onto a LOCAL schema map — the same\n\t// bookkeeping `open` does for a freshly declared schema — without touching\n\t// `#schema`, so a failed migrate never leaves the driver's bookkeeping ahead\n\t// of the real database. The caller commits the map into `#schema` only after\n\t// the upgrade connects successfully.\n\t#applySteps(schema: Map<string, TableSchema>, steps: readonly MigrationStep[]): void {\n\t\t// Deep-equal two index-column-group arrays (order-sensitive: an index over\n\t\t// `[a, b]` is not the same index as `[b, a]`) — mirrors planMigration's own\n\t\t// local `sameIndex` (src/core/helpers.ts).\n\t\tconst sameIndex = (left: readonly string[], right: readonly string[]): boolean =>\n\t\t\tleft.length === right.length && left.every((column, position) => column === right[position])\n\t\tfor (const step of steps) {\n\t\t\tswitch (step.operation) {\n\t\t\t\tcase 'table.add':\n\t\t\t\t\tif (!schema.has(step.table.name)) schema.set(step.table.name, step.table)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'table.remove':\n\t\t\t\t\tschema.delete(step.table)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'column.add': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (\n\t\t\t\t\t\ttable !== undefined &&\n\t\t\t\t\t\t!table.columns.some((column) => column.name === step.column.name)\n\t\t\t\t\t) {\n\t\t\t\t\t\tschema.set(step.table, { ...table, columns: [...table.columns, step.column] })\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'column.remove': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (table !== undefined) {\n\t\t\t\t\t\tschema.set(step.table, {\n\t\t\t\t\t\t\t...table,\n\t\t\t\t\t\t\tcolumns: table.columns.filter((column) => column.name !== step.column),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'index.add': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (table !== undefined) {\n\t\t\t\t\t\tschema.set(step.table, { ...table, indexes: [...table.indexes, step.index] })\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'index.remove': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (table !== undefined) {\n\t\t\t\t\t\tschema.set(step.table, {\n\t\t\t\t\t\t\t...table,\n\t\t\t\t\t\t\tindexes: table.indexes.filter((index) => !sameIndex(index, step.index)),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Runs INSIDE the wrapper's versionchange transaction (see `migrate`\n\t// @remarks). `table.add` / `column.add` are no-ops here — see `migrate`\n\t// @remarks for why. `column.remove` is the one step touching existing rows:\n\t// it walks a live cursor and rewrites each row through the core\n\t// `migrateRows`, updating in place — the only IDB-await-only work permitted\n\t// inside an upgrade transaction.\n\tasync #upgrade(context: IndexedDBUpgradeContext, steps: readonly MigrationStep[]): Promise<void> {\n\t\tfor (const step of steps) {\n\t\t\tswitch (step.operation) {\n\t\t\t\tcase 'table.remove':\n\t\t\t\t\tcontext.drop(step.table)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'index.add': {\n\t\t\t\t\tconst name = deriveIndexName(step.index)\n\t\t\t\t\tconst path = step.index.length === 1 ? step.index[0] : [...step.index]\n\t\t\t\t\tcontext.transaction.objectStore(step.table).createIndex(name, path)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'index.remove':\n\t\t\t\t\tcontext.transaction.objectStore(step.table).deleteIndex(deriveIndexName(step.index))\n\t\t\t\t\tbreak\n\t\t\t\tcase 'column.remove': {\n\t\t\t\t\tconst store = context.store(step.table)\n\t\t\t\t\tlet cursor = await store.cursor()\n\t\t\t\t\twhile (cursor !== null) {\n\t\t\t\t\t\tconst [migrated] = migrateRows([cursor.value], [step])\n\t\t\t\t\t\tawait cursor.update(migrated)\n\t\t\t\t\t\tcursor = await cursor.continue()\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'table.add':\n\t\t\t\tcase 'column.add':\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n","import type { DriverInterface } from '@src/core'\nimport { IndexedDBDriver } from './drivers/IndexedDBDriver.js'\n\n/**\n * Create a persistent IndexedDB {@link DriverInterface} for the core database layer.\n *\n * @remarks\n * Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +\n * relations stack against IndexedDB instead of memory — the `Database` / `Table` /\n * `Query` / relations API is unchanged; only where the bytes live changes. The\n * driver is built on the published `@orkestrel/indexeddb` wrapper in auto-managed\n * mode, so a table added to the `tables` map is created on the next open with no\n * version bump. This unit omits `transaction` / `aggregate` (see\n * {@link IndexedDBDriver} `@remarks`).\n *\n * @param name - The IndexedDB database name to open or create\n * @returns A {@link DriverInterface} backed by IndexedDB\n *\n * @example\n * ```ts\n * import { createDatabase } from '@orkestrel/database'\n * import { stringShape } from '@orkestrel/contract'\n * import { createIndexedDBDriver } from '@orkestrel/database/browser'\n *\n * const db = createDatabase({\n * \tdriver: createIndexedDBDriver('app'),\n * \ttables: { users: { id: stringShape(), name: stringShape() } },\n * })\n * await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to IndexedDB\n * ```\n */\nexport function createIndexedDBDriver(name: string): DriverInterface {\n\treturn new IndexedDBDriver(name)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#name","#database","#stores","#schema","#wrap","#store","#table","#candidates","#require","#applySteps","#upgrade","#reopen"],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/drivers/IndexedDBDriver.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { ColumnType } from '@src/core'\n\n// The column types that are valid, orderable IndexedDB keys (string / number key\n// space). `boolean` / `json` / `blob` are not valid `IDBValidKey`s and would make\n// a range silently miss rows, so they are never pushed down.\nexport const INDEXABLE_TYPES: ReadonlySet<ColumnType> = new Set<ColumnType>([\n\t'text',\n\t'integer',\n\t'real',\n])\n\n// The reserved out-of-line store the driver stamps its DriverMeta into\n// (`meta` / `stamp`). A user table declared with this exact name would\n// collide with the driver's own bookkeeping — callers must avoid it.\nexport const META_STORE = '__meta__'\n","import type { Condition, Criteria, TableSchema } from '@src/core'\nimport type { IndexedDBError } from '@orkestrel/indexeddb'\nimport type { QueryPlan } from './types.js'\nimport { compareValues, DatabaseError } from '@src/core'\nimport { range } from '@orkestrel/indexeddb'\nimport { INDEXABLE_TYPES } from './constants.js'\n\n// The IndexedDB driver's pushdown planner. A pure function over the portable\n// `Criteria`: it decides which index (or the primary key) a read can narrow on\n// and the `IDBKeyRange` to use, so the driver fetches a candidate SUPERSET rather\n// than every row. The core engine then refines that superset to the exact result\n// — so a plan is only ever allowed to over-fetch, never to drop a matching row.\n// Anything it cannot prove range-exact (a non-comparison operator, a non-orderable\n// column type, a nested path, a non-scalar operand) falls through to a full scan.\n\n/**\n * The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one\n * of the six exact key comparisons over scalar operands — else `null`.\n *\n * @remarks\n * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)\n * translate to a key range that a typed (string/number) column can back with an\n * IndexedDB store/index read — see {@link selectPlan} for the caveats that\n * decide WHICH of `below`/`to` may drive a SECONDARY-index read versus the\n * primary store only (a column-type / absent-row concern, not a range-shape\n * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;\n * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/\n * `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand\n * guard (`typeof` string/number) rejects a non-scalar value (e.g. an array, a\n * boolean) that is not a usable key. `between` additionally guards against a\n * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw\n * `DataError` `DOMException` for a lower bound above the upper bound, so a\n * reversed pair returns `null` here (falls back to a full scan, which the\n * engine then correctly resolves to an empty result) rather than letting a\n * native exception escape untyped — the same defensive posture as every other\n * backend, which returns empty for a reversed/empty range instead of throwing.\n * The switch is exhaustive over every {@link ConditionOperator}, so a new\n * operator forces a deliberate decision here rather than silently defaulting\n * to a (possibly lossy) range.\n *\n * @param condition - The condition to translate\n * @returns Its exact key range, or `null` when the operator/operands cannot push\n */\nexport function conditionRange(condition: Condition): IDBKeyRange | null {\n\tconst first = condition.values[0]\n\tconst second = condition.values[1]\n\tswitch (condition.operator) {\n\t\tcase 'equals':\n\t\t\treturn isKey(first) ? range.only(first) : null\n\t\tcase 'above':\n\t\t\treturn isKey(first) ? range.above(first) : null\n\t\tcase 'below':\n\t\t\treturn isKey(first) ? range.below(first) : null\n\t\tcase 'from':\n\t\t\treturn isKey(first) ? range.from(first) : null\n\t\tcase 'to':\n\t\t\treturn isKey(first) ? range.to(first) : null\n\t\tcase 'between':\n\t\t\t// A reversed pair (`first > second`) has no valid IDBKeyRange — native\n\t\t\t// `bound` throws rather than returning empty. Fall back to a full scan\n\t\t\t// instead of letting that DOMException escape (the engine then yields\n\t\t\t// the correct, empty result over `compareValues(value, first) >= 0 &&\n\t\t\t// compareValues(value, second) <= 0`, which no row can satisfy).\n\t\t\treturn isKey(first) && isKey(second) && compareValues(first, second) <= 0\n\t\t\t\t? range.between(first, second)\n\t\t\t\t: null\n\t\tcase 'not':\n\t\tcase 'like':\n\t\tcase 'glob':\n\t\tcase 'starts':\n\t\tcase 'ends':\n\t\tcase 'any':\n\t\tcase 'none':\n\t\tcase 'absent':\n\t\tcase 'present':\n\t\t\treturn null\n\t}\n}\n\n// A scalar IndexedDB key operand — a string or number (the core `Key` space).\nexport function isKey(value: unknown): value is string | number {\n\treturn typeof value === 'string' || typeof value === 'number'\n}\n\n/**\n * Plan an IndexedDB read for a {@link Criteria} — pick the index (or the primary\n * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.\n *\n * @remarks\n * Pushdown is sound ONLY when every condition is `and`-joined: the engine folds\n * conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of\n * each — narrowing on any one is then a valid superset. A single `or` breaks that\n * (a row can match through a later condition the range would exclude), so any `or`\n * forces a full scan. Otherwise it scans the conditions in order and selects the\n * **first** one that is provably range-exact and backed by a key: a comparison\n * operator (`conditionRange`) over a single, orderable (`text`/`integer`/`real`)\n * column that is either the table's primary key (read the store directly, `index:\n * null`) or has a single-column secondary index (named exactly the column — read\n * that index). A condition whose column is a nested {@link FieldPath} array\n * (descends a json value, not a key), is absent from the schema, is a non-orderable\n * type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a\n * non-scalar operand cannot push and is skipped.\n *\n * **`below`/`to` may drive a SECONDARY-index range only when the column has NO\n * absent/null rows to lose — which this planner cannot verify from the schema\n * alone, so it restricts them to the PRIMARY store, where that is always true.**\n * The engine's total order (`compareValues`, see `@src/core`) ranks\n * `undefined` (absent) and `null` BELOW every number/string, so\n * `matchesCondition('below' | 'to', …)` is TRUE for a row whose field is absent\n * or `null` — but a secondary IndexedDB index has NO ENTRY for a row whose\n * indexed field is absent/`null`, so a `below`/`to` range read against that\n * index would SILENTLY DROP those rows (they can never be over-fetched, only\n * missed — the one shape of lossiness this planner must never produce). The\n * table's PRIMARY key is exempt: a row's primary-key value is always present\n * and never `null` (it is the row's identity, enforced at write time), so a\n * `below`/`to` range against the primary store can never exclude an\n * absent/null-keyed row because no such row exists. `equals`/`above`/`from`/\n * `between` stay index-eligible on ANY orderable column, primary or secondary:\n * each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/\n * `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`\n * in the total order, so an absent/null-valued row can never satisfy them — the\n * index's silence on such a row is harmless (it was never going to match).\n * **Declared-type trust caveat:** this reasoning holds under the contract that\n * an {@link INDEXABLE_TYPES} column, once contract-validated at write time,\n * holds only `string | number | null` (or is absent) — never some other\n * runtime value that could rank differently; a driver bypassing the write\n * contract (writing raw rows directly to the store) could defeat this\n * argument, but that is out of scope for a planner reading validated schema\n * metadata.\n *\n * When no condition qualifies the plan is a full scan (`{ index: null, range:\n * null }`) and the engine does everything. The plan is always a SUPERSET of the\n * matching rows — the only correctness contract — so the driver may safely run\n * the exact engine over it.\n *\n * @param criteria - The read specification (its `conditions` drive the plan), or\n * `undefined` for an unconditional read\n * @param schema - The table's schema — its `primary` key and column types\n * @param available - The secondary-index names that physically exist on the store\n * (`store.indexes`); a single-column index is named exactly its column\n * @returns The index + range to read, narrowing to a superset (never lossy)\n *\n * @example\n * ```ts\n * selectPlan({ conditions: [eq('id', 'u1')] }, schema, []) // { index: null, range: only('u1') }\n * selectPlan({ conditions: [from('age', 18)] }, schema, ['age']) // { index: 'age', range: from(18) }\n * selectPlan({ conditions: [contains('name', 'a')] }, schema, []) // { index: null, range: null }\n * ```\n */\nexport function selectPlan(\n\tcriteria: Criteria | undefined,\n\tschema: TableSchema,\n\tavailable: readonly string[],\n): QueryPlan {\n\tconst conditions = criteria?.conditions ?? []\n\t// A single condition's range is a SUPERSET of the result only when the result\n\t// implies that condition — which holds iff every condition is `and`-joined (the\n\t// fold is `c1 && c2 && … && cn`, so the result is a subset of each). A single\n\t// `or` breaks that (a row can match via a later condition the range excludes),\n\t// so any `or` forces a full scan. The first condition's connector only seeds the\n\t// fold and is ignored (AGENTS — the `Condition.connector` contract).\n\tif (conditions.slice(1).some((condition) => condition.connector === 'or')) {\n\t\treturn { index: null, range: null }\n\t}\n\tfor (const condition of conditions) {\n\t\t// An array column is a nested FieldPath into a json value — not a key.\n\t\tif (typeof condition.column !== 'string') continue\n\t\tconst column = schema.columns.find((candidate) => candidate.name === condition.column)\n\t\tif (column === undefined || !INDEXABLE_TYPES.has(column.type)) continue\n\t\tconst keyRange = conditionRange(condition)\n\t\tif (keyRange === null) continue\n\t\tif (condition.column === schema.primary) return { index: null, range: keyRange }\n\t\t// `below`/`to` can silently drop an absent/null-valued row from a SECONDARY\n\t\t// index (see @remarks) — only the primary store (handled above) is safe.\n\t\t// Keep scanning: a later condition may still qualify.\n\t\tif (condition.operator === 'below' || condition.operator === 'to') continue\n\t\tif (available.includes(condition.column)) return { index: condition.column, range: keyRange }\n\t\t// The column is range-exact but has no usable index — keep looking.\n\t}\n\treturn { index: null, range: null }\n}\n\n/**\n * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy\n * — the default mapping used everywhere except inside `migrate()`.\n *\n * @remarks\n * No backend fault may leak through `DriverInterface` as a raw `IndexedDBError`.\n * `CONSTRAINT` (a unique-key violation) is a `CONFLICT` — the same code every\n * other backend uses for a duplicate key. `CLOSED`/`NOT_OPEN`/`INVALID` (the\n * connection is gone, never opened, or the native handle is stale) collapse to\n * `CLOSED`. `QUOTA` and `BLOCKED` are genuine infrastructure faults (`DRIVER`),\n * carrying a machine-readable `context.code` (`'QUOTA'` / `'BLOCKED'`) so a\n * caller can branch without parsing the message; `BLOCKED` additionally marks\n * `context.retryable: true` — a concurrent connection holding the database open\n * is a transient condition, not a permanent one. Every other code (`UPGRADE`\n * here — see {@link mapMigrationError} for the `migrate()`-only remapping to\n * `MIGRATION` — `ABORTED`, `NOT_FOUND`, `DATA`, `OPEN`, `INACTIVE`, `READONLY`,\n * `UNKNOWN`) is an unexpected infrastructure fault and maps to `DRIVER` — the\n * driver opens its own readwrite transactions, so a `READONLY` fault can only\n * mean the backend behaved unexpectedly. The original error is always\n * preserved as `context.cause` for diagnostics.\n *\n * @param error - The backend error to translate\n * @returns The portable `DatabaseError`\n */\nexport function mapIndexedDBError(error: IndexedDBError): DatabaseError {\n\tswitch (error.code) {\n\t\tcase 'CONSTRAINT':\n\t\t\treturn new DatabaseError('CONFLICT', error.message, { cause: error })\n\t\tcase 'CLOSED':\n\t\tcase 'NOT_OPEN':\n\t\tcase 'INVALID':\n\t\t\treturn new DatabaseError('CLOSED', error.message, { cause: error })\n\t\tcase 'QUOTA':\n\t\t\treturn new DatabaseError('DRIVER', error.message, { cause: error, code: 'QUOTA' })\n\t\tcase 'BLOCKED':\n\t\t\treturn new DatabaseError('DRIVER', error.message, {\n\t\t\t\tcause: error,\n\t\t\t\tcode: 'BLOCKED',\n\t\t\t\tretryable: true,\n\t\t\t})\n\t\tcase 'UPGRADE':\n\t\tcase 'ABORTED':\n\t\tcase 'NOT_FOUND':\n\t\tcase 'DATA':\n\t\tcase 'OPEN':\n\t\tcase 'INACTIVE':\n\t\tcase 'READONLY':\n\t\tcase 'UNKNOWN':\n\t\t\treturn new DatabaseError('DRIVER', error.message, { cause: error })\n\t}\n}\n\n/**\n * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy\n * for use INSIDE `migrate()` — the one context where `UPGRADE` means the\n * migration itself failed, not a generic driver fault.\n *\n * @remarks\n * `migrate()` reconnects at a bumped version inside `onupgradeneeded`; a\n * rejection there (an inapplicable step, a native `ConstraintError` from a\n * duplicate index, …) surfaces as `IndexedDBError` `UPGRADE` and must become a\n * `MIGRATION` `DatabaseError` so a caller can distinguish \"this migration plan\n * failed\" from \"the driver hit an unrelated infrastructure fault\". Every other\n * code defers to {@link mapIndexedDBError} unchanged.\n *\n * @param error - The backend error to translate\n * @returns The portable `DatabaseError`\n */\nexport function mapMigrationError(error: IndexedDBError): DatabaseError {\n\tif (error.code === 'UPGRADE') {\n\t\treturn new DatabaseError('MIGRATION', error.message, { cause: error })\n\t}\n\treturn mapIndexedDBError(error)\n}\n\n/**\n * Derive an IndexedDB index name for a declared column group — a bare column\n * name for a single-column index, a deterministic collision-free encoding for a\n * compound one.\n *\n * @remarks\n * Naming a compound index by joining its columns with `_` (`['a', 'b'] →\n * 'a_b'`) collides with a single-column index over a column LITERALLY named\n * `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),\n * which either throws a native `ConstraintError` from a duplicate\n * `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based\n * lookup match the wrong index. A single-column index keeps the BARE column\n * name — {@link selectPlan} matches `available.includes(condition.column)` by\n * that exact name, so a single-column index must stay named after its column\n * verbatim. A compound index instead encodes each column as a LENGTH-PREFIXED\n * segment (`'2#1:a1:b'`), so the boundary between columns is self-describing\n * and cannot be reconstructed by any other column list — including one\n * containing a column that happens to look like an encoded segment.\n *\n * @param columns - The index's column group, in declared order\n * @returns The index name to pass to `createIndex` / read back from `indexNames`\n *\n * @example\n * ```ts\n * deriveIndexName(['age']) // 'age'\n * deriveIndexName(['a', 'b']) // '2#1:a1:b'\n * ```\n */\nexport function deriveIndexName(columns: readonly string[]): string {\n\tconst [column] = columns\n\tif (columns.length === 1 && column !== undefined) return column\n\treturn `${columns.length}#${columns.map((part) => `${part.length}:${part}`).join('')}`\n}\n","import type {\n\tCriteria,\n\tDriverInterface,\n\tDriverMeta,\n\tKey,\n\tMigration,\n\tMigrationStep,\n\tRow,\n\tTableSchema,\n} from '@src/core'\nimport {\n\tapplyCriteria,\n\tcompareValues,\n\tDatabaseError,\n\tdeepEqual,\n\textractKey,\n\tisDriverMeta,\n\tmatchesCriteria,\n\tmigrateRows,\n} from '@src/core'\nimport type {\n\tIndexedDBDatabaseInterface,\n\tIndexedDBStoreInterface,\n\tIndexedDBUpgradeContext,\n\tStoreDefinition,\n} from '@orkestrel/indexeddb'\nimport { createIndexedDBDatabase, isIndexedDBError } from '@orkestrel/indexeddb'\nimport type { QueryPlan } from '../types.js'\nimport { deriveIndexName, mapIndexedDBError, mapMigrationError, selectPlan } from '../helpers.js'\nimport { META_STORE } from '../constants.js'\n\n/**\n * The IndexedDB {@link DriverInterface} — the persistent browser backend, built on\n * the published `@orkestrel/indexeddb` wrapper.\n *\n * @remarks\n * A thin adapter: it implements the storage primitives the core database layer\n * needs (`open` / `close` / `read` / `write` / `delete` / `keys` / `scan` / `clear`\n * / `snapshot`) by delegating to the wrapper's typed store operations — it never\n * touches raw IndexedDB. Rows are stored with **out-of-line keys** (the database\n * passes the key explicitly, `store.set(row, key)`), so each table is declared as a\n * key-path-less store. The wrapper opens in **auto-managed** mode (no fixed\n * version), creating any missing store on demand, so a table added to the schema is\n * created on the next open with no manual version bump. The driver's bulk reads\n * (`scan` / `keys`) use the wrapper's native `getAll` / `getAllKeys`, and `snapshot`\n * rolls back through one atomic wrapper transaction.\n *\n * It also implements the optional native `records` / `count` / `stream` hooks\n * (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link Criteria} into a\n * key-range pushdown over the primary key or a single-column secondary index,\n * fetching a candidate **superset** that the core engine (`applyCriteria` /\n * `matchesCriteria`) then refines — so a native read is byte-identical to a full\n * scan, just cheaper. Pushdown is conservative: only the exact-comparison\n * operators over orderable columns narrow to a range; everything else falls back\n * to a full scan + the engine.\n *\n * @remarks\n * This driver also implements `migrate` / `meta` / `stamp`. `meta` / `stamp`\n * persist the {@link DriverMeta} in a reserved out-of-line store,\n * {@link META_STORE} (`__meta__`) — excluded from a whole-store `snapshot`\n * capture, since it is driver bookkeeping, not caller data. `migrate` applies a\n * {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a\n * store, creating/dropping an index) is legal only inside a versionchange\n * transaction (`onupgradeneeded`), so `migrate` closes the current connection\n * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the\n * plan's steps — dropping stores, adding/removing indexes on the raw\n * `IDBTransaction`, and rewriting rows for `column.remove` via a cursor walk\n * (the one step needing to touch existing data; `column.add` is a no-op — this\n * driver stores whatever a row carries, so there is nothing to backfill). A\n * step referencing an unknown table is validated BEFORE the reconnect, so a\n * `MIGRATION` `DatabaseError` never wastes a version bump.\n *\n * @remarks\n * This unit deliberately OMITS `aggregate` / `transaction`. There is no native\n * `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed\n * `records` covers it. `transaction` is impossible here: the wrapper auto-commits\n * an `IDBTransaction` the moment control yields to a non-IDB `await`, so a\n * BEGIN-now / commit-or-rollback-later handle spanning arbitrary caller code\n * cannot be built on top of it — every atomic multi-op sequence in this driver\n * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.\n */\nexport class IndexedDBDriver implements DriverInterface {\n\treadonly #name: string\n\t#schema = new Map<string, TableSchema>()\n\t#database: IndexedDBDatabaseInterface | undefined\n\n\tconstructor(name: string) {\n\t\tthis.#name = name\n\t}\n\n\tasync open(schema: readonly TableSchema[]): Promise<void> {\n\t\t// The reserved meta store name may never collide with a caller-declared\n\t\t// table — it would silently corrupt this driver's own `meta`/`stamp`\n\t\t// bookkeeping (AGENTS §12 — a programmer error throws).\n\t\tif (schema.some((table) => table.name === META_STORE)) {\n\t\t\tthrow new DatabaseError(\n\t\t\t\t'VALIDATION',\n\t\t\t\t`open: table name '${META_STORE}' is reserved for driver metadata`,\n\t\t\t\t{ table: META_STORE },\n\t\t\t)\n\t\t}\n\t\ttry {\n\t\t\t// Reconnect cleanly so an auto-managed version bump (to create new\n\t\t\t// stores) is never blocked by this driver's own open handle.\n\t\t\tthis.#database?.close()\n\t\t\t// Build the new schema into a LOCAL map first — never mutate `#schema`\n\t\t\t// in place — so a reopen with a REDUCED schema replaces the map\n\t\t\t// wholesale instead of retaining ghost tables the caller no longer\n\t\t\t// declared.\n\t\t\tconst map = new Map<string, TableSchema>()\n\t\t\tfor (const table of schema) map.set(table.name, table)\n\t\t\tconst database = createIndexedDBDatabase({ name: this.#name, stores: this.#stores(map) })\n\t\t\tawait database.connect()\n\t\t\tthis.#database = database\n\t\t\t// Remember the schema so the native `records` / `count` / `stream` hooks\n\t\t\t// can plan a key-range pushdown (the primary key, column types, secondary\n\t\t\t// indexes).\n\t\t\tthis.#schema = map\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.#database?.close()\n\t\tthis.#database = undefined\n\t}\n\n\tasync read(table: string, key: Key): Promise<Row | undefined> {\n\t\ttry {\n\t\t\treturn await this.#store(table).get(key)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync write(table: string, key: Key, row: Row): Promise<void> {\n\t\ttry {\n\t\t\tawait this.#store(table).set(row, key)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync delete(table: string, key: Key): Promise<boolean> {\n\t\ttry {\n\t\t\tconst store = this.#store(table)\n\t\t\tconst present = await store.has(key)\n\t\t\tawait store.remove(key)\n\t\t\treturn present\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync keys(table: string): Promise<readonly Key[]> {\n\t\ttry {\n\t\t\tconst keys = await this.#store(table).keys()\n\t\t\treturn keys.filter((key): key is Key => typeof key === 'string' || typeof key === 'number')\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync *scan(table: string): AsyncIterable<Row> {\n\t\ttry {\n\t\t\tfor (const row of await this.#store(table).records()) yield row\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync clear(table: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.#store(table).clear()\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync records(table: string, criteria: Criteria): Promise<readonly Row[]> {\n\t\ttry {\n\t\t\tconst schema = this.#table(table)\n\t\t\tconst store = this.#store(table)\n\t\t\tconst plan = selectPlan(criteria, schema, store.indexes)\n\t\t\treturn applyCriteria(await this.#candidates(store, schema, plan), criteria)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t// A single-condition native count is exact — never a superset needing a\n\t// re-filter — because EVERY range `selectPlan` can produce excludes an\n\t// absent/null-valued row by construction: `equals`/`above`/`from`/`between`\n\t// are bounded below by a scalar (out-ranking absent/null in the engine's\n\t// total order, @src/core `compareValues`), and `below`/`to` are restricted by\n\t// `selectPlan` to the PRIMARY store, whose key is never absent/null. So a\n\t// native range count over a single condition already equals the engine's\n\t// `matchesCriteria` count for that condition — no row the range returns can\n\t// fail the condition, and no row the range omits could have passed it.\n\tasync count(table: string, criteria: Criteria): Promise<number> {\n\t\ttry {\n\t\t\tconst schema = this.#table(table)\n\t\t\tconst store = this.#store(table)\n\t\t\tconst conditions = criteria.conditions ?? []\n\t\t\tif (conditions.length === 0) return await store.count()\n\t\t\tconst plan = selectPlan(criteria, schema, store.indexes)\n\t\t\t// A single pushable condition is fully expressed by its range → native count.\n\t\t\tif (conditions.length === 1 && plan.range !== null) {\n\t\t\t\treturn plan.index === null\n\t\t\t\t\t? await store.count(plan.range)\n\t\t\t\t\t: await store.index(plan.index).count(plan.range)\n\t\t\t}\n\t\t\t// Otherwise the range is a superset (or a full scan) → engine filters exactly.\n\t\t\t// Order is irrelevant to a count, so the candidates need no re-sort.\n\t\t\tconst candidates =\n\t\t\t\tplan.index === null\n\t\t\t\t\t? await store.records(plan.range)\n\t\t\t\t\t: await store.index(plan.index).records(plan.range)\n\t\t\treturn candidates.reduce(\n\t\t\t\t(total, row) => (matchesCriteria(row, conditions) ? total + 1 : total),\n\t\t\t\t0,\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync *stream(table: string, criteria: Criteria): AsyncIterable<Row> {\n\t\ttry {\n\t\t\tconst schema = this.#table(table)\n\t\t\tconst store = this.#store(table)\n\t\t\tconst plan = selectPlan(criteria, schema, store.indexes)\n\t\t\tconst conditions = criteria.conditions ?? []\n\t\t\tconst offset = criteria.offset ?? 0\n\t\t\tconst limit = criteria.limit\n\t\t\tlet skipped = 0\n\t\t\tlet yielded = 0\n\t\t\tfor (const row of await this.#candidates(store, schema, plan)) {\n\t\t\t\tif (limit !== undefined && yielded >= limit) break\n\t\t\t\tif (!matchesCriteria(row, conditions)) continue\n\t\t\t\tif (skipped < offset) {\n\t\t\t\t\tskipped += 1\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tyielded += 1\n\t\t\t\tyield row\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\tasync snapshot(tables?: readonly string[]): Promise<() => Promise<void>> {\n\t\ttry {\n\t\t\tconst database = this.#require()\n\t\t\t// A whole-store capture excludes the reserved meta store — it is driver\n\t\t\t// bookkeeping, not caller data, and rolling it back would undo a `stamp`\n\t\t\t// unrelated to the caller's snapshot scope. An explicit `tables` list is\n\t\t\t// caller-scoped already and passes through untouched.\n\t\t\tconst names = tables ?? database.stores.filter((name) => name !== META_STORE)\n\t\t\tconst captured = new Map<\n\t\t\t\tstring,\n\t\t\t\t{ readonly keys: readonly IDBValidKey[]; readonly rows: readonly Row[] }\n\t\t\t>()\n\t\t\tif (names.length > 0) {\n\t\t\t\t// Capture EVERY store inside ONE read transaction, so the snapshot is a\n\t\t\t\t// single point-in-time view — a concurrent writer can never leave the\n\t\t\t\t// capture straddling two different states (each store's keys/records\n\t\t\t\t// call previously ran in its OWN implicit transaction).\n\t\t\t\tawait database.read(names, async (transaction) => {\n\t\t\t\t\tfor (const name of names) {\n\t\t\t\t\t\tconst store = transaction.store(name)\n\t\t\t\t\t\tcaptured.set(name, { keys: await store.keys(), rows: await store.records() })\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst current = this.#require()\n\t\t\t\t\tconst restorable = names.filter((name) => current.stores.includes(name))\n\t\t\t\t\tif (restorable.length === 0) return\n\t\t\t\t\t// Restore every captured store in one transaction, so a rollback is atomic.\n\t\t\t\t\tawait current.write(restorable, async (transaction) => {\n\t\t\t\t\t\tfor (const name of restorable) {\n\t\t\t\t\t\t\tconst snapshot = captured.get(name)\n\t\t\t\t\t\t\tif (snapshot === undefined) continue\n\t\t\t\t\t\t\tconst store = transaction.store(name)\n\t\t\t\t\t\t\tawait store.clear()\n\t\t\t\t\t\t\tfor (let index = 0; index < snapshot.keys.length; index += 1) {\n\t\t\t\t\t\t\t\tconst row = snapshot.rows[index]\n\t\t\t\t\t\t\t\tconst key = snapshot.keys[index]\n\t\t\t\t\t\t\t\tif (row === undefined || key === undefined) {\n\t\t\t\t\t\t\t\t\tthrow new DatabaseError('DRIVER', 'IndexedDB snapshot entry is incomplete', {\n\t\t\t\t\t\t\t\t\t\ttable: name,\n\t\t\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tawait store.set(row, key)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthrow this.#wrap(error)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t/**\n\t * Return the persisted {@link DriverMeta}, or `undefined` when the store has\n\t * never been stamped.\n\t *\n\t * @remarks\n\t * Reads `'meta'` from the reserved {@link META_STORE}, narrowing the\n\t * structured-clone value with the core {@link isDriverMeta} guard (never\n\t * asserted, AGENTS §14) — a missing or malformed record returns `undefined`,\n\t * exactly like a fresh, never-stamped store.\n\t *\n\t * @returns The last-stamped {@link DriverMeta}, or `undefined`\n\t */\n\tasync meta(): Promise<DriverMeta | undefined> {\n\t\ttry {\n\t\t\tconst record = await this.#require().store(META_STORE).get('meta')\n\t\t\tif (!isDriverMeta(record)) return undefined\n\t\t\treturn record\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t/**\n\t * Persist `meta` verbatim for a later `meta()` to return.\n\t *\n\t * @param meta - The {@link DriverMeta} to persist\n\t */\n\tasync stamp(meta: DriverMeta): Promise<void> {\n\t\ttry {\n\t\t\tawait this.#require()\n\t\t\t\t.store(META_STORE)\n\t\t\t\t.set({ ...meta }, 'meta')\n\t\t} catch (error) {\n\t\t\tthrow this.#wrap(error)\n\t\t}\n\t}\n\n\t/**\n\t * Apply a {@link Migration} plan by reconnecting at a bumped version and\n\t * running the plan's steps inside the wrapper's `upgrade` hook.\n\t *\n\t * @remarks\n\t * IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes\n\t * the current connection and opens a FRESH one at `version + 1`, declaring\n\t * every currently-known store (plus {@link META_STORE}) so nothing is lost,\n\t * and applying `table.remove` / `index.add` / `index.remove` /\n\t * `column.remove` inside `upgrade`. Every step's `table` is validated against\n\t * the driver's own `#schema` BEFORE the reconnect — an unknown-table step\n\t * throws `DatabaseError` `MIGRATION` without ever bumping the version.\n\t * `table.add` / `column.add` need no upgrade-time action: `table.add` is\n\t * created by the wrapper's built-in create-missing-stores pass (its\n\t * definition is already in the declared `stores`), and this driver stores\n\t * whatever a row carries — there is nothing to backfill for a new column.\n\t * `#schema` bookkeeping is updated to match the applied plan, mirroring what\n\t * `open` tracks, so subsequent pushdown planning and a later `migrate` /\n\t * `open` see the new shape.\n\t *\n\t * @param plan - The migration plan to apply\n\t */\n\tasync migrate(plan: Migration): Promise<void> {\n\t\tfor (const step of plan.steps) {\n\t\t\tif (step.operation !== 'table.add' && !this.#schema.has(step.table)) {\n\t\t\t\tthrow new DatabaseError('MIGRATION', `migrate: unknown table '${step.table}'`, {\n\t\t\t\t\ttable: step.table,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tconst current = this.#require()\n\t\tconst version = current.version\n\t\t// Project the post-migration shape into a LOCAL copy first — `#schema`\n\t\t// stays untouched until the upgrade actually commits, so a mid-upgrade\n\t\t// failure never leaves the driver's bookkeeping ahead of the real database.\n\t\tconst schema = new Map(this.#schema)\n\t\tthis.#applySteps(schema, plan.steps)\n\t\tcurrent.close()\n\t\ttry {\n\t\t\tconst database = createIndexedDBDatabase({\n\t\t\t\tname: this.#name,\n\t\t\t\tversion: version + 1,\n\t\t\t\tstores: this.#stores(schema),\n\t\t\t\tupgrade: this.#upgrade.bind(this, plan.steps),\n\t\t\t})\n\t\t\tawait database.connect()\n\t\t\t// Only on success: adopt the connection AND commit the local map.\n\t\t\tthis.#database = database\n\t\t\tthis.#schema = schema\n\t\t} catch (error) {\n\t\t\t// The old connection was closed to allow the versionchange attempt;\n\t\t\t// reconnect at the PRE-migration schema/version so the driver is left\n\t\t\t// usable, with `#schema` (and the real database) unchanged.\n\t\t\tawait this.#reopen()\n\t\t\t// Inside `migrate()`, an `UPGRADE` fault means THIS migration failed —\n\t\t\t// map it to `MIGRATION`, not the generic `DRIVER` every other caller gets\n\t\t\t// (see `mapMigrationError` @remarks).\n\t\t\tthrow isIndexedDBError(error) ? mapMigrationError(error) : error\n\t\t}\n\t}\n\n\t// === Private\n\n\t#require(): IndexedDBDatabaseInterface {\n\t\tif (this.#database === undefined) {\n\t\t\tthrow new DatabaseError('CLOSED', `IndexedDB database '${this.#name}' is not open`, {\n\t\t\t\tname: this.#name,\n\t\t\t})\n\t\t}\n\t\treturn this.#database\n\t}\n\n\t// The shared backend-fault boundary: no `IndexedDBError` may leak through\n\t// `DriverInterface` — every public method's `catch` routes here. A\n\t// `DatabaseError` this driver threw itself (the `CLOSED` gate, the\n\t// `NOT_FOUND` table guard, `migrate`'s own `MIGRATION` validation) passes\n\t// through unchanged; only a genuine backend `IndexedDBError` is remapped.\n\t#wrap(error: unknown): unknown {\n\t\treturn isIndexedDBError(error) ? mapIndexedDBError(error) : error\n\t}\n\n\t#store(table: string) {\n\t\treturn this.#require().store(table)\n\t}\n\n\t// Project a schema map into the wrapper's declared-stores shape — the\n\t// reserved meta store is always declared alongside every table, out-of-line\n\t// (keys are passed explicitly), with the declared secondary indexes becoming\n\t// each store's `createIndex` definitions. Shared by `open`, `migrate`, and\n\t// `#reopen` so the projection never drifts between them.\n\t#stores(schema: ReadonlyMap<string, TableSchema>): Record<string, StoreDefinition> {\n\t\tconst stores: Record<string, StoreDefinition> = { [META_STORE]: {} }\n\t\tfor (const table of schema.values()) {\n\t\t\tstores[table.name] = {\n\t\t\t\tindexes: table.indexes.map((columns) => {\n\t\t\t\t\tconst [column] = columns\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname: deriveIndexName(columns),\n\t\t\t\t\t\tpath: columns.length === 1 && column !== undefined ? column : [...columns],\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t}\n\t\t}\n\t\treturn stores\n\t}\n\n\t// Reconnect at the CURRENT `#schema` with no version bump (auto-managed\n\t// mode, mirroring `open`) — used to restore a working connection after a\n\t// failed `migrate` left the prior connection closed.\n\tasync #reopen(): Promise<void> {\n\t\tconst database = createIndexedDBDatabase({\n\t\t\tname: this.#name,\n\t\t\tstores: this.#stores(this.#schema),\n\t\t})\n\t\tawait database.connect()\n\t\tthis.#database = database\n\t}\n\n\t// The candidate-superset read for a plan. The primary store already returns rows\n\t// in primary-key order — the same order `scan` yields, which `applyCriteria`\n\t// preserves for an unordered query. A secondary index returns them in INDEX-key\n\t// order, so re-sort by the primary key to reproduce scan order; the engine then\n\t// filters / orders / pages exactly, so a native read equals the scan path.\n\tasync #candidates(\n\t\tstore: IndexedDBStoreInterface,\n\t\tschema: TableSchema,\n\t\tplan: QueryPlan,\n\t): Promise<readonly Row[]> {\n\t\tif (plan.index === null) return store.records(plan.range)\n\t\tconst rows = [...(await store.index(plan.index).records(plan.range))]\n\t\trows.sort((left, right) =>\n\t\t\tcompareValues(extractKey(left, schema.primary), extractKey(right, schema.primary)),\n\t\t)\n\t\treturn rows\n\t}\n\n\t#table(name: string): TableSchema {\n\t\tconst schema = this.#schema.get(name)\n\t\tif (schema === undefined) {\n\t\t\tthrow new DatabaseError('NOT_FOUND', `table '${name}' is not declared`, { table: name })\n\t\t}\n\t\treturn schema\n\t}\n\n\t// Mirror a migration plan's steps onto a LOCAL schema map — the same\n\t// bookkeeping `open` does for a freshly declared schema — without touching\n\t// `#schema`, so a failed migrate never leaves the driver's bookkeeping ahead\n\t// of the real database. The caller commits the map into `#schema` only after\n\t// the upgrade connects successfully.\n\t#applySteps(schema: Map<string, TableSchema>, steps: readonly MigrationStep[]): void {\n\t\tfor (const step of steps) {\n\t\t\tswitch (step.operation) {\n\t\t\t\tcase 'table.add':\n\t\t\t\t\tif (!schema.has(step.table.name)) schema.set(step.table.name, step.table)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'table.remove':\n\t\t\t\t\tschema.delete(step.table)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'column.add': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (\n\t\t\t\t\t\ttable !== undefined &&\n\t\t\t\t\t\t!table.columns.some((column) => column.name === step.column.name)\n\t\t\t\t\t) {\n\t\t\t\t\t\tschema.set(step.table, { ...table, columns: [...table.columns, step.column] })\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'column.remove': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (table !== undefined) {\n\t\t\t\t\t\tschema.set(step.table, {\n\t\t\t\t\t\t\t...table,\n\t\t\t\t\t\t\tcolumns: table.columns.filter((column) => column.name !== step.column),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'index.add': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (table !== undefined) {\n\t\t\t\t\t\tschema.set(step.table, { ...table, indexes: [...table.indexes, step.index] })\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'index.remove': {\n\t\t\t\t\tconst table = schema.get(step.table)\n\t\t\t\t\tif (table !== undefined) {\n\t\t\t\t\t\tschema.set(step.table, {\n\t\t\t\t\t\t\t...table,\n\t\t\t\t\t\t\tindexes: table.indexes.filter((index) => !deepEqual(index, step.index)),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Runs INSIDE the wrapper's versionchange transaction (see `migrate`\n\t// @remarks). `table.add` / `column.add` are no-ops here — see `migrate`\n\t// @remarks for why. `column.remove` is the one step touching existing rows:\n\t// it walks a live cursor and rewrites each row through the core\n\t// `migrateRows`, updating in place — the only IDB-await-only work permitted\n\t// inside an upgrade transaction.\n\tasync #upgrade(steps: readonly MigrationStep[], context: IndexedDBUpgradeContext): Promise<void> {\n\t\tfor (const step of steps) {\n\t\t\tswitch (step.operation) {\n\t\t\t\tcase 'table.remove':\n\t\t\t\t\tcontext.drop(step.table)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'index.add': {\n\t\t\t\t\tconst name = deriveIndexName(step.index)\n\t\t\t\t\tconst [column] = step.index\n\t\t\t\t\tconst path = step.index.length === 1 && column !== undefined ? column : [...step.index]\n\t\t\t\t\tcontext.transaction.objectStore(step.table).createIndex(name, path)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'index.remove':\n\t\t\t\t\tcontext.transaction.objectStore(step.table).deleteIndex(deriveIndexName(step.index))\n\t\t\t\t\tbreak\n\t\t\t\tcase 'column.remove': {\n\t\t\t\t\tconst store = context.store(step.table)\n\t\t\t\t\tlet cursor = await store.cursor()\n\t\t\t\t\twhile (cursor !== null) {\n\t\t\t\t\t\tconst [migrated] = migrateRows([cursor.value], [step])\n\t\t\t\t\t\tif (migrated === undefined) {\n\t\t\t\t\t\t\tthrow new DatabaseError('MIGRATION', 'migrate: transformed row is missing', {\n\t\t\t\t\t\t\t\ttable: step.table,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait cursor.update(migrated)\n\t\t\t\t\t\tcursor = await cursor.continue()\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'table.add':\n\t\t\t\tcase 'column.add':\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n","import type { DriverInterface } from '@src/core'\nimport { IndexedDBDriver } from './drivers/IndexedDBDriver.js'\n\n/**\n * Create a persistent IndexedDB {@link DriverInterface} for the core database layer.\n *\n * @remarks\n * Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +\n * relations stack against IndexedDB instead of memory — the `Database` / `Table` /\n * `Query` / relations API is unchanged; only where the bytes live changes. The\n * driver is built on the published `@orkestrel/indexeddb` wrapper in auto-managed\n * mode, so a table added to the `tables` map is created on the next open with no\n * version bump. This unit omits `transaction` / `aggregate` (see\n * {@link IndexedDBDriver} `@remarks`).\n *\n * @param name - The IndexedDB database name to open or create\n * @returns A {@link DriverInterface} backed by IndexedDB\n *\n * @example\n * ```ts\n * import { createDatabase } from '@orkestrel/database'\n * import { stringShape } from '@orkestrel/contract'\n * import { createIndexedDBDriver } from '@orkestrel/database/browser'\n *\n * const db = createDatabase({\n * \tdriver: createIndexedDBDriver('app'),\n * \ttables: { users: { id: stringShape(), name: stringShape() } },\n * })\n * await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to IndexedDB\n * ```\n */\nexport function createIndexedDBDriver(name: string): DriverInterface {\n\treturn new IndexedDBDriver(name)\n}\n"],"mappings":"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"}