@prisma-idb/runtime-idb 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.mts","names":[],"sources":["../src/idb-middleware.ts","../src/idb-runtime.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4BiB,sBAAsB,kBAAkB;WAC9C;;;;;;;UCVM;;WAEN,SAAS;;WAET,QAAQ;;;;;;;;;;WAUR,UAAU;;WAEV,sBAAsB;;;;;;WAMtB,MAAM;;;;;;;;;;;;UAaA;EACf,MAAM,KAAK,MAAM;aAA0B,OAAO;KAAO,UAAU,wBAAwB,oBAAoB;EAC/G,QAAQ,MAAM,cAAc,UAAU,wBAAwB,QAAQ;;;;;;;;;;;EAWtE,YAAY,sBAAsB,OAAO,qBAAqB,QAAQ;;;;;;;;;;;;;;;EAetE,gBAAgB;EAChB,SAAS;;;;;;;;;;;;;iBAkNK,iBAAiB,SAAS,oBAAoB"}
1
+ {"version":3,"file":"runtime.d.mts","names":[],"sources":["../src/idb-middleware.ts","../src/idb-runtime.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4BiB,sBAAsB,kBAAkB;WAC9C;;;;;;;UCVM;;WAEN,SAAS;;WAET,QAAQ;;;;;;;;;;WAUR,UAAU;;WAEV,sBAAsB;;;;;;WAMtB,MAAM;;;;;;;;;;;;UAaA;EACf,MAAM,KAAK,MAAM;aAA0B,OAAO;KAAO,UAAU,wBAAwB,oBAAoB;EAC/G,QAAQ,MAAM,cAAc,UAAU,wBAAwB,QAAQ;;;;;;;;;;;EAWtE,YAAY,sBAAsB,OAAO,qBAAqB,QAAQ;;;;;;;;;;;;;;;EAetE,gBAAgB;EAChB,SAAS;;;;;;;;;;;;;iBAoNK,iBAAiB,SAAS,oBAAoB"}
package/dist/runtime.mjs CHANGED
@@ -29,18 +29,26 @@ function buildMiddlewareContext(contract) {
29
29
  error: () => void 0
30
30
  },
31
31
  scope: "runtime",
32
- contentHash: async (exec) => {
33
- const plan = exec;
34
- const hashable = {};
35
- for (const [key, value] of Object.entries(plan)) if (key === "meta") hashable["meta"] = { storageHash: value?.["storageHash"] };
36
- else if (typeof value === "function") continue;
37
- else if (typeof IDBKeyRange !== "undefined" && value instanceof IDBKeyRange || isIdbKeyRange(value)) hashable[key] = keyRangeIdentity(value);
38
- else hashable[key] = value;
39
- return hashContent(canonicalStringify(hashable));
40
- }
32
+ contentHash: async (exec) => hashContent(canonicalStringify(hashablePlan(exec)))
41
33
  };
42
34
  }
