@prisma-idb/runtime-idb 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Prisma IDB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # `@prisma-idb/runtime-idb`
2
+
3
+ > Part of the [`@prisma-idb`](https://prisma-idb.dev/) driver stack — IndexedDB for [Prisma 8](https://www.prisma.io/blog/prisma-next-call-for-extension-authors).
4
+
5
+ **runtime-idb** is the `RuntimeCore` subclass that wires together the adapter and driver into a complete Prisma 8 runtime.
6
+
7
+ ## Stack position
8
+
9
+ ```
10
+ family-idb (CLI / config)
11
+ client-idb (ORM query builder)
12
+ runtime-idb ← you are here (RuntimeCore)
13
+ adapter-idb (query AST → IDB plan)
14
+ driver-idb (window.indexedDB wrapper)
15
+ target-idb (identity + migrations)
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Consumed internally by the rest of the `@prisma-idb` family. You generally do not install this package directly — use [`@prisma-idb/family-idb`](https://www.npmjs.com/package/@prisma-idb/family-idb) as your entry point.
21
+
22
+ ## License
23
+
24
+ MIT
@@ -0,0 +1,119 @@
1
+ import { AsyncIterableResult, RuntimeExecuteOptions, RuntimeMiddleware, RuntimeMiddlewareContext, RuntimeStatementStats } from "@prisma/orm-framework/components/runtime";
2
+ import { IdbQueryPlan, IdbRuntimeAdapterInstance } from "@prisma-idb/adapter-idb/runtime";
3
+ import { IdbPlanBody, IdbRuntimeDriverInstance, IdbTransactionScope } from "@prisma-idb/driver-idb/runtime";
4
+ //#region src/idb-middleware.d.ts
5
+ /**
6
+ * IDB-family middleware.
7
+ *
8
+ * Extends the generic `RuntimeMiddleware<IdbPlanBody>` marker. `familyId` is
9
+ * optional so generic cross-family middleware (e.g. telemetry) — which carry
10
+ * no `familyId` — remain assignable. When present, it must be `'idb'`; the
11
+ * runtime can reject mismatches at construction time via
12
+ * `checkMiddlewareCompatibility`.
13
+ *
14
+ * Mirrors the vendor `MongoMiddleware` / `SqlMiddleware` pattern.
15
+ *
16
+ * ## `onRow` backpressure limitation
17
+ *
18
+ * Unlike SQL/Mongo drivers, the IDB driver uses collect-then-yield: all rows
19
+ * are materialized inside the IDB transaction before any are delivered to the
20
+ * middleware pipeline (see ADR 006). This means:
21
+ *
22
+ * - `onRow` fires after the full cursor scan has completed and all rows are in
23
+ * memory. Throwing or aborting inside `onRow` does NOT reduce the number of
24
+ * rows read from the object store.
25
+ * - Use `take(n)` on the query builder to bound materialization, not early
26
+ * exit from `onRow`.
27
+ * - Observation-only hooks (telemetry, cache population, logging) work
28
+ * correctly — they just receive rows from an already-complete scan.
29
+ */
30
+ interface IdbMiddleware extends RuntimeMiddleware<IdbPlanBody> {
31
+ readonly familyId?: "idb";
32
+ }
33
+ //#endregion
34
+ //#region src/idb-runtime.d.ts
35
+ /**
36
+ * Options for creating an IDB runtime instance.
37
+ */
38
+ interface IdbRuntimeOptions {
39
+ /** Instantiated IDB adapter — provides `lower()`. */
40
+ readonly adapter: IdbRuntimeAdapterInstance;
41
+ /** Instantiated IDB driver — provides `execute()` and `close()`. */
42
+ readonly driver: IdbRuntimeDriverInstance;
43
+ /**
44
+ * The resolved IDB contract.
45
+ *
46
+ * Threaded through to the adapter's `lower()` as part of
47
+ * {@link IdbLowererContext} so per-field codec encoding can
48
+ * resolve field→codec mappings from the storage schema. Also used
49
+ * to build the real {@link RuntimeMiddlewareContext} so middleware
50
+ * can inspect contract data.
51
+ */
52
+ readonly contract: Record<string, unknown>;
53
+ /** Optional middleware chain. */
54
+ readonly middleware?: readonly IdbMiddleware[];
55
+ /**
56
+ * Middleware execution context.
57
+ *
58
+ * When omitted, a real context is derived from `contract`.
59
+ */
60
+ readonly ctx?: RuntimeMiddlewareContext;
61
+ }
62
+ /**
63
+ * Public IDB runtime interface.
64
+ *
65
+ * `query()` accepts an {@link IdbQueryPlan} and returns an
66
+ * `AsyncIterableResult<Row>` — an async iterable that also fulfils as a
67
+ * `Row[]` when awaited. `execute()` runs the same plan for its side effects
68
+ * and resolves a {@link RuntimeStatementStats} (`{ affectedRows }`) instead
69
+ * of rows — mirrors the upstream `RuntimeCore` query()/execute() split
70
+ * (rc.4).
71
+ */
72
+ interface IdbRuntime {
73
+ query<Row>(plan: IdbQueryPlan & {
74
+ readonly _row?: Row;
75
+ }, options?: RuntimeExecuteOptions): AsyncIterableResult<Row>;
76
+ execute(plan: IdbQueryPlan, options?: RuntimeExecuteOptions): Promise<RuntimeStatementStats>;
77
+ /**
78
+ * Open a multi-store IDB transaction and return an `IdbTransactionScope`.
79
+ *
80
+ * The scope's `execute(plan)` runs `IdbAtomicPlan`s directly inside the
81
+ * transaction — middleware is bypassed (Issue #6 fix: cache middleware
82
+ * never fires for reads inside a transaction). `commit()` resolves on
83
+ * `tx.oncomplete`; `rollback()` calls `tx.abort()`.
84
+ *
85
+ * Used by `withMutationScope()` in `client-idb`.
86
+ */
87
+ transaction(storeNames: string[], mode?: IDBTransactionMode): Promise<IdbTransactionScope>;
88
+ /**
89
+ * Verify that the live IDB database's contract marker matches the
90
+ * contract this runtime was created with.
91
+ *
92
+ * Reads the `_prisma_next_marker` store and compares the stored
93
+ * `storageHash` against the contract's `storage.storageHash`.
94
+ * Returns `true` when the marker exists and matches, `false` when
95
+ * the marker is absent or mismatched.
96
+ *
97
+ * Call this after construction and before executing queries to
98
+ * detect schema drift. A missing marker means the database was
99
+ * never initialised (run `db migrate` first). A mismatched marker
100
+ * means the database schema has diverged from the contract.
101
+ */
102
+ verifyMarker(): Promise<boolean>;
103
+ close(): Promise<void>;
104
+ }
105
+ /**
106
+ * Create an IDB runtime instance.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const driver = createIDBRuntimeDriver("my-app").create();
111
+ * const runtime = createIdbRuntime({ adapter, driver, contract });
112
+ * await runtime.query(plan)
113
+ * await runtime.close();
114
+ * ```
115
+ */
116
+ declare function createIdbRuntime(options: IdbRuntimeOptions): IdbRuntime;
117
+ //#endregion
118
+ export { type IdbMiddleware, type IdbRuntime, type IdbRuntimeOptions, createIdbRuntime };
119
+ //# sourceMappingURL=runtime.d.mts.map
@@ -0,0 +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"}
@@ -0,0 +1,176 @@
1
+ import { RuntimeCore, checkMiddlewareCompatibility } from "@prisma/orm-framework/components/runtime";
2
+ import { canonicalStringify } from "@prisma/orm-framework/utils/canonical-stringify";
3
+ import { hashContent } from "@prisma/orm-framework/utils/hash-content";
4
+ //#region src/idb-runtime.ts
5
+ /**
6
+ * Build a real {@link RuntimeMiddlewareContext} from the contract
7
+ * so middleware can inspect contract data (plan meta hashes, model
8
+ * names, storage layout) and compute content hashes for cache keys.
9
+ *
10
+ * `contentHash` mirrors the vendor SQL/Mongo runtimes: it
11
+ * canonicalizes the execution plan's structural identity fields
12
+ * and SHA-512 hashes them via WebCrypto. The resulting digest is
13
+ * a bounded, opaque cache key suitable for middleware like
14
+ * a cross-family cache middleware.
15
+ *
16
+ * Non-serializable fields (in-memory filter functions, comparators,
17
+ * `IDBKeyRange` objects) are reduced to their deterministic shape
18
+ * so that two semantically identical plans produce the same hash.
19
+ */
20
+ function buildMiddlewareContext(contract) {
21
+ return {
22
+ contract,
23
+ mode: "permissive",
24
+ planExecutionId: crypto.randomUUID(),
25
+ now: () => Date.now(),
26
+ log: {
27
+ info: () => void 0,
28
+ warn: () => void 0,
29
+ error: () => void 0
30
+ },
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
+ }
41
+ };
42
+ }
43
+ /** Check for IDBKeyRange-like objects (e.g. from fake-indexeddb). */
44
+ function isIdbKeyRange(value) {
45
+ return typeof value === "object" && value !== null && "lower" in value && "upper" in value && "lowerOpen" in value && "upperOpen" in value;
46
+ }
47
+ /** Extract a deterministic identity from an IDBKeyRange. */
48
+ function keyRangeIdentity(range) {
49
+ return {
50
+ lower: range.lower,
51
+ upper: range.upper,
52
+ lowerOpen: range.lowerOpen,
53
+ upperOpen: range.upperOpen
54
+ };
55
+ }
56
+ /**
57
+ * IDB runtime — the `RuntimeCore` subclass for IndexedDB.
58
+ *
59
+ * Wires together:
60
+ * - `lower(plan, ctx)` — delegates to `adapter.lower()` with the contract
61
+ * threaded via {@link IdbLowererContext}, producing an `IdbPlanBody`
62
+ * - `runDriver(exec)` — delegates to `driver.execute()` to run the plan against IDB
63
+ * - `query()` — the concrete row-returning template method from `RuntimeCore`,
64
+ * backed by `runDriver()`
65
+ * - `runExecute(exec)` — drains `runDriver()` and counts rows into a
66
+ * `RuntimeStatementStats`; backs `execute()`, the concrete
67
+ * statement-terminal template method from `RuntimeCore`
68
+ * - `close()` — closes the IDB connection via `driver.close()`
69
+ */
70
+ var IdbRuntimeImpl = class extends RuntimeCore {
71
+ #adapter;
72
+ #driver;
73
+ #contract;
74
+ constructor(options) {
75
+ const ctx = options.ctx ?? buildMiddlewareContext(options.contract);
76
+ const middleware = [...options.middleware ?? []];
77
+ for (const mw of middleware) checkMiddlewareCompatibility(mw, "idb", "idb");
78
+ super({
79
+ middleware,
80
+ ctx
81
+ });
82
+ this.#adapter = options.adapter;
83
+ this.#driver = options.driver;
84
+ this.#contract = options.contract;
85
+ }
86
+ /**
87
+ * Lower an IDB query plan to an IDB plan body via the adapter.
88
+ *
89
+ * Threads the contract through {@link IdbLowererContext} so per-field
90
+ * codec encoding can resolve field→codec mappings from the contract's
91
+ * storage schema.
92
+ */
93
+ lower(plan, ctx) {
94
+ const lowererCtx = {
95
+ ...ctx,
96
+ contract: this.#contract
97
+ };
98
+ return this.#adapter.lower(plan, lowererCtx);
99
+ }
100
+ /**
101
+ * Execute a lowered IDB plan body via the driver.
102
+ *
103
+ * Backs `query()` (inherited concretely from `RuntimeCore`) — rows are
104
+ * yielded as-is from the driver (identity pass-through). All current
105
+ * `idb/*` codecs are identity transforms — no per-field decoding is
106
+ * needed. When per-field codec decoding is added (e.g. decoding
107
+ * `idb/date@1` stored values back to `Date` instances, or custom codec
108
+ * output types), it wires up here.
109
+ */
110
+ runDriver(exec) {
111
+ return this.#driver.execute(exec);
112
+ }
113
+ /**
114
+ * Backs `execute()` (inherited concretely from `RuntimeCore`) — IDB has no
115
+ * native "affected-row count without returning rows" concept, so this
116
+ * drains `runDriver()`'s row stream and counts. The driver already
117
+ * materializes every op's touched rows in memory before yielding (see
118
+ * `IdbRuntimeDriverInstance.execute()`'s collect-then-yield strategy), so
119
+ * draining here has no extra cost beyond what a `query()` call over the
120
+ * same plan would already pay.
121
+ *
122
+ * This counts *rows the driver yields*, which correctly equals "ops
123
+ * applied" for every atomic plan kind: `add`/`put`/`update` each echo the
124
+ * one row they touched, `scan-write` echoes every matched row, and
125
+ * `delete` (`execute/ops.ts`'s `execDelete`) walks a cursor over its
126
+ * key/range and echoes each row it deletes — so a delete matching zero
127
+ * keys correctly resolves `{ affectedRows: 0 }` and one matching N keys
128
+ * (a range delete) resolves `{ affectedRows: N }`. For a `batch` plan,
129
+ * `affectedRows` is the sum of rows every op in the batch yields,
130
+ * *including read ops* (`key-get`/`index-get`/`cursor-scan`) if the batch
131
+ * happens to contain any — it's a row count across the whole plan, not a
132
+ * write-only count.
133
+ */
134
+ async runExecute(exec) {
135
+ let affectedRows = 0;
136
+ for await (const _row of this.runDriver(exec)) affectedRows++;
137
+ return { affectedRows };
138
+ }
139
+ async transaction(storeNames, mode = "readwrite") {
140
+ return this.#driver.transaction(storeNames, mode);
141
+ }
142
+ async close() {
143
+ await this.#driver.close();
144
+ }
145
+ /**
146
+ * Verify that the live IDB marker matches the contract.
147
+ *
148
+ * Compares the stored `storageHash` against `contract.storage.storageHash`.
149
+ * A fresh database (no marker store) returns `false` — the caller should
150
+ * ensure migrations have been applied before running queries.
151
+ */
152
+ async verifyMarker() {
153
+ const marker = await this.#driver.readMarker();
154
+ if (marker === null) return false;
155
+ const contractHash = this.#contract["storage"]?.["storageHash"];
156
+ return typeof contractHash === "string" && marker.storageHash === contractHash;
157
+ }
158
+ };
159
+ /**
160
+ * Create an IDB runtime instance.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const driver = createIDBRuntimeDriver("my-app").create();
165
+ * const runtime = createIdbRuntime({ adapter, driver, contract });
166
+ * await runtime.query(plan)
167
+ * await runtime.close();
168
+ * ```
169
+ */
170
+ function createIdbRuntime(options) {
171
+ return new IdbRuntimeImpl(options);
172
+ }
173
+ //#endregion
174
+ export { createIdbRuntime };
175
+
176
+ //# sourceMappingURL=runtime.mjs.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@prisma-idb/runtime-idb",
3
+ "version": "0.0.0",
4
+ "description": "Prisma 8 IDB family — runtime (RuntimeCore subclass that wires adapter + driver)",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "keywords": [
10
+ "prisma",
11
+ "prisma-8",
12
+ "indexeddb",
13
+ "offline-first",
14
+ "idb"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/prisma-idb/prisma-idb.git",
19
+ "directory": "packages/prisma-orm/runtime-idb"
20
+ },
21
+ "homepage": "https://prisma-idb.dev/",
22
+ "bugs": {
23
+ "url": "https://github.com/prisma-idb/prisma-idb/issues"
24
+ },
25
+ "type": "module",
26
+ "exports": {
27
+ "./runtime": "./dist/runtime.mjs",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "dependencies": {
31
+ "@prisma/orm-framework": "8.0.0-rc.5",
32
+ "@prisma-idb/adapter-idb": "0.0.0",
33
+ "@prisma-idb/driver-idb": "0.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "fake-indexeddb": "^6.2.5",
37
+ "tsdown": "^0.22.14",
38
+ "typescript": "^6.0.3",
39
+ "vitest": "^4.1.10"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsdown",
46
+ "dev": "tsdown --watch",
47
+ "check": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "lint": "prettier --check . && eslint .",
50
+ "format": "prettier --write ."
51
+ }
52
+ }