{"_id":"@aibulat/indexeddb","_rev":"6-8b83a098ce867b0784fedf119798e70d","name":"@aibulat/indexeddb","dist-tags":{"latest":"0.1.6"},"versions":{"0.1.0":{"name":"@aibulat/indexeddb","version":"0.1.0","keywords":["IndexedDB","idb","database","browser","storage","promise","async","ESM","TypeScript"],"author":{"name":"Aibulat"},"license":"MIT","_id":"@aibulat/indexeddb@0.1.0","maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"dist":{"shasum":"84ed1ed8d20fd27dd2aff7abc444c6a40be76512","tarball":"https://registry.npmjs.org/@aibulat/indexeddb/-/indexeddb-0.1.0.tgz","fileCount":14,"integrity":"sha512-90m40iRV7j+TptATPx/agM7SvRSYMSMN8muZNfhd4DfxF9fPrJJZpiNrfWAa62mfgGQ8TQZhSPy8YhHdNBaZ7g==","signatures":[{"sig":"MEYCIQDjKyU2YgTGtNjuSa5b/ME97epN34Ph7MeT06EuRGi7kQIhAKpNUgLdNiiM69SnhkeymNX/lyRFOOW3g3k99On21tqw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":176217},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"2308832545386b91d8f483c81c29e8d2cf7b9a3a","scripts":{"dev":"tsdown --watch","test":"node --test \"test/**/*.test.ts\"","build":"tsdown","prepack":"pnpm run build","typecheck":"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"aibulat","email":"ngmAibulat@gmail.com"},"repository":{"url":"git+https://github.com/ngmaibulat/packages.git","type":"git","directory":"packages/indexeddb"},"_npmVersion":"11.19.0","description":"A small wrapper that makes IndexedDB usable","directories":{},"_nodeVersion":"26.7.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.2.0","tsdown":"^0.22.14","publint":"^0.3.23","typescript":"^5.9.3","@types/chai":"^5.2.2","@types/node":"^22.13.1","fake-indexeddb":"^6.2.5","@arethetypeswrong/core":"^0.18.5","conditional-type-checks":"^1.0.6"},"_npmOperationalInternal":{"tmp":"tmp/indexeddb_0.1.0_1786871201532_0.5750528969923754","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@aibulat/indexeddb","version":"0.1.1","keywords":["IndexedDB","idb","database","browser","storage","promise","async","ESM","TypeScript"],"author":{"name":"Aibulat"},"license":"MIT","_id":"@aibulat/indexeddb@0.1.1","maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"dist":{"shasum":"494c08992b6cec6a60584180d5b09a0e605ca37a","tarball":"https://registry.npmjs.org/@aibulat/indexeddb/-/indexeddb-0.1.1.tgz","fileCount":14,"integrity":"sha512-g6Qt2qkrewwGd6I12lzEEaZ5USoSioV+RJ3D64Zxg7zcneYIycreoTsqXuV91gzhW75+c3vq1m5BNcv36qlgaQ==","signatures":[{"sig":"MEQCICLJfX/ioSAbRBCuAAyP3Gk5zKJdUQpkx7eeCmkBfrO7AiAsx3BysmnZZnBFCD0KcUjDmKl5x7XB50+PhA+DjYrJnA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aibulat%2findexeddb@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":225697},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"bun":">=1.3","node":">=26"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"4669312c7ec7ac4debf694844424c373fe46d17a","scripts":{"dev":"tsdown --watch","test":"node --test \"test/**/*.test.ts\"","build":"tsdown","prepack":"pnpm run build","typecheck":"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0ddc23a5-f4bd-403a-8ef4-377fd66083ca"}},"repository":{"url":"git+https://github.com/ngmaibulat/packages.git","type":"git","directory":"packages/indexeddb"},"_npmVersion":"12.0.2","description":"A small wrapper that makes IndexedDB usable","directories":{},"_nodeVersion":"26.7.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.2.0","tsdown":"^0.22.14","publint":"^0.3.23","typescript":"^5.9.3","@types/chai":"^5.2.2","@types/node":"^26.0.0","@arethetypeswrong/core":"^0.18.5","@aibulat/indexeddb-impl":"^0.1.0","conditional-type-checks":"^1.0.6"},"_npmOperationalInternal":{"tmp":"tmp/indexeddb_0.1.1_1786887295632_0.7350213701330632","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@aibulat/indexeddb","version":"0.1.2","keywords":["IndexedDB","idb","database","browser","storage","promise","async","ESM","TypeScript"],"author":{"name":"Aibulat"},"license":"MIT","_id":"@aibulat/indexeddb@0.1.2","maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"dist":{"shasum":"2c11c2e4d02c28b69ee7e156dc8070394349bb27","tarball":"https://registry.npmjs.org/@aibulat/indexeddb/-/indexeddb-0.1.2.tgz","fileCount":54,"integrity":"sha512-zmTNxYD8drzP8h9lb24PcZeKhIyO+mUF9BHggkfwtiJVIIz8Fen8WuAYi9UfHGHmYvc1vc/niv2uiHLo4Cv8gw==","signatures":[{"sig":"MEYCIQC3dHAOVFyifTQ4kr8lhXVAZIZLDeih6VDo4R8LT6SXMgIhANXR3GG/6TQpHoCnYnyApqKFdwjJj38jKHavWk3gMn+/","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aibulat%2findexeddb@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":979211},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"bun":">=1.3","node":">=26"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./nexie":{"types":"./dist/nexie.d.ts","default":"./dist/nexie.js"},"./package.json":"./package.json"},"gitHead":"69024f47f0e1692601c8069fa118888e0b52e497","scripts":{"dev":"tsdown --watch","test":"node --test \"test/**/*.test.ts\"","build":"tsdown","prepack":"pnpm run build","test:bun":"bun test test/","typecheck":"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0ddc23a5-f4bd-403a-8ef4-377fd66083ca"}},"repository":{"url":"git+https://github.com/ngmaibulat/packages.git","type":"git","directory":"packages/indexeddb"},"_npmVersion":"12.0.2","description":"A small wrapper that makes IndexedDB usable","directories":{},"_nodeVersion":"26.7.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.2.0","tsdown":"^0.22.14","publint":"^0.3.23","typescript":"^5.9.3","@types/chai":"^5.2.2","@types/node":"^26.0.0","@arethetypeswrong/core":"^0.18.5","@aibulat/indexeddb-impl":"^0.1.2","conditional-type-checks":"^1.0.6"},"_npmOperationalInternal":{"tmp":"tmp/indexeddb_0.1.2_1786902855808_0.11087454729687973","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@aibulat/indexeddb","version":"0.1.3","keywords":["IndexedDB","idb","database","browser","storage","promise","async","ESM","TypeScript"],"author":{"name":"Aibulat"},"license":"MIT","_id":"@aibulat/indexeddb@0.1.3","maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"dist":{"shasum":"821acd121ef7a4d8d7ea27a0b7de5775af27c2ec","tarball":"https://registry.npmjs.org/@aibulat/indexeddb/-/indexeddb-0.1.3.tgz","fileCount":57,"integrity":"sha512-zneGYeCZTWXdGYrSK48c/nEhS9cR5cCHRiO3nNs0alYlWMKmb+fe0+IocMfpzIY5s5ArStKBSce5Y2+TcGm3HQ==","signatures":[{"sig":"MEUCIQCesEvG606nza1hUR90dTBdfc1wNAps2u0+hvEA21mavwIgJ7ap8Xl/Zu8uyFKoeSVoZZ4ieSV7E69tc2a3G5T/XGE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aibulat%2findexeddb@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1088808},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"bun":">=1.3","node":">=26"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./nexie":{"types":"./dist/nexie.d.ts","default":"./dist/nexie.js"},"./package.json":"./package.json"},"gitHead":"ed41564820f3ae511e09ee80d622c02b630a4ef4","scripts":{"dev":"tsdown --watch","test":"node --test \"test/**/*.test.ts\"","build":"tsdown","prepack":"pnpm run build","test:bun":"bun test test/","typecheck":"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0ddc23a5-f4bd-403a-8ef4-377fd66083ca"}},"repository":{"url":"git+https://github.com/ngmaibulat/packages.git","type":"git","directory":"packages/indexeddb"},"_npmVersion":"12.0.2","description":"A small wrapper that makes IndexedDB usable","directories":{},"_nodeVersion":"26.7.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.2.0","tsdown":"^0.22.14","publint":"^0.3.23","typescript":"^5.9.3","@types/chai":"^5.2.2","@types/node":"^26.0.0","@arethetypeswrong/core":"^0.18.5","@aibulat/indexeddb-impl":"^0.1.2","conditional-type-checks":"^1.0.6"},"_npmOperationalInternal":{"tmp":"tmp/indexeddb_0.1.3_1786906385441_0.814247620611243","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@aibulat/indexeddb","version":"0.1.5","keywords":["IndexedDB","idb","nexie","dexie","database","orm","query-builder","livequery","browser","storage","promise","async","ESM","TypeScript"],"author":{"name":"Aibulat"},"license":"MIT","_id":"@aibulat/indexeddb@0.1.5","maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"dist":{"shasum":"76b4b7685710828cfaaf73e5c55bfd693ae53527","tarball":"https://registry.npmjs.org/@aibulat/indexeddb/-/indexeddb-0.1.5.tgz","fileCount":57,"integrity":"sha512-gaufKnBYQAXPeZ+RGXwIeDxQ0+irD6NzHuKjRIhti9bskqPN8XRLbJ9xOwW4ci7ftLpQ20iINF52eHVXa8YGgg==","signatures":[{"sig":"MEQCIDjA/13NT426j/TonA5aq/ply0NZGGyopC6FgUHo0RwHAiAgr49t8tVyEcYAKmeC6AVLWvOikdKQJGM9OHMLWttrCg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aibulat%2findexeddb@0.1.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1105508},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"bun":">=1.3","node":">=26"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./nexie":{"types":"./dist/nexie.d.ts","default":"./dist/nexie.js"},"./package.json":"./package.json"},"gitHead":"d4663ad2a0d80c17e644be8f601d1ea3f2bb38b5","scripts":{"dev":"tsdown --watch","test":"node --test \"test/**/*.test.ts\"","build":"tsdown","prepack":"pnpm run build","test:bun":"bun test test/","typecheck":"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0ddc23a5-f4bd-403a-8ef4-377fd66083ca"}},"repository":{"url":"git+https://github.com/ngmaibulat/packages.git","type":"git","directory":"packages/indexeddb"},"_npmVersion":"12.0.2","description":"IndexedDB with usability: a small promise wrapper over the raw API, plus Nexie, a Dexie-compatible high-level API, from one package","directories":{},"_nodeVersion":"26.7.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.2.0","tsdown":"^0.22.14","publint":"^0.3.23","typescript":"^5.9.3","@types/chai":"^5.2.2","@types/node":"^26.0.0","@arethetypeswrong/core":"^0.18.5","@aibulat/indexeddb-impl":"^0.1.2","conditional-type-checks":"^1.0.6"},"_npmOperationalInternal":{"tmp":"tmp/indexeddb_0.1.5_1786952827775_0.9731102590191296","host":"s3://npm-registry-packages-npm-production"}},"0.1.6":{"name":"@aibulat/indexeddb","version":"0.1.6","description":"IndexedDB with usability: a small promise wrapper over the raw API, plus Nexie, a Dexie-compatible high-level API, from one package","type":"module","sideEffects":true,"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./nexie":{"types":"./dist/nexie.d.ts","default":"./dist/nexie.js"},"./package.json":"./package.json"},"scripts":{"build":"tsdown","dev":"tsdown --watch","typecheck":"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json","test":"node --test \"test/**/*.test.ts\"","test:bun":"bun test test/","prepack":"pnpm run build"},"publishConfig":{"access":"public"},"keywords":["IndexedDB","idb","nexie","dexie","database","orm","query-builder","livequery","browser","storage","promise","async","ESM","TypeScript"],"repository":{"type":"git","url":"git+https://github.com/ngmaibulat/packages.git","directory":"packages/indexeddb"},"bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","author":{"name":"Aibulat"},"license":"MIT","devDependencies":{"@arethetypeswrong/core":"^0.18.5","@types/chai":"^5.2.2","@types/node":"^26.0.0","chai":"^5.2.0","conditional-type-checks":"^1.0.6","@aibulat/indexeddb-impl":"^0.1.2","publint":"^0.3.23","tsdown":"^0.22.14","typescript":"^5.9.3"},"engines":{"node":">=26","bun":">=1.3"},"gitHead":"c17373957fcc4be2a88e69b1939bd5f74a24059b","_id":"@aibulat/indexeddb@0.1.6","_nodeVersion":"26.7.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-/UTkkvk2za99kqe9+Vk0p3nVJSgmQVr3LpQ7I6bvF1qjP39GkS9xrdIBz6oxyD39JgCnrz/X2oTQPU718oFiJA==","shasum":"b621cf1e42fdb747c5283aedba6bc9309cf70517","tarball":"https://registry.npmjs.org/@aibulat/indexeddb/-/indexeddb-0.1.6.tgz","fileCount":58,"unpackedSize":1188690,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aibulat%2findexeddb@0.1.6","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIH22YkEkc85fJ27V7Xa/aY1vG6ov3lxurWyFkgV4sNaTAiBEX5zlSYlPM0kqdSSbLTArSWSgHKQBN7VROHpe6GRwCw=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0ddc23a5-f4bd-403a-8ef4-377fd66083ca"}},"directories":{},"maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/indexeddb_0.1.6_1786976816566_0.07159851069408618"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-16T09:06:41.310Z","modified":"2026-08-17T14:26:57.010Z","0.1.0":"2026-08-16T09:06:41.680Z","0.1.1":"2026-08-16T13:34:55.803Z","0.1.2":"2026-08-16T17:54:15.957Z","0.1.3":"2026-08-16T18:53:05.582Z","0.1.5":"2026-08-17T07:47:07.971Z","0.1.6":"2026-08-17T14:26:56.706Z"},"bugs":{"url":"https://github.com/ngmaibulat/packages/issues"},"author":{"name":"Aibulat"},"license":"MIT","homepage":"https://github.com/ngmaibulat/packages/tree/main/packages/indexeddb#readme","keywords":["IndexedDB","idb","nexie","dexie","database","orm","query-builder","livequery","browser","storage","promise","async","ESM","TypeScript"],"repository":{"type":"git","url":"git+https://github.com/ngmaibulat/packages.git","directory":"packages/indexeddb"},"description":"IndexedDB with usability: a small promise wrapper over the raw API, plus Nexie, a Dexie-compatible high-level API, from one package","maintainers":[{"name":"aibulat","email":"ngmAibulat@gmail.com"}],"readme":"# @aibulat/indexeddb\n\nIndexedDB with usability. One package, **two APIs** — install it once and import the one that suits the code you are writing.\n\n> A fork of [`idb`](https://github.com/jakearchibald/idb) by Jake Archibald, forked at v8.0.3. The low-level API is an **API-compatible superset** of idb: everything idb does works the same way, plus fixes and additions upstream has not shipped — see [Changes](#changes) for the list. Ships ESM only, and is maintained as part of the [`@aibulat`](https://github.com/ngmaibulat/packages) workspace. See [LICENSE](LICENSE) for the retained upstream notice.\n\n## Two APIs\n\n|                     | [Part 1 — Low-level](#part-1--low-level-api-aibulatindexeddb)                    | [Part 2 — Nexie, high-level](#part-2--nexie-the-high-level-api-aibulatindexeddbnexie) |\n| ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| **Import**          | `@aibulat/indexeddb`                                                             | `@aibulat/indexeddb/nexie`                                                            |\n| **Shape**           | `openDB()`, object stores, transactions, cursors                                 | `new Nexie()`, schema DSL, query builder                                              |\n| **Mental model**    | raw IndexedDB, with promises and types                                           | a small ORM over IndexedDB                                                            |\n| **Size**            | ~1.9 kB brotli'd                                                                 | ~124 kB unminified                                                                    |\n| **Transactions**    | native — they auto-commit if you `await` anything else                           | join automatically across `await`                                                     |\n| **Reactive queries**| —                                                                                | `liveQuery`                                                                           |\n| **Coming from**     | `idb` — it is a superset, nothing to change                                      | `dexie` — migration is a rename                                                       |\n\nThe two module graphs are **disjoint**: nothing in the Nexie graph imports the low-level entry, so importing one never pulls in the other. Choosing Nexie costs the low-level bundle nothing, and the ~1.9 kB figure above stays true for anyone who never touches `./nexie`.\n\n1. [Installation](#installation)\n1. [Changes](#changes)\n1. [Browser support](#browser-support)\n1. [**Part 1 — Low-level API** (`@aibulat/indexeddb`)](#part-1--low-level-api-aibulatindexeddb)\n   1. [`openDB`](#opendb)\n   1. [`deleteDB`](#deletedb)\n   1. [`unwrap`](#unwrap)\n   1. [`wrap`](#wrap)\n   1. [General enhancements](#general-enhancements)\n   1. [`IDBDatabase` enhancements](#idbdatabase-enhancements)\n   1. [`IDBTransaction` enhancements](#idbtransaction-enhancements)\n   1. [`IDBCursor` enhancements](#idbcursor-enhancements)\n   1. [Async iterators](#async-iterators)\n   1. [`getAll` options](#getall-options)\n   1. [`ignoreConstraints`](#ignoreconstraints)\n   1. [Closing with `using`](#closing-with-using)\n   1. [Examples](#examples)\n   1. [TypeScript](#typescript)\n1. [**Part 2 — Nexie, the high-level API** (`@aibulat/indexeddb/nexie`)](#part-2--nexie-the-high-level-api-aibulatindexeddbnexie)\n   1. [Coming from Dexie](#coming-from-dexie)\n   1. [Schema](#schema)\n   1. [Querying](#querying)\n   1. [Transactions](#transactions)\n   1. [Opening a database you did not declare](#opening-a-database-you-did-not-declare)\n   1. [Options](#options)\n   1. [`liveQuery`](#livequery)\n   1. [Hooks, events and middleware](#hooks-events-and-middleware)\n   1. [Typing tables](#typing-tables)\n   1. [Errors](#errors)\n   1. [Differences from Dexie](#differences-from-dexie)\n1. [Developing](#developing)\n\n# Installation\n\n## Using a package manager\n\n```sh\npnpm add @aibulat/indexeddb\n```\n\nThat one install carries both APIs. Then, assuming you're using a module-compatible system (like Vite, webpack, Rollup etc), import whichever one you want:\n\n```js\n// Part 1 — the low-level API.\nimport { openDB, deleteDB, wrap, unwrap } from '@aibulat/indexeddb';\n\nasync function doDatabaseStuff() {\n  const db = await openDB(…);\n}\n```\n\n```js\n// Part 2 — Nexie, the high-level API. Same package, no extra dependency.\nimport Nexie, { liveQuery } from '@aibulat/indexeddb/nexie';\n\nconst db = new Nexie('MyDB');\ndb.version(1).stores({ friends: '++id, name, age' });\n```\n\n## Directly in a browser\n\nThis package is **ESM only** — there is no CommonJS build and no UMD global. Load it as a module:\n\n```html\n<script type=\"module\">\n  import {\n    openDB,\n    deleteDB,\n    wrap,\n    unwrap,\n  } from 'https://cdn.jsdelivr.net/npm/@aibulat/indexeddb@0/+esm';\n\n  async function doDatabaseStuff() {\n    const db = await openDB(…);\n  }\n</script>\n```\n\nIf you need a global for a non-module script, assign one yourself:\n\n```html\n<script type=\"module\">\n  import * as idb from 'https://cdn.jsdelivr.net/npm/@aibulat/indexeddb@0/+esm';\n  globalThis.idb = idb;\n</script>\n```\n\n# Changes\n\n[See details of (potentially) breaking changes](CHANGELOG.md).\n\n# Browser support\n\nThis library targets modern browsers, as in Chrome, Firefox, Safari, and other browsers that use those engines, such as Edge. IE is not supported.\n\n# Part 1 — Low-level API (`@aibulat/indexeddb`)\n\n```js\nimport { openDB, deleteDB, wrap, unwrap, ignoreConstraints } from '@aibulat/indexeddb';\n```\n\nEverything in this part is the package's root entry: a thin wrapper that mostly mirrors the IndexedDB API, with small improvements that make a big difference to usability. You still think in object stores, transactions and cursors — that is the point of it. If you would rather describe a schema and query it, skip to [Part 2](#part-2--nexie-the-high-level-api-aibulatindexeddbnexie).\n\n## `openDB`\n\nThis method opens a database, and returns a promise for an enhanced [`IDBDatabase`](https://w3c.github.io/IndexedDB/#database-interface).\n\n```js\nconst db = await openDB(name, version, {\n  upgrade(db, oldVersion, newVersion, transaction, event) {\n    // …\n  },\n  blocked(currentVersion, blockedVersion, event) {\n    // …\n  },\n  blocking(currentVersion, blockedVersion, event) {\n    // …\n  },\n  terminated() {\n    // …\n  },\n});\n```\n\n- `name`: Name of the database.\n- `version` (optional): Schema version, or `undefined` to open the current version.\n- `upgrade` (optional): Called if this version of the database has never been opened before. Use it to specify the schema for the database. This is similar to the [`upgradeneeded` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/upgradeneeded_event) in plain IndexedDB.\n  - `db`: An enhanced `IDBDatabase`.\n  - `oldVersion`: Last version of the database opened by the user.\n  - `newVersion`: Whatever new version you provided.\n  - `transaction`: An enhanced transaction for this upgrade. This is useful if you need to get data from other stores as part of a migration.\n  - `event`: The event object for the associated `upgradeneeded` event.\n- `blocked` (optional): Called if there are older versions of the database open on the origin, so this version cannot open. This is similar to the [`blocked` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/blocked_event) in plain IndexedDB.\n  - `currentVersion`: Version of the database that's blocking this one.\n  - `blockedVersion`: The version of the database being blocked (whatever version you provided to `openDB`).\n  - `event`: The event object for the associated `blocked` event.\n- `blocking` (optional): Called if this connection is blocking a future version of the database from opening. This is similar to the [`versionchange` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/versionchange_event) in plain IndexedDB.\n  - `currentVersion`: Version of the open database (whatever version you provided to `openDB`).\n  - `blockedVersion`: The version of the database that's being blocked.\n  - `event`: The event object for the associated `versionchange` event.\n- `terminated` (optional): Called if the browser abnormally terminates the connection, but not on regular closures like calling `db.close()`. This is similar to the [`close` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close_event) in plain IndexedDB.\n\n## `deleteDB`\n\nDeletes a database.\n\n```js\nawait deleteDB(name, {\n  blocked() {\n    // …\n  },\n});\n```\n\n- `name`: Name of the database.\n- `blocked` (optional): Called if the database already exists and there are open connections that don’t close in response to a versionchange event, the request will be blocked until they all close.\n  - `currentVersion`: Version of the database that's blocking the delete operation.\n  - `event`: The event object for the associated 'versionchange' event.\n\n## `unwrap`\n\nTakes an enhanced IndexedDB object and returns the plain unmodified one.\n\n```js\nconst unwrapped = unwrap(wrapped);\n```\n\nThis is useful if, for some reason, you want to drop back into plain IndexedDB. Promises will also be converted back into `IDBRequest` objects.\n\n## `wrap`\n\nTakes an IDB object and returns a version enhanced by this library.\n\n```js\nconst wrapped = wrap(unwrapped);\n```\n\nThis is useful if some third party code gives you an `IDBDatabase` object and you want it to have the features of this library.\n\n## General enhancements\n\nOnce you've opened the database the API is the same as IndexedDB, except for a few changes to make things easier.\n\nFirstly, any method that usually returns an `IDBRequest` object will now return a promise for the result.\n\n```js\nconst store = db.transaction(storeName).objectStore(storeName);\nconst value = await store.get(key);\n```\n\n### Promises & throwing\n\nThe library turns all `IDBRequest` objects into promises, but it doesn't know in advance which methods may return promises.\n\nAs a result, methods such as `store.put` may throw instead of returning a promise.\n\nIf you're using async functions, there's no observable difference.\n\nBecause you get a promise rather than the `IDBRequest`, assigning `onsuccess` or `onerror` to the result does nothing — there is no request there to assign them to. Await the promise instead, or use [`unwrap`](#unwrap) to get the real `IDBRequest` back.\n\n### Transaction lifetime\n\nTL;DR: **Do not `await` other things between the start and end of your transaction**, otherwise the transaction will close before you're done.\n\nAn IDB transaction auto-closes if it doesn't have anything left do once microtasks have been processed. As a result, this works fine:\n\n```js\nconst tx = db.transaction('keyval', 'readwrite');\nconst store = tx.objectStore('keyval');\nconst val = (await store.get('counter')) || 0;\nawait store.put(val + 1, 'counter');\nawait tx.done;\n```\n\nBut this doesn't:\n\n```js\nconst tx = db.transaction('keyval', 'readwrite');\nconst store = tx.objectStore('keyval');\nconst val = (await store.get('counter')) || 0;\n// This is where things go wrong:\nconst newVal = await fetch('/increment?val=' + val);\n// And this throws an error:\nawait store.put(newVal, 'counter');\nawait tx.done;\n```\n\nIn this case, the transaction closes while the browser is fetching, so `store.put` fails.\n\n## `IDBDatabase` enhancements\n\n### Shortcuts to get/set from an object store\n\nIt's common to create a transaction for a single action, so helper methods are included for this:\n\n```js\n// Get a value from a store:\nconst value = await db.get(storeName, key);\n// Set a value in a store:\nawait db.put(storeName, value, key);\n```\n\nThe shortcuts are: `get`, `getKey`, `getAll`, `getAllKeys`, `getAllRecords`, `count`, `put`, `add`, `delete`, and `clear`. Each method takes a `storeName` argument, the name of the object store, and the rest of the arguments are the same as the equivalent `IDBObjectStore` method.\n\n### Shortcuts to get from an index\n\nThe shortcuts are: `getFromIndex`, `getKeyFromIndex`, `getAllFromIndex`, `getAllKeysFromIndex`, `getAllRecordsFromIndex`, and `countFromIndex`.\n\n```js\n// Get a value from an index:\nconst value = await db.getFromIndex(storeName, indexName, key);\n```\n\nEach method takes `storeName` and `indexName` arguments, followed by the rest of the arguments from the equivalent `IDBIndex` method.\n\n## `IDBTransaction` enhancements\n\n### `tx.store`\n\nIf a transaction involves a single store, the `store` property will reference that store.\n\n```js\nconst tx = db.transaction('whatever');\nconst store = tx.store;\n```\n\nIf a transaction involves multiple stores, `tx.store` is undefined, you need to use `tx.objectStore(storeName)` to get the stores.\n\n### `tx.done`\n\nTransactions have a `.done` promise which resolves when the transaction completes successfully, and otherwise rejects with the [transaction error](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/error).\n\n```js\nconst tx = db.transaction(storeName, 'readwrite');\nawait Promise.all([\n  tx.store.put('bar', 'foo'),\n  tx.store.put('world', 'hello'),\n  tx.done,\n]);\n```\n\nIf you're writing to the database, `tx.done` is the signal that everything was successfully committed to the database. However, it's still beneficial to await the individual operations, as you'll see the error that caused the transaction to fail.\n\n`tx.done` rejects with the error that actually caused the failure — a `ConstraintError` for a duplicate key, a `QuotaExceededError` when storage is full — rather than a generic `AbortError`. Only an abort with no other cause, such as an explicit `tx.abort()`, gives you an `AbortError`. That one is synthesised here rather than read off the transaction (which has no error to read), and its message is `A request was aborted.` — the same name idb uses, not the same message, so match on `name`, not on text.\n\nYou don't have to await `tx.done`. For a read there's often no reason to, and a transaction that fails unobserved won't produce an unhandled rejection.\n\nA failed operation aborts its transaction for you, so you rarely need `tx.abort()` by hand. It matters when *your own* code throws part-way through, and you want the writes already made to roll back:\n\n```js\nconst tx = db.transaction(storeName, 'readwrite');\n\ntry {\n  await tx.store.put(await computeSomething(), 'key');\n  await tx.done;\n} catch (err) {\n  tx.abort();\n  throw err;\n}\n```\n\n## `IDBCursor` enhancements\n\nCursor advance methods (`advance`, `continue`, `continuePrimaryKey`) return a promise for the cursor, or null if there are no further values to provide.\n\n```js\nlet cursor = await db.transaction(storeName).store.openCursor();\n\nwhile (cursor) {\n  console.log(cursor.key, cursor.value);\n  cursor = await cursor.continue();\n}\n```\n\n## Async iterators\n\nYou can iterate over stores, indexes, and cursors:\n\n```js\nconst tx = db.transaction(storeName);\n\nfor await (const cursor of tx.store) {\n  // …\n}\n```\n\nEach yielded object is an `IDBCursor`. You can optionally use the advance methods to skip items (within an async iterator they return void):\n\n```js\nconst tx = db.transaction(storeName);\n\nfor await (const cursor of tx.store) {\n  console.log(cursor.value);\n  // Skip the next item\n  cursor.advance(2);\n}\n```\n\nIf you don't manually advance the cursor, `cursor.continue()` is called for you.\n\nStores and indexes also have an `iterate` method which has the same signature as `openCursor`, but returns an async iterator:\n\n```js\nconst index = db.transaction('books').store.index('author');\n\nfor await (const cursor of index.iterate('Douglas Adams')) {\n  console.log(cursor.value);\n}\n```\n\n`iterateKeys` is the same thing over `openKeyCursor`, for when you only need keys and would rather not read every value off disk:\n\n```js\nfor await (const cursor of db.transaction('books').store.iterateKeys()) {\n  console.log(cursor.key);\n}\n```\n\n## `getAll` options\n\n`getAll`, `getAllKeys` and `getAllRecords` accept an options object in place of the `query`/`count` arguments, which is the only way to ask for records in reverse:\n\n```js\nconst store = db.transaction('books').store;\n\nconst newest = await store.getAll({ direction: 'prev', count: 10 });\n```\n\n`direction` is `'next' | 'prev'` on a store, and the full `IDBCursorDirection` on an index. `getAllRecords` returns `{ key, primaryKey, value }` objects rather than bare values, so one call gives you keys and values together.\n\nThese need browser support for [`getAllRecords`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllRecords) (Chrome/Edge 141+). Feature-detect with `'getAllRecords' in IDBObjectStore.prototype`.\n\n## `ignoreConstraints`\n\nBy default a duplicate key aborts the whole transaction, so one bad record throws away an entire bulk insert. `ignoreConstraints` handles the `ConstraintError` at the request, leaving the transaction free to commit, and resolves with `undefined` for the record that was skipped:\n\n```js\nimport { ignoreConstraints, openDB } from '@aibulat/indexeddb';\n\nconst tx = db.transaction('books', 'readwrite');\nconst keys = await Promise.all(\n  books.map((book) => ignoreConstraints(tx.store.add(book))),\n);\nawait tx.done;\n// keys holds a key per book, and undefined where one already existed.\n```\n\nAny other error still rejects, and still aborts the transaction.\n\nTwo rules, both enforced with a `TypeError` rather than silently doing the wrong thing:\n\n- **Call it in the same turn as the write**, before awaiting anything. The suppression is a listener on the request's `error` event, and once that event has fired the `ConstraintError` has already reached the transaction and aborted it — swallowing it from the promise at that point would report a clean `undefined` for a write that took the whole transaction down.\n- **Pass the promise of a store or index write** — `tx.store.add(...)`, `index.put(...)` and so on. The `db.add()` / `db.put()` shortcuts open and close a transaction of their own, and by the time you hold their promise there is no request left to attach to.\n\nThe listener is removed once the request settles, so a request object that outlives the operation — a cursor's, say — is not left with a constraint-swallowing listener on whatever it does next.\n\n## Closing with `using`\n\nA database is a disposable resource, so it closes itself at the end of the scope if you declare it with [`using`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management):\n\n```ts\nusing db = await openDB('my-db', 1);\nawait db.put('keyval', 'hello', 'greeting');\n// db.close() runs here, however the scope exits.\n```\n\nThis needs `Symbol.dispose`, which is TypeScript 5.2+ with `ESNext.Disposable` in your `lib`. Nothing is required of consumers who don't use it: the declaration disappears when the lib is absent, rather than failing to compile.\n\n## Examples\n\n### Keyval store\n\nThis is very similar to `localStorage`, but async. If this is _all_ you need, you may be interested in [idb-keyval](https://www.npmjs.com/package/idb-keyval). You can always upgrade to this library later.\n\n```js\nimport { openDB } from '@aibulat/indexeddb';\n\nconst dbPromise = openDB('keyval-store', 1, {\n  upgrade(db) {\n    db.createObjectStore('keyval');\n  },\n});\n\nexport async function get(key) {\n  return (await dbPromise).get('keyval', key);\n}\nexport async function set(key, val) {\n  return (await dbPromise).put('keyval', val, key);\n}\nexport async function del(key) {\n  return (await dbPromise).delete('keyval', key);\n}\nexport async function clear() {\n  return (await dbPromise).clear('keyval');\n}\nexport async function keys() {\n  return (await dbPromise).getAllKeys('keyval');\n}\n```\n\n### Article store\n\n```js\nimport { openDB } from '@aibulat/indexeddb';\n\nasync function demo() {\n  const db = await openDB('Articles', 1, {\n    upgrade(db) {\n      // Create a store of objects\n      const store = db.createObjectStore('articles', {\n        // The 'id' property of the object will be the key.\n        keyPath: 'id',\n        // If it isn't explicitly set, create a value by auto incrementing.\n        autoIncrement: true,\n      });\n      // Create an index on the 'date' property of the objects.\n      store.createIndex('date', 'date');\n    },\n  });\n\n  // Add an article:\n  await db.add('articles', {\n    title: 'Article 1',\n    date: new Date('2019-01-01'),\n    body: '…',\n  });\n\n  // Add multiple articles in one transaction:\n  {\n    const tx = db.transaction('articles', 'readwrite');\n    await Promise.all([\n      tx.store.add({\n        title: 'Article 2',\n        date: new Date('2019-01-01'),\n        body: '…',\n      }),\n      tx.store.add({\n        title: 'Article 3',\n        date: new Date('2019-01-02'),\n        body: '…',\n      }),\n      tx.done,\n    ]);\n  }\n\n  // Get all the articles in date order:\n  console.log(await db.getAllFromIndex('articles', 'date'));\n\n  // Add 'And, happy new year!' to all articles on 2019-01-01:\n  {\n    const tx = db.transaction('articles', 'readwrite');\n    const index = tx.store.index('date');\n\n    for await (const cursor of index.iterate(new Date('2019-01-01'))) {\n      const article = { ...cursor.value };\n      article.body += ' And, happy new year!';\n      cursor.update(article);\n    }\n\n    await tx.done;\n  }\n}\n```\n\n## TypeScript\n\nThis library is fully typed, and you can improve things by providing types for your database:\n\n```ts\nimport { openDB, DBSchema } from '@aibulat/indexeddb';\n\ninterface MyDB extends DBSchema {\n  'favourite-number': {\n    key: string;\n    value: number;\n  };\n  products: {\n    value: {\n      name: string;\n      price: number;\n      productCode: string;\n    };\n    key: string;\n    indexes: { 'by-price': number };\n  };\n}\n\nasync function demo() {\n  const db = await openDB<MyDB>('my-db', 1, {\n    upgrade(db) {\n      db.createObjectStore('favourite-number');\n\n      const productStore = db.createObjectStore('products', {\n        keyPath: 'productCode',\n      });\n      productStore.createIndex('by-price', 'price');\n    },\n  });\n\n  // This works\n  await db.put('favourite-number', 7, 'Jen');\n  // This fails at compile time, as the 'favourite-number' store expects a number.\n  await db.put('favourite-number', 'Twelve', 'Jake');\n}\n```\n\nTo define types for your database, extend `DBSchema` with an interface where the keys are the names of your object stores.\n\nFor each value, provide an object where `value` is the type of values within the store, and `key` is the type of keys within the store.\n\nOptionally, `indexes` can contain a map of index names, to the type of key within that index.\n\nProvide this interface when calling `openDB`, and from then on your database will be strongly typed. This also allows your IDE to autocomplete the names of stores and indexes.\n\n### Assembling a schema across files\n\nEvery piece of the above is exported as a named type, so a schema, a migration or a set of callbacks can live in its own module and still be checked against the database:\n\n```ts\nimport type {\n  DBSchema, DBSchemaValue, IndexKeys,        // the schema and its parts\n  StoreNames, StoreKey, StoreValue,          // resolve names/keys/values against a schema\n  IndexNames, IndexKey,                      // the same for a store's indexes\n  OpenDBUpgradeCallback, OpenDBBlockedCallback,\n  OpenDBBlockingCallback, OpenDBTerminatedCallback,\n  DeleteDBBlockedCallback, DeleteDBCallbacks,\n  IDBTransactionOptions,                     // { durability?: 'default' | 'strict' | 'relaxed' }\n  IDBPStoreGetAllOptions, IDBPIndexGetAllOptions,\n} from '@aibulat/indexeddb';\n\n// migrations.ts\nexport const upgrade: OpenDBUpgradeCallback<MyDB> = (db, oldVersion, newVersion, tx) => {\n  if (oldVersion < 1) db.createObjectStore('favourite-number');\n  // `tx` is typed as the versionchange transaction over every store in MyDB.\n};\n\n// main.ts\nconst db = await openDB<MyDB>('my-db', 1, { upgrade, terminated() { reconnect(); } });\nconst tx = db.transaction('products', 'readwrite', { durability: 'relaxed' } satisfies IDBTransactionOptions);\n```\n\n`test/types.test.ts` is the compile-time contract for that surface: it asserts each name exists and composes as shown, and it fails `typecheck` — not `test` — if one drifts.\n\n### Opting out of types\n\nIf you call `openDB` without providing types, your database will use basic types. However, sometimes you'll need to interact with stores that aren't in your schema, perhaps during upgrades. In that case you can cast.\n\nLet's say we were renaming the 'favourite-number' store to 'fave-nums':\n\n```ts\nimport { openDB, DBSchema, IDBPDatabase } from '@aibulat/indexeddb';\n\ninterface MyDBV1 extends DBSchema {\n  'favourite-number': { key: string; value: number };\n}\n\ninterface MyDBV2 extends DBSchema {\n  'fave-num': { key: string; value: number };\n}\n\nconst db = await openDB<MyDBV2>('my-db', 2, {\n  async upgrade(db, oldVersion) {\n    // Cast a reference of the database to the old schema.\n    const v1Db = db as unknown as IDBPDatabase<MyDBV1>;\n\n    if (oldVersion < 1) {\n      v1Db.createObjectStore('favourite-number');\n    }\n    if (oldVersion < 2) {\n      const store = v1Db.createObjectStore('favourite-number');\n      store.name = 'fave-num';\n    }\n  },\n});\n```\n\nYou can also cast to a typeless database by omitting the type, eg `db as IDBPDatabase`.\n\nNote: Types like `IDBPDatabase` are used by TypeScript only. The implementation uses proxies under the hood.\n\n# Part 2 — Nexie, the high-level API (`@aibulat/indexeddb/nexie`)\n\n```ts\nimport Nexie, { liveQuery } from '@aibulat/indexeddb/nexie';\n```\n\nEverything in [Part 1](#part-1--low-level-api-aibulatindexeddb) is the *low-level* API: you still think in object stores, transactions and cursors. **Nexie** is the other API in this package — a re-implementation of the Dexie 4 API, at its own subpath. You describe a schema and query it; transactions survive `await` instead of auto-committing; `liveQuery` re-runs a query when its own results could have changed.\n\n```sh\nnpm install @aibulat/indexeddb   # same package, no extra dependency\n```\n\n```ts\nimport Nexie from '@aibulat/indexeddb/nexie';\n\nconst db = new Nexie('MyDB');\ndb.version(1).stores({ friends: '++id, name, age' });\n\nawait db.friends.add({ name: 'Alice', age: 30 });\nconst grownups = await db.friends.where('age').above(25).toArray();\n```\n\nThe two entries are **disjoint**: nothing under `nexie` imports the low-level entry, so `dist/index.js` is unchanged by its existence and importing one never pulls in the other. `dist/nexie.js` is around 124 kB unminified; the ~1.9 kB figure at the top of this file is the `.` entry and stays true.\n\n## Coming from Dexie\n\nMigration is a rename and nothing else:\n\n```diff\n- import Dexie from 'dexie';\n+ import Nexie from '@aibulat/indexeddb/nexie';\n\n- const db = new Dexie('MyDB');\n+ const db = new Nexie('MyDB');\n\n  db.version(1).stores({ friends: '++id,name,age' });\n  await db.friends.where('age').above(25).toArray();\n```\n\nDexie-branded *identifiers* are renamed — `NexieError`, `Nexie.Promise`, `Nexie.addons`, `Nexie.errnames`, `Nexie.currentTransaction`. API-visible *strings* are not, because code matches on them: error `name` values stay `'ConstraintError'` and friends, so `.catch('ConstraintError', handler)` still works, as do the schema DSL, the `'rw!'` / `'r?'` mode strings and the `':id'` magic index.\n\nThis is a clean-room implementation, not a port: Dexie is Apache-2.0, this package is MIT, and no Dexie code was copied.\n\n## Schema\n\nA version declares its stores with the Dexie schema DSL, unchanged:\n\n```ts\ndb.version(1).stores({\n    friends: '++id, name, age, *tags, [name+age], &email',\n});\n```\n\n| Token          | Meaning                                                |\n| -------------- | ------------------------------------------------------ |\n| `++id`         | Auto-incrementing primary key                          |\n| `id`           | Primary key, supplied by you                           |\n| `name`         | Indexed property                                       |\n| `&email`       | Unique index                                           |\n| `*tags`        | Multi-entry index — one entry per array element        |\n| `[name+age]`   | Compound index                                         |\n| _(leading `,`)_ | Outbound primary key: no key stored inside the record |\n\nA compound index also answers queries on its **leading prefix**: with only `[name+age]` declared, `db.friends.where('name').equals('Alice')` works. That is a correctness feature rather than an optimisation — Dexie users rely on it. `':id'` is the magic index naming the primary key itself.\n\nLater versions get an upgrade function, run once per version between the stored one and the current:\n\n```ts\ndb.version(2)\n    .stores({ friends: '++id, name, age, email' })\n    .upgrade((tx) =>\n        tx.table('friends').toCollection().modify((f) => {\n            f.email ??= `${f.name}@example.com`;\n        }),\n    );\n```\n\nSet a store to `null` to drop it. `db.tables` lists the `Table` objects, and each table is also a property on the database (`db.friends`) as long as the name does not collide with a `Nexie` member.\n\n## Querying\n\n`table.where()` returns a `WhereClause`; every operator on it returns a `Collection`, which is lazy until you materialise it:\n\n```ts\nawait db.friends.get(1);\nawait db.friends.where('age').between(20, 40).toArray();\nawait db.friends.where('name').startsWithIgnoreCase('a').limit(10).toArray();\nawait db.friends.where('age').anyOf([20, 30, 40]).reverse().sortBy('name');\nawait db.friends.where({ name: 'Alice', age: 30 }).first();\nawait db.friends.orderBy('age').offset(10).limit(5).toArray();\nawait db.friends.filter((f) => f.name.length > 4).each((f) => console.log(f));\n```\n\nAll 18 `WhereClause` operators are present — `equals`, `notEqual`, `above`, `aboveOrEqual`, `below`, `belowOrEqual`, `between`, `startsWith`, `anyOf`, `noneOf`, `inAnyRange`, `startsWithAnyOf` and the four `…IgnoreCase` variants — along with `or()` unions, `distinct()`, `until()`, `and()` and the `each*` family. Materialisers: `toArray`, `first`, `last`, `count`, `keys`, `primaryKeys`, `uniqueKeys`, `sortBy`.\n\nWrites:\n\n```ts\nawait db.friends.add({ name: 'Alice', age: 30 });   // key written back onto the object\nawait db.friends.put({ id: 1, name: 'Alice', age: 31 });\nawait db.friends.update(1, { age: 32 });\nawait db.friends.upsert(1, { age: 33 });\nawait db.friends.bulkAdd(records, { allKeys: true });\nawait db.friends.where('age').below(18).delete();\nawait db.friends.where('age').above(65).modify({ retired: true });\n```\n\n`update`, `upsert`, `modify` and `bulkUpdate` take an `UpdateSpec`, which accepts the modifier functions exported alongside the class:\n\n```ts\nimport { add, remove, replacePrefix } from '@aibulat/indexeddb/nexie';\n\nawait db.friends.update(1, { age: add(1), tags: remove(['new']) });\n// `add`/`remove` take a number (or bigint) for numeric fields and an array for array fields.\n```\n\n## Transactions\n\n```ts\nawait db.transaction('rw', db.friends, async () => {\n    await db.friends.add({ name: 'Alice', age: 30 });\n    await db.friends.where('name').equals('Bob').delete();\n});\n```\n\nTable calls inside the scope join the transaction automatically, across `await` — there is nothing to thread through. That works by tracking the transaction in a zone that survives suspension, so **every promise you await inside a scope must be one of ours**. Awaiting a native promise (a `fetch`, a foreign library) loses the transaction, and the operation after it would otherwise open a second one silently. Use the escape hatch:\n\n```ts\nconst data = await Nexie.waitFor(fetch('/api/friends').then((r) => r.json()));\n```\n\n`Nexie.currentTransaction` is the transaction the calling code is inside, or `null`.\n\nIf a foreign `await` does slip through, you get a **`ForeignAwaitError`** naming the fix rather than a second transaction opened behind your back:\n\n```\nfriends: an open transaction on this table is waiting on a promise this library\ndid not create, so its scope has been lost. Await only Nexie promises inside a\ntransaction, wrap foreign ones in Nexie.waitFor(), or use\nNexie.ignoreTransaction() if this call is genuinely unrelated to it.\n```\n\n`Nexie.ignoreTransaction(fn)` is the other side of that: it runs `fn` outside the ambient transaction, so bookkeeping that must survive a rollback of the work that triggered it gets a transaction of its own.\n\nNested scopes join the enclosing transaction when they can. A `'rw'` scope inside an `'r'` one is a `SubTransactionError`, as is a nested scope naming a table the parent did not include; the Dexie modifiers work as documented — `'rw!'` always opens a fresh top-level transaction, `'rw?'` joins the parent when it is still active and otherwise opens its own.\n\nThe `Transaction` object (`Nexie.currentTransaction`, or the argument to a scope) carries `on('complete')`, `on('error')` and `on('abort')`, and `Nexie.waitFor()` may be outstanding more than once at a time.\n\n### Unhandled rejections inside a scope\n\nAn operation started inside a transaction scope, an `on('populate')` subscriber or a `version().upgrade()` callback and never awaited still counts. If it rejects and nothing handles it by the end of the tick, the enclosing scope fails with that error — the transaction aborts, `open()` rejects — the way it does in Dexie, so a fire-and-forget write that hits a `ConstraintError` cannot leave you with a partially committed transaction that reported success.\n\nOutside any scope, an unhandled Nexie promise rejection is reported the way a native one is: as an `unhandledrejection` event where the host has `PromiseRejectionEvent` (browsers), otherwise through `console.error`. Set `NexiePromise.onUnhandled` to route those somewhere else.\n\n## Opening a database you did not declare\n\nSkip `version().stores()` entirely and Nexie reads the schema out of the database instead — for tooling, migrations, or just finding out what is in there:\n\n```ts\nconst db = new Nexie('SomeoneElsesDB');\nawait db.open();\n\ndb.dynamicallyOpened();                      // true\ndb.tables.map((t) => t.name);                // whatever is actually there\nawait db.table('friends').toArray();         // and it works\n```\n\nOpening a database that does not exist this way is a `NoSuchDatabaseError`, not a silently created empty one — pass `{ allowEmptyDB: true }` if creating it is what you meant. Related statics: `Nexie.exists(name)`, `Nexie.getDatabaseNames()` and `Nexie.delete(name)`.\n\n## Options\n\n```ts\nnew Nexie('MyDB', {\n    autoOpen: true,                          // open on first use (default)\n    allowEmptyDB: false,                     // see above\n    chromeTransactionDurability: 'relaxed',  // faster commits, Chromium reads it\n    modifyChunkSize: 200,                    // records per write-back request\n    maxConnections: 100,                     // leak warning threshold\n    addons: [],\n    indexedDB, IDBKeyRange,                  // inject an implementation\n});\n```\n\n`Nexie.debug = true` turns on the engine's own invariant assertion — cheap, and worth having on in development. `Nexie.semVer` is the library version.\n\nIn a browser, a page frozen into the **bfcache** has its database closed on `pagehide` and reopened on `pageshow`, because a browser may close those connections while the page sits there and hand it back looking intact.\n\n## `liveQuery`\n\nA query that re-runs itself when its own result could have changed:\n\n```ts\nimport { liveQuery } from '@aibulat/indexeddb/nexie';\n\nconst subscription = liveQuery(() =>\n    db.friends.where('age').above(25).toArray(),\n).subscribe((friends) => render(friends));\n\n// later\nsubscription.unsubscribe();\n```\n\nThe querier runs in a zone that records every read it makes, down to the key ranges. Each committed transaction publishes what it wrote, and the query re-runs only where the two intersect — so a `liveQuery` over `db.friends.get(7)` ignores writes to every other friend. Writes made through a second connection, or in another tab (via `BroadcastChannel`, feature-detected), come through the same path.\n\nTwo things to know:\n\n- The same zone rule applies: **await only Nexie promises inside a querier**, or the reads after that point go unrecorded and the query stops re-running for them.\n- Invalidation is exact on primary keys, and on secondary indexes for `add`, `put` and `delete` — a `put` reads the record it displaces, so a query watching the *old* value of a renamed field is woken too. That read happens only while something is subscribed, so an application with no `liveQuery` pays nothing for it. Range deletes are the one case still widened to the whole index, since being precise there would mean reading an unbounded number of records. The approximation is one-directional by design: it re-runs a query that need not have re-run, never the reverse.\n\n## Hooks, events and middleware\n\nCRUD hooks fire around every write, whichever API path reached it:\n\n```ts\ndb.friends.hook('creating', (primKey, obj) => {\n    obj.createdAt = Date.now();\n});\ndb.friends.hook('reading', (obj) => decorate(obj));\ndb.friends.hook('updating', (mods, primKey, obj) => ({ updatedAt: Date.now() }));\ndb.friends.hook('deleting', (primKey, obj) => audit(obj));\n```\n\nDatabase events cover the lifecycle:\n\n```ts\ndb.on('populate', () => db.friends.bulkAdd(seedData));\ndb.on('blocked', () => console.warn('another tab is holding the old version'));\ndb.on('ready', () => console.log('open and usable'));          // fires once, or at once if already open\ndb.on('ready', () => console.log('every open'), true);          // sticky: fires on every (re)open\ndb.on('versionchange', () => db.close());\ndb.on('close', () => console.log('connection gone'));\n```\n\nUnderneath both, `db.use()` installs a middleware over DBCore — the layer every read and every write passes through. The library's own CRUD hooks and observability are built on it rather than beside it, which is what keeps the extension point exercised:\n\n```ts\ndb.use({\n    stack: 'dbcore',\n    name: 'logger',\n    create: (down) => ({\n        table: (name) => {\n            const table = down.table(name);\n            return {\n                ...table,\n                mutate: (req) => {\n                    console.log(name, req.type);\n                    return table.mutate(req);\n                },\n            };\n        },\n    }),\n});\n```\n\n`mutate`, `get`, `getMany`, `count`, `query` and `openCursor` are all interceptable. `mapToClass`, `defineClass`, the `Entity` base class and `Nexie.addons` are present too, and hooks and class mappings survive a later `db.version(n).stores()` declaration.\n\n## Typing tables\n\n`Table<T, TKey, TInsertType>` takes the same three parameters as Dexie's, and `EntityTable` derives the key and insert types from the entity so an auto-incremented `id` need not be optional on the way in:\n\n```ts\nimport { Nexie, type EntityTable } from '@aibulat/indexeddb/nexie';\n\ninterface Friend { id: number; name: string; age: number }\n\nconst db = new Nexie('friends') as Nexie & {\n    friends: EntityTable<Friend, 'id'>;      // key: number, insert type: Omit<Friend, 'id'>\n};\ndb.version(1).stores({ friends: '++id, name, age' });\n\nawait db.friends.add({ name: 'Alice', age: 30 });   // no id required\nconst alice = await db.friends.get({ name: 'Alice' }); // criteria form of get()\n```\n\n`InsertType`, `IDType` and `NonInsertProps` are exported alongside for building your own.\n\n## Errors\n\n```ts\ntry {\n    await db.friends.add({ name: 'Alice', email: 'taken@example.com' });\n} catch (error) {\n    if (error instanceof Nexie.ConstraintError) { /* … */ }\n}\n\nawait db.friends.add(friend).catch('ConstraintError', handleDuplicate);\n```\n\nEvery error is a `NexieError` carrying the Dexie `name` string, so both forms work and `Nexie.errnames.Constraint === 'ConstraintError'` holds. The named-`catch` form is on Nexie's own promises, which is what every API here returns. `exceptions` and `errnames` are exported from the subpath if you need them directly, and the constructors are also mounted on the class (`Nexie.ConstraintError`, `Nexie.ModifyError`, …).\n\nAlongside the mirrored IndexedDB `DOMException` names there are Nexie-only ones: `OpenFailedError`, `SchemaError`, `UpgradeError`, `InvalidTableError`, `NoSuchDatabaseError`, `PrematureCommitError`, `ModifyError`, `BulkError` and `ForeignAwaitError` (see [Transactions](#transactions)).\n\n## Differences from Dexie\n\nThe API surface is complete, with two deliberate exceptions:\n\n- **No query result cache.** Dexie's `cache: 'immutable' | 'cloned'` keeps query results in memory and updates them optimistically. It is a performance layer rather than a correctness one — `liveQuery` is exact without it — and it carries the highest bug density per line in Dexie, so it is not here. The DBCore read path every query now goes through is the seam it would plug into.\n- **No `Dexie.Table<T, K>` namespace shim.** Import the types instead: `import type { Table, Collection } from '@aibulat/indexeddb/nexie'`.\n\nTwo things are shaped differently rather than missing: `Nexie.vip(fn)` is a function rather than a property, and long-stack support is replaced by `Nexie.debug`, which asserts the engine's own invariant instead of rewriting stack traces.\n\nTwo things are stricter than Dexie, on purpose: awaiting a foreign promise inside a scope is a `ForeignAwaitError` rather than a silently opened second transaction, and an unhandled rejection outside any scope is reported (see [Transactions](#transactions)) rather than dropped.\n\n# Developing\n\nThis package lives in the [`@aibulat/packages`](https://github.com/ngmaibulat/packages) workspace. From the package directory:\n\n```sh\npnpm run build       # tsdown -> dist/, plus publint and attw\npnpm run dev         # tsdown --watch\npnpm run typecheck   # both the src and test projects\npnpm run test        # node:test against @aibulat/indexeddb-impl\npnpm run test:bun    # the same suite under Bun, and the totals must match\n```\n\nThe suite is 485 tests over `node:test`, run against the sibling [`@aibulat/indexeddb-impl`](../indexeddb-impl) rather than a real browser, so it needs no web server and runs in CI. That package has to be **built** first — the exports map resolves into its `dist/`, and a fresh checkout has none. Some of the assertions are compile-time `typeAssert<IsExact<…>>` checks from `conditional-type-checks`; those fail `typecheck`, not `test`.\n\nIt must report **identical totals under Node and Bun**. That is not ceremony: Nexie's transaction zone rests on the normative ordering of `Await` and promise-resolve-thenable jobs, and Bun is JSC where Node is V8. A divergence there is a bug in the design rather than a runtime quirk, which is why there are no per-runtime expectations to absorb one.\n\nRun a single file or a single test:\n\n```sh\nnode --test test/open.test.ts\nnode --test --test-name-pattern=\"upgrade\" test/open.test.ts\n```\n\nBecause `@aibulat/indexeddb-impl` is a reimplementation rather than a real engine, it can differ from browsers at the edges — two tests carry comments where they had to be adjusted for it. Worth a manual browser check against `dist/index.js` before releasing anything behaviourally risky.\n","readmeFilename":"README.md"}