43
- /** Check for IDBKeyRange-like objects (e.g. from fake-indexeddb). */
35
+ /**
36
+ * Reduce a plan to the fields that decide cache-key equality, so two
37
+ * semantically identical plans hash the same.
38
+ *
39
+ * - `meta` keeps only `storageHash`, its identity field.
40
+ * - Functions (`IdbRowFilter`, `IdbRowComparator`) are dropped; they cannot be hashed.
41
+ * - Key ranges collapse to their bounds.
42
+ */
43
+ function hashablePlan(exec) {
44
+ const hashable = {};
45
+ for (const [key, value] of Object.entries(exec)) if (key === "meta") hashable["meta"] = { storageHash: value?.["storageHash"] };
46
+ else if (typeof value === "function") continue;
47
+ else if (isIdbKeyRange(value)) hashable[key] = keyRangeIdentity(value);
48
+ else hashable[key] = value;
49
+ return hashable;
50
+ }
51
+ /** Duck-typed so it also matches ranges from fake-indexeddb, where `instanceof IDBKeyRange` fails. */
44
52
  function isIdbKeyRange(value) {
45
53
  return typeof value === "object" && value !== null && "lower" in value && "upper" in value && "lowerOpen" in value && "upperOpen" in value;
46
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.mjs","names":["meta","#adapter","#driver","#contract"],"sources":["../src/idb-runtime.ts"],"sourcesContent":["import type { CodecCallContext } from \"@prisma/orm-framework/components/codec\";\nimport {\n AsyncIterableResult,\n checkMiddlewareCompatibility,\n RuntimeCore,\n type ExecutionPlan,\n type RuntimeExecuteOptions,\n type RuntimeMiddlewareContext,\n type RuntimeStatementStats,\n} from \"@prisma/orm-framework/components/runtime\";\nimport { canonicalStringify } from \"@prisma/orm-framework/utils/canonical-stringify\";\nimport { hashContent } from \"@prisma/orm-framework/utils/hash-content\";\nimport type { IdbLowererContext, IdbQueryPlan, IdbRuntimeAdapterInstance } from \"@prisma-idb/adapter-idb/runtime\";\nimport type { IdbPlanBody, IdbRuntimeDriverInstance, IdbTransactionScope } from \"@prisma-idb/driver-idb/runtime\";\nimport type { IdbMiddleware } from \"./idb-middleware\";\n\n/**\n * Options for creating an IDB runtime instance.\n */\nexport interface IdbRuntimeOptions {\n /** Instantiated IDB adapter — provides `lower()`. */\n readonly adapter: IdbRuntimeAdapterInstance;\n /** Instantiated IDB driver — provides `execute()` and `close()`. */\n readonly driver: IdbRuntimeDriverInstance;\n /**\n * The resolved IDB contract.\n *\n * Threaded through to the adapter's `lower()` as part of\n * {@link IdbLowererContext} so per-field codec encoding can\n * resolve field→codec mappings from the storage schema. Also used\n * to build the real {@link RuntimeMiddlewareContext} so middleware\n * can inspect contract data.\n */\n readonly contract: Record<string, unknown>;\n /** Optional middleware chain. */\n readonly middleware?: readonly IdbMiddleware[];\n /**\n * Middleware execution context.\n *\n * When omitted, a real context is derived from `contract`.\n */\n readonly ctx?: RuntimeMiddlewareContext;\n}\n\n/**\n * Public IDB runtime interface.\n *\n * `query()` accepts an {@link IdbQueryPlan} and returns an\n * `AsyncIterableResult<Row>` — an async iterable that also fulfils as a\n * `Row[]` when awaited. `execute()` runs the same plan for its side effects\n * and resolves a {@link RuntimeStatementStats} (`{ affectedRows }`) instead\n * of rows — mirrors the upstream `RuntimeCore` query()/execute() split\n * (rc.4).\n */\nexport interface IdbRuntime {\n query<Row>(plan: IdbQueryPlan & { readonly _row?: Row }, options?: RuntimeExecuteOptions): AsyncIterableResult<Row>;\n execute(plan: IdbQueryPlan, options?: RuntimeExecuteOptions): Promise<RuntimeStatementStats>;\n /**\n * Open a multi-store IDB transaction and return an `IdbTransactionScope`.\n *\n * The scope's `execute(plan)` runs `IdbAtomicPlan`s directly inside the\n * transaction — middleware is bypassed (Issue #6 fix: cache middleware\n * never fires for reads inside a transaction). `commit()` resolves on\n * `tx.oncomplete`; `rollback()` calls `tx.abort()`.\n *\n * Used by `withMutationScope()` in `client-idb`.\n */\n transaction(storeNames: string[], mode?: IDBTransactionMode): Promise<IdbTransactionScope>;\n /**\n * Verify that the live IDB database's contract marker matches the\n * contract this runtime was created with.\n *\n * Reads the `_prisma_next_marker` store and compares the stored\n * `storageHash` against the contract's `storage.storageHash`.\n * Returns `true` when the marker exists and matches, `false` when\n * the marker is absent or mismatched.\n *\n * Call this after construction and before executing queries to\n * detect schema drift. A missing marker means the database was\n * never initialised (run `db migrate` first). A mismatched marker\n * means the database schema has diverged from the contract.\n */\n verifyMarker(): Promise<boolean>;\n close(): Promise<void>;\n}\n\n/**\n * Build a real {@link RuntimeMiddlewareContext} from the contract\n * so middleware can inspect contract data (plan meta hashes, model\n * names, storage layout) and compute content hashes for cache keys.\n *\n * `contentHash` mirrors the vendor SQL/Mongo runtimes: it\n * canonicalizes the execution plan's structural identity fields\n * and SHA-512 hashes them via WebCrypto. The resulting digest is\n * a bounded, opaque cache key suitable for middleware like\n * a cross-family cache middleware.\n *\n * Non-serializable fields (in-memory filter functions, comparators,\n * `IDBKeyRange` objects) are reduced to their deterministic shape\n * so that two semantically identical plans produce the same hash.\n */\nfunction buildMiddlewareContext(contract: Record<string, unknown>): RuntimeMiddlewareContext {\n return {\n contract,\n mode: \"permissive\",\n // v0.12.0: per-execute correlation id required on the middleware context.\n // The default ctx is built once per runtime; consumers needing per-execute\n // ids should supply their own `ctx`.\n planExecutionId: crypto.randomUUID(),\n now: () => Date.now(),\n log: {\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n },\n // Always \"runtime\": IdbTransactionScope.execute() bypasses the middleware\n // chain entirely, so middleware is only ever invoked from the top-level\n // query()/execute() paths, which is exactly the \"runtime\" scope. There is\n // no IDB connection pool, so \"connection\" is not applicable either.\n scope: \"runtime\",\n contentHash: async (exec: ExecutionPlan) => {\n // Reduce the plan to its structural identity for hashing.\n // Exclude functions (IdbRowFilter, IdbRowComparator) and\n // collapse IDBKeyRange to its bounds so deterministic\n // comparisons work.\n const plan = exec as unknown as Record<string, unknown>;\n const hashable: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(plan)) {\n if (key === \"meta\") {\n // meta.storageHash is the primary identity field\n const meta = value as Record<string, unknown> | undefined;\n hashable[\"meta\"] = { storageHash: meta?.[\"storageHash\"] };\n } else if (typeof value === \"function\") {\n // Skip in-memory filters/comparators — not hashable\n continue;\n } else if ((typeof IDBKeyRange !== \"undefined\" && value instanceof IDBKeyRange) || isIdbKeyRange(value)) {\n // Collapse IDBKeyRange to its bounds\n hashable[key] = keyRangeIdentity(value as IDBKeyRange);\n } else {\n hashable[key] = value;\n }\n }\n return hashContent(canonicalStringify(hashable));\n },\n };\n}\n\n/** Check for IDBKeyRange-like objects (e.g. from fake-indexeddb). */\nfunction isIdbKeyRange(value: unknown): value is IDBKeyRange {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"lower\" in value &&\n \"upper\" in value &&\n \"lowerOpen\" in value &&\n \"upperOpen\" in value\n );\n}\n\n/** Extract a deterministic identity from an IDBKeyRange. */\nfunction keyRangeIdentity(range: IDBKeyRange): Record<string, unknown> {\n return {\n lower: range.lower,\n upper: range.upper,\n lowerOpen: range.lowerOpen,\n upperOpen: range.upperOpen,\n };\n}\n\n/**\n * IDB runtime — the `RuntimeCore` subclass for IndexedDB.\n *\n * Wires together:\n * - `lower(plan, ctx)` — delegates to `adapter.lower()` with the contract\n * threaded via {@link IdbLowererContext}, producing an `IdbPlanBody`\n * - `runDriver(exec)` — delegates to `driver.execute()` to run the plan against IDB\n * - `query()` — the concrete row-returning template method from `RuntimeCore`,\n * backed by `runDriver()`\n * - `runExecute(exec)` — drains `runDriver()` and counts rows into a\n * `RuntimeStatementStats`; backs `execute()`, the concrete\n * statement-terminal template method from `RuntimeCore`\n * - `close()` — closes the IDB connection via `driver.close()`\n */\nclass IdbRuntimeImpl extends RuntimeCore<IdbQueryPlan, IdbPlanBody, IdbMiddleware> implements IdbRuntime {\n readonly #adapter: IdbRuntimeAdapterInstance;\n readonly #driver: IdbRuntimeDriverInstance;\n readonly #contract: Record<string, unknown>;\n\n constructor(options: IdbRuntimeOptions) {\n const ctx = options.ctx ?? buildMiddlewareContext(options.contract);\n const middleware = [...(options.middleware ?? [])];\n for (const mw of middleware) {\n checkMiddlewareCompatibility(mw, \"idb\", \"idb\");\n }\n super({ middleware, ctx });\n this.#adapter = options.adapter;\n this.#driver = options.driver;\n this.#contract = options.contract;\n }\n\n /**\n * Lower an IDB query plan to an IDB plan body via the adapter.\n *\n * Threads the contract through {@link IdbLowererContext} so per-field\n * codec encoding can resolve field→codec mappings from the contract's\n * storage schema.\n */\n protected override lower(plan: IdbQueryPlan, ctx: CodecCallContext): Promise<IdbPlanBody> {\n const lowererCtx: IdbLowererContext = {\n ...ctx,\n contract: this.#contract,\n };\n return this.#adapter.lower(plan, lowererCtx);\n }\n\n /**\n * Execute a lowered IDB plan body via the driver.\n *\n * Backs `query()` (inherited concretely from `RuntimeCore`) — rows are\n * yielded as-is from the driver (identity pass-through). All current\n * `idb/*` codecs are identity transforms — no per-field decoding is\n * needed. When per-field codec decoding is added (e.g. decoding\n * `idb/date@1` stored values back to `Date` instances, or custom codec\n * output types), it wires up here.\n */\n protected override runDriver(exec: IdbPlanBody): AsyncIterable<Record<string, unknown>> {\n return this.#driver.execute(exec);\n }\n\n /**\n * Backs `execute()` (inherited concretely from `RuntimeCore`) — IDB has no\n * native \"affected-row count without returning rows\" concept, so this\n * drains `runDriver()`'s row stream and counts. The driver already\n * materializes every op's touched rows in memory before yielding (see\n * `IdbRuntimeDriverInstance.execute()`'s collect-then-yield strategy), so\n * draining here has no extra cost beyond what a `query()` call over the\n * same plan would already pay.\n *\n * This counts *rows the driver yields*, which correctly equals \"ops\n * applied\" for every atomic plan kind: `add`/`put`/`update` each echo the\n * one row they touched, `scan-write` echoes every matched row, and\n * `delete` (`execute/ops.ts`'s `execDelete`) walks a cursor over its\n * key/range and echoes each row it deletes — so a delete matching zero\n * keys correctly resolves `{ affectedRows: 0 }` and one matching N keys\n * (a range delete) resolves `{ affectedRows: N }`. For a `batch` plan,\n * `affectedRows` is the sum of rows every op in the batch yields,\n * *including read ops* (`key-get`/`index-get`/`cursor-scan`) if the batch\n * happens to contain any — it's a row count across the whole plan, not a\n * write-only count.\n */\n protected override async runExecute(exec: IdbPlanBody): Promise<RuntimeStatementStats> {\n let affectedRows = 0;\n for await (const _row of this.runDriver(exec)) {\n affectedRows++;\n }\n return { affectedRows };\n }\n\n async transaction(storeNames: string[], mode: IDBTransactionMode = \"readwrite\"): Promise<IdbTransactionScope> {\n return this.#driver.transaction(storeNames, mode);\n }\n\n override async close(): Promise<void> {\n await this.#driver.close();\n }\n\n /**\n * Verify that the live IDB marker matches the contract.\n *\n * Compares the stored `storageHash` against `contract.storage.storageHash`.\n * A fresh database (no marker store) returns `false` — the caller should\n * ensure migrations have been applied before running queries.\n */\n async verifyMarker(): Promise<boolean> {\n const marker = await this.#driver.readMarker();\n if (marker === null) return false;\n const contractStorage = this.#contract[\"storage\"] as Record<string, unknown> | undefined;\n const contractHash = contractStorage?.[\"storageHash\"];\n return typeof contractHash === \"string\" && marker.storageHash === contractHash;\n }\n}\n\n/**\n * Create an IDB runtime instance.\n *\n * @example\n * ```ts\n * const driver = createIDBRuntimeDriver(\"my-app\").create();\n * const runtime = createIdbRuntime({ adapter, driver, contract });\n * await runtime.query(plan)\n * await runtime.close();\n * ```\n */\nexport function createIdbRuntime(options: IdbRuntimeOptions): IdbRuntime {\n return new IdbRuntimeImpl(options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqGA,SAAS,uBAAuB,UAA6D;CAC3F,OAAO;EACL;EACA,MAAM;EAIN,iBAAiB,OAAO,WAAW;EACnC,WAAW,KAAK,IAAI;EACpB,KAAK;GACH,YAAY,KAAA;GACZ,YAAY,KAAA;GACZ,aAAa,KAAA;EACf;EAKA,OAAO;EACP,aAAa,OAAO,SAAwB;GAK1C,MAAM,OAAO;GACb,MAAM,WAAoC,CAAC;GAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAC5C,IAAI,QAAQ,QAGV,SAAS,UAAU,EAAE,aAAaA,QAAO,eAAe;QACnD,IAAI,OAAO,UAAU,YAE1B;QACK,IAAK,OAAO,gBAAgB,eAAe,iBAAiB,eAAgB,cAAc,KAAK,GAEpG,SAAS,OAAO,iBAAiB,KAAoB;QAErD,SAAS,OAAO;GAGpB,OAAO,YAAY,mBAAmB,QAAQ,CAAC;EACjD;CACF;AACF;;AAGA,SAAS,cAAc,OAAsC;CAC3D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,WAAW,SACX,WAAW,SACX,eAAe,SACf,eAAe;AAEnB;;AAGA,SAAS,iBAAiB,OAA6C;CACrE,OAAO;EACL,OAAO,MAAM;EACb,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,MAAM;CACnB;AACF;;;;;;;;;;;;;;;AAgBA,IAAM,iBAAN,cAA6B,YAA4E;CACvG;CACA;CACA;CAEA,YAAY,SAA4B;EACtC,MAAM,MAAM,QAAQ,OAAO,uBAAuB,QAAQ,QAAQ;EAClE,MAAM,aAAa,CAAC,GAAI,QAAQ,cAAc,CAAC,CAAE;EACjD,KAAK,MAAM,MAAM,YACf,6BAA6B,IAAI,OAAO,KAAK;EAE/C,MAAM;GAAE;GAAY;EAAI,CAAC;EACzB,KAAKC,WAAW,QAAQ;EACxB,KAAKC,UAAU,QAAQ;EACvB,KAAKC,YAAY,QAAQ;CAC3B;;;;;;;;CASA,MAAyB,MAAoB,KAA6C;EACxF,MAAM,aAAgC;GACpC,GAAG;GACH,UAAU,KAAKA;EACjB;EACA,OAAO,KAAKF,SAAS,MAAM,MAAM,UAAU;CAC7C;;;;;;;;;;;CAYA,UAA6B,MAA2D;EACtF,OAAO,KAAKC,QAAQ,QAAQ,IAAI;CAClC;;;;;;;;;;;;;;;;;;;;;;CAuBA,MAAyB,WAAW,MAAmD;EACrF,IAAI,eAAe;EACnB,WAAW,MAAM,QAAQ,KAAK,UAAU,IAAI,GAC1C;EAEF,OAAO,EAAE,aAAa;CACxB;CAEA,MAAM,YAAY,YAAsB,OAA2B,aAA2C;EAC5G,OAAO,KAAKA,QAAQ,YAAY,YAAY,IAAI;CAClD;CAEA,MAAe,QAAuB;EACpC,MAAM,KAAKA,QAAQ,MAAM;CAC3B;;;;;;;;CASA,MAAM,eAAiC;EACrC,MAAM,SAAS,MAAM,KAAKA,QAAQ,WAAW;EAC7C,IAAI,WAAW,MAAM,OAAO;EAE5B,MAAM,eADkB,KAAKC,UAAU,UACH,GAAG;EACvC,OAAO,OAAO,iBAAiB,YAAY,OAAO,gBAAgB;CACpE;AACF;;;;;;;;;;;;AAaA,SAAgB,iBAAiB,SAAwC;CACvE,OAAO,IAAI,eAAe,OAAO;AACnC"}
1
+ {"version":3,"file":"runtime.mjs","names":["meta","#adapter","#driver","#contract"],"sources":["../src/idb-runtime.ts"],"sourcesContent":["import type { CodecCallContext } from \"@prisma/orm-framework/components/codec\";\nimport {\n AsyncIterableResult,\n checkMiddlewareCompatibility,\n RuntimeCore,\n type ExecutionPlan,\n type RuntimeExecuteOptions,\n type RuntimeMiddlewareContext,\n type RuntimeStatementStats,\n} from \"@prisma/orm-framework/components/runtime\";\nimport { canonicalStringify } from \"@prisma/orm-framework/utils/canonical-stringify\";\nimport { hashContent } from \"@prisma/orm-framework/utils/hash-content\";\nimport type { IdbLowererContext, IdbQueryPlan, IdbRuntimeAdapterInstance } from \"@prisma-idb/adapter-idb/runtime\";\nimport type { IdbPlanBody, IdbRuntimeDriverInstance, IdbTransactionScope } from \"@prisma-idb/driver-idb/runtime\";\nimport type { IdbMiddleware } from \"./idb-middleware\";\n\n/**\n * Options for creating an IDB runtime instance.\n */\nexport interface IdbRuntimeOptions {\n /** Instantiated IDB adapter — provides `lower()`. */\n readonly adapter: IdbRuntimeAdapterInstance;\n /** Instantiated IDB driver — provides `execute()` and `close()`. */\n readonly driver: IdbRuntimeDriverInstance;\n /**\n * The resolved IDB contract.\n *\n * Threaded through to the adapter's `lower()` as part of\n * {@link IdbLowererContext} so per-field codec encoding can\n * resolve field→codec mappings from the storage schema. Also used\n * to build the real {@link RuntimeMiddlewareContext} so middleware\n * can inspect contract data.\n */\n readonly contract: Record<string, unknown>;\n /** Optional middleware chain. */\n readonly middleware?: readonly IdbMiddleware[];\n /**\n * Middleware execution context.\n *\n * When omitted, a real context is derived from `contract`.\n */\n readonly ctx?: RuntimeMiddlewareContext;\n}\n\n/**\n * Public IDB runtime interface.\n *\n * `query()` accepts an {@link IdbQueryPlan} and returns an\n * `AsyncIterableResult<Row>` — an async iterable that also fulfils as a\n * `Row[]` when awaited. `execute()` runs the same plan for its side effects\n * and resolves a {@link RuntimeStatementStats} (`{ affectedRows }`) instead\n * of rows — mirrors the upstream `RuntimeCore` query()/execute() split\n * (rc.4).\n */\nexport interface IdbRuntime {\n query<Row>(plan: IdbQueryPlan & { readonly _row?: Row }, options?: RuntimeExecuteOptions): AsyncIterableResult<Row>;\n execute(plan: IdbQueryPlan, options?: RuntimeExecuteOptions): Promise<RuntimeStatementStats>;\n /**\n * Open a multi-store IDB transaction and return an `IdbTransactionScope`.\n *\n * The scope's `execute(plan)` runs `IdbAtomicPlan`s directly inside the\n * transaction — middleware is bypassed (Issue #6 fix: cache middleware\n * never fires for reads inside a transaction). `commit()` resolves on\n * `tx.oncomplete`; `rollback()` calls `tx.abort()`.\n *\n * Used by `withMutationScope()` in `client-idb`.\n */\n transaction(storeNames: string[], mode?: IDBTransactionMode): Promise<IdbTransactionScope>;\n /**\n * Verify that the live IDB database's contract marker matches the\n * contract this runtime was created with.\n *\n * Reads the `_prisma_next_marker` store and compares the stored\n * `storageHash` against the contract's `storage.storageHash`.\n * Returns `true` when the marker exists and matches, `false` when\n * the marker is absent or mismatched.\n *\n * Call this after construction and before executing queries to\n * detect schema drift. A missing marker means the database was\n * never initialised (run `db migrate` first). A mismatched marker\n * means the database schema has diverged from the contract.\n */\n verifyMarker(): Promise<boolean>;\n close(): Promise<void>;\n}\n\n/**\n * Build a real {@link RuntimeMiddlewareContext} from the contract\n * so middleware can inspect contract data (plan meta hashes, model\n * names, storage layout) and compute content hashes for cache keys.\n *\n * `contentHash` mirrors the vendor SQL/Mongo runtimes: it\n * canonicalizes the execution plan's structural identity fields\n * and SHA-512 hashes them via WebCrypto. The resulting digest is\n * a bounded, opaque cache key suitable for middleware like\n * a cross-family cache middleware.\n *\n * Non-serializable fields (in-memory filter functions, comparators,\n * `IDBKeyRange` objects) are reduced to their deterministic shape\n * so that two semantically identical plans produce the same hash.\n */\nfunction buildMiddlewareContext(contract: Record<string, unknown>): RuntimeMiddlewareContext {\n return {\n contract,\n mode: \"permissive\",\n // v0.12.0: per-execute correlation id required on the middleware context.\n // The default ctx is built once per runtime; consumers needing per-execute\n // ids should supply their own `ctx`.\n planExecutionId: crypto.randomUUID(),\n now: () => Date.now(),\n log: {\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n },\n // Always \"runtime\": IdbTransactionScope.execute() bypasses the middleware\n // chain entirely, so middleware is only ever invoked from the top-level\n // query()/execute() paths, which is exactly the \"runtime\" scope. There is\n // no IDB connection pool, so \"connection\" is not applicable either.\n scope: \"runtime\",\n contentHash: async (exec: ExecutionPlan) => hashContent(canonicalStringify(hashablePlan(exec))),\n };\n}\n\n/**\n * Reduce a plan to the fields that decide cache-key equality, so two\n * semantically identical plans hash the same.\n *\n * - `meta` keeps only `storageHash`, its identity field.\n * - Functions (`IdbRowFilter`, `IdbRowComparator`) are dropped; they cannot be hashed.\n * - Key ranges collapse to their bounds.\n */\nfunction hashablePlan(exec: ExecutionPlan): Record<string, unknown> {\n const hashable: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(exec as unknown as Record<string, unknown>)) {\n if (key === \"meta\") {\n const meta = value as Record<string, unknown> | undefined;\n hashable[\"meta\"] = { storageHash: meta?.[\"storageHash\"] };\n } else if (typeof value === \"function\") {\n continue;\n } else if (isIdbKeyRange(value)) {\n hashable[key] = keyRangeIdentity(value);\n } else {\n hashable[key] = value;\n }\n }\n return hashable;\n}\n\n/** Duck-typed so it also matches ranges from fake-indexeddb, where `instanceof IDBKeyRange` fails. */\nfunction isIdbKeyRange(value: unknown): value is IDBKeyRange {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"lower\" in value &&\n \"upper\" in value &&\n \"lowerOpen\" in value &&\n \"upperOpen\" in value\n );\n}\n\n/** Extract a deterministic identity from an IDBKeyRange. */\nfunction keyRangeIdentity(range: IDBKeyRange): Record<string, unknown> {\n return {\n lower: range.lower,\n upper: range.upper,\n lowerOpen: range.lowerOpen,\n upperOpen: range.upperOpen,\n };\n}\n\n/**\n * IDB runtime — the `RuntimeCore` subclass for IndexedDB.\n *\n * Wires together:\n * - `lower(plan, ctx)` — delegates to `adapter.lower()` with the contract\n * threaded via {@link IdbLowererContext}, producing an `IdbPlanBody`\n * - `runDriver(exec)` — delegates to `driver.execute()` to run the plan against IDB\n * - `query()` — the concrete row-returning template method from `RuntimeCore`,\n * backed by `runDriver()`\n * - `runExecute(exec)` — drains `runDriver()` and counts rows into a\n * `RuntimeStatementStats`; backs `execute()`, the concrete\n * statement-terminal template method from `RuntimeCore`\n * - `close()` — closes the IDB connection via `driver.close()`\n */\nclass IdbRuntimeImpl extends RuntimeCore<IdbQueryPlan, IdbPlanBody, IdbMiddleware> implements IdbRuntime {\n readonly #adapter: IdbRuntimeAdapterInstance;\n readonly #driver: IdbRuntimeDriverInstance;\n readonly #contract: Record<string, unknown>;\n\n constructor(options: IdbRuntimeOptions) {\n const ctx = options.ctx ?? buildMiddlewareContext(options.contract);\n const middleware = [...(options.middleware ?? [])];\n for (const mw of middleware) {\n checkMiddlewareCompatibility(mw, \"idb\", \"idb\");\n }\n super({ middleware, ctx });\n this.#adapter = options.adapter;\n this.#driver = options.driver;\n this.#contract = options.contract;\n }\n\n /**\n * Lower an IDB query plan to an IDB plan body via the adapter.\n *\n * Threads the contract through {@link IdbLowererContext} so per-field\n * codec encoding can resolve field→codec mappings from the contract's\n * storage schema.\n */\n protected override lower(plan: IdbQueryPlan, ctx: CodecCallContext): Promise<IdbPlanBody> {\n const lowererCtx: IdbLowererContext = {\n ...ctx,\n contract: this.#contract,\n };\n return this.#adapter.lower(plan, lowererCtx);\n }\n\n /**\n * Execute a lowered IDB plan body via the driver.\n *\n * Backs `query()` (inherited concretely from `RuntimeCore`) — rows are\n * yielded as-is from the driver (identity pass-through). All current\n * `idb/*` codecs are identity transforms — no per-field decoding is\n * needed. When per-field codec decoding is added (e.g. decoding\n * `idb/date@1` stored values back to `Date` instances, or custom codec\n * output types), it wires up here.\n */\n protected override runDriver(exec: IdbPlanBody): AsyncIterable<Record<string, unknown>> {\n return this.#driver.execute(exec);\n }\n\n /**\n * Backs `execute()` (inherited concretely from `RuntimeCore`) — IDB has no\n * native \"affected-row count without returning rows\" concept, so this\n * drains `runDriver()`'s row stream and counts. The driver already\n * materializes every op's touched rows in memory before yielding (see\n * `IdbRuntimeDriverInstance.execute()`'s collect-then-yield strategy), so\n * draining here has no extra cost beyond what a `query()` call over the\n * same plan would already pay.\n *\n * This counts *rows the driver yields*, which correctly equals \"ops\n * applied\" for every atomic plan kind: `add`/`put`/`update` each echo the\n * one row they touched, `scan-write` echoes every matched row, and\n * `delete` (`execute/ops.ts`'s `execDelete`) walks a cursor over its\n * key/range and echoes each row it deletes — so a delete matching zero\n * keys correctly resolves `{ affectedRows: 0 }` and one matching N keys\n * (a range delete) resolves `{ affectedRows: N }`. For a `batch` plan,\n * `affectedRows` is the sum of rows every op in the batch yields,\n * *including read ops* (`key-get`/`index-get`/`cursor-scan`) if the batch\n * happens to contain any — it's a row count across the whole plan, not a\n * write-only count.\n */\n protected override async runExecute(exec: IdbPlanBody): Promise<RuntimeStatementStats> {\n let affectedRows = 0;\n for await (const _row of this.runDriver(exec)) {\n affectedRows++;\n }\n return { affectedRows };\n }\n\n async transaction(storeNames: string[], mode: IDBTransactionMode = \"readwrite\"): Promise<IdbTransactionScope> {\n return this.#driver.transaction(storeNames, mode);\n }\n\n override async close(): Promise<void> {\n await this.#driver.close();\n }\n\n /**\n * Verify that the live IDB marker matches the contract.\n *\n * Compares the stored `storageHash` against `contract.storage.storageHash`.\n * A fresh database (no marker store) returns `false` — the caller should\n * ensure migrations have been applied before running queries.\n */\n async verifyMarker(): Promise<boolean> {\n const marker = await this.#driver.readMarker();\n if (marker === null) return false;\n const contractStorage = this.#contract[\"storage\"] as Record<string, unknown> | undefined;\n const contractHash = contractStorage?.[\"storageHash\"];\n return typeof contractHash === \"string\" && marker.storageHash === contractHash;\n }\n}\n\n/**\n * Create an IDB runtime instance.\n *\n * @example\n * ```ts\n * const driver = createIDBRuntimeDriver(\"my-app\").create();\n * const runtime = createIdbRuntime({ adapter, driver, contract });\n * await runtime.query(plan)\n * await runtime.close();\n * ```\n */\nexport function createIdbRuntime(options: IdbRuntimeOptions): IdbRuntime {\n return new IdbRuntimeImpl(options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqGA,SAAS,uBAAuB,UAA6D;CAC3F,OAAO;EACL;EACA,MAAM;EAIN,iBAAiB,OAAO,WAAW;EACnC,WAAW,KAAK,IAAI;EACpB,KAAK;GACH,YAAY,KAAA;GACZ,YAAY,KAAA;GACZ,aAAa,KAAA;EACf;EAKA,OAAO;EACP,aAAa,OAAO,SAAwB,YAAY,mBAAmB,aAAa,IAAI,CAAC,CAAC;CAChG;AACF;;;;;;;;;AAUA,SAAS,aAAa,MAA8C;CAClE,MAAM,WAAoC,CAAC;CAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAA0C,GAClF,IAAI,QAAQ,QAEV,SAAS,UAAU,EAAE,aAAaA,QAAO,eAAe;MACnD,IAAI,OAAO,UAAU,YAC1B;MACK,IAAI,cAAc,KAAK,GAC5B,SAAS,OAAO,iBAAiB,KAAK;MAEtC,SAAS,OAAO;CAGpB,OAAO;AACT;;AAGA,SAAS,cAAc,OAAsC;CAC3D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,WAAW,SACX,WAAW,SACX,eAAe,SACf,eAAe;AAEnB;;AAGA,SAAS,iBAAiB,OAA6C;CACrE,OAAO;EACL,OAAO,MAAM;EACb,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,MAAM;CACnB;AACF;;;;;;;;;;;;;;;AAgBA,IAAM,iBAAN,cAA6B,YAA4E;CACvG;CACA;CACA;CAEA,YAAY,SAA4B;EACtC,MAAM,MAAM,QAAQ,OAAO,uBAAuB,QAAQ,QAAQ;EAClE,MAAM,aAAa,CAAC,GAAI,QAAQ,cAAc,CAAC,CAAE;EACjD,KAAK,MAAM,MAAM,YACf,6BAA6B,IAAI,OAAO,KAAK;EAE/C,MAAM;GAAE;GAAY;EAAI,CAAC;EACzB,KAAKC,WAAW,QAAQ;EACxB,KAAKC,UAAU,QAAQ;EACvB,KAAKC,YAAY,QAAQ;CAC3B;;;;;;;;CASA,MAAyB,MAAoB,KAA6C;EACxF,MAAM,aAAgC;GACpC,GAAG;GACH,UAAU,KAAKA;EACjB;EACA,OAAO,KAAKF,SAAS,MAAM,MAAM,UAAU;CAC7C;;;;;;;;;;;CAYA,UAA6B,MAA2D;EACtF,OAAO,KAAKC,QAAQ,QAAQ,IAAI;CAClC;;;;;;;;;;;;;;;;;;;;;;CAuBA,MAAyB,WAAW,MAAmD;EACrF,IAAI,eAAe;EACnB,WAAW,MAAM,QAAQ,KAAK,UAAU,IAAI,GAC1C;EAEF,OAAO,EAAE,aAAa;CACxB;CAEA,MAAM,YAAY,YAAsB,OAA2B,aAA2C;EAC5G,OAAO,KAAKA,QAAQ,YAAY,YAAY,IAAI;CAClD;CAEA,MAAe,QAAuB;EACpC,MAAM,KAAKA,QAAQ,MAAM;CAC3B;;;;;;;;CASA,MAAM,eAAiC;EACrC,MAAM,SAAS,MAAM,KAAKA,QAAQ,WAAW;EAC7C,IAAI,WAAW,MAAM,OAAO;EAE5B,MAAM,eADkB,KAAKC,UAAU,UACH,GAAG;EACvC,OAAO,OAAO,iBAAiB,YAAY,OAAO,gBAAgB;CACpE;AACF;;;;;;;;;;;;AAaA,SAAgB,iBAAiB,SAAwC;CACvE,OAAO,IAAI,eAAe,OAAO;AACnC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma-idb/runtime-idb",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Prisma 8 IDB family — runtime (RuntimeCore subclass that wires adapter + driver)",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -28,8 +28,8 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@prisma-idb/adapter-idb": "0.8.0",
32
- "@prisma-idb/driver-idb": "0.8.0",
31
+ "@prisma-idb/adapter-idb": "0.9.1",
32
+ "@prisma-idb/driver-idb": "0.9.1",
33
33
  "@prisma/orm-framework": "8.0.0-rc.12"
34
34
  },
35
35
  "devDependencies": {