@prisma-idb/client-idb 0.11.0 → 0.13.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.
Files changed (37) hide show
  1. package/README.md +6 -0
  2. package/dist/client-auto.d.mts +2 -2
  3. package/dist/client-auto.d.mts.map +1 -1
  4. package/dist/client-auto.mjs +44 -3
  5. package/dist/client-auto.mjs.map +1 -1
  6. package/dist/client.d.mts +3 -2
  7. package/dist/client.mjs +3 -2
  8. package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
  9. package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
  10. package/dist/internal.d.mts +5 -4
  11. package/dist/internal.d.mts.map +1 -1
  12. package/dist/internal.mjs +2 -2
  13. package/dist/{managed-client-CxlNmERd.mjs → managed-client-DMk8ERfX.mjs} +12 -5
  14. package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
  15. package/dist/{managed-client-CS78EjPu.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
  16. package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
  17. package/dist/mutation-executor-IuJmyWEG.mjs +2132 -0
  18. package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
  19. package/dist/{executor-Bp-hdRg2.d.mts → mutation-scope-CE3GMR4X.d.mts} +79 -28
  20. package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
  21. package/dist/{idb-orm-BitJ-EMq.mjs → orm-transaction-BGx1eukw.mjs} +366 -558
  22. package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
  23. package/dist/{idb-orm-9XN3c9ly.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
  24. package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
  25. package/dist/orm.d.mts +13 -4
  26. package/dist/orm.d.mts.map +1 -1
  27. package/dist/orm.mjs +3 -3
  28. package/package.json +9 -9
  29. package/dist/executor-Bp-hdRg2.d.mts.map +0 -1
  30. package/dist/idb-orm-9XN3c9ly.d.mts.map +0 -1
  31. package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
  32. package/dist/managed-client-CS78EjPu.d.mts.map +0 -1
  33. package/dist/managed-client-CxlNmERd.mjs.map +0 -1
  34. package/dist/mutation-executor-SJqTuSbb.mjs +0 -1196
  35. package/dist/mutation-executor-SJqTuSbb.mjs.map +0 -1
  36. package/dist/mutation-scope-1hS0ogGa.d.mts +0 -33
  37. package/dist/mutation-scope-1hS0ogGa.d.mts.map +0 -1
package/README.md CHANGED
@@ -28,6 +28,12 @@ const open = await db.orm.todo.where({ done: false }).orderBy({ title: "asc" }).
28
28
 
29
29
  The `internal` subpath is for companion packages such as `sync-extension-idb`. Application code uses the entry points above. For nested writes, pass a relation callback to `create()` or `update()`; the client supplies the relation mutator.
30
30
 
31
+ ## Primary-key updates
32
+
33
+ `update()`, `updateAll()`, `updateCount()` and the update arm of `upsert()` reject changes to an existing row’s primary key with code `PRIMARY_KEY_CHANGE_UNSUPPORTED`. The entire mutation rolls back, including referential actions and sync outbox changes. The update input types leave out primary-key fields, so typed code can't set one. This applies to models the contract resolves; for any other model name, only the runtime check applies. At runtime, a patch that repeats the existing key is still allowed, and a query with no matching rows performs no write.
34
+
35
+ Primary keys are immutable. Delete the row and create it again only after handling dependent records. Restrictive relations can block the delete, and cascading relations can delete dependent records. See [ADR 020](../docs/adrs/ADR%20020%20-%20Primary%20Keys%20Are%20Immutable.md) for the reasons.
36
+
31
37
  ## Documentation
32
38
 
33
39
  - [Quick Start](https://prisma-idb.dev/docs/prisma-8/getting-started)
@@ -1,5 +1,5 @@
1
- import { l as IdbContract } from "./executor-Bp-hdRg2.mjs";
2
- import { a as IdbClientOptions, i as IdbClient, n as ManagedIdbClientOptions, o as createIdbClient, r as createManagedIdbClient, t as ManagedIdbClient } from "./managed-client-CS78EjPu.mjs";
1
+ import { d as IdbContract } from "./mutation-scope-CE3GMR4X.mjs";
2
+ import { a as IdbClientOptions, i as IdbClient, n as ManagedIdbClientOptions, o as createIdbClient, r as createManagedIdbClient, t as ManagedIdbClient } from "./managed-client-eksFK4UF.mjs";
3
3
  import { Contract } from "@prisma/orm-framework/contract/types";
4
4
  import { ContractSpace } from "@prisma/orm-framework/components/control";
5
5
  import { IdbExtensionSpace } from "@prisma-idb/family-idb/control";
@@ -1 +1 @@
1
- {"version":3,"file":"client-auto.d.mts","names":[],"sources":["../src/core/auto-migrate.ts","../src/core/managed-auto-migrate.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UA4BiB,yBAAyB,kBAAkB;WACjD,eAAe,cAAc;WAC7B;;WAEA,UAAU;;;;;;;;;WASV,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6ChB,6BAA6B,kBAAkB,aACnE,SAAS,yBAAyB,aACjC,QAAQ,UAAU;;;;;;;;;;;;;;;iBCzDL,2BAA2B,kBAAkB,aAC3D,SAAS,yBAAyB,aACjC,iBAAiB,UAAU"}
1
+ {"version":3,"file":"client-auto.d.mts","names":[],"sources":["../src/core/auto-migrate.ts","../src/core/managed-auto-migrate.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UAmCiB,yBAAyB,kBAAkB;WACjD,eAAe,cAAc;WAC7B;;WAEA,UAAU;;;;;;;;;WASV,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6ChB,6BAA6B,kBAAkB,aACnE,SAAS,yBAAyB,aACjC,QAAQ,UAAU;;;;;;;;;;;;;;;iBChEL,2BAA2B,kBAAkB,aAC3D,SAAS,yBAAyB,aACjC,iBAAiB,UAAU"}
@@ -1,4 +1,6 @@
1
- import { n as createIdbClient, t as createManagedIdbClient } from "./managed-client-CxlNmERd.mjs";
1
+ import { J as getStoreName } from "./mutation-executor-IuJmyWEG.mjs";
2
+ import { n as createIdbClient, t as createManagedIdbClient } from "./managed-client-DMk8ERfX.mjs";
3
+ import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
2
4
  import { isIdbDdlOp, openAndUpgrade, readMarker } from "@prisma-idb/target-idb/runtime";
3
5
  import { APP_SPACE_ID } from "@prisma/orm-framework/components/control";
4
6
  import { canonicalizeJson } from "@prisma/orm-framework/components/utils";
@@ -17,7 +19,7 @@ async function sha256Hex(input) {
17
19
  *
18
20
  * The framework version hashes with Node's `node:crypto` `createHash`, which
19
21
  * does not exist in the browser — so importing it into the runtime auto-migrate
20
- * integrity check (PLAN Issue #23 / ADR 199) throws `createHash is not a
22
+ * integrity check throws `createHash is not a
21
23
  * function` on every client init and breaks the whole app. This version is
22
24
  * **byte-identical** to the framework's: it reuses the same `canonicalizeJson`
23
25
  * and the same nested SHA-256/hex scheme — strip `migrationHash` from the
@@ -138,9 +140,48 @@ async function autoMigrate(input) {
138
140
  markers: orderedPending.map((space) => ({
139
141
  space: space.spaceId,
140
142
  storageHash: space.storageHash
141
- }))
143
+ })),
144
+ onTransformRecords: createTransformRecordsCallback(input.contractSpace.contractJson, input.extensions ?? [])
142
145
  });
143
146
  }
147
+ /**
148
+ * Build the `openAndUpgrade` callback that runs extension hooks after a `transformRecords` op.
149
+ * Hooks see app model names, so they never learn the app's store naming. A store with no app
150
+ * model, such as an extension's own store, skips the hooks.
151
+ */
152
+ function createTransformRecordsCallback(contract, extensions) {
153
+ const models = Object.keys(domainModelsAtDefaultNamespace(contract.domain));
154
+ const hooks = extensions.flatMap((extension) => extension.onTransformRecords ? [extension.onTransformRecords] : []);
155
+ return (tx, op, onDone) => {
156
+ const modelName = models.find((model) => getStoreName(contract, model) === op.storeName);
157
+ if (modelName === void 0) {
158
+ onDone();
159
+ return;
160
+ }
161
+ /** Run extension hooks in order inside the active upgrade transaction. */
162
+ const runHook = (index) => {
163
+ const hook = hooks[index];
164
+ if (!hook) {
165
+ onDone();
166
+ return;
167
+ }
168
+ let completed = false;
169
+ /** Settle each hook once so a duplicate callback cannot advance the upgrade twice. */
170
+ const next = (error) => {
171
+ if (completed) return;
172
+ completed = true;
173
+ if (error === void 0) runHook(index + 1);
174
+ else onDone(error);
175
+ };
176
+ try {
177
+ hook(tx, op, modelName, next);
178
+ } catch (error) {
179
+ next(error);
180
+ }
181
+ };
182
+ runHook(0);
183
+ };
184
+ }
144
185
  /** Validates bundled descriptors before the migration loop opens the database. */
145
186
  function resolveContractSpaces(contractSpace, extensions) {
146
187
  const spaces = [{
@@ -1 +1 @@
1
- {"version":3,"file":"client-auto.mjs","names":[],"sources":["../src/core/migration-hash.ts","../src/core/auto-migrate.ts","../src/core/managed-auto-migrate.ts"],"sourcesContent":["import type { MigrationPackage } from \"@prisma/orm-framework/components/control\";\nimport { canonicalizeJson } from \"@prisma/orm-framework/components/utils\";\n\n/** Hex-encode the bytes of a digest buffer. */\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** SHA-256 → lowercase hex, via WebCrypto (`crypto.subtle`) — works in the browser. */\nasync function sha256Hex(input: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(input));\n return toHex(digest);\n}\n\n/**\n * Browser-safe re-implementation of `computeMigrationHash` from\n * `@prisma/orm-toolchain/migration-tools/hash`.\n *\n * The framework version hashes with Node's `node:crypto` `createHash`, which\n * does not exist in the browser — so importing it into the runtime auto-migrate\n * integrity check (PLAN Issue #23 / ADR 199) throws `createHash is not a\n * function` on every client init and breaks the whole app. This version is\n * **byte-identical** to the framework's: it reuses the same `canonicalizeJson`\n * and the same nested SHA-256/hex scheme — strip `migrationHash` from the\n * metadata, hash the canonicalized metadata and ops separately, then hash\n * the canonicalized pair of those two hashes, as bare lowercase hex (no\n * `sha256:` prefix — dropped repo-wide at rc.4). Reusing the\n * identical canonicalization + algorithm guarantees the result matches the\n * `migrationHash` the CLI recorded, so the integrity check stays meaningful.\n *\n * `crypto.subtle.digest` is async, hence the `Promise` return (the framework's\n * Node version is synchronous). Callers in the async migration path await it.\n */\nexport async function computeMigrationHash(\n metadata: MigrationPackage[\"metadata\"],\n ops: MigrationPackage[\"ops\"]\n): Promise<string> {\n // v0.12.0 strips only `migrationHash` before hashing (`hints`/`labels` were\n // removed from the on-disk manifest schema entirely).\n const stripped: Record<string, unknown> = { ...metadata };\n delete stripped[\"migrationHash\"];\n const inner = await Promise.all([sha256Hex(canonicalizeJson(stripped)), sha256Hex(canonicalizeJson(ops))]);\n return sha256Hex(canonicalizeJson(inner));\n}\n","import type { Contract } from \"@prisma/orm-framework/contract/types\";\nimport type { ContractSpace, MigrationPackage } from \"@prisma/orm-framework/components/control\";\nimport { APP_SPACE_ID } from \"@prisma/orm-framework/components/control\";\nimport type { IdbExtensionSpace } from \"@prisma-idb/family-idb/control\";\n// Browser-safe (WebCrypto) hash — the framework's `@prisma/orm-toolchain/migration-tools/hash`\n// uses `node:crypto` and throws in the browser (PLAN Issue #23 regression).\nimport { computeMigrationHash } from \"./migration-hash\";\n// Import from `./runtime` (not `./migration`) so `MigrationCLI` → `node:fs`\n// is not bundled into the browser client.\nimport { isIdbDdlOp, openAndUpgrade, readMarker, type IdbDdlOp } from \"@prisma-idb/target-idb/runtime\";\nimport { createIdbClient, type IdbClient } from \"./idb-client\";\nimport type { IdbContract } from \"./types\";\n\n// ── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Options for {@link createAutoMigratingIdbClient}.\n *\n * `contractSpace` is the bundled artefact produced at design time by\n * `prisma-idb migration contract-space`. It carries the canonical\n * contract JSON, the ordered list of migration packages, and the head\n * ref the runtime walks toward.\n *\n * `extensions` is an optional list of additional contract spaces contributed\n * by IDB extensions (e.g. the sync extension). Each extension space migrates\n * independently of the application space and writes its own marker row keyed\n * by the extension's `spaceId`.\n */\nexport interface AutoMigrateClientOptions<TContract extends IdbContract> {\n readonly contractSpace: ContractSpace<TContract>;\n readonly dbName: string;\n /** IDB factory override — primarily for tests. Defaults to `indexedDB`. */\n readonly factory?: IDBFactory;\n /**\n * Additional contract spaces from IDB extensions.\n *\n * Every space with pending work — the application space plus each\n * extension — is applied together in one `upgradeneeded` transaction,\n * which also writes every space's marker. Each space still gets its own\n * marker row in `_prisma_next_marker`.\n */\n readonly extensions?: ReadonlyArray<IdbExtensionSpace>;\n}\n\n/**\n * Create a typed IDB client, applying any pending migrations from the bundled\n * `contractSpace` first.\n *\n * **What runs**:\n *\n * 1. Open the database at the current local version, read the marker from\n * `_prisma_next_marker`. (Null for a fresh database)\n * 2. If the marker hash equals `contractSpace.headRef.hash`, the database\n * is already at the target — return the client immediately.\n * 3. Otherwise, walk `contractSpace.migrations` from the marker hash (or\n * `null` for fresh) to `headRef.hash`, collecting each pending\n * package's `ops` in chain order.\n * 4. If `extensions` are configured, repeat steps 1–3 for each extension\n * space, then combine every space's pending ops into ONE reopen at\n * `db.version + 1` so `upgradeneeded` fires once; apply every collected\n * op (all spaces) inside that single version-change transaction, which\n * also writes every migrated space's marker.\n * 5. Hand back the typed `IdbClient`.\n *\n * Every pending op is applied exactly as planned, destructive ones included.\n * The developer reviewed them when running `prisma-idb migration plan`,\n * which warns about any that delete data. Skipping some ops would leave the\n * database claiming a schema it doesn't have, and refusing them would stop\n * the app from opening for every user.\n *\n * **What does NOT run in the browser**:\n *\n * The planner does not ship to the browser. The differ does not run. Live-DB\n * schema introspection does not happen. All the planning was done once at\n * design time and is encoded in the bundled `ops.json` blobs inside\n * `contractSpace.migrations`.\n *\n * @example\n * ```ts\n * import { createAutoMigratingIdbClient } from '@prisma-idb/client-idb/client-auto';\n * import { contractSpace } from './prisma/contract-space.generated';\n *\n * const db = await createAutoMigratingIdbClient({ contractSpace, dbName: 'my-app' });\n * const users = await db.orm.users.all().toArray();\n * ```\n */\nexport async function createAutoMigratingIdbClient<TContract extends IdbContract>(\n options: AutoMigrateClientOptions<TContract>\n): Promise<IdbClient<TContract>> {\n const factory = options.factory ?? indexedDB;\n\n await autoMigrate({\n // The public `AutoMigrateClientOptions<TContract>` is generic over the\n // user's narrow IDB contract; the internal `autoMigrate` only consumes\n // chain-walking fields, so widen to `ContractSpace<Contract>` here.\n contractSpace: options.contractSpace as unknown as ContractSpace<Contract>,\n dbName: options.dbName,\n factory,\n ...(options.extensions !== undefined ? { extensions: options.extensions } : {}),\n });\n\n return createIdbClient({\n contract: options.contractSpace.contractJson,\n dbName: options.dbName,\n factory,\n });\n}\n\n// ── Core migration loop ──────────────────────────────────────────────────────\n\n/**\n * The migration loop. Exported for tests.\n *\n * Collects pending ops from the app space and every extension space, then\n * applies all of them in a single `upgradeneeded` transaction (one IDB\n * version bump total). The same transaction writes the marker of every\n * space that migrated, so schema changes and markers commit together.\n *\n * @internal Prefer {@link createAutoMigratingIdbClient}.\n */\nexport async function autoMigrate(input: {\n // `ContractSpace<Contract>` instead of `<unknown>` so the generic\n // constraint `TContract extends Contract` from the framework is satisfied.\n // The internal apply path only reads `headRef.hash` and `migrations`, so\n // the precise contract shape inside `contractJson` doesn't matter here.\n readonly contractSpace: ContractSpace<Contract>;\n readonly dbName: string;\n readonly factory: IDBFactory;\n readonly extensions?: ReadonlyArray<IdbExtensionSpace>;\n}): Promise<void> {\n const { dbName, factory } = input;\n\n const spaces = resolveContractSpaces(input.contractSpace, input.extensions ?? []);\n\n // Read current DB version once (all spaces share the same IDB database).\n const { currentVersion: initialVersion } = await openAndReadMarker(dbName, factory, APP_SPACE_ID);\n\n // Collect pending work per space without applying yet, so a broken chain in\n // any space fails before the database is touched.\n const pendingPerSpace: Array<{ spaceId: string; ops: IdbDdlOp[]; storageHash: string }> = [];\n\n for (const space of spaces) {\n const targetHash = space.contractSpace.headRef.hash;\n const { markerHash } = await openAndReadMarker(dbName, factory, space.spaceId);\n if (markerHash === targetHash) continue;\n\n const pendingOps = await walkChain({\n markerHash,\n headHash: targetHash,\n migrations: space.contractSpace.migrations,\n });\n // Push whenever the marker is behind `targetHash` — even if every package\n // walked had zero ops (a hash-only \"bridge\" migration, e.g. re-emitting\n // the contract under a new hashing algorithm with no structural change).\n // Gating on `pendingOps.length > 0` here would skip the marker write\n // below for that space, and since nothing ever changes on a later\n // retry either, the marker gets stuck at the old hash forever — the\n // space never converges to `targetHash`, even though there was never\n // any actual work to do.\n pendingPerSpace.push({ spaceId: space.spaceId, ops: pendingOps, storageHash: targetHash });\n }\n\n if (pendingPerSpace.length === 0) return;\n\n // Combine every pending space into ONE upgradeneeded transaction (one IDB\n // version bump total), which also writes every space's marker. A failure\n // partway through rolls back all spaces' schema changes and markers, and a\n // multi-tab user hits one versionchange/blocked cycle on cold start instead\n // of one per space.\n //\n // DDL op order is extensions (alphabetical by spaceId) first, app space\n // last, matching upstream ADR 212's convention. That's safe even though the\n // app space creates `_prisma_next_marker`: openAndUpgrade writes the\n // markers only after every op has run.\n const orderedPending = [...pendingPerSpace].sort((a, b) => {\n if (a.spaceId === APP_SPACE_ID) return 1;\n if (b.spaceId === APP_SPACE_ID) return -1;\n return a.spaceId.localeCompare(b.spaceId);\n });\n\n await openAndUpgrade({\n factory,\n dbName,\n targetVersion: initialVersion + 1,\n ops: orderedPending.flatMap((space) => space.ops),\n markers: orderedPending.map((space) => ({ space: space.spaceId, storageHash: space.storageHash })),\n });\n}\n\n/** Validates bundled descriptors before the migration loop opens the database. */\nfunction resolveContractSpaces(\n contractSpace: ContractSpace<Contract>,\n extensions: ReadonlyArray<IdbExtensionSpace>\n): Array<{ spaceId: string; contractSpace: ContractSpace<Contract> }> {\n // Validate and collect app-first. The combined upgrade applies extensions\n // first, matching upstream ADR 212.\n const spaces: Array<{ spaceId: string; contractSpace: ContractSpace<Contract> }> = [\n { spaceId: APP_SPACE_ID, contractSpace },\n ...extensions,\n ];\n\n // Reject reserved/duplicate extension space IDs before touching the\n // database. Without this, two extensions sharing a spaceId would combine\n // their pending DDL and the upgrade would try to create the same stores\n // twice; an extension using APP_SPACE_ID would have its marker write\n // silently overwrite (or be overwritten by) the app space's marker.\n const seenSpaceIds = new Set<string>();\n for (const extension of extensions) {\n if (extension.spaceId === APP_SPACE_ID || seenSpaceIds.has(extension.spaceId)) {\n throw new Error(`Invalid duplicate or reserved extension space ID: \"${extension.spaceId}\"`);\n }\n seenSpaceIds.add(extension.spaceId);\n }\n\n // Descriptor self-consistency (upstream ADR 212): a space's pinned headRef\n // must match the hash actually embedded in its contractJson. Catches an\n // extension author who edited the contract source without regenerating\n // migrations/refs/head.json — fails loudly here instead of surfacing as a\n // confusing chain-walk error deep inside walkChain.\n for (const space of spaces) {\n const declaredHash = space.contractSpace.contractJson.storage.storageHash;\n const headHash = space.contractSpace.headRef.hash;\n if (declaredHash !== headHash) {\n throw new Error(\n `Contract space \"${space.spaceId}\" is internally inconsistent: ` +\n `contractJson.storage.storageHash (${declaredHash}) does not match ` +\n `headRef.hash (${headHash}). The contract source likely changed without ` +\n \"regenerating migrations/refs/head.json for this space — rebuild its migration chain.\"\n );\n }\n }\n\n return spaces;\n}\n\n/**\n * Walk the migration chain from `markerHash` (or `null` for a fresh DB) to\n * `headHash`, collecting every pending package's ops in order.\n *\n * Throws on chain discontinuity (no package whose `from === cursor`) so\n * misconfigured `contractSpace` inputs fail loudly rather than silently\n * leaving the DB at an intermediate state.\n */\nasync function walkChain(input: {\n readonly markerHash: string | null;\n readonly headHash: string;\n readonly migrations: readonly MigrationPackage[];\n}): Promise<IdbDdlOp[]> {\n const byFrom = new Map<string | null, MigrationPackage>();\n for (const pkg of input.migrations) {\n byFrom.set(pkg.metadata.from, pkg);\n }\n\n const pendingOps: IdbDdlOp[] = [];\n let cursor: string | null = input.markerHash;\n const visited = new Set<string | null>();\n\n while (cursor !== input.headHash) {\n if (visited.has(cursor)) {\n throw new Error(\n `Auto-migration chain contains a cycle at hash ${JSON.stringify(cursor)}. ` +\n \"Re-run `prisma-idb migration contract-space` to rebuild a valid chain.\"\n );\n }\n visited.add(cursor);\n const next = byFrom.get(cursor);\n if (!next) {\n throw new Error(\n `Auto-migration chain broken: no migration package with from === ${JSON.stringify(cursor)}. ` +\n \"Verify that contract-space.generated.ts is up to date by re-running \" +\n \"`prisma-idb migration contract-space`.\"\n );\n }\n const computedHash = await computeMigrationHash(next.metadata, next.ops);\n if (computedHash !== next.metadata.migrationHash) {\n throw new Error(\n `Migration package \"${next.dirName}\" failed integrity check: ` +\n `stored migrationHash ${next.metadata.migrationHash} does not match ` +\n `computed hash ${computedHash}. ` +\n \"The ops may have been edited after the package was generated. \" +\n \"Re-run `prisma-idb migration plan` to regenerate the package.\"\n );\n }\n for (const op of next.ops) {\n if (!isIdbDdlOp(op)) {\n throw new Error(`Non-IDB operation found in migration package ${next.dirName}: ${JSON.stringify(op)}`);\n }\n pendingOps.push(op);\n }\n cursor = next.metadata.to;\n }\n\n return pendingOps;\n}\n\n/**\n * Open the database at its current local version (no version arg), read the\n * marker for `spaceId`, then close the connection. Returns the current integer\n * version so the caller can compute `currentVersion + 1` for the upgrade re-open.\n */\nfunction openAndReadMarker(\n dbName: string,\n factory: IDBFactory,\n spaceId: string\n): Promise<{ currentVersion: number; markerHash: string | null }> {\n return new Promise((resolve, reject) => {\n let req: IDBOpenDBRequest;\n try {\n req = factory.open(dbName);\n } catch (err) {\n reject(err);\n return;\n }\n\n req.onsuccess = () => {\n const db = req.result;\n const currentVersion = db.version;\n void (async () => {\n try {\n const record = await readMarker(db, spaceId);\n resolve({ currentVersion, markerHash: record?.storageHash ?? null });\n } catch (err) {\n reject(err);\n } finally {\n db.close();\n }\n })();\n };\n req.onerror = () => {\n reject(req.error ?? new Error(`IDB open failed while reading marker for \"${dbName}\"`));\n };\n });\n}\n","/**\n * `createManagedAutoIdbClient` — `createManagedIdbClient` pre-wired to\n * `createAutoMigratingIdbClient`. Composing those two by hand means writing\n * `dbName` twice, into two independent option bags with nothing tying them\n * together — `createManagedIdbClient`'s `dbName` is used only by `reset()`'s\n * `deleteDatabase` call, so if it ever drifts from the factory's own\n * `dbName` (typo, copy-paste into a second app), `reset()` silently deletes\n * the wrong database (or nothing) while the real one keeps its data. This\n * reads `dbName` (and a `factory` override, if set) once and threads it to\n * both layers, so they can't diverge.\n */\n\nimport { createAutoMigratingIdbClient } from \"./auto-migrate\";\nimport type { AutoMigrateClientOptions } from \"./auto-migrate\";\nimport type { IdbClient } from \"./idb-client\";\nimport { createManagedIdbClient } from \"./managed-client\";\nimport type { ManagedIdbClient } from \"./managed-client\";\nimport type { IdbContract } from \"./types\";\n\n/**\n * @example\n * ```ts\n * import { createManagedAutoIdbClient } from '@prisma-idb/client-idb/client-auto';\n *\n * const db = createManagedAutoIdbClient({ contractSpace, dbName: 'my-app' });\n *\n * const client = await db.get();\n * // ... on logout:\n * await db.reset();\n * ```\n */\nexport function createManagedAutoIdbClient<TContract extends IdbContract>(\n options: AutoMigrateClientOptions<TContract>\n): ManagedIdbClient<IdbClient<TContract>> {\n return createManagedIdbClient(() => createAutoMigratingIdbClient(options), {\n dbName: options.dbName,\n ...(options.factory !== undefined ? { idbFactory: options.factory } : {}),\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-auto.mjs","names":[],"sources":["../src/core/migration-hash.ts","../src/core/auto-migrate.ts","../src/core/managed-auto-migrate.ts"],"sourcesContent":["import type { MigrationPackage } from \"@prisma/orm-framework/components/control\";\nimport { canonicalizeJson } from \"@prisma/orm-framework/components/utils\";\n\n/** Hex-encode the bytes of a digest buffer. */\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** SHA-256 → lowercase hex, via WebCrypto (`crypto.subtle`) — works in the browser. */\nasync function sha256Hex(input: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(input));\n return toHex(digest);\n}\n\n/**\n * Browser-safe re-implementation of `computeMigrationHash` from\n * `@prisma/orm-toolchain/migration-tools/hash`.\n *\n * The framework version hashes with Node's `node:crypto` `createHash`, which\n * does not exist in the browser — so importing it into the runtime auto-migrate\n * integrity check throws `createHash is not a\n * function` on every client init and breaks the whole app. This version is\n * **byte-identical** to the framework's: it reuses the same `canonicalizeJson`\n * and the same nested SHA-256/hex scheme — strip `migrationHash` from the\n * metadata, hash the canonicalized metadata and ops separately, then hash\n * the canonicalized pair of those two hashes, as bare lowercase hex (no\n * `sha256:` prefix — dropped repo-wide at rc.4). Reusing the\n * identical canonicalization + algorithm guarantees the result matches the\n * `migrationHash` the CLI recorded, so the integrity check stays meaningful.\n *\n * `crypto.subtle.digest` is async, hence the `Promise` return (the framework's\n * Node version is synchronous). Callers in the async migration path await it.\n */\nexport async function computeMigrationHash(\n metadata: MigrationPackage[\"metadata\"],\n ops: MigrationPackage[\"ops\"]\n): Promise<string> {\n // Exclude the stored digest so it cannot become part of its own preimage.\n const stripped: Record<string, unknown> = { ...metadata };\n delete stripped[\"migrationHash\"];\n const inner = await Promise.all([sha256Hex(canonicalizeJson(stripped)), sha256Hex(canonicalizeJson(ops))]);\n return sha256Hex(canonicalizeJson(inner));\n}\n","import { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\nimport type { Contract } from \"@prisma/orm-framework/contract/types\";\nimport type { ContractSpace, MigrationPackage } from \"@prisma/orm-framework/components/control\";\nimport { APP_SPACE_ID } from \"@prisma/orm-framework/components/control\";\nimport type { IdbExtensionSpace } from \"@prisma-idb/family-idb/control\";\n// Browser-safe (WebCrypto) hash — the framework's `@prisma/orm-toolchain/migration-tools/hash`\n// uses `node:crypto`, which is unavailable in the browser.\nimport { computeMigrationHash } from \"./migration-hash\";\n// Import from `./runtime` (not `./migration`) so `MigrationCLI` → `node:fs`\n// is not bundled into the browser client.\nimport {\n isIdbDdlOp,\n openAndUpgrade,\n readMarker,\n type IdbDdlOp,\n type TransformRecordsOp,\n} from \"@prisma-idb/target-idb/runtime\";\nimport { createIdbClient, type IdbClient } from \"./idb-client\";\nimport { getStoreName, type IdbContract } from \"./types\";\n\n// ── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Options for {@link createAutoMigratingIdbClient}.\n *\n * `contractSpace` is the bundled artefact produced at design time by\n * `prisma-idb migration contract-space`. It carries the canonical\n * contract JSON, the ordered list of migration packages, and the head\n * ref the runtime walks toward.\n *\n * `extensions` is an optional list of additional contract spaces contributed\n * by IDB extensions (e.g. the sync extension). Each extension space migrates\n * independently of the application space and writes its own marker row keyed\n * by the extension's `spaceId`.\n */\nexport interface AutoMigrateClientOptions<TContract extends IdbContract> {\n readonly contractSpace: ContractSpace<TContract>;\n readonly dbName: string;\n /** IDB factory override — primarily for tests. Defaults to `indexedDB`. */\n readonly factory?: IDBFactory;\n /**\n * Additional contract spaces from IDB extensions.\n *\n * Every space with pending work — the application space plus each\n * extension — is applied together in one `upgradeneeded` transaction,\n * which also writes every space's marker. Each space still gets its own\n * marker row in `_prisma_next_marker`.\n */\n readonly extensions?: ReadonlyArray<IdbExtensionSpace>;\n}\n\n/**\n * Create a typed IDB client, applying any pending migrations from the bundled\n * `contractSpace` first.\n *\n * **What runs**:\n *\n * 1. Open the database at the current local version, read the marker from\n * `_prisma_next_marker`. (Null for a fresh database)\n * 2. If the marker hash equals `contractSpace.headRef.hash`, the database\n * is already at the target — return the client immediately.\n * 3. Otherwise, walk `contractSpace.migrations` from the marker hash (or\n * `null` for fresh) to `headRef.hash`, collecting each pending\n * package's `ops` in chain order.\n * 4. If `extensions` are configured, repeat steps 1–3 for each extension\n * space, then combine every space's pending ops into ONE reopen at\n * `db.version + 1` so `upgradeneeded` fires once; apply every collected\n * op (all spaces) inside that single version-change transaction, which\n * also writes every migrated space's marker.\n * 5. Hand back the typed `IdbClient`.\n *\n * Every pending op is applied exactly as planned, destructive ones included.\n * The developer reviewed them when running `prisma-idb migration plan`,\n * which warns about any that delete data. Skipping some ops would leave the\n * database claiming a schema it doesn't have, and refusing them would stop\n * the app from opening for every user.\n *\n * **What does NOT run in the browser**:\n *\n * The planner does not ship to the browser. The differ does not run. Live-DB\n * schema introspection does not happen. All the planning was done once at\n * design time and is encoded in the bundled `ops.json` blobs inside\n * `contractSpace.migrations`.\n *\n * @example\n * ```ts\n * import { createAutoMigratingIdbClient } from '@prisma-idb/client-idb/client-auto';\n * import { contractSpace } from './prisma/contract-space.generated';\n *\n * const db = await createAutoMigratingIdbClient({ contractSpace, dbName: 'my-app' });\n * const users = await db.orm.users.all().toArray();\n * ```\n */\nexport async function createAutoMigratingIdbClient<TContract extends IdbContract>(\n options: AutoMigrateClientOptions<TContract>\n): Promise<IdbClient<TContract>> {\n const factory = options.factory ?? indexedDB;\n\n await autoMigrate({\n // The public `AutoMigrateClientOptions<TContract>` is generic over the\n // user's narrow IDB contract; the internal `autoMigrate` only consumes\n // chain-walking fields, so widen to `ContractSpace<Contract>` here.\n contractSpace: options.contractSpace as unknown as ContractSpace<Contract>,\n dbName: options.dbName,\n factory,\n ...(options.extensions !== undefined ? { extensions: options.extensions } : {}),\n });\n\n return createIdbClient({\n contract: options.contractSpace.contractJson,\n dbName: options.dbName,\n factory,\n });\n}\n\n// ── Core migration loop ──────────────────────────────────────────────────────\n\n/**\n * The migration loop. Exported for tests.\n *\n * Collects pending ops from the app space and every extension space, then\n * applies all of them in a single `upgradeneeded` transaction (one IDB\n * version bump total). The same transaction writes the marker of every\n * space that migrated, so schema changes and markers commit together.\n *\n * @internal Prefer {@link createAutoMigratingIdbClient}.\n */\nexport async function autoMigrate(input: {\n // `ContractSpace<Contract>` instead of `<unknown>` so the generic\n // constraint `TContract extends Contract` from the framework is satisfied.\n // The internal apply path only reads `headRef.hash` and `migrations`, so\n // the precise contract shape inside `contractJson` doesn't matter here.\n readonly contractSpace: ContractSpace<Contract>;\n readonly dbName: string;\n readonly factory: IDBFactory;\n readonly extensions?: ReadonlyArray<IdbExtensionSpace>;\n}): Promise<void> {\n const { dbName, factory } = input;\n\n const spaces = resolveContractSpaces(input.contractSpace, input.extensions ?? []);\n\n // Read current DB version once (all spaces share the same IDB database).\n const { currentVersion: initialVersion } = await openAndReadMarker(dbName, factory, APP_SPACE_ID);\n\n // Collect pending work per space without applying yet, so a broken chain in\n // any space fails before the database is touched.\n const pendingPerSpace: Array<{ spaceId: string; ops: IdbDdlOp[]; storageHash: string }> = [];\n\n for (const space of spaces) {\n const targetHash = space.contractSpace.headRef.hash;\n const { markerHash } = await openAndReadMarker(dbName, factory, space.spaceId);\n if (markerHash === targetHash) continue;\n\n const pendingOps = await walkChain({\n markerHash,\n headHash: targetHash,\n migrations: space.contractSpace.migrations,\n });\n // Push whenever the marker is behind `targetHash` — even if every package\n // walked had zero ops (a hash-only \"bridge\" migration, e.g. re-emitting\n // the contract under a new hashing algorithm with no structural change).\n // Gating on `pendingOps.length > 0` here would skip the marker write\n // below for that space, and since nothing ever changes on a later\n // retry either, the marker gets stuck at the old hash forever — the\n // space never converges to `targetHash`, even though there was never\n // any actual work to do.\n pendingPerSpace.push({ spaceId: space.spaceId, ops: pendingOps, storageHash: targetHash });\n }\n\n if (pendingPerSpace.length === 0) return;\n\n // Combine every pending space into ONE upgradeneeded transaction (one IDB\n // version bump total), which also writes every space's marker. A failure\n // partway through rolls back all spaces' schema changes and markers, and a\n // multi-tab user hits one versionchange/blocked cycle on cold start instead\n // of one per space.\n //\n // DDL op order is extensions (alphabetical by spaceId) first, app space\n // last, matching upstream ADR 212's convention. That's safe even though the\n // app space creates `_prisma_next_marker`: openAndUpgrade writes the\n // markers only after every op has run.\n const orderedPending = [...pendingPerSpace].sort((a, b) => {\n if (a.spaceId === APP_SPACE_ID) return 1;\n if (b.spaceId === APP_SPACE_ID) return -1;\n return a.spaceId.localeCompare(b.spaceId);\n });\n\n await openAndUpgrade({\n factory,\n dbName,\n targetVersion: initialVersion + 1,\n ops: orderedPending.flatMap((space) => space.ops),\n markers: orderedPending.map((space) => ({ space: space.spaceId, storageHash: space.storageHash })),\n onTransformRecords: createTransformRecordsCallback(input.contractSpace.contractJson, input.extensions ?? []),\n });\n}\n\n/**\n * Build the `openAndUpgrade` callback that runs extension hooks after a `transformRecords` op.\n * Hooks see app model names, so they never learn the app's store naming. A store with no app\n * model, such as an extension's own store, skips the hooks.\n */\nfunction createTransformRecordsCallback(contract: Contract, extensions: ReadonlyArray<IdbExtensionSpace>) {\n const models = Object.keys(domainModelsAtDefaultNamespace(contract.domain));\n const hooks = extensions.flatMap((extension) => (extension.onTransformRecords ? [extension.onTransformRecords] : []));\n return (tx: IDBTransaction, op: TransformRecordsOp, onDone: (error?: unknown) => void): void => {\n const modelName = models.find((model) => getStoreName(contract as IdbContract, model) === op.storeName);\n if (modelName === undefined) {\n onDone();\n return;\n }\n /** Run extension hooks in order inside the active upgrade transaction. */\n const runHook = (index: number): void => {\n const hook = hooks[index];\n if (!hook) {\n onDone();\n return;\n }\n let completed = false;\n /** Settle each hook once so a duplicate callback cannot advance the upgrade twice. */\n const next = (error?: unknown): void => {\n if (completed) return;\n completed = true;\n if (error === undefined) runHook(index + 1);\n else onDone(error);\n };\n try {\n hook(tx, op, modelName, next);\n } catch (error) {\n next(error);\n }\n };\n runHook(0);\n };\n}\n\n/** Validates bundled descriptors before the migration loop opens the database. */\nfunction resolveContractSpaces(\n contractSpace: ContractSpace<Contract>,\n extensions: ReadonlyArray<IdbExtensionSpace>\n): Array<{ spaceId: string; contractSpace: ContractSpace<Contract> }> {\n // Validate and collect app-first. The combined upgrade applies extensions\n // first, matching upstream ADR 212.\n const spaces: Array<{ spaceId: string; contractSpace: ContractSpace<Contract> }> = [\n { spaceId: APP_SPACE_ID, contractSpace },\n ...extensions,\n ];\n\n // Reject reserved/duplicate extension space IDs before touching the\n // database. Without this, two extensions sharing a spaceId would combine\n // their pending DDL and the upgrade would try to create the same stores\n // twice; an extension using APP_SPACE_ID would have its marker write\n // silently overwrite (or be overwritten by) the app space's marker.\n const seenSpaceIds = new Set<string>();\n for (const extension of extensions) {\n if (extension.spaceId === APP_SPACE_ID || seenSpaceIds.has(extension.spaceId)) {\n throw new Error(`Invalid duplicate or reserved extension space ID: \"${extension.spaceId}\"`);\n }\n seenSpaceIds.add(extension.spaceId);\n }\n\n // Descriptor self-consistency (upstream ADR 212): a space's pinned headRef\n // must match the hash actually embedded in its contractJson. Catches an\n // extension author who edited the contract source without regenerating\n // migrations/refs/head.json — fails loudly here instead of surfacing as a\n // confusing chain-walk error deep inside walkChain.\n for (const space of spaces) {\n const declaredHash = space.contractSpace.contractJson.storage.storageHash;\n const headHash = space.contractSpace.headRef.hash;\n if (declaredHash !== headHash) {\n throw new Error(\n `Contract space \"${space.spaceId}\" is internally inconsistent: ` +\n `contractJson.storage.storageHash (${declaredHash}) does not match ` +\n `headRef.hash (${headHash}). The contract source likely changed without ` +\n \"regenerating migrations/refs/head.json for this space — rebuild its migration chain.\"\n );\n }\n }\n\n return spaces;\n}\n\n/**\n * Walk the migration chain from `markerHash` (or `null` for a fresh DB) to\n * `headHash`, collecting every pending package's ops in order.\n *\n * Throws on chain discontinuity (no package whose `from === cursor`) so\n * misconfigured `contractSpace` inputs fail loudly rather than silently\n * leaving the DB at an intermediate state.\n */\nasync function walkChain(input: {\n readonly markerHash: string | null;\n readonly headHash: string;\n readonly migrations: readonly MigrationPackage[];\n}): Promise<IdbDdlOp[]> {\n const byFrom = new Map<string | null, MigrationPackage>();\n for (const pkg of input.migrations) {\n byFrom.set(pkg.metadata.from, pkg);\n }\n\n const pendingOps: IdbDdlOp[] = [];\n let cursor: string | null = input.markerHash;\n const visited = new Set<string | null>();\n\n while (cursor !== input.headHash) {\n if (visited.has(cursor)) {\n throw new Error(\n `Auto-migration chain contains a cycle at hash ${JSON.stringify(cursor)}. ` +\n \"Re-run `prisma-idb migration contract-space` to rebuild a valid chain.\"\n );\n }\n visited.add(cursor);\n const next = byFrom.get(cursor);\n if (!next) {\n throw new Error(\n `Auto-migration chain broken: no migration package with from === ${JSON.stringify(cursor)}. ` +\n \"Verify that contract-space.generated.ts is up to date by re-running \" +\n \"`prisma-idb migration contract-space`.\"\n );\n }\n const computedHash = await computeMigrationHash(next.metadata, next.ops);\n if (computedHash !== next.metadata.migrationHash) {\n throw new Error(\n `Migration package \"${next.dirName}\" failed integrity check: ` +\n `stored migrationHash ${next.metadata.migrationHash} does not match ` +\n `computed hash ${computedHash}. ` +\n \"The ops may have been edited after the package was generated. \" +\n \"Re-run `prisma-idb migration plan` to regenerate the package.\"\n );\n }\n for (const op of next.ops) {\n if (!isIdbDdlOp(op)) {\n throw new Error(`Non-IDB operation found in migration package ${next.dirName}: ${JSON.stringify(op)}`);\n }\n pendingOps.push(op);\n }\n cursor = next.metadata.to;\n }\n\n return pendingOps;\n}\n\n/**\n * Open the database at its current local version (no version arg), read the\n * marker for `spaceId`, then close the connection. Returns the current integer\n * version so the caller can compute `currentVersion + 1` for the upgrade re-open.\n */\nfunction openAndReadMarker(\n dbName: string,\n factory: IDBFactory,\n spaceId: string\n): Promise<{ currentVersion: number; markerHash: string | null }> {\n return new Promise((resolve, reject) => {\n let req: IDBOpenDBRequest;\n try {\n req = factory.open(dbName);\n } catch (err) {\n reject(err);\n return;\n }\n\n req.onsuccess = () => {\n const db = req.result;\n const currentVersion = db.version;\n void (async () => {\n try {\n const record = await readMarker(db, spaceId);\n resolve({ currentVersion, markerHash: record?.storageHash ?? null });\n } catch (err) {\n reject(err);\n } finally {\n db.close();\n }\n })();\n };\n req.onerror = () => {\n reject(req.error ?? new Error(`IDB open failed while reading marker for \"${dbName}\"`));\n };\n });\n}\n","/**\n * `createManagedAutoIdbClient` — `createManagedIdbClient` pre-wired to\n * `createAutoMigratingIdbClient`. Composing those two by hand means writing\n * `dbName` twice, into two independent option bags with nothing tying them\n * together — `createManagedIdbClient`'s `dbName` is used only by `reset()`'s\n * `deleteDatabase` call, so if it ever drifts from the factory's own\n * `dbName` (typo, copy-paste into a second app), `reset()` silently deletes\n * the wrong database (or nothing) while the real one keeps its data. This\n * reads `dbName` (and a `factory` override, if set) once and threads it to\n * both layers, so they can't diverge.\n */\n\nimport { createAutoMigratingIdbClient } from \"./auto-migrate\";\nimport type { AutoMigrateClientOptions } from \"./auto-migrate\";\nimport type { IdbClient } from \"./idb-client\";\nimport { createManagedIdbClient } from \"./managed-client\";\nimport type { ManagedIdbClient } from \"./managed-client\";\nimport type { IdbContract } from \"./types\";\n\n/**\n * @example\n * ```ts\n * import { createManagedAutoIdbClient } from '@prisma-idb/client-idb/client-auto';\n *\n * const db = createManagedAutoIdbClient({ contractSpace, dbName: 'my-app' });\n *\n * const client = await db.get();\n * // ... on logout:\n * await db.reset();\n * ```\n */\nexport function createManagedAutoIdbClient<TContract extends IdbContract>(\n options: AutoMigrateClientOptions<TContract>\n): ManagedIdbClient<IdbClient<TContract>> {\n return createManagedIdbClient(() => createAutoMigratingIdbClient(options), {\n dbName: options.dbName,\n ...(options.factory !== undefined ? { idbFactory: options.factory } : {}),\n });\n}\n"],"mappings":"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"}
package/dist/client.d.mts CHANGED
@@ -1,2 +1,3 @@
1
- import { a as IdbClientOptions, i as IdbClient, n as ManagedIdbClientOptions, o as createIdbClient, r as createManagedIdbClient, t as ManagedIdbClient } from "./managed-client-CS78EjPu.mjs";
2
- export { type IdbClient, type IdbClientOptions, type ManagedIdbClient, type ManagedIdbClientOptions, createIdbClient, createManagedIdbClient };
1
+ import { n as IdbTransactionCommittedEarlyError, t as IdbOrmTransaction } from "./orm-transaction-B_ZJ92uY.mjs";
2
+ import { a as IdbClientOptions, i as IdbClient, n as ManagedIdbClientOptions, o as createIdbClient, r as createManagedIdbClient, t as ManagedIdbClient } from "./managed-client-eksFK4UF.mjs";
3
+ export { type IdbClient, type IdbClientOptions, type IdbOrmTransaction, IdbTransactionCommittedEarlyError, type ManagedIdbClient, type ManagedIdbClientOptions, createIdbClient, createManagedIdbClient };
package/dist/client.mjs CHANGED
@@ -1,2 +1,3 @@
1
- import { n as createIdbClient, t as createManagedIdbClient } from "./managed-client-CxlNmERd.mjs";
2
- export { createIdbClient, createManagedIdbClient };
1
+ import { t as IdbTransactionCommittedEarlyError } from "./orm-transaction-BGx1eukw.mjs";
2
+ import { n as createIdbClient, t as createManagedIdbClient } from "./managed-client-DMk8ERfX.mjs";
3
+ export { IdbTransactionCommittedEarlyError, createIdbClient, createManagedIdbClient };
@@ -0,0 +1,21 @@
1
+ import { d as IdbContract } from "./mutation-scope-CE3GMR4X.mjs";
2
+ import { IdbTransactionScope } from "@prisma-idb/driver-idb/runtime";
3
+ //#region src/core/decimal-identity.d.ts
4
+ /** A legacy store or sync metadata set contains several spellings of one Decimal key. */
5
+ declare class IdbDecimalIdentityConflictError extends Error {
6
+ readonly code = "DECIMAL_IDENTITY_CONFLICT";
7
+ constructor(modelName: string);
8
+ }
9
+ /** Whether a primary key contains a contract-declared Decimal field. */
10
+ declare function hasDecimalPrimaryKey(contract: IdbContract, modelName: string): boolean;
11
+ /** Compare scalar or compound primary keys using Decimal codecs and native IDB equality elsewhere. */
12
+ declare function decimalKeysEqual(contract: IdbContract, modelName: string, left: unknown, right: unknown): boolean;
13
+ /** Keep an existing Decimal primary-key spelling when the supplied value identifies the same row. */
14
+ declare function preserveDecimalKeySpelling(contract: IdbContract, modelName: string, record: Record<string, unknown>, storedKey: unknown): Record<string, unknown>;
15
+ /** Read the physical key first, then check Decimal aliases without choosing a legacy duplicate. */
16
+ declare function resolveDecimalKey(scope: Pick<IdbTransactionScope, "execute">, contract: IdbContract, modelName: string, key: unknown, snapshot?: readonly Record<string, unknown>[]): Promise<Record<string, unknown> | undefined>;
17
+ /** Wrap the transaction seam so nested, bulk and raw sync writes share the same identity rules. */
18
+ declare function withDecimalIdentityScope(scope: IdbTransactionScope, contract: IdbContract): IdbTransactionScope;
19
+ //#endregion
20
+ export { resolveDecimalKey as a, preserveDecimalKeySpelling as i, decimalKeysEqual as n, withDecimalIdentityScope as o, hasDecimalPrimaryKey as r, IdbDecimalIdentityConflictError as t };
21
+ //# sourceMappingURL=decimal-identity-D_1J9lPN.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal-identity-D_1J9lPN.d.mts","names":[],"sources":["../src/core/decimal-identity.ts"],"mappings":";;;;cAgBa,wCAAwC;WAC1C;EAEG,YAAA;;;iBASE,qBAAqB,UAAU,aAAa;;iBAM5C,iBAAiB,UAAU,aAAa,mBAAmB,eAAe;;iBAe1E,2BACd,UAAU,aACV,mBACA,QAAQ,yBACR,qBACC;;iBAiBmB,kBACpB,OAAO,KAAK,iCACZ,UAAU,aACV,mBACA,cACA,oBAAoB,4BACnB,QAAQ;;iBAqHK,yBAAyB,OAAO,qBAAqB,UAAU,cAAc"}
@@ -1,7 +1,8 @@
1
- import { l as IdbContract } from "./executor-Bp-hdRg2.mjs";
2
- import "./mutation-scope-1hS0ogGa.mjs";
3
- import { IdbFilterExpr } from "@prisma-idb/adapter-idb/runtime";
1
+ import { d as IdbContract } from "./mutation-scope-CE3GMR4X.mjs";
2
+ import { a as resolveDecimalKey, i as preserveDecimalKeySpelling, n as decimalKeysEqual, o as withDecimalIdentityScope, r as hasDecimalPrimaryKey, t as IdbDecimalIdentityConflictError } from "./decimal-identity-D_1J9lPN.mjs";
3
+ import "@prisma/orm-framework/contract/types";
4
4
  import { IdbTransactionScope } from "@prisma-idb/driver-idb/runtime";
5
+ import { IdbFilterExpr } from "@prisma-idb/adapter-idb/runtime";
5
6
  //#region src/core/mutation-executor.d.ts
6
7
  /**
7
8
  * Transitively walks the `onDelete` cascade graph from `modelName`, collecting
@@ -35,5 +36,5 @@ declare function collectDeleteStoreNames(contract: IdbContract, modelName: strin
35
36
  */
36
37
  declare function applyReferentialActionsForRow(scope: IdbTransactionScope, contract: IdbContract, modelName: string, row: Record<string, unknown>, visited?: Set<string>): Promise<void>;
37
38
  //#endregion
38
- export { applyReferentialActionsForRow, collectDeleteStoreNames };
39
+ export { IdbDecimalIdentityConflictError, applyReferentialActionsForRow, collectDeleteStoreNames, decimalKeysEqual, hasDecimalPrimaryKey, preserveDecimalKeySpelling, resolveDecimalKey, withDecimalIdentityScope };
39
40
  //# sourceMappingURL=internal.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.mts","names":[],"sources":["../src/core/mutation-executor.ts"],"mappings":";;;;;;;;;;;;;;;;iBAqzCgB,wBAAwB,UAAU,aAAa;;;;;;;;;;;;;;;;;;;iBAsCzC,8BACpB,OAAO,qBACP,UAAU,aACV,mBACA,KAAK,yBACL,UAAS,cACR"}
1
+ {"version":3,"file":"internal.d.mts","names":[],"sources":["../src/core/mutation-executor.ts"],"mappings":";;;;;;;;;;;;;;;;;iBA62CgB,wBAAwB,UAAU,aAAa;;;;;;;;;;;;;;;;;;;iBAuCzC,8BACpB,OAAO,qBACP,UAAU,aACV,mBACA,KAAK,yBACL,UAAS,cACR"}
package/dist/internal.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { r as collectDeleteStoreNames, t as applyReferentialActionsForRow } from "./mutation-executor-SJqTuSbb.mjs";
2
- export { applyReferentialActionsForRow, collectDeleteStoreNames };
1
+ import { B as resolveDecimalKey, H as withDecimalIdentityScope, I as IdbDecimalIdentityConflictError, L as decimalKeysEqual, R as hasDecimalPrimaryKey, r as collectDeleteStoreNames, t as applyReferentialActionsForRow, z as preserveDecimalKeySpelling } from "./mutation-executor-IuJmyWEG.mjs";
2
+ export { IdbDecimalIdentityConflictError, applyReferentialActionsForRow, collectDeleteStoreNames, decimalKeysEqual, hasDecimalPrimaryKey, preserveDecimalKeySpelling, resolveDecimalKey, withDecimalIdentityScope };
@@ -1,8 +1,8 @@
1
- import { y as withMutationScope } from "./mutation-executor-SJqTuSbb.mjs";
2
- import { t as idbOrm } from "./idb-orm-BitJ-EMq.mjs";
3
- import { IdbAdapter } from "@prisma-idb/adapter-idb/runtime";
4
- import { idbCodecLookup } from "@prisma-idb/target-idb/runtime";
1
+ import { w as withMutationScope } from "./mutation-executor-IuJmyWEG.mjs";
2
+ import { n as runOrmTransaction, r as idbOrm } from "./orm-transaction-BGx1eukw.mjs";
5
3
  import { createIDBRuntimeDriver } from "@prisma-idb/driver-idb/runtime";
4
+ import { idbCodecLookup } from "@prisma-idb/target-idb/runtime";
5
+ import { IdbAdapter } from "@prisma-idb/adapter-idb/runtime";
6
6
  import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
7
7
  //#region src/core/idb-client.ts
8
8
  /**
@@ -40,6 +40,10 @@ function createIdbClient(options) {
40
40
  contract: options.contract,
41
41
  executor: runtime
42
42
  }),
43
+ transaction: (rootKeys, fn) => runOrmTransaction({
44
+ contract: options.contract,
45
+ executor: runtime
46
+ }, rootKeys, fn),
43
47
  withTransaction: (storeNames, fn) => withMutationScope(runtime, storeNames, fn),
44
48
  verifyMarker: () => runtime.verifyMarker(),
45
49
  async close() {
@@ -76,6 +80,7 @@ function createManagedIdbClient(open, options) {
76
80
  let client = null;
77
81
  let clientPromise = null;
78
82
  let resetPromise = null;
83
+ /** Wait for a reset before sharing or opening the singleton client. */
79
84
  async function get() {
80
85
  if (resetPromise) await resetPromise;
81
86
  if (client) return client;
@@ -87,12 +92,14 @@ function createManagedIdbClient(open, options) {
87
92
  client = fresh;
88
93
  return fresh;
89
94
  }
95
+ /** Release the cached connection so a later get can open it again. */
90
96
  async function close() {
91
97
  if (!client) return;
92
98
  await client.close();
93
99
  client = null;
94
100
  clientPromise = null;
95
101
  }
102
+ /** Wait for an in-flight open, then close and delete the database before allowing new opens. */
96
103
  async function reset() {
97
104
  if (resetPromise) return resetPromise;
98
105
  resetPromise = (async () => {
@@ -120,4 +127,4 @@ function createManagedIdbClient(open, options) {
120
127
  //#endregion
121
128
  export { createIdbClient as n, createManagedIdbClient as t };
122
129
 
123
- //# sourceMappingURL=managed-client-CxlNmERd.mjs.map
130
+ //# sourceMappingURL=managed-client-DMk8ERfX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-client-DMk8ERfX.mjs","names":[],"sources":["../src/core/idb-client.ts","../src/core/managed-client.ts"],"sourcesContent":["import { IdbAdapter } from \"@prisma-idb/adapter-idb/runtime\";\nimport { createIDBRuntimeDriver } from \"@prisma-idb/driver-idb/runtime\";\nimport type { IdbTransactionScope } from \"@prisma-idb/driver-idb/runtime\";\nimport type { IdbMiddleware } from \"@prisma-idb/runtime-idb/runtime\";\nimport { createIdbRuntime } from \"@prisma-idb/runtime-idb/runtime\";\nimport { idbCodecLookup } from \"@prisma-idb/target-idb/runtime\";\nimport { withMutationScope } from \"./mutation-scope\";\nimport { idbOrm } from \"./idb-orm\";\nimport type { IdbOrmClient } from \"./idb-orm\";\nimport { runOrmTransaction } from \"./orm-transaction\";\nimport type { IdbOrmTransaction } from \"./orm-transaction\";\nimport type { IdbContract } from \"./types\";\n\n/** The contract, database name, and runtime hooks used to create a client. */\nexport interface IdbClientOptions<TContract extends IdbContract> {\n readonly contract: TContract;\n readonly dbName: string;\n // No version — the migration runner owns the IDB version integer (ADR 001).\n /** IDB factory override — primarily for tests and custom browser realms. */\n readonly factory?: IDBFactory;\n readonly middleware?: readonly IdbMiddleware[];\n}\n\n/** A typed ORM and the connection and transaction operations that own its lifetime. */\nexport interface IdbClient<TContract extends IdbContract> {\n readonly orm: IdbOrmClient<TContract>;\n /**\n * Run `fn` in one readwrite transaction over the models behind `rootKeys`.\n *\n * `tx` has the ORM accessors of the listed root keys only, and every\n * operation on it takes part in the transaction, including foreign-key\n * checks, referential actions and `include`. The transaction commits when\n * `fn` resolves and rolls back when it throws. A failed operation aborts the\n * whole transaction, even if `fn` catches the error. The callback may return\n * an ORM query result directly, without an `async` wrapper.\n *\n * Await only `tx` operations inside `fn`. IndexedDB commits a transaction\n * once it has no pending request, so awaiting a timer or a network call ends\n * the transaction early (ADR 005). The next operation, or a later throw,\n * then fails with `IdbTransactionCommittedEarlyError`, and the writes made\n * before that point stay saved. Do not call `transaction()` from inside `fn`;\n * awaiting the inner call counts as awaiting other work and ends the outer transaction early.\n *\n * @example\n * ```ts\n * await db.transaction([\"users\", \"posts\"], async (tx) => {\n * const user = await tx.users.create({ id: \"u1\", name: \"Alice\" });\n * await tx.posts.create({ id: \"p1\", authorId: user.id, title: \"First\" });\n * });\n * ```\n */\n transaction<TRootKey extends string & keyof TContract[\"roots\"], T>(\n rootKeys: readonly TRootKey[],\n fn: (tx: IdbOrmTransaction<TContract, TRootKey>) => PromiseLike<T>\n ): Promise<T>;\n /**\n * Run `fn` inside a single multi-store readwrite IDB transaction.\n *\n * Opens the transaction, passes an `IdbTransactionScope` to `fn`, then\n * commits on success or rolls back on error. Equivalent to calling\n * `withMutationScope(runtime, storeNames, fn)`.\n *\n * Useful from test harnesses and from any caller that has an `IdbClient`\n * but not the raw runtime reference.\n */\n withTransaction<T>(storeNames: string[], fn: (scope: IdbTransactionScope) => Promise<T>): Promise<T>;\n verifyMarker(): Promise<boolean>;\n close(): Promise<void>;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\n/**\n * Creates a typed IDB client from a contract and a database name.\n *\n * Assembles the full runtime stack (driver → adapter → runtime → ORM) internally.\n * Equivalent to `postgres({ contract, url })` in `@prisma/orm-postgres/runtime`.\n *\n * The IDB database version is not exposed — it is managed by the migration runner\n * per ADR 001. The driver opens at the current database version, which is correct\n * for a runtime that only reads/writes (no DDL).\n *\n * @example\n * ```ts\n * import { createIdbClient } from '@prisma-idb/client-idb/client';\n * import contract from './contract';\n *\n * export const db = createIdbClient({ contract, dbName: 'my-app' });\n *\n * // Later:\n * const users = await db.orm.users.all().toArray();\n * ```\n */\nexport function createIdbClient<TContract extends IdbContract>(\n options: IdbClientOptions<TContract>\n): IdbClient<TContract> {\n const driver = createIDBRuntimeDriver(\n options.dbName,\n undefined,\n options.factory !== undefined ? { factory: options.factory } : undefined\n ).create();\n const adapter = new IdbAdapter(idbCodecLookup);\n const runtime = createIdbRuntime({\n adapter,\n driver,\n contract: options.contract as Record<string, unknown>,\n ...(options.middleware !== undefined && options.middleware.length > 0 ? { middleware: options.middleware } : {}),\n });\n const orm = idbOrm({ contract: options.contract, executor: runtime });\n\n return {\n orm,\n transaction: (rootKeys, fn) => runOrmTransaction({ contract: options.contract, executor: runtime }, rootKeys, fn),\n withTransaction: <T>(storeNames: string[], fn: (scope: IdbTransactionScope) => Promise<T>) =>\n withMutationScope(runtime, storeNames, fn),\n verifyMarker: () => runtime.verifyMarker(),\n async close() {\n await runtime.close();\n },\n [Symbol.asyncDispose]() {\n return this.close();\n },\n };\n}\n","/**\n * `createManagedIdbClient` — a singleton, race-safe wrapper around any IDB\n * client factory (`createIdbClient`, `createAutoMigratingIdbClient`, or a\n * sync-extension factory like `createAutoMigratingSyncIdbClient` — anything\n * returning a `close(): Promise<void>`-shaped client).\n *\n * Every consumer of this package that wants a module-level singleton client\n * plus a \"wipe local data\" reset (e.g. on logout) needs the same race-safety:\n * `reset()` must never call `indexedDB.deleteDatabase` while a `get()` open\n * is still in flight, since a still-opening connection would block (or worse,\n * leave a stale handle once that open resolves after the delete). This was\n * previously hand-rolled per app with diverging correctness; see ADR-less\n * discussion in the kanban example's `db.ts` history.\n */\n\nexport interface ManagedIdbClientOptions {\n /** IDB database name — passed to `indexedDB.deleteDatabase` on `reset()`. */\n readonly dbName: string;\n /** IDB factory override — primarily for tests. Defaults to `indexedDB`. */\n readonly idbFactory?: IDBFactory;\n}\n\n/** A shared client handle with coordinated opening and database reset. */\nexport interface ManagedIdbClient<TClient> {\n /** Opens the client on first call and caches it; concurrent calls share one open. */\n get(): Promise<TClient>;\n /** Closes the cached client, if any. Safe to call when nothing is open. */\n close(): Promise<void>;\n /**\n * Closes and deletes the database — waits out any `get()` open already in\n * flight first. `onblocked` (another tab still has the DB open) is NOT\n * terminal — per spec, the delete request stays pending, and still fires\n * `onsuccess` once every blocking connection closes — so this promise\n * stays pending through it too, settling only once `deleteDatabase`\n * actually reaches `onsuccess` or `onerror`. Neither resolving early\n * (claims success before the old data is actually gone) nor rejecting\n * early (reports failure on a delete that may still succeed) would be\n * accurate.\n */\n reset(): Promise<void>;\n}\n\n/**\n * Wraps `open` (a client factory, e.g. `() => createAutoMigratingIdbClient({...})`)\n * with a module-scoped singleton: `get()` opens once and caches the result,\n * `close()` releases it, `reset()` closes and wipes the underlying database.\n *\n * @example\n * ```ts\n * import { createManagedIdbClient } from '@prisma-idb/client-idb/client';\n *\n * const db = createManagedIdbClient(\n * () => createAutoMigratingIdbClient({ contractSpace, dbName: 'my-app' }),\n * { dbName: 'my-app' }\n * );\n *\n * const client = await db.get();\n * // ... on logout:\n * await db.reset();\n * ```\n */\nexport function createManagedIdbClient<TClient extends { close(): Promise<void> }>(\n open: () => Promise<TClient>,\n options: ManagedIdbClientOptions\n): ManagedIdbClient<TClient> {\n const { dbName, idbFactory = indexedDB } = options;\n\n let client: TClient | null = null;\n let clientPromise: Promise<TClient> | null = null;\n // Gate between get() and reset() — set synchronously (no `await` before\n // either reads/writes it) so the two never interleave: see module doc comment.\n let resetPromise: Promise<void> | null = null;\n\n /** Wait for a reset before sharing or opening the singleton client. */\n async function get(): Promise<TClient> {\n if (resetPromise) await resetPromise;\n if (client) return client;\n clientPromise ??= open().catch((err: unknown) => {\n // Clear on rejection so a later get() call retries instead of\n // re-awaiting the same rejected promise forever.\n clientPromise = null;\n throw err;\n });\n\n const fresh = await clientPromise;\n client = fresh;\n return fresh;\n }\n\n /** Release the cached connection so a later get can open it again. */\n async function close(): Promise<void> {\n if (!client) return;\n await client.close();\n client = null;\n clientPromise = null;\n }\n\n /** Wait for an in-flight open, then close and delete the database before allowing new opens. */\n async function reset(): Promise<void> {\n if (resetPromise) return resetPromise;\n resetPromise = (async () => {\n if (clientPromise) await clientPromise.catch(() => {});\n await close();\n await new Promise<void>((resolve, reject) => {\n const req = idbFactory.deleteDatabase(dbName);\n req.onsuccess = () => resolve();\n req.onerror = () => reject(req.error as Error);\n // Deliberately no-op: `onblocked` only means the request is waiting\n // on another open connection, not that it failed — `onsuccess` (or\n // `onerror`) still fires later once that connection closes. Settling\n // here either way would misreport the outcome; see the `reset()`\n // doc comment above.\n req.onblocked = () => {};\n });\n })();\n try {\n await resetPromise;\n } finally {\n resetPromise = null;\n }\n }\n\n return { get, close, reset };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4FA,SAAgB,gBACd,SACsB;CACtB,MAAM,SAAS,uBACb,QAAQ,QACR,KAAA,GACA,QAAQ,YAAY,KAAA,IAAY,EAAE,SAAS,QAAQ,QAAQ,IAAI,KAAA,CACjE,CAAC,CAAC,OAAO;CACT,MAAM,UAAU,IAAI,WAAW,cAAc;CAC7C,MAAM,UAAU,iBAAiB;EAC/B;EACA;EACA,UAAU,QAAQ;EAClB,GAAI,QAAQ,eAAe,KAAA,KAAa,QAAQ,WAAW,SAAS,IAAI,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;CAChH,CAAC;CAGD,OAAO;EACL,KAHU,OAAO;GAAE,UAAU,QAAQ;GAAU,UAAU;EAAQ,CAG/D;EACF,cAAc,UAAU,OAAO,kBAAkB;GAAE,UAAU,QAAQ;GAAU,UAAU;EAAQ,GAAG,UAAU,EAAE;EAChH,kBAAqB,YAAsB,OACzC,kBAAkB,SAAS,YAAY,EAAE;EAC3C,oBAAoB,QAAQ,aAAa;EACzC,MAAM,QAAQ;GACZ,MAAM,QAAQ,MAAM;EACtB;EACA,CAAC,OAAO,gBAAgB;GACtB,OAAO,KAAK,MAAM;EACpB;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;AC7DA,SAAgB,uBACd,MACA,SAC2B;CAC3B,MAAM,EAAE,QAAQ,aAAa,cAAc;CAE3C,IAAI,SAAyB;CAC7B,IAAI,gBAAyC;CAG7C,IAAI,eAAqC;;CAGzC,eAAe,MAAwB;EACrC,IAAI,cAAc,MAAM;EACxB,IAAI,QAAQ,OAAO;EACnB,kBAAkB,KAAK,CAAC,CAAC,OAAO,QAAiB;GAG/C,gBAAgB;GAChB,MAAM;EACR,CAAC;EAED,MAAM,QAAQ,MAAM;EACpB,SAAS;EACT,OAAO;CACT;;CAGA,eAAe,QAAuB;EACpC,IAAI,CAAC,QAAQ;EACb,MAAM,OAAO,MAAM;EACnB,SAAS;EACT,gBAAgB;CAClB;;CAGA,eAAe,QAAuB;EACpC,IAAI,cAAc,OAAO;EACzB,gBAAgB,YAAY;GAC1B,IAAI,eAAe,MAAM,cAAc,YAAY,CAAC,CAAC;GACrD,MAAM,MAAM;GACZ,MAAM,IAAI,SAAe,SAAS,WAAW;IAC3C,MAAM,MAAM,WAAW,eAAe,MAAM;IAC5C,IAAI,kBAAkB,QAAQ;IAC9B,IAAI,gBAAgB,OAAO,IAAI,KAAc;IAM7C,IAAI,kBAAkB,CAAC;GACzB,CAAC;EACH,EAAA,CAAG;EACH,IAAI;GACF,MAAM;EACR,UAAU;GACR,eAAe;EACjB;CACF;CAEA,OAAO;EAAE;EAAK;EAAO;CAAM;AAC7B"}
@@ -1,8 +1,9 @@
1
- import { l as IdbContract } from "./executor-Bp-hdRg2.mjs";
2
- import { t as IdbOrmClient } from "./idb-orm-9XN3c9ly.mjs";
1
+ import { d as IdbContract } from "./mutation-scope-CE3GMR4X.mjs";
2
+ import { i as IdbOrmClient, t as IdbOrmTransaction } from "./orm-transaction-B_ZJ92uY.mjs";
3
3
  import { IdbTransactionScope } from "@prisma-idb/driver-idb/runtime";
4
4
  import { IdbMiddleware } from "@prisma-idb/runtime-idb/runtime";
5
5
  //#region src/core/idb-client.d.ts
6
+ /** The contract, database name, and runtime hooks used to create a client. */
6
7
  interface IdbClientOptions<TContract extends IdbContract> {
7
8
  readonly contract: TContract;
8
9
  readonly dbName: string;
@@ -10,8 +11,35 @@ interface IdbClientOptions<TContract extends IdbContract> {
10
11
  readonly factory?: IDBFactory;
11
12
  readonly middleware?: readonly IdbMiddleware[];
12
13
  }
14
+ /** A typed ORM and the connection and transaction operations that own its lifetime. */
13
15
  interface IdbClient<TContract extends IdbContract> {
14
16
  readonly orm: IdbOrmClient<TContract>;
17
+ /**
18
+ * Run `fn` in one readwrite transaction over the models behind `rootKeys`.
19
+ *
20
+ * `tx` has the ORM accessors of the listed root keys only, and every
21
+ * operation on it takes part in the transaction, including foreign-key
22
+ * checks, referential actions and `include`. The transaction commits when
23
+ * `fn` resolves and rolls back when it throws. A failed operation aborts the
24
+ * whole transaction, even if `fn` catches the error. The callback may return
25
+ * an ORM query result directly, without an `async` wrapper.
26
+ *
27
+ * Await only `tx` operations inside `fn`. IndexedDB commits a transaction
28
+ * once it has no pending request, so awaiting a timer or a network call ends
29
+ * the transaction early (ADR 005). The next operation, or a later throw,
30
+ * then fails with `IdbTransactionCommittedEarlyError`, and the writes made
31
+ * before that point stay saved. Do not call `transaction()` from inside `fn`;
32
+ * awaiting the inner call counts as awaiting other work and ends the outer transaction early.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * await db.transaction(["users", "posts"], async (tx) => {
37
+ * const user = await tx.users.create({ id: "u1", name: "Alice" });
38
+ * await tx.posts.create({ id: "p1", authorId: user.id, title: "First" });
39
+ * });
40
+ * ```
41
+ */
42
+ transaction<TRootKey extends string & keyof TContract["roots"], T>(rootKeys: readonly TRootKey[], fn: (tx: IdbOrmTransaction<TContract, TRootKey>) => PromiseLike<T>): Promise<T>;
15
43
  /**
16
44
  * Run `fn` inside a single multi-store readwrite IDB transaction.
17
45
  *
@@ -71,6 +99,7 @@ interface ManagedIdbClientOptions {
71
99
  /** IDB factory override — primarily for tests. Defaults to `indexedDB`. */
72
100
  readonly idbFactory?: IDBFactory;
73
101
  }
102
+ /** A shared client handle with coordinated opening and database reset. */
74
103
  interface ManagedIdbClient<TClient> {
75
104
  /** Opens the client on first call and caches it; concurrent calls share one open. */
76
105
  get(): Promise<TClient>;
@@ -113,4 +142,4 @@ declare function createManagedIdbClient<TClient extends {
113
142
  }>(open: () => Promise<TClient>, options: ManagedIdbClientOptions): ManagedIdbClient<TClient>;
114
143
  //#endregion
115
144
  export { IdbClientOptions as a, IdbClient as i, ManagedIdbClientOptions as n, createIdbClient as o, createManagedIdbClient as r, ManagedIdbClient as t };
116
- //# sourceMappingURL=managed-client-CS78EjPu.d.mts.map
145
+ //# sourceMappingURL=managed-client-eksFK4UF.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-client-eksFK4UF.d.mts","names":[],"sources":["../src/core/idb-client.ts","../src/core/managed-client.ts"],"mappings":";;;;;;UAciB,iBAAiB,kBAAkB;WACzC,UAAU;WACV;;WAGA,UAAU;WACV,sBAAsB;;;UAIhB,UAAU,kBAAkB;WAClC,KAAK,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;EA0B3B,YAAY,gCAAgC,oBAAoB,GAC9D,mBAAmB,YACnB,KAAK,IAAI,kBAAkB,WAAW,cAAc,YAAY,KAC/D,QAAQ;;;;;;;;;;;EAWX,gBAAgB,GAAG,sBAAsB,KAAK,OAAO,wBAAwB,QAAQ,KAAK,QAAQ;EAClG,gBAAgB;EAChB,SAAS;GACR,OAAO,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;iBAwBX,gBAAgB,kBAAkB,aAChD,SAAS,iBAAiB,aACzB,UAAU;;;;;;;;;;;;;;;;;UC/EI;;WAEN;;WAEA,aAAa;;;UAIP,iBAAiB;;EAEhC,OAAO,QAAQ;;EAEf,SAAS;;;;;;;;;;;;EAYT,SAAS;;;;;;;;;;;;;;;;;;;;;iBAsBK,uBAAuB;EAAkB,SAAS;GAChE,YAAY,QAAQ,UACpB,SAAS,0BACR,iBAAiB"}