├── README.md ├── karma.conf.cjs ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── Browserbase.spec.ts ├── Browserbase.ts ├── TypeEventTarget.ts └── index.ts ├── test └── test-browserbase.js ├── tsconfig.json └── vitest.config.ts /README.md: -------------------------------------------------------------------------------- 1 | # Browserbase 2 | 3 | Browserbase is a wrapper around the IndexedDB browser database which makes it easier to use. It provides 4 | 5 | - a Promise-based API using native browser promises (provide your own polyfill for IE 11) 6 | - easy versioning with indexes 7 | - events for open, close, and error 8 | - cancelable events for blocked and versionchange (see [IndexedDB documentation](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/onversionchange)) 9 | - change events for any changes, even when they originate from another tab 10 | 11 | To learn more about IndexedDB (which will help you with this API) read through the interfaces at 12 | https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API. 13 | 14 | ## Why another wrapper? 15 | 16 | Dexie was the only robust wrapper with a decent API at the time I wrote Browserbase, but it is much larger than it needs 17 | to be and catches errors in your code giving you a `console.warn` about them. Libraries should never do this. 18 | 19 | ## Overview of Browserbase vs IndexedDB Interfaces 20 | 21 | I will attempt to summarize the IndexedDB interfaces and how Browserbase wraps them. 22 | 23 | Here is a list of the main IndexedDB interfaces. I skip over the request interfaces. 24 | 25 | - `IDBEnvironment` just says that `window` should have a property called `indexedDB` which is a `IDBFactory`. 26 | - `IDBFactory` is `window.indexedDB` and defines the `open`, `deleteDatabase`, and `cmp` methods. 27 | - `IDBDatabase` is the database connection you get with a successful `open` and lets you create transactions. 28 | - `IDBTransaction` is a transaction with an `objectStore()` method that returns an object store. 29 | - `IDBObjectStore` is an object store (or table in RDBMS databases) with methods for reading and writing and accessing indexes. 30 | - `IDBIndex` is an index in an object store that lets you look up objects (and ranges) by a predefined index. 31 | - `IDBCursor` lets you iterate over objects in a store one at a time for better memory usage (e.g. if you have millions of records). 32 | - `IDBKeyRange` helps you define a range with min/max records on an index to select a range of objects. 33 | 34 | When you create a new Browserbase instance it does not interact with any IndexedDB interfaces until you call `open()`. 35 | This then opens an IndexedDB database assigning the `IDBDatabase` instance to the `db` property. 36 | 37 | Most actions in IndexedDB are performed within a transaction. You don't have to "commit" a transaction, you just create 38 | a new transaction object and access stores, indexes, and cursors from it. Everything you do on the store, index, or 39 | cursor is part of the transaction, and you can continue using that transaction immediately after actions complete. The 40 | transaction is offically finished once there is nothing being done within it during a microtask/frame. 41 | 42 | Browserbase attempts to hide transactions for simplification. It provides the following interfaces. 43 | 44 | - `Browserbase` represents the database connection, provides events, provides database versioning, and provides access to 45 | the object stores. 46 | - `ObjectStore` represents an object store, but it doesn't access an actual object store until calling an action so that 47 | it can create a new transaction before it does. 48 | - `Where` helps creating a range for reading and writing data in bulk from/to the database. It will use indexes and 49 | cursors as needed. 50 | 51 | Browserbase knows that often you are only performing a single action within a transaction. So it tries to simplify 52 | transactions by making them implicit. When you perform an `add` or a `put` on a store it automatically creates a 53 | `readwrite` transaction with that one object store for you and runs the operation within it. 54 | 55 | The `where()` API will use an object store if the primary key (or nothing) is passed in, and will use an index if the 56 | property is passed in. When using methods like `forEach` it will use a cursor to iterate over the records. 57 | 58 | ## API 59 | 60 | To keep small, Browserbase doesn't provide too many features on top of IndexedDB, opting to just the API that will make it 61 | IndexedDB easier to use (at least, easier to use in the author's opinion). 62 | 63 | ### Versioning 64 | 65 | Versioning is simplified. You provide a string of new indexes for each new version, with the first being the primary 66 | key. For primary keys, use a "++" prefix to indicate auto-increment and leave it empty if the key isn't part of the 67 | object. For indexes, use a "-" index to delete a previously defined index, use "&" to indicate a unique index, and use 68 | "\*" for a multiEntry index. You shouldn't ever change existing versions, only add new ones. 69 | 70 | Example: 71 | 72 | ```js 73 | // Initial version, should remain the same with later updates 74 | db.version(1, { 75 | friends: 'fullName, age', 76 | }); 77 | 78 | // Next version, we don't add any indexes, but we want to run our own update code to prepopulate the database 79 | db.version(2, {}, function (oldVersion, transaction) { 80 | // prepopulate with some initial data 81 | transaction.objectStore('friends').put({ fullName: 'Tom' }); 82 | }); 83 | 84 | // Remove the age index and add one for birthdate, add another object store with an auto-incrementing primary key 85 | // that isn't part of the object, and a multiEntry index on the labels array. 86 | db.version(3, { 87 | friends: 'birthdate, -age', 88 | events: '++, date, *labels', 89 | }); 90 | 91 | db.open().then(() => { 92 | console.log('database is now open'); 93 | }); 94 | ``` 95 | 96 | After the database is opened, a property will be added to the database instance for each object store in the 97 | database. This is how you will work with the data in the database. 98 | 99 | Example: 100 | 101 | ```js 102 | // Create the object store "foo" 103 | db.version(1, { foo: 'id' }); 104 | 105 | // Will be triggered once for any add, put, or delete done in any browser tab. The object will be null when it was 106 | // deleted, so use the key when object is null. 107 | db.addEventListener('change', event => { 108 | console.log('Object with key', event.key, 'was', event.obj === null ? 'deleted' : 'saved'); 109 | }); 110 | 111 | db.open().then( 112 | () => { 113 | db.stores.foo.put({ id: 'bar' }).then(() => { 114 | console.log('An object was saved to the database.'); 115 | }); 116 | }, 117 | err => { 118 | console.warn('There was an error opening the database:', err); 119 | } 120 | ); 121 | ``` 122 | 123 | TODO complete documentation 124 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | frameworks: ['mocha', 'karma-typescript'], 4 | 5 | files: [{ pattern: 'node_modules/expect.js/index.js' }, { pattern: 'src/**/*.ts' }], 6 | 7 | preprocessors: { 8 | '**/*.ts': ['karma-typescript'], 9 | }, 10 | 11 | reporters: ['dots', 'karma-typescript'], 12 | 13 | browsers: ['ChromeHeadless'], 14 | 15 | singleRun: true, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | // base path that will be used to resolve all patterns (eg. files, exclude) 5 | basePath: '', 6 | 7 | client: { 8 | captureConsole: true 9 | }, 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['source-map-support', 'mocha', 'chai', 'sinon-chai'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'test/**/*.js' 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | exclude: [ 25 | ], 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | 'test/**/*.js': ['rollup'] 31 | }, 32 | 33 | rollupPreprocessor: { 34 | plugins: [ ], 35 | output: { 36 | format: 'iife', 37 | sourcemap: 'inline' 38 | } 39 | }, 40 | 41 | // test results reporter to use 42 | // possible values: 'dots', 'progress' 43 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 44 | reporters: ['mocha'], 45 | 46 | // Configure mocha so that the DEBUG link in Chrome will show a report in HTML. 47 | client: { 48 | mocha: { 49 | reporter: 'html' 50 | } 51 | }, 52 | 53 | 54 | // web server port 55 | port: 9876, 56 | 57 | 58 | // enable / disable colors in the output (reporters and logs) 59 | colors: true, 60 | 61 | 62 | // level of logging 63 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 64 | logLevel: config.LOG_INFO, 65 | 66 | 67 | // enable / disable watching file and executing tests whenever any file changes 68 | autoWatch: true, 69 | 70 | 71 | // start these browsers 72 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 73 | browsers: ['Chrome'], 74 | 75 | 76 | // Continuous Integration mode 77 | // if true, Karma captures browsers, runs the tests and exits 78 | singleRun: false, 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserbase", 3 | "version": "3.0.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "browserbase", 9 | "version": "3.0.3", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "fake-indexeddb": "^6.0.0", 13 | "jsdom": "^24.1.0", 14 | "typescript": "^5.4.5", 15 | "vitest": "^1.6.0" 16 | } 17 | }, 18 | "node_modules/@esbuild/aix-ppc64": { 19 | "version": "0.21.5", 20 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", 21 | "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", 22 | "cpu": [ 23 | "ppc64" 24 | ], 25 | "dev": true, 26 | "optional": true, 27 | "os": [ 28 | "aix" 29 | ], 30 | "engines": { 31 | "node": ">=12" 32 | } 33 | }, 34 | "node_modules/@esbuild/android-arm": { 35 | "version": "0.21.5", 36 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", 37 | "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", 38 | "cpu": [ 39 | "arm" 40 | ], 41 | "dev": true, 42 | "optional": true, 43 | "os": [ 44 | "android" 45 | ], 46 | "engines": { 47 | "node": ">=12" 48 | } 49 | }, 50 | "node_modules/@esbuild/android-arm64": { 51 | "version": "0.21.5", 52 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", 53 | "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", 54 | "cpu": [ 55 | "arm64" 56 | ], 57 | "dev": true, 58 | "optional": true, 59 | "os": [ 60 | "android" 61 | ], 62 | "engines": { 63 | "node": ">=12" 64 | } 65 | }, 66 | "node_modules/@esbuild/android-x64": { 67 | "version": "0.21.5", 68 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", 69 | "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", 70 | "cpu": [ 71 | "x64" 72 | ], 73 | "dev": true, 74 | "optional": true, 75 | "os": [ 76 | "android" 77 | ], 78 | "engines": { 79 | "node": ">=12" 80 | } 81 | }, 82 | "node_modules/@esbuild/darwin-arm64": { 83 | "version": "0.21.5", 84 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", 85 | "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", 86 | "cpu": [ 87 | "arm64" 88 | ], 89 | "dev": true, 90 | "optional": true, 91 | "os": [ 92 | "darwin" 93 | ], 94 | "engines": { 95 | "node": ">=12" 96 | } 97 | }, 98 | "node_modules/@esbuild/darwin-x64": { 99 | "version": "0.21.5", 100 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", 101 | "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", 102 | "cpu": [ 103 | "x64" 104 | ], 105 | "dev": true, 106 | "optional": true, 107 | "os": [ 108 | "darwin" 109 | ], 110 | "engines": { 111 | "node": ">=12" 112 | } 113 | }, 114 | "node_modules/@esbuild/freebsd-arm64": { 115 | "version": "0.21.5", 116 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", 117 | "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", 118 | "cpu": [ 119 | "arm64" 120 | ], 121 | "dev": true, 122 | "optional": true, 123 | "os": [ 124 | "freebsd" 125 | ], 126 | "engines": { 127 | "node": ">=12" 128 | } 129 | }, 130 | "node_modules/@esbuild/freebsd-x64": { 131 | "version": "0.21.5", 132 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", 133 | "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", 134 | "cpu": [ 135 | "x64" 136 | ], 137 | "dev": true, 138 | "optional": true, 139 | "os": [ 140 | "freebsd" 141 | ], 142 | "engines": { 143 | "node": ">=12" 144 | } 145 | }, 146 | "node_modules/@esbuild/linux-arm": { 147 | "version": "0.21.5", 148 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", 149 | "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", 150 | "cpu": [ 151 | "arm" 152 | ], 153 | "dev": true, 154 | "optional": true, 155 | "os": [ 156 | "linux" 157 | ], 158 | "engines": { 159 | "node": ">=12" 160 | } 161 | }, 162 | "node_modules/@esbuild/linux-arm64": { 163 | "version": "0.21.5", 164 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", 165 | "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", 166 | "cpu": [ 167 | "arm64" 168 | ], 169 | "dev": true, 170 | "optional": true, 171 | "os": [ 172 | "linux" 173 | ], 174 | "engines": { 175 | "node": ">=12" 176 | } 177 | }, 178 | "node_modules/@esbuild/linux-ia32": { 179 | "version": "0.21.5", 180 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", 181 | "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", 182 | "cpu": [ 183 | "ia32" 184 | ], 185 | "dev": true, 186 | "optional": true, 187 | "os": [ 188 | "linux" 189 | ], 190 | "engines": { 191 | "node": ">=12" 192 | } 193 | }, 194 | "node_modules/@esbuild/linux-loong64": { 195 | "version": "0.21.5", 196 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", 197 | "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", 198 | "cpu": [ 199 | "loong64" 200 | ], 201 | "dev": true, 202 | "optional": true, 203 | "os": [ 204 | "linux" 205 | ], 206 | "engines": { 207 | "node": ">=12" 208 | } 209 | }, 210 | "node_modules/@esbuild/linux-mips64el": { 211 | "version": "0.21.5", 212 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", 213 | "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", 214 | "cpu": [ 215 | "mips64el" 216 | ], 217 | "dev": true, 218 | "optional": true, 219 | "os": [ 220 | "linux" 221 | ], 222 | "engines": { 223 | "node": ">=12" 224 | } 225 | }, 226 | "node_modules/@esbuild/linux-ppc64": { 227 | "version": "0.21.5", 228 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", 229 | "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", 230 | "cpu": [ 231 | "ppc64" 232 | ], 233 | "dev": true, 234 | "optional": true, 235 | "os": [ 236 | "linux" 237 | ], 238 | "engines": { 239 | "node": ">=12" 240 | } 241 | }, 242 | "node_modules/@esbuild/linux-riscv64": { 243 | "version": "0.21.5", 244 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", 245 | "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", 246 | "cpu": [ 247 | "riscv64" 248 | ], 249 | "dev": true, 250 | "optional": true, 251 | "os": [ 252 | "linux" 253 | ], 254 | "engines": { 255 | "node": ">=12" 256 | } 257 | }, 258 | "node_modules/@esbuild/linux-s390x": { 259 | "version": "0.21.5", 260 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", 261 | "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", 262 | "cpu": [ 263 | "s390x" 264 | ], 265 | "dev": true, 266 | "optional": true, 267 | "os": [ 268 | "linux" 269 | ], 270 | "engines": { 271 | "node": ">=12" 272 | } 273 | }, 274 | "node_modules/@esbuild/linux-x64": { 275 | "version": "0.21.5", 276 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", 277 | "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", 278 | "cpu": [ 279 | "x64" 280 | ], 281 | "dev": true, 282 | "optional": true, 283 | "os": [ 284 | "linux" 285 | ], 286 | "engines": { 287 | "node": ">=12" 288 | } 289 | }, 290 | "node_modules/@esbuild/netbsd-x64": { 291 | "version": "0.21.5", 292 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", 293 | "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", 294 | "cpu": [ 295 | "x64" 296 | ], 297 | "dev": true, 298 | "optional": true, 299 | "os": [ 300 | "netbsd" 301 | ], 302 | "engines": { 303 | "node": ">=12" 304 | } 305 | }, 306 | "node_modules/@esbuild/openbsd-x64": { 307 | "version": "0.21.5", 308 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", 309 | "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", 310 | "cpu": [ 311 | "x64" 312 | ], 313 | "dev": true, 314 | "optional": true, 315 | "os": [ 316 | "openbsd" 317 | ], 318 | "engines": { 319 | "node": ">=12" 320 | } 321 | }, 322 | "node_modules/@esbuild/sunos-x64": { 323 | "version": "0.21.5", 324 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", 325 | "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", 326 | "cpu": [ 327 | "x64" 328 | ], 329 | "dev": true, 330 | "optional": true, 331 | "os": [ 332 | "sunos" 333 | ], 334 | "engines": { 335 | "node": ">=12" 336 | } 337 | }, 338 | "node_modules/@esbuild/win32-arm64": { 339 | "version": "0.21.5", 340 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", 341 | "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", 342 | "cpu": [ 343 | "arm64" 344 | ], 345 | "dev": true, 346 | "optional": true, 347 | "os": [ 348 | "win32" 349 | ], 350 | "engines": { 351 | "node": ">=12" 352 | } 353 | }, 354 | "node_modules/@esbuild/win32-ia32": { 355 | "version": "0.21.5", 356 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", 357 | "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", 358 | "cpu": [ 359 | "ia32" 360 | ], 361 | "dev": true, 362 | "optional": true, 363 | "os": [ 364 | "win32" 365 | ], 366 | "engines": { 367 | "node": ">=12" 368 | } 369 | }, 370 | "node_modules/@esbuild/win32-x64": { 371 | "version": "0.21.5", 372 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", 373 | "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", 374 | "cpu": [ 375 | "x64" 376 | ], 377 | "dev": true, 378 | "optional": true, 379 | "os": [ 380 | "win32" 381 | ], 382 | "engines": { 383 | "node": ">=12" 384 | } 385 | }, 386 | "node_modules/@jest/schemas": { 387 | "version": "29.6.3", 388 | "dev": true, 389 | "license": "MIT", 390 | "dependencies": { 391 | "@sinclair/typebox": "^0.27.8" 392 | }, 393 | "engines": { 394 | "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 395 | } 396 | }, 397 | "node_modules/@jridgewell/sourcemap-codec": { 398 | "version": "1.4.15", 399 | "dev": true, 400 | "license": "MIT" 401 | }, 402 | "node_modules/@rollup/rollup-android-arm-eabi": { 403 | "version": "4.22.2", 404 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.2.tgz", 405 | "integrity": "sha512-8Ao+EDmTPjZ1ZBABc1ohN7Ylx7UIYcjReZinigedTOnGFhIctyGPxY2II+hJ6gD2/vkDKZTyQ0e7++kwv6wDrw==", 406 | "cpu": [ 407 | "arm" 408 | ], 409 | "dev": true, 410 | "optional": true, 411 | "os": [ 412 | "android" 413 | ] 414 | }, 415 | "node_modules/@rollup/rollup-android-arm64": { 416 | "version": "4.22.2", 417 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.2.tgz", 418 | "integrity": "sha512-I+B1v0a4iqdS9DvYt1RJZ3W+Oh9EVWjbY6gp79aAYipIbxSLEoQtFQlZEnUuwhDXCqMxJ3hluxKAdPD+GiluFQ==", 419 | "cpu": [ 420 | "arm64" 421 | ], 422 | "dev": true, 423 | "optional": true, 424 | "os": [ 425 | "android" 426 | ] 427 | }, 428 | "node_modules/@rollup/rollup-darwin-arm64": { 429 | "version": "4.22.2", 430 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.2.tgz", 431 | "integrity": "sha512-BTHO7rR+LC67OP7I8N8GvdvnQqzFujJYWo7qCQ8fGdQcb8Gn6EQY+K1P+daQLnDCuWKbZ+gHAQZuKiQkXkqIYg==", 432 | "cpu": [ 433 | "arm64" 434 | ], 435 | "dev": true, 436 | "optional": true, 437 | "os": [ 438 | "darwin" 439 | ] 440 | }, 441 | "node_modules/@rollup/rollup-darwin-x64": { 442 | "version": "4.22.2", 443 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.2.tgz", 444 | "integrity": "sha512-1esGwDNFe2lov4I6GsEeYaAMHwkqk0IbuGH7gXGdBmd/EP9QddJJvTtTF/jv+7R8ZTYPqwcdLpMTxK8ytP6k6Q==", 445 | "cpu": [ 446 | "x64" 447 | ], 448 | "dev": true, 449 | "optional": true, 450 | "os": [ 451 | "darwin" 452 | ] 453 | }, 454 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 455 | "version": "4.22.2", 456 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.2.tgz", 457 | "integrity": "sha512-GBHuY07x96OTEM3OQLNaUSUwrOhdMea/LDmlFHi/HMonrgF6jcFrrFFwJhhe84XtA1oK/Qh4yFS+VMREf6dobg==", 458 | "cpu": [ 459 | "arm" 460 | ], 461 | "dev": true, 462 | "optional": true, 463 | "os": [ 464 | "linux" 465 | ] 466 | }, 467 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 468 | "version": "4.22.2", 469 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.2.tgz", 470 | "integrity": "sha512-Dbfa9Sc1G1lWxop0gNguXOfGhaXQWAGhZUcqA0Vs6CnJq8JW/YOw/KvyGtQFmz4yDr0H4v9X248SM7bizYj4yQ==", 471 | "cpu": [ 472 | "arm" 473 | ], 474 | "dev": true, 475 | "optional": true, 476 | "os": [ 477 | "linux" 478 | ] 479 | }, 480 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 481 | "version": "4.22.2", 482 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.2.tgz", 483 | "integrity": "sha512-Z1YpgBvFYhZIyBW5BoopwSg+t7yqEhs5HCei4JbsaXnhz/eZehT18DaXl957aaE9QK7TRGFryCAtStZywcQe1A==", 484 | "cpu": [ 485 | "arm64" 486 | ], 487 | "dev": true, 488 | "optional": true, 489 | "os": [ 490 | "linux" 491 | ] 492 | }, 493 | "node_modules/@rollup/rollup-linux-arm64-musl": { 494 | "version": "4.22.2", 495 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.2.tgz", 496 | "integrity": "sha512-66Zszr7i/JaQ0u/lefcfaAw16wh3oT72vSqubIMQqWzOg85bGCPhoeykG/cC5uvMzH80DQa2L539IqKht6twVA==", 497 | "cpu": [ 498 | "arm64" 499 | ], 500 | "dev": true, 501 | "optional": true, 502 | "os": [ 503 | "linux" 504 | ] 505 | }, 506 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 507 | "version": "4.22.2", 508 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.2.tgz", 509 | "integrity": "sha512-HpJCMnlMTfEhwo19bajvdraQMcAq3FX08QDx3OfQgb+414xZhKNf3jNvLFYKbbDSGBBrQh5yNwWZrdK0g0pokg==", 510 | "cpu": [ 511 | "ppc64" 512 | ], 513 | "dev": true, 514 | "optional": true, 515 | "os": [ 516 | "linux" 517 | ] 518 | }, 519 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 520 | "version": "4.22.2", 521 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.2.tgz", 522 | "integrity": "sha512-/egzQzbOSRef2vYCINKITGrlwkzP7uXRnL+xU2j75kDVp3iPdcF0TIlfwTRF8woBZllhk3QaxNOEj2Ogh3t9hg==", 523 | "cpu": [ 524 | "riscv64" 525 | ], 526 | "dev": true, 527 | "optional": true, 528 | "os": [ 529 | "linux" 530 | ] 531 | }, 532 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 533 | "version": "4.22.2", 534 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.2.tgz", 535 | "integrity": "sha512-qgYbOEbrPfEkH/OnUJd1/q4s89FvNJQIUldx8X2F/UM5sEbtkqZpf2s0yly2jSCKr1zUUOY1hnTP2J1WOzMAdA==", 536 | "cpu": [ 537 | "s390x" 538 | ], 539 | "dev": true, 540 | "optional": true, 541 | "os": [ 542 | "linux" 543 | ] 544 | }, 545 | "node_modules/@rollup/rollup-linux-x64-gnu": { 546 | "version": "4.22.2", 547 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.2.tgz", 548 | "integrity": "sha512-a0lkvNhFLhf+w7A95XeBqGQaG0KfS3hPFJnz1uraSdUe/XImkp/Psq0Ca0/UdD5IEAGoENVmnYrzSC9Y2a2uKQ==", 549 | "cpu": [ 550 | "x64" 551 | ], 552 | "dev": true, 553 | "optional": true, 554 | "os": [ 555 | "linux" 556 | ] 557 | }, 558 | "node_modules/@rollup/rollup-linux-x64-musl": { 559 | "version": "4.22.2", 560 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.2.tgz", 561 | "integrity": "sha512-sSWBVZgzwtsuG9Dxi9kjYOUu/wKW+jrbzj4Cclabqnfkot8Z3VEHcIgyenA3lLn/Fu11uDviWjhctulkhEO60g==", 562 | "cpu": [ 563 | "x64" 564 | ], 565 | "dev": true, 566 | "optional": true, 567 | "os": [ 568 | "linux" 569 | ] 570 | }, 571 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 572 | "version": "4.22.2", 573 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.2.tgz", 574 | "integrity": "sha512-t/YgCbZ638R/r7IKb9yCM6nAek1RUvyNdfU0SHMDLOf6GFe/VG1wdiUAsxTWHKqjyzkRGg897ZfCpdo1bsCSsA==", 575 | "cpu": [ 576 | "arm64" 577 | ], 578 | "dev": true, 579 | "optional": true, 580 | "os": [ 581 | "win32" 582 | ] 583 | }, 584 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 585 | "version": "4.22.2", 586 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.2.tgz", 587 | "integrity": "sha512-kTmX5uGs3WYOA+gYDgI6ITkZng9SP71FEMoHNkn+cnmb9Zuyyay8pf0oO5twtTwSjNGy1jlaWooTIr+Dw4tIbw==", 588 | "cpu": [ 589 | "ia32" 590 | ], 591 | "dev": true, 592 | "optional": true, 593 | "os": [ 594 | "win32" 595 | ] 596 | }, 597 | "node_modules/@rollup/rollup-win32-x64-msvc": { 598 | "version": "4.22.2", 599 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.2.tgz", 600 | "integrity": "sha512-Yy8So+SoRz8I3NS4Bjh91BICPOSVgdompTIPYTByUqU66AXSIOgmW3Lv1ke3NORPqxdF+RdrZET+8vYai6f4aA==", 601 | "cpu": [ 602 | "x64" 603 | ], 604 | "dev": true, 605 | "optional": true, 606 | "os": [ 607 | "win32" 608 | ] 609 | }, 610 | "node_modules/@sinclair/typebox": { 611 | "version": "0.27.8", 612 | "dev": true, 613 | "license": "MIT" 614 | }, 615 | "node_modules/@types/estree": { 616 | "version": "1.0.5", 617 | "dev": true, 618 | "license": "MIT" 619 | }, 620 | "node_modules/@types/node": { 621 | "version": "20.11.14", 622 | "dev": true, 623 | "license": "MIT", 624 | "optional": true, 625 | "peer": true, 626 | "dependencies": { 627 | "undici-types": "~5.26.4" 628 | } 629 | }, 630 | "node_modules/@vitest/expect": { 631 | "version": "1.6.0", 632 | "dev": true, 633 | "license": "MIT", 634 | "dependencies": { 635 | "@vitest/spy": "1.6.0", 636 | "@vitest/utils": "1.6.0", 637 | "chai": "^4.3.10" 638 | }, 639 | "funding": { 640 | "url": "https://opencollective.com/vitest" 641 | } 642 | }, 643 | "node_modules/@vitest/runner": { 644 | "version": "1.6.0", 645 | "dev": true, 646 | "license": "MIT", 647 | "dependencies": { 648 | "@vitest/utils": "1.6.0", 649 | "p-limit": "^5.0.0", 650 | "pathe": "^1.1.1" 651 | }, 652 | "funding": { 653 | "url": "https://opencollective.com/vitest" 654 | } 655 | }, 656 | "node_modules/@vitest/runner/node_modules/p-limit": { 657 | "version": "5.0.0", 658 | "dev": true, 659 | "license": "MIT", 660 | "dependencies": { 661 | "yocto-queue": "^1.0.0" 662 | }, 663 | "engines": { 664 | "node": ">=18" 665 | }, 666 | "funding": { 667 | "url": "https://github.com/sponsors/sindresorhus" 668 | } 669 | }, 670 | "node_modules/@vitest/runner/node_modules/yocto-queue": { 671 | "version": "1.0.0", 672 | "dev": true, 673 | "license": "MIT", 674 | "engines": { 675 | "node": ">=12.20" 676 | }, 677 | "funding": { 678 | "url": "https://github.com/sponsors/sindresorhus" 679 | } 680 | }, 681 | "node_modules/@vitest/snapshot": { 682 | "version": "1.6.0", 683 | "dev": true, 684 | "license": "MIT", 685 | "dependencies": { 686 | "magic-string": "^0.30.5", 687 | "pathe": "^1.1.1", 688 | "pretty-format": "^29.7.0" 689 | }, 690 | "funding": { 691 | "url": "https://opencollective.com/vitest" 692 | } 693 | }, 694 | "node_modules/@vitest/spy": { 695 | "version": "1.6.0", 696 | "dev": true, 697 | "license": "MIT", 698 | "dependencies": { 699 | "tinyspy": "^2.2.0" 700 | }, 701 | "funding": { 702 | "url": "https://opencollective.com/vitest" 703 | } 704 | }, 705 | "node_modules/@vitest/utils": { 706 | "version": "1.6.0", 707 | "dev": true, 708 | "license": "MIT", 709 | "dependencies": { 710 | "diff-sequences": "^29.6.3", 711 | "estree-walker": "^3.0.3", 712 | "loupe": "^2.3.7", 713 | "pretty-format": "^29.7.0" 714 | }, 715 | "funding": { 716 | "url": "https://opencollective.com/vitest" 717 | } 718 | }, 719 | "node_modules/acorn": { 720 | "version": "8.11.3", 721 | "dev": true, 722 | "license": "MIT", 723 | "bin": { 724 | "acorn": "bin/acorn" 725 | }, 726 | "engines": { 727 | "node": ">=0.4.0" 728 | } 729 | }, 730 | "node_modules/acorn-walk": { 731 | "version": "8.3.2", 732 | "dev": true, 733 | "license": "MIT", 734 | "engines": { 735 | "node": ">=0.4.0" 736 | } 737 | }, 738 | "node_modules/agent-base": { 739 | "version": "7.1.1", 740 | "dev": true, 741 | "license": "MIT", 742 | "dependencies": { 743 | "debug": "^4.3.4" 744 | }, 745 | "engines": { 746 | "node": ">= 14" 747 | } 748 | }, 749 | "node_modules/assertion-error": { 750 | "version": "1.1.0", 751 | "dev": true, 752 | "license": "MIT", 753 | "engines": { 754 | "node": "*" 755 | } 756 | }, 757 | "node_modules/asynckit": { 758 | "version": "0.4.0", 759 | "dev": true, 760 | "license": "MIT" 761 | }, 762 | "node_modules/cac": { 763 | "version": "6.7.14", 764 | "dev": true, 765 | "license": "MIT", 766 | "engines": { 767 | "node": ">=8" 768 | } 769 | }, 770 | "node_modules/chai": { 771 | "version": "4.4.1", 772 | "dev": true, 773 | "license": "MIT", 774 | "dependencies": { 775 | "assertion-error": "^1.1.0", 776 | "check-error": "^1.0.3", 777 | "deep-eql": "^4.1.3", 778 | "get-func-name": "^2.0.2", 779 | "loupe": "^2.3.6", 780 | "pathval": "^1.1.1", 781 | "type-detect": "^4.0.8" 782 | }, 783 | "engines": { 784 | "node": ">=4" 785 | } 786 | }, 787 | "node_modules/check-error": { 788 | "version": "1.0.3", 789 | "dev": true, 790 | "license": "MIT", 791 | "dependencies": { 792 | "get-func-name": "^2.0.2" 793 | }, 794 | "engines": { 795 | "node": "*" 796 | } 797 | }, 798 | "node_modules/combined-stream": { 799 | "version": "1.0.8", 800 | "dev": true, 801 | "license": "MIT", 802 | "dependencies": { 803 | "delayed-stream": "~1.0.0" 804 | }, 805 | "engines": { 806 | "node": ">= 0.8" 807 | } 808 | }, 809 | "node_modules/confbox": { 810 | "version": "0.1.7", 811 | "dev": true, 812 | "license": "MIT" 813 | }, 814 | "node_modules/cross-spawn": { 815 | "version": "7.0.3", 816 | "dev": true, 817 | "license": "MIT", 818 | "dependencies": { 819 | "path-key": "^3.1.0", 820 | "shebang-command": "^2.0.0", 821 | "which": "^2.0.1" 822 | }, 823 | "engines": { 824 | "node": ">= 8" 825 | } 826 | }, 827 | "node_modules/cross-spawn/node_modules/which": { 828 | "version": "2.0.2", 829 | "dev": true, 830 | "license": "ISC", 831 | "dependencies": { 832 | "isexe": "^2.0.0" 833 | }, 834 | "bin": { 835 | "node-which": "bin/node-which" 836 | }, 837 | "engines": { 838 | "node": ">= 8" 839 | } 840 | }, 841 | "node_modules/cssstyle": { 842 | "version": "4.0.1", 843 | "dev": true, 844 | "license": "MIT", 845 | "dependencies": { 846 | "rrweb-cssom": "^0.6.0" 847 | }, 848 | "engines": { 849 | "node": ">=18" 850 | } 851 | }, 852 | "node_modules/cssstyle/node_modules/rrweb-cssom": { 853 | "version": "0.6.0", 854 | "dev": true, 855 | "license": "MIT" 856 | }, 857 | "node_modules/data-urls": { 858 | "version": "5.0.0", 859 | "dev": true, 860 | "license": "MIT", 861 | "dependencies": { 862 | "whatwg-mimetype": "^4.0.0", 863 | "whatwg-url": "^14.0.0" 864 | }, 865 | "engines": { 866 | "node": ">=18" 867 | } 868 | }, 869 | "node_modules/debug": { 870 | "version": "4.3.4", 871 | "dev": true, 872 | "license": "MIT", 873 | "dependencies": { 874 | "ms": "2.1.2" 875 | }, 876 | "engines": { 877 | "node": ">=6.0" 878 | }, 879 | "peerDependenciesMeta": { 880 | "supports-color": { 881 | "optional": true 882 | } 883 | } 884 | }, 885 | "node_modules/decimal.js": { 886 | "version": "10.4.3", 887 | "dev": true, 888 | "license": "MIT" 889 | }, 890 | "node_modules/deep-eql": { 891 | "version": "4.1.3", 892 | "dev": true, 893 | "license": "MIT", 894 | "dependencies": { 895 | "type-detect": "^4.0.0" 896 | }, 897 | "engines": { 898 | "node": ">=6" 899 | } 900 | }, 901 | "node_modules/delayed-stream": { 902 | "version": "1.0.0", 903 | "dev": true, 904 | "license": "MIT", 905 | "engines": { 906 | "node": ">=0.4.0" 907 | } 908 | }, 909 | "node_modules/diff-sequences": { 910 | "version": "29.6.3", 911 | "dev": true, 912 | "license": "MIT", 913 | "engines": { 914 | "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 915 | } 916 | }, 917 | "node_modules/entities": { 918 | "version": "4.5.0", 919 | "dev": true, 920 | "license": "BSD-2-Clause", 921 | "engines": { 922 | "node": ">=0.12" 923 | }, 924 | "funding": { 925 | "url": "https://github.com/fb55/entities?sponsor=1" 926 | } 927 | }, 928 | "node_modules/esbuild": { 929 | "version": "0.21.5", 930 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", 931 | "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", 932 | "dev": true, 933 | "hasInstallScript": true, 934 | "bin": { 935 | "esbuild": "bin/esbuild" 936 | }, 937 | "engines": { 938 | "node": ">=12" 939 | }, 940 | "optionalDependencies": { 941 | "@esbuild/aix-ppc64": "0.21.5", 942 | "@esbuild/android-arm": "0.21.5", 943 | "@esbuild/android-arm64": "0.21.5", 944 | "@esbuild/android-x64": "0.21.5", 945 | "@esbuild/darwin-arm64": "0.21.5", 946 | "@esbuild/darwin-x64": "0.21.5", 947 | "@esbuild/freebsd-arm64": "0.21.5", 948 | "@esbuild/freebsd-x64": "0.21.5", 949 | "@esbuild/linux-arm": "0.21.5", 950 | "@esbuild/linux-arm64": "0.21.5", 951 | "@esbuild/linux-ia32": "0.21.5", 952 | "@esbuild/linux-loong64": "0.21.5", 953 | "@esbuild/linux-mips64el": "0.21.5", 954 | "@esbuild/linux-ppc64": "0.21.5", 955 | "@esbuild/linux-riscv64": "0.21.5", 956 | "@esbuild/linux-s390x": "0.21.5", 957 | "@esbuild/linux-x64": "0.21.5", 958 | "@esbuild/netbsd-x64": "0.21.5", 959 | "@esbuild/openbsd-x64": "0.21.5", 960 | "@esbuild/sunos-x64": "0.21.5", 961 | "@esbuild/win32-arm64": "0.21.5", 962 | "@esbuild/win32-ia32": "0.21.5", 963 | "@esbuild/win32-x64": "0.21.5" 964 | } 965 | }, 966 | "node_modules/estree-walker": { 967 | "version": "3.0.3", 968 | "dev": true, 969 | "license": "MIT", 970 | "dependencies": { 971 | "@types/estree": "^1.0.0" 972 | } 973 | }, 974 | "node_modules/execa": { 975 | "version": "8.0.1", 976 | "dev": true, 977 | "license": "MIT", 978 | "dependencies": { 979 | "cross-spawn": "^7.0.3", 980 | "get-stream": "^8.0.1", 981 | "human-signals": "^5.0.0", 982 | "is-stream": "^3.0.0", 983 | "merge-stream": "^2.0.0", 984 | "npm-run-path": "^5.1.0", 985 | "onetime": "^6.0.0", 986 | "signal-exit": "^4.1.0", 987 | "strip-final-newline": "^3.0.0" 988 | }, 989 | "engines": { 990 | "node": ">=16.17" 991 | }, 992 | "funding": { 993 | "url": "https://github.com/sindresorhus/execa?sponsor=1" 994 | } 995 | }, 996 | "node_modules/fake-indexeddb": { 997 | "version": "6.0.0", 998 | "dev": true, 999 | "license": "Apache-2.0", 1000 | "engines": { 1001 | "node": ">=18" 1002 | } 1003 | }, 1004 | "node_modules/form-data": { 1005 | "version": "4.0.0", 1006 | "dev": true, 1007 | "license": "MIT", 1008 | "dependencies": { 1009 | "asynckit": "^0.4.0", 1010 | "combined-stream": "^1.0.8", 1011 | "mime-types": "^2.1.12" 1012 | }, 1013 | "engines": { 1014 | "node": ">= 6" 1015 | } 1016 | }, 1017 | "node_modules/fsevents": { 1018 | "version": "2.3.3", 1019 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1020 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1021 | "dev": true, 1022 | "hasInstallScript": true, 1023 | "optional": true, 1024 | "os": [ 1025 | "darwin" 1026 | ], 1027 | "engines": { 1028 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1029 | } 1030 | }, 1031 | "node_modules/get-func-name": { 1032 | "version": "2.0.2", 1033 | "dev": true, 1034 | "license": "MIT", 1035 | "engines": { 1036 | "node": "*" 1037 | } 1038 | }, 1039 | "node_modules/get-stream": { 1040 | "version": "8.0.1", 1041 | "dev": true, 1042 | "license": "MIT", 1043 | "engines": { 1044 | "node": ">=16" 1045 | }, 1046 | "funding": { 1047 | "url": "https://github.com/sponsors/sindresorhus" 1048 | } 1049 | }, 1050 | "node_modules/html-encoding-sniffer": { 1051 | "version": "4.0.0", 1052 | "dev": true, 1053 | "license": "MIT", 1054 | "dependencies": { 1055 | "whatwg-encoding": "^3.1.1" 1056 | }, 1057 | "engines": { 1058 | "node": ">=18" 1059 | } 1060 | }, 1061 | "node_modules/http-proxy-agent": { 1062 | "version": "7.0.2", 1063 | "dev": true, 1064 | "license": "MIT", 1065 | "dependencies": { 1066 | "agent-base": "^7.1.0", 1067 | "debug": "^4.3.4" 1068 | }, 1069 | "engines": { 1070 | "node": ">= 14" 1071 | } 1072 | }, 1073 | "node_modules/https-proxy-agent": { 1074 | "version": "7.0.4", 1075 | "dev": true, 1076 | "license": "MIT", 1077 | "dependencies": { 1078 | "agent-base": "^7.0.2", 1079 | "debug": "4" 1080 | }, 1081 | "engines": { 1082 | "node": ">= 14" 1083 | } 1084 | }, 1085 | "node_modules/human-signals": { 1086 | "version": "5.0.0", 1087 | "dev": true, 1088 | "license": "Apache-2.0", 1089 | "engines": { 1090 | "node": ">=16.17.0" 1091 | } 1092 | }, 1093 | "node_modules/iconv-lite": { 1094 | "version": "0.6.3", 1095 | "dev": true, 1096 | "license": "MIT", 1097 | "dependencies": { 1098 | "safer-buffer": ">= 2.1.2 < 3.0.0" 1099 | }, 1100 | "engines": { 1101 | "node": ">=0.10.0" 1102 | } 1103 | }, 1104 | "node_modules/is-potential-custom-element-name": { 1105 | "version": "1.0.1", 1106 | "dev": true, 1107 | "license": "MIT" 1108 | }, 1109 | "node_modules/is-stream": { 1110 | "version": "3.0.0", 1111 | "dev": true, 1112 | "license": "MIT", 1113 | "engines": { 1114 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 1115 | }, 1116 | "funding": { 1117 | "url": "https://github.com/sponsors/sindresorhus" 1118 | } 1119 | }, 1120 | "node_modules/isexe": { 1121 | "version": "2.0.0", 1122 | "dev": true, 1123 | "license": "ISC" 1124 | }, 1125 | "node_modules/jsdom": { 1126 | "version": "24.1.0", 1127 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", 1128 | "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", 1129 | "dev": true, 1130 | "dependencies": { 1131 | "cssstyle": "^4.0.1", 1132 | "data-urls": "^5.0.0", 1133 | "decimal.js": "^10.4.3", 1134 | "form-data": "^4.0.0", 1135 | "html-encoding-sniffer": "^4.0.0", 1136 | "http-proxy-agent": "^7.0.2", 1137 | "https-proxy-agent": "^7.0.4", 1138 | "is-potential-custom-element-name": "^1.0.1", 1139 | "nwsapi": "^2.2.10", 1140 | "parse5": "^7.1.2", 1141 | "rrweb-cssom": "^0.7.0", 1142 | "saxes": "^6.0.0", 1143 | "symbol-tree": "^3.2.4", 1144 | "tough-cookie": "^4.1.4", 1145 | "w3c-xmlserializer": "^5.0.0", 1146 | "webidl-conversions": "^7.0.0", 1147 | "whatwg-encoding": "^3.1.1", 1148 | "whatwg-mimetype": "^4.0.0", 1149 | "whatwg-url": "^14.0.0", 1150 | "ws": "^8.17.0", 1151 | "xml-name-validator": "^5.0.0" 1152 | }, 1153 | "engines": { 1154 | "node": ">=18" 1155 | }, 1156 | "peerDependencies": { 1157 | "canvas": "^2.11.2" 1158 | }, 1159 | "peerDependenciesMeta": { 1160 | "canvas": { 1161 | "optional": true 1162 | } 1163 | } 1164 | }, 1165 | "node_modules/local-pkg": { 1166 | "version": "0.5.0", 1167 | "dev": true, 1168 | "license": "MIT", 1169 | "dependencies": { 1170 | "mlly": "^1.4.2", 1171 | "pkg-types": "^1.0.3" 1172 | }, 1173 | "engines": { 1174 | "node": ">=14" 1175 | }, 1176 | "funding": { 1177 | "url": "https://github.com/sponsors/antfu" 1178 | } 1179 | }, 1180 | "node_modules/loupe": { 1181 | "version": "2.3.7", 1182 | "dev": true, 1183 | "license": "MIT", 1184 | "dependencies": { 1185 | "get-func-name": "^2.0.1" 1186 | } 1187 | }, 1188 | "node_modules/magic-string": { 1189 | "version": "0.30.10", 1190 | "dev": true, 1191 | "license": "MIT", 1192 | "dependencies": { 1193 | "@jridgewell/sourcemap-codec": "^1.4.15" 1194 | } 1195 | }, 1196 | "node_modules/merge-stream": { 1197 | "version": "2.0.0", 1198 | "dev": true, 1199 | "license": "MIT" 1200 | }, 1201 | "node_modules/mime-db": { 1202 | "version": "1.52.0", 1203 | "dev": true, 1204 | "license": "MIT", 1205 | "engines": { 1206 | "node": ">= 0.6" 1207 | } 1208 | }, 1209 | "node_modules/mime-types": { 1210 | "version": "2.1.35", 1211 | "dev": true, 1212 | "license": "MIT", 1213 | "dependencies": { 1214 | "mime-db": "1.52.0" 1215 | }, 1216 | "engines": { 1217 | "node": ">= 0.6" 1218 | } 1219 | }, 1220 | "node_modules/mimic-fn": { 1221 | "version": "4.0.0", 1222 | "dev": true, 1223 | "license": "MIT", 1224 | "engines": { 1225 | "node": ">=12" 1226 | }, 1227 | "funding": { 1228 | "url": "https://github.com/sponsors/sindresorhus" 1229 | } 1230 | }, 1231 | "node_modules/mlly": { 1232 | "version": "1.7.0", 1233 | "dev": true, 1234 | "license": "MIT", 1235 | "dependencies": { 1236 | "acorn": "^8.11.3", 1237 | "pathe": "^1.1.2", 1238 | "pkg-types": "^1.1.0", 1239 | "ufo": "^1.5.3" 1240 | } 1241 | }, 1242 | "node_modules/ms": { 1243 | "version": "2.1.2", 1244 | "dev": true, 1245 | "license": "MIT" 1246 | }, 1247 | "node_modules/nanoid": { 1248 | "version": "3.3.7", 1249 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 1250 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 1251 | "dev": true, 1252 | "funding": [ 1253 | { 1254 | "type": "github", 1255 | "url": "https://github.com/sponsors/ai" 1256 | } 1257 | ], 1258 | "bin": { 1259 | "nanoid": "bin/nanoid.cjs" 1260 | }, 1261 | "engines": { 1262 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1263 | } 1264 | }, 1265 | "node_modules/npm-run-path": { 1266 | "version": "5.3.0", 1267 | "dev": true, 1268 | "license": "MIT", 1269 | "dependencies": { 1270 | "path-key": "^4.0.0" 1271 | }, 1272 | "engines": { 1273 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 1274 | }, 1275 | "funding": { 1276 | "url": "https://github.com/sponsors/sindresorhus" 1277 | } 1278 | }, 1279 | "node_modules/npm-run-path/node_modules/path-key": { 1280 | "version": "4.0.0", 1281 | "dev": true, 1282 | "license": "MIT", 1283 | "engines": { 1284 | "node": ">=12" 1285 | }, 1286 | "funding": { 1287 | "url": "https://github.com/sponsors/sindresorhus" 1288 | } 1289 | }, 1290 | "node_modules/nwsapi": { 1291 | "version": "2.2.10", 1292 | "dev": true, 1293 | "license": "MIT" 1294 | }, 1295 | "node_modules/onetime": { 1296 | "version": "6.0.0", 1297 | "dev": true, 1298 | "license": "MIT", 1299 | "dependencies": { 1300 | "mimic-fn": "^4.0.0" 1301 | }, 1302 | "engines": { 1303 | "node": ">=12" 1304 | }, 1305 | "funding": { 1306 | "url": "https://github.com/sponsors/sindresorhus" 1307 | } 1308 | }, 1309 | "node_modules/parse5": { 1310 | "version": "7.1.2", 1311 | "dev": true, 1312 | "license": "MIT", 1313 | "dependencies": { 1314 | "entities": "^4.4.0" 1315 | }, 1316 | "funding": { 1317 | "url": "https://github.com/inikulin/parse5?sponsor=1" 1318 | } 1319 | }, 1320 | "node_modules/path-key": { 1321 | "version": "3.1.1", 1322 | "dev": true, 1323 | "license": "MIT", 1324 | "engines": { 1325 | "node": ">=8" 1326 | } 1327 | }, 1328 | "node_modules/pathe": { 1329 | "version": "1.1.2", 1330 | "dev": true, 1331 | "license": "MIT" 1332 | }, 1333 | "node_modules/pathval": { 1334 | "version": "1.1.1", 1335 | "dev": true, 1336 | "license": "MIT", 1337 | "engines": { 1338 | "node": "*" 1339 | } 1340 | }, 1341 | "node_modules/picocolors": { 1342 | "version": "1.1.0", 1343 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", 1344 | "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", 1345 | "dev": true 1346 | }, 1347 | "node_modules/pkg-types": { 1348 | "version": "1.1.1", 1349 | "dev": true, 1350 | "license": "MIT", 1351 | "dependencies": { 1352 | "confbox": "^0.1.7", 1353 | "mlly": "^1.7.0", 1354 | "pathe": "^1.1.2" 1355 | } 1356 | }, 1357 | "node_modules/postcss": { 1358 | "version": "8.4.47", 1359 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", 1360 | "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", 1361 | "dev": true, 1362 | "funding": [ 1363 | { 1364 | "type": "opencollective", 1365 | "url": "https://opencollective.com/postcss/" 1366 | }, 1367 | { 1368 | "type": "tidelift", 1369 | "url": "https://tidelift.com/funding/github/npm/postcss" 1370 | }, 1371 | { 1372 | "type": "github", 1373 | "url": "https://github.com/sponsors/ai" 1374 | } 1375 | ], 1376 | "dependencies": { 1377 | "nanoid": "^3.3.7", 1378 | "picocolors": "^1.1.0", 1379 | "source-map-js": "^1.2.1" 1380 | }, 1381 | "engines": { 1382 | "node": "^10 || ^12 || >=14" 1383 | } 1384 | }, 1385 | "node_modules/pretty-format": { 1386 | "version": "29.7.0", 1387 | "dev": true, 1388 | "license": "MIT", 1389 | "dependencies": { 1390 | "@jest/schemas": "^29.6.3", 1391 | "ansi-styles": "^5.0.0", 1392 | "react-is": "^18.0.0" 1393 | }, 1394 | "engines": { 1395 | "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 1396 | } 1397 | }, 1398 | "node_modules/pretty-format/node_modules/ansi-styles": { 1399 | "version": "5.2.0", 1400 | "dev": true, 1401 | "license": "MIT", 1402 | "engines": { 1403 | "node": ">=10" 1404 | }, 1405 | "funding": { 1406 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1407 | } 1408 | }, 1409 | "node_modules/psl": { 1410 | "version": "1.9.0", 1411 | "dev": true, 1412 | "license": "MIT" 1413 | }, 1414 | "node_modules/punycode": { 1415 | "version": "2.3.1", 1416 | "dev": true, 1417 | "license": "MIT", 1418 | "engines": { 1419 | "node": ">=6" 1420 | } 1421 | }, 1422 | "node_modules/querystringify": { 1423 | "version": "2.2.0", 1424 | "dev": true, 1425 | "license": "MIT" 1426 | }, 1427 | "node_modules/react-is": { 1428 | "version": "18.3.1", 1429 | "dev": true, 1430 | "license": "MIT" 1431 | }, 1432 | "node_modules/requires-port": { 1433 | "version": "1.0.0", 1434 | "dev": true, 1435 | "license": "MIT" 1436 | }, 1437 | "node_modules/rollup": { 1438 | "version": "4.22.2", 1439 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.2.tgz", 1440 | "integrity": "sha512-JWWpTrZmqQGQWt16xvNn6KVIUz16VtZwl984TKw0dfqqRpFwtLJYYk1/4BTgplndMQKWUk/yB4uOShYmMzA2Vg==", 1441 | "dev": true, 1442 | "dependencies": { 1443 | "@types/estree": "1.0.5" 1444 | }, 1445 | "bin": { 1446 | "rollup": "dist/bin/rollup" 1447 | }, 1448 | "engines": { 1449 | "node": ">=18.0.0", 1450 | "npm": ">=8.0.0" 1451 | }, 1452 | "optionalDependencies": { 1453 | "@rollup/rollup-android-arm-eabi": "4.22.2", 1454 | "@rollup/rollup-android-arm64": "4.22.2", 1455 | "@rollup/rollup-darwin-arm64": "4.22.2", 1456 | "@rollup/rollup-darwin-x64": "4.22.2", 1457 | "@rollup/rollup-linux-arm-gnueabihf": "4.22.2", 1458 | "@rollup/rollup-linux-arm-musleabihf": "4.22.2", 1459 | "@rollup/rollup-linux-arm64-gnu": "4.22.2", 1460 | "@rollup/rollup-linux-arm64-musl": "4.22.2", 1461 | "@rollup/rollup-linux-powerpc64le-gnu": "4.22.2", 1462 | "@rollup/rollup-linux-riscv64-gnu": "4.22.2", 1463 | "@rollup/rollup-linux-s390x-gnu": "4.22.2", 1464 | "@rollup/rollup-linux-x64-gnu": "4.22.2", 1465 | "@rollup/rollup-linux-x64-musl": "4.22.2", 1466 | "@rollup/rollup-win32-arm64-msvc": "4.22.2", 1467 | "@rollup/rollup-win32-ia32-msvc": "4.22.2", 1468 | "@rollup/rollup-win32-x64-msvc": "4.22.2", 1469 | "fsevents": "~2.3.2" 1470 | } 1471 | }, 1472 | "node_modules/rrweb-cssom": { 1473 | "version": "0.7.0", 1474 | "dev": true, 1475 | "license": "MIT" 1476 | }, 1477 | "node_modules/safer-buffer": { 1478 | "version": "2.1.2", 1479 | "dev": true, 1480 | "license": "MIT" 1481 | }, 1482 | "node_modules/saxes": { 1483 | "version": "6.0.0", 1484 | "dev": true, 1485 | "license": "ISC", 1486 | "dependencies": { 1487 | "xmlchars": "^2.2.0" 1488 | }, 1489 | "engines": { 1490 | "node": ">=v12.22.7" 1491 | } 1492 | }, 1493 | "node_modules/shebang-command": { 1494 | "version": "2.0.0", 1495 | "dev": true, 1496 | "license": "MIT", 1497 | "dependencies": { 1498 | "shebang-regex": "^3.0.0" 1499 | }, 1500 | "engines": { 1501 | "node": ">=8" 1502 | } 1503 | }, 1504 | "node_modules/shebang-regex": { 1505 | "version": "3.0.0", 1506 | "dev": true, 1507 | "license": "MIT", 1508 | "engines": { 1509 | "node": ">=8" 1510 | } 1511 | }, 1512 | "node_modules/siginfo": { 1513 | "version": "2.0.0", 1514 | "dev": true, 1515 | "license": "ISC" 1516 | }, 1517 | "node_modules/signal-exit": { 1518 | "version": "4.1.0", 1519 | "dev": true, 1520 | "license": "ISC", 1521 | "engines": { 1522 | "node": ">=14" 1523 | }, 1524 | "funding": { 1525 | "url": "https://github.com/sponsors/isaacs" 1526 | } 1527 | }, 1528 | "node_modules/source-map-js": { 1529 | "version": "1.2.1", 1530 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1531 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1532 | "dev": true, 1533 | "engines": { 1534 | "node": ">=0.10.0" 1535 | } 1536 | }, 1537 | "node_modules/stackback": { 1538 | "version": "0.0.2", 1539 | "dev": true, 1540 | "license": "MIT" 1541 | }, 1542 | "node_modules/std-env": { 1543 | "version": "3.7.0", 1544 | "dev": true, 1545 | "license": "MIT" 1546 | }, 1547 | "node_modules/strip-final-newline": { 1548 | "version": "3.0.0", 1549 | "dev": true, 1550 | "license": "MIT", 1551 | "engines": { 1552 | "node": ">=12" 1553 | }, 1554 | "funding": { 1555 | "url": "https://github.com/sponsors/sindresorhus" 1556 | } 1557 | }, 1558 | "node_modules/strip-literal": { 1559 | "version": "2.1.0", 1560 | "dev": true, 1561 | "license": "MIT", 1562 | "dependencies": { 1563 | "js-tokens": "^9.0.0" 1564 | }, 1565 | "funding": { 1566 | "url": "https://github.com/sponsors/antfu" 1567 | } 1568 | }, 1569 | "node_modules/strip-literal/node_modules/js-tokens": { 1570 | "version": "9.0.0", 1571 | "dev": true, 1572 | "license": "MIT" 1573 | }, 1574 | "node_modules/symbol-tree": { 1575 | "version": "3.2.4", 1576 | "dev": true, 1577 | "license": "MIT" 1578 | }, 1579 | "node_modules/tinybench": { 1580 | "version": "2.8.0", 1581 | "dev": true, 1582 | "license": "MIT" 1583 | }, 1584 | "node_modules/tinypool": { 1585 | "version": "0.8.4", 1586 | "dev": true, 1587 | "license": "MIT", 1588 | "engines": { 1589 | "node": ">=14.0.0" 1590 | } 1591 | }, 1592 | "node_modules/tinyspy": { 1593 | "version": "2.2.1", 1594 | "dev": true, 1595 | "license": "MIT", 1596 | "engines": { 1597 | "node": ">=14.0.0" 1598 | } 1599 | }, 1600 | "node_modules/tough-cookie": { 1601 | "version": "4.1.4", 1602 | "dev": true, 1603 | "license": "BSD-3-Clause", 1604 | "dependencies": { 1605 | "psl": "^1.1.33", 1606 | "punycode": "^2.1.1", 1607 | "universalify": "^0.2.0", 1608 | "url-parse": "^1.5.3" 1609 | }, 1610 | "engines": { 1611 | "node": ">=6" 1612 | } 1613 | }, 1614 | "node_modules/tr46": { 1615 | "version": "5.0.0", 1616 | "dev": true, 1617 | "license": "MIT", 1618 | "dependencies": { 1619 | "punycode": "^2.3.1" 1620 | }, 1621 | "engines": { 1622 | "node": ">=18" 1623 | } 1624 | }, 1625 | "node_modules/type-detect": { 1626 | "version": "4.0.8", 1627 | "dev": true, 1628 | "license": "MIT", 1629 | "engines": { 1630 | "node": ">=4" 1631 | } 1632 | }, 1633 | "node_modules/typescript": { 1634 | "version": "5.4.5", 1635 | "dev": true, 1636 | "license": "Apache-2.0", 1637 | "bin": { 1638 | "tsc": "bin/tsc", 1639 | "tsserver": "bin/tsserver" 1640 | }, 1641 | "engines": { 1642 | "node": ">=14.17" 1643 | } 1644 | }, 1645 | "node_modules/ufo": { 1646 | "version": "1.5.3", 1647 | "dev": true, 1648 | "license": "MIT" 1649 | }, 1650 | "node_modules/undici-types": { 1651 | "version": "5.26.5", 1652 | "dev": true, 1653 | "license": "MIT", 1654 | "optional": true, 1655 | "peer": true 1656 | }, 1657 | "node_modules/universalify": { 1658 | "version": "0.2.0", 1659 | "dev": true, 1660 | "license": "MIT", 1661 | "engines": { 1662 | "node": ">= 4.0.0" 1663 | } 1664 | }, 1665 | "node_modules/url-parse": { 1666 | "version": "1.5.10", 1667 | "dev": true, 1668 | "license": "MIT", 1669 | "dependencies": { 1670 | "querystringify": "^2.1.1", 1671 | "requires-port": "^1.0.0" 1672 | } 1673 | }, 1674 | "node_modules/vite": { 1675 | "version": "5.4.7", 1676 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", 1677 | "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", 1678 | "dev": true, 1679 | "dependencies": { 1680 | "esbuild": "^0.21.3", 1681 | "postcss": "^8.4.43", 1682 | "rollup": "^4.20.0" 1683 | }, 1684 | "bin": { 1685 | "vite": "bin/vite.js" 1686 | }, 1687 | "engines": { 1688 | "node": "^18.0.0 || >=20.0.0" 1689 | }, 1690 | "funding": { 1691 | "url": "https://github.com/vitejs/vite?sponsor=1" 1692 | }, 1693 | "optionalDependencies": { 1694 | "fsevents": "~2.3.3" 1695 | }, 1696 | "peerDependencies": { 1697 | "@types/node": "^18.0.0 || >=20.0.0", 1698 | "less": "*", 1699 | "lightningcss": "^1.21.0", 1700 | "sass": "*", 1701 | "sass-embedded": "*", 1702 | "stylus": "*", 1703 | "sugarss": "*", 1704 | "terser": "^5.4.0" 1705 | }, 1706 | "peerDependenciesMeta": { 1707 | "@types/node": { 1708 | "optional": true 1709 | }, 1710 | "less": { 1711 | "optional": true 1712 | }, 1713 | "lightningcss": { 1714 | "optional": true 1715 | }, 1716 | "sass": { 1717 | "optional": true 1718 | }, 1719 | "sass-embedded": { 1720 | "optional": true 1721 | }, 1722 | "stylus": { 1723 | "optional": true 1724 | }, 1725 | "sugarss": { 1726 | "optional": true 1727 | }, 1728 | "terser": { 1729 | "optional": true 1730 | } 1731 | } 1732 | }, 1733 | "node_modules/vite-node": { 1734 | "version": "1.6.0", 1735 | "dev": true, 1736 | "license": "MIT", 1737 | "dependencies": { 1738 | "cac": "^6.7.14", 1739 | "debug": "^4.3.4", 1740 | "pathe": "^1.1.1", 1741 | "picocolors": "^1.0.0", 1742 | "vite": "^5.0.0" 1743 | }, 1744 | "bin": { 1745 | "vite-node": "vite-node.mjs" 1746 | }, 1747 | "engines": { 1748 | "node": "^18.0.0 || >=20.0.0" 1749 | }, 1750 | "funding": { 1751 | "url": "https://opencollective.com/vitest" 1752 | } 1753 | }, 1754 | "node_modules/vitest": { 1755 | "version": "1.6.0", 1756 | "dev": true, 1757 | "license": "MIT", 1758 | "dependencies": { 1759 | "@vitest/expect": "1.6.0", 1760 | "@vitest/runner": "1.6.0", 1761 | "@vitest/snapshot": "1.6.0", 1762 | "@vitest/spy": "1.6.0", 1763 | "@vitest/utils": "1.6.0", 1764 | "acorn-walk": "^8.3.2", 1765 | "chai": "^4.3.10", 1766 | "debug": "^4.3.4", 1767 | "execa": "^8.0.1", 1768 | "local-pkg": "^0.5.0", 1769 | "magic-string": "^0.30.5", 1770 | "pathe": "^1.1.1", 1771 | "picocolors": "^1.0.0", 1772 | "std-env": "^3.5.0", 1773 | "strip-literal": "^2.0.0", 1774 | "tinybench": "^2.5.1", 1775 | "tinypool": "^0.8.3", 1776 | "vite": "^5.0.0", 1777 | "vite-node": "1.6.0", 1778 | "why-is-node-running": "^2.2.2" 1779 | }, 1780 | "bin": { 1781 | "vitest": "vitest.mjs" 1782 | }, 1783 | "engines": { 1784 | "node": "^18.0.0 || >=20.0.0" 1785 | }, 1786 | "funding": { 1787 | "url": "https://opencollective.com/vitest" 1788 | }, 1789 | "peerDependencies": { 1790 | "@edge-runtime/vm": "*", 1791 | "@types/node": "^18.0.0 || >=20.0.0", 1792 | "@vitest/browser": "1.6.0", 1793 | "@vitest/ui": "1.6.0", 1794 | "happy-dom": "*", 1795 | "jsdom": "*" 1796 | }, 1797 | "peerDependenciesMeta": { 1798 | "@edge-runtime/vm": { 1799 | "optional": true 1800 | }, 1801 | "@types/node": { 1802 | "optional": true 1803 | }, 1804 | "@vitest/browser": { 1805 | "optional": true 1806 | }, 1807 | "@vitest/ui": { 1808 | "optional": true 1809 | }, 1810 | "happy-dom": { 1811 | "optional": true 1812 | }, 1813 | "jsdom": { 1814 | "optional": true 1815 | } 1816 | } 1817 | }, 1818 | "node_modules/w3c-xmlserializer": { 1819 | "version": "5.0.0", 1820 | "dev": true, 1821 | "license": "MIT", 1822 | "dependencies": { 1823 | "xml-name-validator": "^5.0.0" 1824 | }, 1825 | "engines": { 1826 | "node": ">=18" 1827 | } 1828 | }, 1829 | "node_modules/webidl-conversions": { 1830 | "version": "7.0.0", 1831 | "dev": true, 1832 | "license": "BSD-2-Clause", 1833 | "engines": { 1834 | "node": ">=12" 1835 | } 1836 | }, 1837 | "node_modules/whatwg-encoding": { 1838 | "version": "3.1.1", 1839 | "dev": true, 1840 | "license": "MIT", 1841 | "dependencies": { 1842 | "iconv-lite": "0.6.3" 1843 | }, 1844 | "engines": { 1845 | "node": ">=18" 1846 | } 1847 | }, 1848 | "node_modules/whatwg-mimetype": { 1849 | "version": "4.0.0", 1850 | "dev": true, 1851 | "license": "MIT", 1852 | "engines": { 1853 | "node": ">=18" 1854 | } 1855 | }, 1856 | "node_modules/whatwg-url": { 1857 | "version": "14.0.0", 1858 | "dev": true, 1859 | "license": "MIT", 1860 | "dependencies": { 1861 | "tr46": "^5.0.0", 1862 | "webidl-conversions": "^7.0.0" 1863 | }, 1864 | "engines": { 1865 | "node": ">=18" 1866 | } 1867 | }, 1868 | "node_modules/why-is-node-running": { 1869 | "version": "2.2.2", 1870 | "dev": true, 1871 | "license": "MIT", 1872 | "dependencies": { 1873 | "siginfo": "^2.0.0", 1874 | "stackback": "0.0.2" 1875 | }, 1876 | "bin": { 1877 | "why-is-node-running": "cli.js" 1878 | }, 1879 | "engines": { 1880 | "node": ">=8" 1881 | } 1882 | }, 1883 | "node_modules/ws": { 1884 | "version": "8.18.0", 1885 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 1886 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 1887 | "dev": true, 1888 | "engines": { 1889 | "node": ">=10.0.0" 1890 | }, 1891 | "peerDependencies": { 1892 | "bufferutil": "^4.0.1", 1893 | "utf-8-validate": ">=5.0.2" 1894 | }, 1895 | "peerDependenciesMeta": { 1896 | "bufferutil": { 1897 | "optional": true 1898 | }, 1899 | "utf-8-validate": { 1900 | "optional": true 1901 | } 1902 | } 1903 | }, 1904 | "node_modules/xml-name-validator": { 1905 | "version": "5.0.0", 1906 | "dev": true, 1907 | "license": "Apache-2.0", 1908 | "engines": { 1909 | "node": ">=18" 1910 | } 1911 | }, 1912 | "node_modules/xmlchars": { 1913 | "version": "2.2.0", 1914 | "dev": true, 1915 | "license": "MIT" 1916 | } 1917 | } 1918 | } 1919 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserbase", 3 | "version": "3.0.3", 4 | "description": "IndexedDB wrapper providing promises, easy versioning, and events, including change events across tabs.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "tsc --watch", 8 | "build": "tsc", 9 | "test": "vitest", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/dabblewriter/browserbase.git" 15 | }, 16 | "author": "Jacob Wright", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/dabblewriter/browserbase/issues" 20 | }, 21 | "homepage": "https://github.com/dabblewriter/browserbase#readme", 22 | "devDependencies": { 23 | "fake-indexeddb": "^6.0.0", 24 | "jsdom": "^24.1.0", 25 | "typescript": "^5.4.5", 26 | "vitest": "^1.6.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Browserbase.spec.ts: -------------------------------------------------------------------------------- 1 | import indexeddb, { IDBKeyRange, IDBTransaction } from 'fake-indexeddb'; 2 | import { afterEach, beforeEach, describe, expect, it } from 'vitest'; 3 | import { Browserbase, ObjectStore } from './Browserbase'; 4 | 5 | // Skipping files that produce errors because either vitest isn't catching them, or fake-indexeddb is throwing them 6 | // twice, the second time delayed. I think the latter. 7 | 8 | globalThis.indexedDB = indexeddb; 9 | globalThis.IDBTransaction = IDBTransaction; 10 | globalThis.IDBKeyRange = IDBKeyRange; 11 | 12 | describe('Browserbase', () => { 13 | let db: Browserbase<{ 14 | foo: ObjectStore<{ key: string; test?: boolean; name?: string; date?: Date; unique?: number }>; 15 | bar: ObjectStore<{ key: string }>; 16 | baz: ObjectStore<{ id: string }>; 17 | }>; 18 | 19 | beforeEach(() => { 20 | db = new Browserbase('test' + (Math.random() + '').slice(2)); 21 | }); 22 | 23 | afterEach(async () => { 24 | db.close(); 25 | Browserbase.deleteDatabase('test'); 26 | }); 27 | 28 | it('should fail if no versions were set', () => { 29 | return db.open().then( 30 | () => { 31 | throw new Error('It opened just fine'); 32 | }, 33 | err => { 34 | // It caused an error as it should have 35 | } 36 | ); 37 | }); 38 | 39 | it('should create a version with an object store', async () => { 40 | db.version(1, { foo: 'bar' }); 41 | await db.open(); 42 | expect(db.stores).to.have.property('foo'); 43 | }); 44 | 45 | it('should create a version with multiple object stores', async () => { 46 | db.version(1, { foo: 'bar', bar: 'foo' }); 47 | await db.open(); 48 | expect(db.stores).to.have.property('foo'); 49 | expect(db.stores).to.have.property('bar'); 50 | }); 51 | 52 | it('should add onto existing versions', async () => { 53 | db.version(1, { foo: 'bar', bar: 'foo' }); 54 | db.version(2, { foo: 'foobar' }); 55 | await db.open(); 56 | expect(db.db!.transaction('foo').objectStore('foo').indexNames.contains('foobar')).toBe(true); 57 | }); 58 | 59 | it('should add onto existing versions which have already been created', async () => { 60 | db.version(1, { foo: 'bar', bar: 'foo' }); 61 | await db.open(); 62 | expect(db.db!.transaction('foo').objectStore('foo').indexNames.contains('foobar')).toBe(false); 63 | db.close(); 64 | db.version(2, { foo: 'foobar' }); 65 | await db.open(); 66 | expect(db.db!.transaction('foo').objectStore('foo').indexNames.contains('foobar')).toBe(true); 67 | }); 68 | 69 | it('should support deleting indexes from previous versions', async () => { 70 | db.version(1, { foo: 'bar', bar: 'foo' }); 71 | db.version(2, { foo: 'foobar' }); 72 | db.version(3, { foo: '-foobar' }); 73 | await db.open(); 74 | expect(db.db!.transaction('foo').objectStore('foo').indexNames.contains('foobar')).toBe(false); 75 | }); 76 | 77 | it('should delete indexes from previous versions that already exist', async () => { 78 | db.version(1, { foo: 'bar', bar: 'foo' }); 79 | db.version(2, { foo: 'foobar' }); 80 | await db.open(); 81 | expect(db.db!.transaction('foo').objectStore('foo').indexNames.contains('foobar')).toBe(true); 82 | db.close(); 83 | db.version(3, { foo: '-foobar' }); 84 | await db.open(); 85 | expect(db.db!.transaction('foo').objectStore('foo').indexNames.contains('foobar')).toBe(false); 86 | }); 87 | 88 | it('should add objects to the store', async () => { 89 | db.version(1, { foo: 'key' }); 90 | await db.open(); 91 | await db.stores.foo.add({ key: 'abc' }); 92 | const obj_1 = await db.stores.foo.get('abc'); 93 | expect(obj_1.key).to.equal('abc'); 94 | }); 95 | 96 | it.skip('should fail to add objects that already exist', async () => { 97 | db.version(1, { foo: 'key' }); 98 | await db.open(); 99 | await db.stores.foo.add({ key: 'abc' }); 100 | const obj_1 = await db.stores.foo.get('abc'); 101 | expect(obj_1.key).to.equal('abc'); 102 | try { 103 | await db.stores.foo.add({ key: 'abc' }); 104 | expect(false).toBe(true); 105 | } catch (err) { 106 | // good, good 107 | expect(err.name).to.equal('ConstraintError'); 108 | } 109 | }); 110 | 111 | it('should add objects to the store in bulk', async () => { 112 | db.version(1, { foo: 'key' }); 113 | await db.open(); 114 | await db.stores.foo.bulkAdd([{ key: 'abc' }, { key: 'abcc' }]); 115 | const arr = await db.stores.foo.getAll(); 116 | expect(arr).to.eql([{ key: 'abc' }, { key: 'abcc' }]); 117 | }); 118 | 119 | it('should save objects to the store', async () => { 120 | db.version(1, { foo: 'key' }); 121 | await db.open(); 122 | await db.stores.foo.put({ key: 'abc' }); 123 | const obj_1 = await db.stores.foo.get('abc'); 124 | expect(obj_1.key).to.equal('abc'); 125 | await db.stores.foo.put({ key: 'abc', test: true }); 126 | const obj_2 = await db.stores.foo.get('abc'); 127 | expect(obj_2.test).toBe(true); 128 | }); 129 | 130 | it('should save objects to the store in bulk', async () => { 131 | db.version(1, { foo: 'key' }); 132 | await db.open(); 133 | await db.stores.foo.bulkPut([{ key: 'abc' }, { key: 'abcc' }]); 134 | const arr = await db.stores.foo.getAll(); 135 | expect(arr).to.eql([{ key: 'abc' }, { key: 'abcc' }]); 136 | }); 137 | 138 | it('should delete objects from the store', async () => { 139 | db.version(1, { foo: 'key' }); 140 | await db.open(); 141 | await db.stores.foo.put({ key: 'abc' }); 142 | const obj_1 = await db.stores.foo.get('abc'); 143 | expect(obj_1.key).to.equal('abc'); 144 | await db.stores.foo.delete('abc'); 145 | const obj_2 = await db.stores.foo.get('abc'); 146 | expect(obj_2).toBe(undefined); 147 | }); 148 | 149 | it('should dispatch a change for add/put/delete', async () => { 150 | let lastChange: any, lastKey: string | undefined; 151 | db.addEventListener('change', ({ detail: { obj, key } }) => { 152 | lastChange = obj; 153 | lastKey = key; 154 | }); 155 | 156 | db.version(1, { foo: 'key' }); 157 | await db.open(); 158 | await db.stores.foo.put({ key: 'abc' }); 159 | expect(lastChange).to.eql({ key: 'abc' }); 160 | expect(lastKey).to.equal('abc'); 161 | await db.stores.foo.delete('abc'); 162 | expect(lastChange).to.equal(null); 163 | expect(lastKey).to.equal('abc'); 164 | }); 165 | 166 | it('should allow one transaction for many puts', async () => { 167 | db.version(1, { foo: 'key' }); 168 | await db.open(); 169 | expect(db.stores.foo._transStore('readonly').transaction).not.to.equal(db._current); 170 | const trans = db.start(); 171 | trans.stores.foo.put({ key: 'test1' }); 172 | trans.stores.foo.put({ key: 'test2' }); 173 | trans.stores.foo.put({ key: 'test3' }); 174 | expect(trans.stores.foo._transStore('readonly').transaction).to.equal(trans._current); 175 | return await trans.commit(); 176 | }); 177 | 178 | it.skip('should not report success if the transaction fails', async () => { 179 | db.version(1, { foo: 'key, &unique' }); 180 | let success1: boolean | undefined; 181 | let success2: boolean | undefined; 182 | 183 | await db.open(); 184 | const trans = db.start(); 185 | trans.stores.foo.add({ key: 'test1' }).then( 186 | id => { 187 | success1 = true; 188 | }, 189 | err => { 190 | success1 = false; 191 | } 192 | ); 193 | trans.stores.foo.put({ key: 'test2', unique: 10 }).then( 194 | id_1 => { 195 | success2 = true; 196 | }, 197 | err_1 => { 198 | success2 = false; 199 | } 200 | ); 201 | trans.stores.foo.add({ key: 'test1' }); 202 | trans.stores.foo.put({ key: 'test3', unique: 10 }); 203 | try { 204 | await trans.commit(); 205 | } catch { 206 | expect(success1).toBe(false); 207 | expect(success2).toBe(false); 208 | } 209 | const obj_2 = await db.stores.foo.get('test1'); 210 | expect(obj_2).toBe(undefined); 211 | }); 212 | 213 | it.skip('should not report to finish if the transaction fails', async () => { 214 | db.version(1, { foo: 'key, &unique' }); 215 | let success = false; 216 | 217 | await db.open(); 218 | const trans = db.start(); 219 | trans.stores.foo.add({ key: 'test1', unique: 10 }); 220 | trans.stores.foo.add({ key: 'test2', unique: 11 }); 221 | trans.stores.foo.add({ key: 'test3', unique: 12 }); 222 | try { 223 | await trans.commit(); 224 | db.stores.foo.addEventListener('change', () => { 225 | success = true; 226 | }); 227 | await db.stores.foo.where('key').update(obj_1 => { 228 | if (obj_1.key === 'test2') { 229 | obj_1.unique = 15; 230 | return obj_1; 231 | } else if (obj_1.key === 'test3') { 232 | obj_1.unique = 10; 233 | return obj_1; 234 | } 235 | }); 236 | } catch {} 237 | expect(success).toBe(false); 238 | const res = await db.stores.foo.getAll(); 239 | expect(res).to.eql([ 240 | { key: 'test1', unique: 10 }, 241 | { key: 'test2', unique: 11 }, 242 | { key: 'test3', unique: 12 }, 243 | ]); 244 | }); 245 | 246 | it('should get all objects', async () => { 247 | db.version(1, { foo: 'key' }); 248 | await db.open(); 249 | const trans = db.start(); 250 | trans.stores.foo.put({ key: 'test1' }); 251 | trans.stores.foo.put({ key: 'test2' }); 252 | trans.stores.foo.put({ key: 'test3' }); 253 | await trans.commit(); 254 | const objects = await db.stores.foo.getAll(); 255 | expect(objects).to.have.length(3); 256 | }); 257 | 258 | it('should set keyPath on the store', async () => { 259 | db.version(1, { foo: 'key', bar: ', test', baz: '++id' }); 260 | await db.open(); 261 | expect(db.stores.foo.keyPath).to.equal('key'); 262 | expect(db.stores.bar.keyPath).to.equal(null); 263 | expect(db.stores.baz.keyPath).to.equal('id'); 264 | }); 265 | 266 | it('should get a range of objects', async () => { 267 | db.version(1, { foo: 'key' }); 268 | await db.open(); 269 | const trans = db.start(); 270 | trans.stores.foo.put({ key: 'test1' }); 271 | trans.stores.foo.put({ key: 'test2' }); 272 | trans.stores.foo.put({ key: 'test3' }); 273 | trans.stores.foo.put({ key: 'test4' }); 274 | trans.stores.foo.put({ key: 'test5' }); 275 | trans.stores.foo.put({ key: 'test6' }); 276 | await trans.commit(); 277 | const objects = await db.stores.foo.where('key').startsAt('test2').endsBefore('test5').getAll(); 278 | expect(objects).to.eql([{ key: 'test2' }, { key: 'test3' }, { key: 'test4' }]); 279 | }); 280 | 281 | it('should get a range of objects with limit', async () => { 282 | db.version(1, { foo: 'key' }); 283 | await db.open(); 284 | const trans = db.start(); 285 | trans.stores.foo.put({ key: 'test1' }); 286 | trans.stores.foo.put({ key: 'test2' }); 287 | trans.stores.foo.put({ key: 'test3' }); 288 | trans.stores.foo.put({ key: 'test4' }); 289 | trans.stores.foo.put({ key: 'test5' }); 290 | trans.stores.foo.put({ key: 'test6' }); 291 | await trans.commit(); 292 | const objects = await db.stores.foo.where('key').startsAt('test2').endsBefore('test5').limit(2).getAll(); 293 | expect(objects).to.eql([{ key: 'test2' }, { key: 'test3' }]); 294 | }); 295 | 296 | it('should cursor over a range of objects with limit', async () => { 297 | db.version(1, { foo: 'key' }); 298 | await db.open(); 299 | const trans = db.start(); 300 | let objects: any[] = []; 301 | trans.stores.foo.put({ key: 'test1' }); 302 | trans.stores.foo.put({ key: 'test2' }); 303 | trans.stores.foo.put({ key: 'test3' }); 304 | trans.stores.foo.put({ key: 'test4' }); 305 | trans.stores.foo.put({ key: 'test5' }); 306 | trans.stores.foo.put({ key: 'test6' }); 307 | await trans.commit(); 308 | await db.stores.foo 309 | .where('key') 310 | .startsAt('test2') 311 | .endsBefore('test5') 312 | .limit(2) 313 | .forEach(obj_1 => objects.push(obj_1)); 314 | expect(objects).to.eql([{ key: 'test2' }, { key: 'test3' }]); 315 | }); 316 | 317 | it('should delete a range of objects', async () => { 318 | db.version(1, { foo: 'key' }); 319 | await db.open(); 320 | const trans = db.start(); 321 | trans.stores.foo.put({ key: 'test1' }); 322 | trans.stores.foo.put({ key: 'test2' }); 323 | trans.stores.foo.put({ key: 'test3' }); 324 | trans.stores.foo.put({ key: 'test4' }); 325 | trans.stores.foo.put({ key: 'test5' }); 326 | trans.stores.foo.put({ key: 'test6' }); 327 | await trans.commit(); 328 | await db.stores.foo.where('key').startsAfter('test2').endsAt('test5').deleteAll(); 329 | const objects = await db.stores.foo.getAll(); 330 | expect(objects).to.eql([{ key: 'test1' }, { key: 'test2' }, { key: 'test6' }]); 331 | }); 332 | 333 | it('should update a range of objects', async () => { 334 | db.version(1, { foo: 'key' }); 335 | await db.open(); 336 | const trans = db.start(); 337 | trans.stores.foo.put({ key: 'test1' }); 338 | trans.stores.foo.put({ key: 'test2' }); 339 | trans.stores.foo.put({ key: 'test3' }); 340 | trans.stores.foo.put({ key: 'test4' }); 341 | trans.stores.foo.put({ key: 'test5' }); 342 | trans.stores.foo.put({ key: 'test6' }); 343 | await trans.commit(); 344 | await db.stores.foo 345 | .where('key') 346 | .startsAt('test2') 347 | .endsAt('test5') 348 | .update(obj_1 => { 349 | if (obj_1.key === 'test2') return null; 350 | if (obj_1.key === 'test5') return; 351 | obj_1.name = obj_1.key; 352 | return obj_1; 353 | }); 354 | const objects = await db.stores.foo.getAll(); 355 | expect(objects).to.eql([ 356 | { key: 'test1' }, 357 | { key: 'test3', name: 'test3' }, 358 | { key: 'test4', name: 'test4' }, 359 | { key: 'test5' }, 360 | { key: 'test6' }, 361 | ]); 362 | }); 363 | 364 | it('should handle compound indexes', async () => { 365 | db.version(1, { foo: 'key, [name + date]' }); 366 | 367 | await db.open(); 368 | const trans = db.start(); 369 | trans.stores.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 370 | trans.stores.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 371 | trans.stores.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 372 | trans.stores.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 373 | trans.stores.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 374 | await trans.commit(); 375 | const objs = await db.stores.foo.where('[name+ date]').getAll(); 376 | expect(objs).to.eql([ 377 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 378 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 379 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 380 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 381 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 382 | ]); 383 | const rows = await db.stores.foo 384 | .where('[name+date]') 385 | .startsAt(['a', new Date('2005-01-01')]) 386 | .reverse() 387 | .getAll(); 388 | expect(rows).to.eql([ 389 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 390 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 391 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 392 | ]); 393 | }); 394 | 395 | it('should handle compound primary keys', async () => { 396 | db.version(1, { foo: '[name + date]' }); 397 | 398 | await db.open(); 399 | const trans = db.start(); 400 | trans.stores.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 401 | trans.stores.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 402 | trans.stores.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 403 | trans.stores.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 404 | trans.stores.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 405 | await trans.commit(); 406 | const objs = await db.stores.foo.where().getAll(); 407 | expect(objs).to.eql([ 408 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 409 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 410 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 411 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 412 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 413 | ]); 414 | const objs_1 = await db.stores.foo 415 | .where() 416 | .startsAt(['a', new Date('2005-01-01')]) 417 | .reverse() 418 | .getAll(); 419 | expect(objs_1).to.eql([ 420 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 421 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 422 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 423 | ]); 424 | }); 425 | 426 | it('should handle compound indexes with startsWith', async () => { 427 | db.version(1, { foo: 'key, [name + date]' }); 428 | 429 | await db.open(); 430 | const trans = db.start(); 431 | trans.stores.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 432 | trans.stores.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 433 | trans.stores.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 434 | trans.stores.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 435 | trans.stores.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 436 | await trans.commit(); 437 | const objs = await db.stores.foo.where('[name+ date]').startsWith(['a']).getAll(); 438 | expect(objs).to.eql([ 439 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 440 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 441 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 442 | ]); 443 | const objs_1 = await db.stores.foo.where('[name+date]').startsWith(['b']).reverse().getAll(); 444 | expect(objs_1).to.eql([ 445 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 446 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 447 | ]); 448 | }); 449 | }); 450 | -------------------------------------------------------------------------------- /src/Browserbase.ts: -------------------------------------------------------------------------------- 1 | import { TypedEventTarget } from './TypeEventTarget'; 2 | 3 | const maxString = String.fromCharCode(65535); 4 | const noop = (data: T) => data; 5 | 6 | export interface StoresDefinitions { 7 | [storeName: string]: string; 8 | } 9 | 10 | export interface VersionDefinition { 11 | version: number; 12 | stores: StoresDefinitions; 13 | upgradeFunction?: UpgradeFunction; 14 | } 15 | 16 | export type UpgradeFunction = (oldVersion?: number, transaction?: IDBTransaction) => void; 17 | export type IDBTransactionMode = 'readonly' | 'readwrite' | 'versionchange'; 18 | export type CursorIterator = (cursor: IDBCursor, transaction: IDBTransaction) => false | any; 19 | 20 | export interface ChangeDetail extends StoreChangeDetail { 21 | store: ObjectStore; 22 | } 23 | 24 | export interface StoreChangeDetail { 25 | obj: T; 26 | key: K; 27 | declaredFrom: 'local' | 'remote'; 28 | } 29 | 30 | export interface UpgradeDetail { 31 | upgradedFrom: number; 32 | } 33 | 34 | export interface BrowserbaseEventMap { 35 | create: Event; 36 | upgrade: CustomEvent; 37 | open: Event; 38 | error: ErrorEvent; 39 | change: CustomEvent; 40 | blocked: Event; 41 | close: Event; 42 | } 43 | 44 | export interface ObjectStoreEventMap { 45 | change: CustomEvent>; 46 | } 47 | 48 | export interface StoreIterator { 49 | (obj: Type, cursor: IDBCursor, transaction: IDBTransaction): R; 50 | } 51 | 52 | export type ObjectStoreMap>> = { 53 | [key in keyof T]: ObjectStore; 54 | }; 55 | 56 | interface ErrorDispatcher { 57 | dispatchError: (err: Error) => void; 58 | } 59 | 60 | interface BrowserbaseConstructor { 61 | new = {}>( 62 | name: string, 63 | options?: { dontDispatch?: boolean }, 64 | parent?: Browserbase 65 | ): Browserbase; 66 | } 67 | 68 | const transactionPromise = new WeakMap>(); 69 | 70 | /** 71 | * A nice promise-based syntax on indexedDB also providing events when open, closed, and whenever data is changed. 72 | * Dispatches the change events even when the change did not originate in this browser tab. 73 | * 74 | * Versioning is simplified. You provide a string of new indexes for each new version, with the first being the primary 75 | * key. For primary keys, use a "++" prefix to indicate auto-increment, leave it empty if the key isn't part of the 76 | * object. For indexes, use a "-" index to delete a defined index, use "&" to indicate a unique index, use "*" for a 77 | * multiEntry index, and use "[field + anotherField]" for compound indexes. Examples: 78 | * 79 | * // Initial version, should remain the same with updates 80 | * db.version(1, { 81 | * friends: 'fullName, age' 82 | * }); 83 | * 84 | * // Next version, we don't add any indexes, but we want to run our own update code to prepopulate the database 85 | * db.version(2, {}, function(oldVersion, transaction) { 86 | * // prepopulate with some initial data 87 | * transaction.objectStore('friends').put({ fullName: 'Tom' }); 88 | * }); 89 | * 90 | * // Remove the age index and add one for birthdate, add another object store with an auto-incrementing primary key 91 | * // that isn't part of the object, and a multiEntry index on the labels array. 92 | * db.version(3, { 93 | * friends: 'birthdate, -age, [lastName + firstName]', 94 | * events: '++, date, *labels' 95 | * }); 96 | * 97 | * 98 | * After the database is opened, a property will be added to the database instance for each object store in the 99 | * database. This is how you will work with the data in the database. For e.g. 100 | * 101 | * db.version(1, { foo: 'id' }); 102 | * 103 | * // Will be triggered once for any add, put, or delete done in any browser tab. The object will be null when it was 104 | * // deleted, so use the key when object is null. 105 | * db.on('change', (object, key) => { 106 | * console.log('Object with key', key, 'was', object === null ? 'deleted' : 'saved'); 107 | * }); 108 | * 109 | * db.open().then(() => { 110 | * db.foo.put({ id: 'bar' }).then(() => { 111 | * console.log('An object was saved to the database.'); 112 | * }); 113 | * }, err => { 114 | * console.warn('There was an error opening the database:', err); 115 | * }); 116 | */ 117 | export class Browserbase = {}> extends TypedEventTarget { 118 | /** 119 | * Deletes a database by name. 120 | */ 121 | static deleteDatabase(name: string) { 122 | return requestToPromise(indexedDB.deleteDatabase(name)); 123 | } 124 | 125 | db: IDBDatabase | null; 126 | stores: Stores; 127 | 128 | _parent?: this; 129 | _current: IDBTransaction | null; 130 | _dispatchRemote: boolean; 131 | _versionMap: Record; 132 | _versionHandlers: Record; 133 | _channel: BroadcastChannel | null; 134 | _opening?: Promise; 135 | _closed?: boolean = true; 136 | 137 | /** 138 | * Creates a new indexeddb database with the given name. 139 | */ 140 | constructor(public name: string, public options: { dontDispatch?: boolean } = {}, parent?: Browserbase) { 141 | super(); 142 | this.db = null; 143 | this.stores = {} as Stores; 144 | this._dispatchRemote = false; 145 | this._current = null; 146 | this._versionMap = {}; 147 | this._versionHandlers = {}; 148 | this._channel = null; 149 | this._parent = parent as this; 150 | } 151 | 152 | /** 153 | * Defines a version for the database. Additional versions may be added, but existing version should not be changed. 154 | */ 155 | version(version: number, stores: StoresDefinitions, upgradeFunction?: UpgradeFunction) { 156 | this._versionMap[version] = stores; 157 | if (upgradeFunction) { 158 | this._versionHandlers[version] = upgradeFunction; 159 | } 160 | return this; 161 | } 162 | 163 | /** 164 | * Returns a list of the defined versions. 165 | */ 166 | getVersions() { 167 | return Object.keys(this._versionMap).map(key => { 168 | const version = parseInt(key); 169 | return { version, stores: this._versionMap[version], upgradeFunction: this._versionHandlers[version] }; 170 | }); 171 | } 172 | 173 | /** 174 | * Whether this database is open or closed. 175 | */ 176 | isOpen() { 177 | return Boolean(this.db); 178 | } 179 | 180 | /** 181 | * Open a database, call this after defining versions. 182 | */ 183 | open() { 184 | this._closed = false; 185 | 186 | if (this._opening) { 187 | return this._opening; 188 | } 189 | 190 | if (!Object.keys(this._versionMap).length) { 191 | return Promise.reject(new Error('Must declare at least a version 1 schema for Browserbase')); 192 | } 193 | 194 | let version = Object.keys(this._versionMap) 195 | .map(key => parseInt(key)) 196 | .sort((a, b) => a - b) 197 | .pop(); 198 | let upgradedFrom: number | null = null; 199 | 200 | return (this._opening = new Promise((resolve, reject) => { 201 | let request = indexedDB.open(this.name, version); 202 | request.onsuccess = successHandler(resolve); 203 | request.onerror = errorHandler(reject, this); 204 | request.onblocked = event => { 205 | const blockedEvent = new Event('blocked', { cancelable: true }); 206 | this.dispatchEvent(blockedEvent); 207 | if (!blockedEvent.defaultPrevented) { 208 | if (!event.newVersion || event.newVersion < event.oldVersion) { 209 | console.warn(`Browserbase.delete('${this.name}') was blocked`); 210 | } else { 211 | console.warn(`Upgrade '${this.name}' blocked by other connection holding version ${event.oldVersion}`); 212 | } 213 | } 214 | }; 215 | request.onupgradeneeded = event => { 216 | this.db = request.result; 217 | this.db.onerror = errorHandler(reject, this); 218 | this.db.onabort = errorHandler(() => reject(new Error('Abort')), this); 219 | let oldVersion = event.oldVersion > Math.pow(2, 62) ? 0 : event.oldVersion; // Safari 8 fix. 220 | upgradedFrom = oldVersion; 221 | upgrade(oldVersion, request.transaction, this.db, this._versionMap, this._versionHandlers, this); 222 | }; 223 | }).then(db => { 224 | this.db = db; 225 | onOpen(this); 226 | if (upgradedFrom === 0) this.dispatchEvent(new Event('create')); 227 | else if (upgradedFrom) this.dispatchEvent(new CustomEvent('upgrade', { detail: { upgradedFrom } })); 228 | this.dispatchEvent(new Event('open')); 229 | })); 230 | } 231 | 232 | /** 233 | * Closes the database. 234 | */ 235 | close() { 236 | this._closed = true; 237 | if (!this.db) return; 238 | this.db.close(); 239 | this._opening = undefined; 240 | onClose(this); 241 | } 242 | 243 | /** 244 | * Deletes this database. 245 | */ 246 | deleteDatabase() { 247 | this.close(); 248 | return Browserbase.deleteDatabase(this.name); 249 | } 250 | 251 | /** 252 | * Starts a multi-store transaction. All store methods on the returned database clone will be part of this transaction 253 | * until the next tick or until calling db.commit(). 254 | */ 255 | start(storeNames?: string[] | IDBTransaction, mode: IDBTransactionMode = 'readwrite'): this { 256 | if (!storeNames) storeNames = Array.from(this.db.objectStoreNames); 257 | if (this._current) throw new Error('Cannot start a new transaction on an existing transaction browserbase'); 258 | 259 | const Constructor = this.constructor as BrowserbaseConstructor; 260 | const db = new Constructor(this.name, this.options, this); 261 | db.db = this.db; 262 | db._channel = this._channel; 263 | Object.keys(this.stores).forEach((key: keyof Stores & string) => { 264 | const store = this.stores[key]; 265 | if (!(store instanceof ObjectStore)) return; 266 | const childStore = new ObjectStore(db, store.name, store.keyPath) as any; 267 | db.stores[key] = childStore; 268 | childStore.store = store.store; 269 | childStore.revive = store.revive; 270 | }); 271 | 272 | try { 273 | const trans = (db._current = 274 | storeNames instanceof IDBTransaction ? storeNames : this.db.transaction(safariMultiStoreFix(storeNames), mode)); 275 | transactionPromise.set( 276 | trans, 277 | requestToPromise(trans, null, db).then( 278 | result => { 279 | if (db._current === trans) db._current = null; 280 | return result; 281 | }, 282 | error => { 283 | if (db._current === trans) db._current = null; 284 | this.dispatchEvent(new ErrorEvent('error', { error })); 285 | return Promise.reject(error); 286 | } 287 | ) 288 | ); 289 | } catch (error) { 290 | Promise.resolve().then(() => { 291 | this.dispatchEvent(new ErrorEvent('error', { error })); 292 | }); 293 | throw error; 294 | } 295 | 296 | return db as this; 297 | } 298 | 299 | /** 300 | * Finishes a started transaction so that other transactions may be run. This is not needed for a transaction to run, 301 | * but it allows other transactions to be run in this thread. It ought to be called to avoid conflicts with other 302 | * code elsewhere. 303 | */ 304 | commit(options?: { remoteChange?: boolean }) { 305 | if (!this._current) throw new Error('There is no current transaction to commit.'); 306 | const promise = transactionPromise.get(this._current); 307 | if (options && options.remoteChange) { 308 | this._dispatchRemote = true; 309 | promise.then(() => (this._dispatchRemote = false)); 310 | } 311 | this._current = null; 312 | return promise; 313 | } 314 | 315 | /** 316 | * Dispatches a change event when an object is being added, saved, or deleted. When deleted, the object will be null. 317 | */ 318 | dispatchChange( 319 | store: ObjectStore, 320 | obj: any, 321 | key: any, 322 | from: 'local' | 'remote' = 'local', 323 | dispatchRemote = false 324 | ) { 325 | const declaredFrom = this._dispatchRemote || dispatchRemote ? 'remote' : from; 326 | store.dispatchEvent(new CustomEvent('change', { detail: { obj, key, declaredFrom } })); 327 | this.dispatchEvent(new CustomEvent('change', { detail: { store, obj, key, declaredFrom } })); 328 | 329 | if (from === 'local' && this._channel) { 330 | postMessage(this, { path: `${store.name}/${key}`, obj }); 331 | } 332 | } 333 | 334 | /** 335 | * Dispatch an error event. 336 | */ 337 | dispatchError(error: Error) { 338 | this.dispatchEvent(new ErrorEvent('error', { error })); 339 | } 340 | 341 | /** 342 | * Creates or updates a store with the given indexesString. If null will delete the store. 343 | */ 344 | upgradeStore(storeName: string, indexesString: string) { 345 | if (!this._current) this.start().upgradeStore(storeName, indexesString); 346 | else upgradeStore(this.db, this._current, storeName, indexesString); 347 | } 348 | } 349 | 350 | /** 351 | * An abstraction on object stores, allowing to more easily work with them without needing to always explicitly create a 352 | * transaction first. Also helps with ranges and indexes and promises. 353 | */ 354 | export class ObjectStore extends TypedEventTarget< 355 | ObjectStoreEventMap 356 | > { 357 | /** 358 | * Set this function to alter objects to be stored in this database store. 359 | */ 360 | store: (obj: Type) => Type; 361 | 362 | /** 363 | * Set this function to alter objects when they are retrieved from this database store. 364 | */ 365 | revive: (obj: Type) => Type; 366 | 367 | constructor(public db: Browserbase, public name: string, public keyPath: string | string[]) { 368 | super(); 369 | this.db = db; 370 | this.store = noop; 371 | this.revive = noop; 372 | } 373 | 374 | _transStore(mode: IDBTransactionMode) { 375 | if (!this.db._current && !this.db.db) { 376 | throw new Error('Database is not opened'); 377 | } 378 | try { 379 | let trans = this.db._current || this.db.db.transaction(this.name, mode); 380 | return trans.objectStore(this.name); 381 | } catch (error) { 382 | Promise.resolve().then(() => { 383 | this.db.dispatchEvent(new ErrorEvent('error', { error })); 384 | }); 385 | throw error; 386 | } 387 | } 388 | 389 | /** 390 | * Dispatches a change event. 391 | */ 392 | dispatchChange(obj: Type, key: Key) { 393 | this.db.dispatchChange(this, obj, key); 394 | } 395 | 396 | /** 397 | * Dispatch an error event. 398 | */ 399 | dispatchError(error: Error) { 400 | this.db.dispatchError(error); 401 | } 402 | 403 | /** 404 | * Get an object from the store by its primary key 405 | */ 406 | get(key: Key) { 407 | return requestToPromise(this._transStore('readonly').get(key), null, this).then(this.revive); 408 | } 409 | 410 | /** 411 | * Get all objects in this object store. To get only a range, use where() 412 | */ 413 | async getAll() { 414 | const results = await requestToPromise(this._transStore('readonly').getAll(), null, this); 415 | return results.map(this.revive); 416 | } 417 | 418 | /** 419 | * Gets the count of all objects in this store 420 | */ 421 | count() { 422 | return requestToPromise(this._transStore('readonly').count(), null, this); 423 | } 424 | 425 | /** 426 | * Adds an object to the store. If an object with the given key already exists, it will not overwrite it. 427 | */ 428 | async add(obj: Type, key?: Key) { 429 | let store = this._transStore('readwrite'); 430 | key = await requestToPromise(store.add(this.store(obj), key), store.transaction, this); 431 | this.dispatchChange(obj, key); 432 | return key; 433 | } 434 | 435 | /** 436 | * Adds an array of objects to the store in once transaction. You can also call startTransaction and use add(). 437 | */ 438 | async addAll(array: Type[]) { 439 | let store = this._transStore('readwrite'); 440 | await Promise.all( 441 | array.map(async obj => { 442 | const key = await requestToPromise(store.add(this.store(obj)), store.transaction, this); 443 | this.dispatchChange(obj, key); 444 | }) 445 | ); 446 | } 447 | 448 | /** 449 | * Adds an array of objects to the store in once transaction. You can also call startTransaction and use add(). Alias 450 | * of addAll(). 451 | */ 452 | async bulkAdd(array: Type[]) { 453 | await this.addAll(array); 454 | } 455 | 456 | /** 457 | * Saves an object to the store. If an object with the given key already exists, it will overwrite it. 458 | */ 459 | async put(obj: Type, key?: Key) { 460 | let store = this._transStore('readwrite'); 461 | key = await requestToPromise(store.put(this.store(obj), key), store.transaction, this); 462 | this.dispatchChange(obj, key); 463 | return key; 464 | } 465 | 466 | /** 467 | * Saves an array of objects to the store in once transaction. You can also call startTransaction and use put(). 468 | */ 469 | async putAll(array: Type[]) { 470 | let store = this._transStore('readwrite'); 471 | await Promise.all( 472 | array.map(async obj => { 473 | const key = await requestToPromise(store.put(this.store(obj)), store.transaction, this); 474 | this.dispatchChange(obj, key); 475 | }) 476 | ); 477 | } 478 | 479 | /** 480 | * Saves an array of objects to the store in once transaction. You can also call startTransaction and use put(). Alias 481 | * of putAll(). 482 | */ 483 | async bulkPut(array: Type[]) { 484 | await this.putAll(array); 485 | } 486 | 487 | /** 488 | * Deletes an object from the store. 489 | */ 490 | async delete(key: Key) { 491 | let store = this._transStore('readwrite'); 492 | await requestToPromise(store.delete(key), store.transaction, this); 493 | this.dispatchChange(null, key); 494 | } 495 | 496 | /** 497 | * Deletes all objects from a store. 498 | */ 499 | deleteAll() { 500 | return this.where().deleteAll(); 501 | } 502 | 503 | /** 504 | * Use to get a subset of items from the store by id or index. Returns a Where object to allow setting the range and 505 | * limit. 506 | */ 507 | where(index = '') { 508 | index = index.replace(/\s/g, ''); 509 | return new Where(this, index === this.keyPath ? '' : index); 510 | } 511 | } 512 | 513 | /** 514 | * Helps with a ranged getAll or openCursor by helping to create the range and providing a nicer API with returning a 515 | * promise or iterating through with a callback. 516 | */ 517 | export class Where { 518 | protected _upper: IDBValidKey | undefined; 519 | protected _lower: IDBValidKey | undefined; 520 | protected _upperOpen: boolean; 521 | protected _lowerOpen: boolean; 522 | protected _value: IDBValidKey | undefined; 523 | protected _limit: number | undefined; 524 | protected _direction: IDBCursorDirection; 525 | 526 | constructor(public store: ObjectStore, public index: string) { 527 | this._upper = undefined; 528 | this._lower = undefined; 529 | this._upperOpen = false; 530 | this._lowerOpen = false; 531 | this._value = undefined; 532 | this._limit = undefined; 533 | this._direction = 'next'; 534 | } 535 | 536 | /** 537 | * Dispatches a change event. 538 | */ 539 | dispatchChange(obj: Type, key: Key) { 540 | this.store.dispatchChange(obj, key); 541 | } 542 | 543 | /** 544 | * Dispatch an error event. 545 | */ 546 | dispatchError(error: Error) { 547 | this.store.dispatchError(error); 548 | } 549 | 550 | /** 551 | * Set greater than the value provided. 552 | */ 553 | startsAfter(value: IDBValidKey) { 554 | this._lower = value; 555 | this._lowerOpen = true; 556 | return this; 557 | } 558 | 559 | /** 560 | * Set greater than or equal to the value provided. 561 | */ 562 | startsAt(value: IDBValidKey) { 563 | this._lower = value; 564 | this._lowerOpen = false; 565 | return this; 566 | } 567 | 568 | /** 569 | * Set less than the value provided. 570 | */ 571 | endsBefore(value: IDBValidKey) { 572 | this._upper = value; 573 | this._upperOpen = true; 574 | return this; 575 | } 576 | 577 | /** 578 | * Set less than or equal to the value provided. 579 | */ 580 | endsAt(value: IDBValidKey) { 581 | this._upper = value; 582 | this._upperOpen = false; 583 | return this; 584 | } 585 | 586 | /** 587 | * Set the exact match, no range. 588 | */ 589 | equals(value: IDBValidKey) { 590 | this._value = value; 591 | return this; 592 | } 593 | 594 | /** 595 | * Sets the upper and lower bounds to match any string starting with this prefix. 596 | */ 597 | startsWith(prefix: IDBValidKey) { 598 | const endsAt: IDBValidKey = Array.isArray(prefix) ? prefix.concat([[]]) : prefix + maxString; 599 | return this.startsAt(prefix).endsAt(endsAt); 600 | } 601 | 602 | /** 603 | * Limit the return results to the given count. 604 | */ 605 | limit(count: number) { 606 | this._limit = count; 607 | return this; 608 | } 609 | 610 | /** 611 | * Reverses the direction a cursor will get things. 612 | */ 613 | reverse() { 614 | this._direction = 'prev'; 615 | return this; 616 | } 617 | 618 | /** 619 | * Converts this Where to its IDBKeyRange equivalent. 620 | */ 621 | toRange() { 622 | if (this._upper !== undefined && this._lower !== undefined) { 623 | return IDBKeyRange.bound(this._lower, this._upper, this._lowerOpen, this._upperOpen); 624 | } else if (this._upper !== undefined) { 625 | return IDBKeyRange.upperBound(this._upper, this._upperOpen); 626 | } else if (this._lower !== undefined) { 627 | return IDBKeyRange.lowerBound(this._lower, this._lowerOpen); 628 | } else if (this._value !== undefined) { 629 | return IDBKeyRange.only(this._value); 630 | } 631 | } 632 | 633 | /** 634 | * Get all the objects matching the range limited by the limit. 635 | */ 636 | async getAll() { 637 | let range = this.toRange(); 638 | // Handle reverse with cursor 639 | if (this._direction === 'prev') { 640 | let results: Type[] = []; 641 | if (this._limit <= 0) return Promise.resolve(results); 642 | await this.forEach(obj => results.push(this.store.revive(obj))); 643 | return results; 644 | } 645 | 646 | let store = this.store._transStore('readonly'); 647 | let source = this.index ? store.index(this.index) : store; 648 | const records = await requestToPromise(source.getAll(range, this._limit), null, this); 649 | return records.map(this.store.revive); 650 | } 651 | 652 | /** 653 | * Get all the keys matching the range limited by the limit. 654 | */ 655 | async getAllKeys() { 656 | let range = this.toRange(); 657 | // Handle reverse with cursor 658 | if (this._direction === 'prev') { 659 | let results: Key[] = []; 660 | if (this._limit <= 0) return Promise.resolve(results); 661 | await this.cursor(cursor => results.push(cursor.key as Key), 'readonly', true); 662 | return results; 663 | } 664 | 665 | let store = this.store._transStore('readonly'); 666 | let source = this.index ? store.index(this.index) : store; 667 | return requestToPromise(source.getAllKeys(range, this._limit), null, this); 668 | } 669 | 670 | /** 671 | * Gets a single object, the first one matching the criteria 672 | */ 673 | async get() { 674 | const rows = await this.limit(1).getAll(); 675 | return this.store.revive(rows[0]); 676 | } 677 | 678 | /** 679 | * Gets a single key, the first one matching the criteria 680 | */ 681 | async getKey() { 682 | // Allow reverse() to be used by going through the getAllKeys method 683 | const rows = await this.limit(1).getAllKeys(); 684 | return rows[0]; 685 | } 686 | 687 | /** 688 | * Gets the count of the objects matching the criteria 689 | */ 690 | count() { 691 | let range = this.toRange(); 692 | let store = this.store._transStore('readonly'); 693 | let source = this.index ? store.index(this.index) : store; 694 | return requestToPromise(source.count(range), null, this); 695 | } 696 | 697 | /** 698 | * Deletes all the objects within this range. 699 | */ 700 | async deleteAll() { 701 | // Uses a cursor to delete so that each item can get a change event dispatched for it 702 | const promises = await this.map(async (_, cursor, trans) => { 703 | let key = cursor.primaryKey as Key; 704 | await requestToPromise(cursor.delete(), trans, this); 705 | this.dispatchChange(null, key); 706 | }, 'readwrite'); 707 | await Promise.all(promises); 708 | } 709 | 710 | /** 711 | * Uses a cursor to efficiently iterate over the objects matching the range calling the iterator for each one. 712 | */ 713 | cursor(iterator: CursorIterator, mode: IDBTransactionMode = 'readonly', keyCursor = false) { 714 | return new Promise((resolve, reject) => { 715 | let range = this.toRange(); 716 | let store = this.store._transStore(mode); 717 | let source = this.index ? store.index(this.index) : store; 718 | let method: 'openKeyCursor' | 'openCursor' = keyCursor ? 'openKeyCursor' : 'openCursor'; 719 | let request = source[method](range, this._direction); 720 | let count = 0; 721 | request.onsuccess = () => { 722 | var cursor = request.result; 723 | if (cursor) { 724 | let result = iterator(cursor, store.transaction); 725 | if (this._limit !== undefined && ++count >= this._limit) result = false; 726 | if (result !== false) cursor.continue(); 727 | else resolve(); 728 | } else { 729 | resolve(); 730 | } 731 | }; 732 | request.onerror = errorHandler(reject, this); 733 | }); 734 | } 735 | 736 | /** 737 | * Updates objects using a cursor to update many objects at once matching the range. 738 | */ 739 | async update(iterator: StoreIterator) { 740 | const promises = await this.map(async (object, cursor, trans) => { 741 | let key = cursor.primaryKey as Key; 742 | let newValue = iterator(object, cursor, trans); 743 | if (newValue === null) { 744 | await requestToPromise(cursor.delete(), trans, this); 745 | this.dispatchChange(null, key); 746 | } else if (newValue !== undefined) { 747 | await requestToPromise(cursor.update(this.store.store(newValue)), trans, this); 748 | this.dispatchChange(newValue, key); 749 | } 750 | }, 'readwrite'); 751 | await Promise.all(promises); 752 | } 753 | 754 | /** 755 | * Uses a cursor to efficiently iterate over the objects matching the range calling the iterator for each one. 756 | */ 757 | forEach(iterator: StoreIterator, mode: IDBTransactionMode = 'readonly') { 758 | return this.cursor((cursor, trans) => { 759 | iterator(this.store.revive((cursor as any).value as Type), cursor, trans); 760 | }, mode); 761 | } 762 | 763 | /** 764 | * Uses a cursor to efficiently iterate over the objects matching the range calling the iterator for each one and 765 | * returning the results of the iterator in an array. 766 | */ 767 | async map(iterator: StoreIterator, mode: IDBTransactionMode = 'readonly') { 768 | let results: R[] = []; 769 | await this.forEach((object, cursor, trans) => { 770 | results.push(iterator(object, cursor, trans)); 771 | }, mode); 772 | return results; 773 | } 774 | } 775 | 776 | function requestToPromise( 777 | request: any, 778 | transaction?: IDBTransaction, 779 | errorDispatcher?: ErrorDispatcher 780 | ): Promise { 781 | return new Promise((resolve, reject) => { 782 | if (transaction) { 783 | let promise = transactionPromise.get(transaction); 784 | if (!promise) { 785 | promise = requestToPromise(transaction, null, errorDispatcher); 786 | } 787 | promise = promise.then( 788 | () => resolve(request.result), 789 | err => { 790 | let requestError; 791 | try { 792 | requestError = request.error; 793 | } catch (e) {} 794 | reject(requestError || err); 795 | return Promise.reject(err); 796 | } 797 | ); 798 | transactionPromise.set(transaction, promise); 799 | } else if (request.onsuccess === null) { 800 | request.onsuccess = successHandler(resolve); 801 | } 802 | if (request.oncomplete === null) request.oncomplete = successHandler(resolve); 803 | if (request.onerror === null) request.onerror = errorHandler(reject, errorDispatcher); 804 | if (request.onabort === null) request.onabort = () => reject(new Error('Abort')); 805 | }); 806 | } 807 | 808 | function successHandler(resolve: (result: any) => void) { 809 | return (event: Event) => resolve((event.target as any).result); 810 | } 811 | 812 | function errorHandler(reject: (err: Error) => void, errorDispatcher?: ErrorDispatcher) { 813 | return (event: Event) => { 814 | reject((event.target as any).error); 815 | errorDispatcher && errorDispatcher.dispatchError((event.target as any).error); 816 | }; 817 | } 818 | 819 | function safariMultiStoreFix(storeNames: DOMStringList | string[]) { 820 | return storeNames.length === 1 ? storeNames[0] : Array.from(storeNames); 821 | } 822 | 823 | function upgrade( 824 | oldVersion: number, 825 | transaction: IDBTransaction, 826 | db: IDBDatabase, 827 | versionMap: Record, 828 | versionHandlers: Record, 829 | browserbase: Browserbase 830 | ) { 831 | let versions; 832 | // Optimization for creating a new database. A version 0 may be used as the "latest" version to create a database. 833 | if (oldVersion === 0 && versionMap[0]) { 834 | versions = [0]; 835 | } else { 836 | versions = Object.keys(versionMap) 837 | .map(key => parseInt(key)) 838 | .filter(version => version > oldVersion) 839 | .sort((a, b) => a - b); 840 | } 841 | 842 | versions.forEach(version => { 843 | const stores = versionMap[version]; 844 | Object.keys(stores).forEach(name => { 845 | const indexesString = stores[name]; 846 | upgradeStore(db, transaction, name, indexesString); 847 | }); 848 | 849 | const handler = versionHandlers[version]; 850 | if (handler) { 851 | // Ensure browserbase has the current object stores for working with in the handler 852 | addStores(browserbase, db, transaction); 853 | handler(oldVersion, transaction); 854 | } 855 | }); 856 | } 857 | 858 | function upgradeStore(db: IDBDatabase, transaction: IDBTransaction, storeName: string, indexesString: string) { 859 | const indexes = indexesString && indexesString.split(/\s*,\s*/); 860 | let store; 861 | 862 | if (indexesString === null) { 863 | db.deleteObjectStore(storeName); 864 | return; 865 | } 866 | 867 | if (db.objectStoreNames.contains(storeName)) { 868 | store = transaction.objectStore(storeName); 869 | } else { 870 | store = db.createObjectStore(storeName, getStoreOptions(indexes.shift())); 871 | } 872 | 873 | indexes.forEach(name => { 874 | if (!name) return; 875 | if (name[0] === '-') return store.deleteIndex(name.replace(/^-[&*]?/, '')); 876 | 877 | let options: IDBIndexParameters = {}; 878 | 879 | name = name.replace(/\s/g, ''); 880 | if (name[0] === '&') { 881 | name = name.slice(1); 882 | options.unique = true; 883 | } else if (name[0] === '*') { 884 | name = name.slice(1); 885 | options.multiEntry = true; 886 | } 887 | let keyPath = name[0] === '[' ? name.replace(/^\[|\]$/g, '').split(/\+/) : name; 888 | store.createIndex(name, keyPath, options); 889 | }); 890 | } 891 | 892 | function onOpen(browserbase: Browserbase) { 893 | const db = browserbase.db; 894 | 895 | db.onversionchange = event => { 896 | const versionEvent = new Event('versionchange', { cancelable: true }); 897 | browserbase.dispatchEvent(versionEvent); 898 | if (!versionEvent.defaultPrevented) { 899 | if (event.newVersion > 0) { 900 | console.warn( 901 | `Another connection wants to upgrade database '${browserbase.name}'. Closing db now to resume the upgrade.` 902 | ); 903 | } else { 904 | console.warn( 905 | `Another connection wants to delete database '${browserbase.name}'. Closing db now to resume the delete request.` 906 | ); 907 | } 908 | browserbase.close(); 909 | } 910 | }; 911 | db.onclose = async () => { 912 | if (!browserbase._closed) { 913 | delete browserbase._opening; 914 | await browserbase.open(); 915 | browserbase.dispatchEvent(new Event('recreated')); 916 | } 917 | }; 918 | db.onerror = event => browserbase.dispatchEvent(new ErrorEvent('error', { error: (event.target as any).error })); 919 | if (!browserbase.options.dontDispatch) { 920 | browserbase._channel = createChannel(browserbase); 921 | } 922 | 923 | // Store keyPath's for each store 924 | addStores(browserbase, db, db.transaction(safariMultiStoreFix(db.objectStoreNames), 'readonly')); 925 | } 926 | 927 | function createChannel(browserbase: Browserbase) { 928 | const channel = new BroadcastChannel(`browserbase/${browserbase.name}`); 929 | channel.onmessage = event => { 930 | try { 931 | const { path, obj } = event.data; 932 | const [storeName, key] = path.split('/'); 933 | const store = (browserbase.stores as ObjectStoreMap)[storeName]; 934 | if (store) { 935 | browserbase.dispatchChange(store, obj, key, 'remote'); 936 | } else { 937 | console.warn(`A change event came from another tab for store "${storeName}", but no such store exists.`); 938 | } 939 | } catch (err) { 940 | console.warn('Error parsing object change from browserbase:', err); 941 | } 942 | }; 943 | return browserbase._channel; 944 | } 945 | 946 | function postMessage(browserbase: Browserbase, message: any) { 947 | if (!browserbase._channel) return; 948 | try { 949 | browserbase._channel.postMessage(message); 950 | } catch (e) { 951 | // If the channel is closed, create a new one and try again 952 | if (e.name === 'InvalidStateError') { 953 | browserbase._channel = createChannel(browserbase); 954 | postMessage(browserbase, message); 955 | } 956 | } 957 | } 958 | 959 | function addStores(browserbase: Browserbase, db: IDBDatabase, transaction: IDBTransaction) { 960 | const names = db.objectStoreNames; 961 | for (let i = 0; i < names.length; i++) { 962 | const name = names[i]; 963 | (browserbase.stores as ObjectStoreMap)[name] = new ObjectStore( 964 | browserbase, 965 | name, 966 | transaction.objectStore(name).keyPath 967 | ); 968 | } 969 | } 970 | 971 | function onClose(browserbase: Browserbase) { 972 | if (browserbase._channel) browserbase._channel.close(); 973 | browserbase._channel = null; 974 | browserbase.db = null; 975 | browserbase.dispatchEvent(new Event('close')); 976 | } 977 | 978 | function getStoreOptions(keyString: string) { 979 | let keyPath: string | string[] = keyString.replace(/\s/g, ''); 980 | let storeOptions: IDBObjectStoreParameters = {}; 981 | if (keyPath.slice(0, 2) === '++') { 982 | keyPath = keyPath.replace('++', ''); 983 | storeOptions.autoIncrement = true; 984 | } else if (keyPath[0] === '[') { 985 | keyPath = keyPath.replace(/^\[|\]$/g, '').split(/\+/); 986 | } 987 | if (keyPath) storeOptions.keyPath = keyPath; 988 | return storeOptions; 989 | } 990 | -------------------------------------------------------------------------------- /src/TypeEventTarget.ts: -------------------------------------------------------------------------------- 1 | type EventMap = { 2 | [key in keyof T]: Event; 3 | }; 4 | 5 | export class TypedEventTarget> extends EventTarget { 6 | addEventListener( 7 | type: K, 8 | listener: (this: this, ev: T[K]) => any, 9 | options?: boolean | AddEventListenerOptions 10 | ): void; 11 | addEventListener( 12 | type: string, 13 | listener: EventListenerOrEventListenerObject, 14 | options?: boolean | AddEventListenerOptions 15 | ): void; 16 | addEventListener( 17 | type: string, 18 | listener: EventListenerOrEventListenerObject, 19 | options?: boolean | AddEventListenerOptions 20 | ): void { 21 | super.addEventListener(type, listener, options); 22 | } 23 | 24 | removeEventListener( 25 | type: K, 26 | listener: (this: this, ev: T[K]) => any, 27 | options?: boolean | EventListenerOptions 28 | ): void; 29 | removeEventListener( 30 | type: string, 31 | listener: EventListenerOrEventListenerObject, 32 | options?: boolean | EventListenerOptions 33 | ): void; 34 | removeEventListener( 35 | type: string, 36 | listener: EventListenerOrEventListenerObject, 37 | options?: boolean | EventListenerOptions 38 | ): void { 39 | super.removeEventListener(type, listener, options); 40 | } 41 | 42 | dispatchEvent(event: T[K]): boolean; 43 | dispatchEvent(event: Event): boolean; 44 | dispatchEvent(event: Event): boolean { 45 | return super.dispatchEvent(event); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Browserbase'; 2 | export * from './TypeEventTarget'; 3 | -------------------------------------------------------------------------------- /test/test-browserbase.js: -------------------------------------------------------------------------------- 1 | import Browserbase from '../src/browserbase'; 2 | 3 | 4 | describe('Browserbase', () => { 5 | let db; 6 | 7 | beforeEach(() => { 8 | db = new Browserbase('test'); 9 | }); 10 | 11 | afterEach(() => { 12 | db.close(); 13 | Browserbase.deleteDatabase('test'); 14 | }); 15 | 16 | it('should fail if no versions were set', () => { 17 | return db.open().then(() => { 18 | throw new Error('It opened just fine'); 19 | }, err => { 20 | // It caused an error as it should have 21 | }); 22 | }); 23 | 24 | it('should create a version with an object store', () => { 25 | db.version(1, { foo: 'bar' }); 26 | return db.open().then(() => { 27 | expect(db).to.have.property('foo'); 28 | }); 29 | }); 30 | 31 | it('should create a version with multiple object stores', () => { 32 | db.version(1, { foo: 'bar', bar: 'foo' }); 33 | return db.open().then(() => { 34 | expect(db).to.have.property('foo'); 35 | expect(db).to.have.property('bar'); 36 | }); 37 | }); 38 | 39 | it('should add onto existing versions', () => { 40 | db.version(1, { foo: 'bar', bar: 'foo' }); 41 | db.version(2, { foo: 'foobar' }); 42 | return db.open().then(() => { 43 | expect(db.db.transaction('foo').objectStore('foo').indexNames.contains('foobar')).to.be.true; 44 | }); 45 | }); 46 | 47 | it('should add onto existing versions which have already been created', () => { 48 | db.version(1, { foo: 'bar', bar: 'foo' }); 49 | return db.open().then(() => { 50 | expect(db.db.transaction('foo').objectStore('foo').indexNames.contains('foobar')).to.be.false; 51 | db.close(); 52 | db.version(2, { foo: 'foobar' }); 53 | return db.open().then(() => { 54 | expect(db.db.transaction('foo').objectStore('foo').indexNames.contains('foobar')).to.be.true; 55 | }); 56 | }); 57 | }); 58 | 59 | it('should support deleting indexes from previous versions', () => { 60 | db.version(1, { foo: 'bar', bar: 'foo' }); 61 | db.version(2, { foo: 'foobar' }); 62 | db.version(3, { foo: '-foobar' }); 63 | return db.open().then(() => { 64 | expect(db.db.transaction('foo').objectStore('foo').indexNames.contains('foobar')).to.be.false; 65 | }); 66 | }); 67 | 68 | it('should delete indexes from previous versions that already exist', () => { 69 | db.version(1, { foo: 'bar', bar: 'foo' }); 70 | db.version(2, { foo: 'foobar' }); 71 | return db.open().then(() => { 72 | expect(db.db.transaction('foo').objectStore('foo').indexNames.contains('foobar')).to.be.true; 73 | db.close(); 74 | db.version(3, { foo: '-foobar' }); 75 | return db.open().then(() => { 76 | expect(db.db.transaction('foo').objectStore('foo').indexNames.contains('foobar')).to.be.false; 77 | }); 78 | }); 79 | }); 80 | 81 | it('should add objects to the store', () => { 82 | db.version(1, { foo: 'key' }); 83 | return db.open().then(() => { 84 | return db.foo.add({ key: 'abc' }).then(() => { 85 | return db.foo.get('abc').then(obj => { 86 | expect(obj.key).to.equal('abc'); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | it('should fail to add objects that already exist', () => { 93 | db.version(1, { foo: 'key' }); 94 | return db.open().then(() => { 95 | return db.foo.add({ key: 'abc' }).then(() => { 96 | return db.foo.get('abc').then(obj => { 97 | expect(obj.key).to.equal('abc'); 98 | return db.foo.add({ key: 'abc' }).then(() => { 99 | throw new Error('Did not fail'); 100 | }, err => { 101 | // good, good 102 | expect(err.name).to.equal('ConstraintError'); 103 | }); 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | it('should add objects to the store in bulk', () => { 110 | db.version(1, { foo: 'key' }); 111 | return db.open().then(() => { 112 | return db.foo.bulkAdd([{ key: 'abc' }, { key: 'abcc' }]).then(() => { 113 | return db.foo.getAll().then(arr => { 114 | expect(arr).to.deep.equal([{ key: 'abc' }, { key: 'abcc' }]); 115 | }); 116 | }); 117 | }); 118 | }); 119 | 120 | it('should save objects to the store', () => { 121 | db.version(1, { foo: 'key' }); 122 | return db.open().then(() => { 123 | return db.foo.put({ key: 'abc' }).then(() => { 124 | return db.foo.get('abc').then(obj => { 125 | expect(obj.key).to.equal('abc'); 126 | return db.foo.put({ key: 'abc', test: true }).then(() => { 127 | return db.foo.get('abc').then(obj => { 128 | expect(obj.test).to.be.true; 129 | }); 130 | }); 131 | }); 132 | }); 133 | }); 134 | }); 135 | 136 | it('should save objects to the store in bulk', () => { 137 | db.version(1, { foo: 'key' }); 138 | return db.open().then(() => { 139 | return db.foo.bulkPut([{ key: 'abc' }, { key: 'abcc' }]).then(() => { 140 | return db.foo.getAll().then(arr => { 141 | expect(arr).to.deep.equal([{ key: 'abc' }, { key: 'abcc' }]); 142 | }); 143 | }); 144 | }); 145 | }); 146 | 147 | it('should delete objects from the store', () => { 148 | db.version(1, { foo: 'key' }); 149 | return db.open().then(() => { 150 | return db.foo.put({ key: 'abc' }).then(() => { 151 | return db.foo.get('abc').then(obj => { 152 | expect(obj.key).to.equal('abc'); 153 | return db.foo.delete('abc').then(() => { 154 | return db.foo.get('abc').then(obj => { 155 | expect(obj).to.be.undefined; 156 | }); 157 | }); 158 | }); 159 | }); 160 | }); 161 | }); 162 | 163 | it('should dispatch a change for add/put/delete', () => { 164 | let lastChange, lastKey; 165 | db.on('change', (storeName, obj, key) => { 166 | lastChange = obj; 167 | lastKey = key; 168 | }); 169 | 170 | db.version(1, { foo: 'key' }); 171 | return db.open().then(() => { 172 | return db.foo.put({ key: 'abc' }).then(() => { 173 | expect(lastChange).to.deep.equal({ key: 'abc' }); 174 | expect(lastKey).to.equal('abc'); 175 | return db.foo.delete('abc').then(() => { 176 | expect(lastChange).to.equal(null); 177 | expect(lastKey).to.equal('abc'); 178 | }); 179 | }); 180 | }); 181 | }); 182 | 183 | it('should allow one transaction for many puts', () => { 184 | db.version(1, { foo: 'key' }); 185 | return db.open().then(() => { 186 | expect(db.foo._transStore('readonly').transaction).to.not.equal(db._current); 187 | const trans = db.start(); 188 | trans.foo.put({ key: 'test1' }); 189 | trans.foo.put({ key: 'test2' }); 190 | trans.foo.put({ key: 'test3' }); 191 | expect(trans.foo._transStore('readonly').transaction).to.equal(trans._current); 192 | return trans.commit(); 193 | }); 194 | }); 195 | 196 | it('should not report success if the transaction fails', () => { 197 | db.version(1, { foo: 'key, &unique' }); 198 | let success1; 199 | let success2; 200 | 201 | return db.open().then(() => { 202 | const trans = db.start(); 203 | trans.foo.add({ key: 'test1' }).then(id => { 204 | success1 = true; 205 | }, err => { 206 | success1 = false; 207 | }); 208 | trans.foo.put({ key: 'test2', unique: 10 }).then(id => { 209 | success2 = true; 210 | }, err => { 211 | success2 = false; 212 | }); 213 | trans.foo.add({ key: 'test1' }); 214 | trans.foo.put({ key: 'test3', unique: 10 }); 215 | return trans.commit().catch(() => { 216 | expect(success1, 'add did not give an error').to.be.false; 217 | expect(success2, 'put did not give an error').to.be.false; 218 | }).then(() => { 219 | return db.foo.get('test1'); 220 | }).then(obj => { 221 | expect(obj).to.be.undefined; 222 | }); 223 | }); 224 | }); 225 | 226 | it('should not report to finish if the transaction fails', () => { 227 | db.version(1, { foo: 'key, &unique' }); 228 | let success = false; 229 | 230 | return db.open().then(() => { 231 | const trans = db.start(); 232 | trans.foo.add({ key: 'test1', unique: 10 }); 233 | trans.foo.add({ key: 'test2', unique: 11 }); 234 | trans.foo.add({ key: 'test3', unique: 12 }); 235 | return trans.commit().then(() => { 236 | db.foo.on('change', (obj, key, from) => { 237 | success = true; 238 | }); 239 | return db.foo.where('key').update(obj => { 240 | if (obj.key === 'test2') { 241 | obj.unique = 15; 242 | return obj; 243 | } else if (obj.key === 'test3') { 244 | obj.unique = 10; 245 | return obj; 246 | } 247 | }); 248 | }).catch(() => {}).then(() => { 249 | expect(success).to.be.false; 250 | return db.foo.getAll().then(res => { 251 | expect(res).to.deep.equal([ 252 | { key: 'test1', unique: 10 }, 253 | { key: 'test2', unique: 11 }, 254 | { key: 'test3', unique: 12 } 255 | ]); 256 | }); 257 | }); 258 | }); 259 | }); 260 | 261 | it('should get all objects', () => { 262 | db.version(1, { foo: 'key' }); 263 | return db.open().then(() => { 264 | const trans = db.start(); 265 | trans.foo.put({ key: 'test1' }); 266 | trans.foo.put({ key: 'test2' }); 267 | trans.foo.put({ key: 'test3' }); 268 | return trans.commit().then(() => { 269 | return db.foo.getAll().then(objects => { 270 | expect(objects).to.have.lengthOf(3); 271 | }); 272 | }); 273 | }); 274 | }); 275 | 276 | it('should set keyPath on the store', () => { 277 | db.version(1, { foo: 'key', bar: ', test', baz: '++id' }); 278 | return db.open().then(() => { 279 | expect(db.foo.keyPath).to.equal('key'); 280 | expect(db.bar.keyPath).to.equal(null); 281 | expect(db.baz.keyPath).to.equal('id'); 282 | }); 283 | }); 284 | 285 | it('should get a range of objects', () => { 286 | db.version(1, { foo: 'key' }); 287 | return db.open().then(() => { 288 | const trans = db.start(); 289 | trans.foo.put({ key: 'test1' }); 290 | trans.foo.put({ key: 'test2' }); 291 | trans.foo.put({ key: 'test3' }); 292 | trans.foo.put({ key: 'test4' }); 293 | trans.foo.put({ key: 'test5' }); 294 | trans.foo.put({ key: 'test6' }); 295 | return trans.commit().then(() => { 296 | return db.foo.where('key').startsAt('test2').endsBefore('test5').getAll().then(objects => { 297 | expect(objects).to.deep.equal([ 298 | { key: 'test2' }, 299 | { key: 'test3' }, 300 | { key: 'test4' }, 301 | ]); 302 | }); 303 | }); 304 | }); 305 | }); 306 | 307 | it('should get a range of objects with limit', () => { 308 | db.version(1, { foo: 'key' }); 309 | return db.open().then(() => { 310 | const trans = db.start(); 311 | trans.foo.put({ key: 'test1' }); 312 | trans.foo.put({ key: 'test2' }); 313 | trans.foo.put({ key: 'test3' }); 314 | trans.foo.put({ key: 'test4' }); 315 | trans.foo.put({ key: 'test5' }); 316 | trans.foo.put({ key: 'test6' }); 317 | return trans.commit().then(() => { 318 | return db.foo.where('key').startsAt('test2').endsBefore('test5').limit(2).getAll().then(objects => { 319 | expect(objects).to.deep.equal([ 320 | { key: 'test2' }, 321 | { key: 'test3' }, 322 | ]); 323 | }); 324 | }); 325 | }); 326 | }); 327 | 328 | it('should cursor over a range of objects with limit', () => { 329 | db.version(1, { foo: 'key' }); 330 | return db.open().then(() => { 331 | const trans = db.start(); 332 | let objects = []; 333 | trans.foo.put({ key: 'test1' }); 334 | trans.foo.put({ key: 'test2' }); 335 | trans.foo.put({ key: 'test3' }); 336 | trans.foo.put({ key: 'test4' }); 337 | trans.foo.put({ key: 'test5' }); 338 | trans.foo.put({ key: 'test6' }); 339 | return trans.commit().then(() => { 340 | return db.foo.where('key').startsAt('test2').endsBefore('test5').limit(2) 341 | .forEach(obj => objects.push(obj)).then(() => { 342 | expect(objects).to.deep.equal([ 343 | { key: 'test2' }, 344 | { key: 'test3' }, 345 | ]); 346 | }); 347 | }); 348 | }); 349 | }); 350 | 351 | it('should delete a range of objects', () => { 352 | db.version(1, { foo: 'key' }); 353 | return db.open().then(() => { 354 | const trans = db.start(); 355 | trans.foo.put({ key: 'test1' }); 356 | trans.foo.put({ key: 'test2' }); 357 | trans.foo.put({ key: 'test3' }); 358 | trans.foo.put({ key: 'test4' }); 359 | trans.foo.put({ key: 'test5' }); 360 | trans.foo.put({ key: 'test6' }); 361 | return trans.commit().then(() => { 362 | return db.foo.where('key').startsAfter('test2').endsAt('test5').deleteAll().then(() => db.foo.getAll()).then(objects => { 363 | expect(objects).to.deep.equal([ 364 | { key: 'test1' }, 365 | { key: 'test2' }, 366 | { key: 'test6' }, 367 | ]); 368 | }); 369 | }); 370 | }); 371 | }); 372 | 373 | it('should update a range of objects', () => { 374 | db.version(1, { foo: 'key' }); 375 | return db.open().then(() => { 376 | const trans = db.start(); 377 | trans.foo.put({ key: 'test1' }); 378 | trans.foo.put({ key: 'test2' }); 379 | trans.foo.put({ key: 'test3' }); 380 | trans.foo.put({ key: 'test4' }); 381 | trans.foo.put({ key: 'test5' }); 382 | trans.foo.put({ key: 'test6' }); 383 | return trans.commit().then(() => { 384 | return db.foo.where('key').startsAt('test2').endsAt('test5').update(obj => { 385 | if (obj.key === 'test2') return null; 386 | if (obj.key === 'test5') return; 387 | obj.name = obj.key; 388 | return obj; 389 | }).then(() => db.foo.getAll()).then(objects => { 390 | expect(objects).to.deep.equal([ 391 | { key: 'test1' }, 392 | { key: 'test3', name: 'test3' }, 393 | { key: 'test4', name: 'test4' }, 394 | { key: 'test5' }, 395 | { key: 'test6' }, 396 | ]); 397 | }); 398 | }); 399 | }); 400 | }); 401 | 402 | it('should handle compound indexes', () => { 403 | db.version(1, { foo: 'key, [name + date]' }); 404 | 405 | return db.open().then(() => { 406 | const trans = db.start(); 407 | trans.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 408 | trans.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 409 | trans.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 410 | trans.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 411 | trans.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 412 | return trans.commit().then(() => { 413 | return db.foo.where('[name+ date]').getAll(); 414 | }).then(objs => { 415 | expect(objs).to.deep.equal([ 416 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 417 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 418 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 419 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 420 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 421 | ]); 422 | 423 | return db.foo.where('[name+date]').startsAt(['a', new Date('2005-01-01')]).reverse().getAll(); 424 | }).then(objs => { 425 | expect(objs).to.deep.equal([ 426 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 427 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 428 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 429 | ]); 430 | }); 431 | }); 432 | }); 433 | 434 | it('should handle compound primary keys', () => { 435 | db.version(1, { foo: '[name + date]' }); 436 | 437 | return db.open().then(() => { 438 | const trans = db.start(); 439 | trans.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 440 | trans.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 441 | trans.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 442 | trans.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 443 | trans.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 444 | return trans.commit().then(() => { 445 | return db.foo.where().getAll(); 446 | }).then(objs => { 447 | expect(objs).to.deep.equal([ 448 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 449 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 450 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 451 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 452 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 453 | ]); 454 | 455 | return db.foo.where().startsAt(['a', new Date('2005-01-01')]).reverse().getAll(); 456 | }).then(objs => { 457 | expect(objs).to.deep.equal([ 458 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 459 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 460 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 461 | ]); 462 | }); 463 | }); 464 | }); 465 | 466 | it('should handle compound indexes with startsWith', () => { 467 | db.version(1, { foo: 'key, [name + date]' }); 468 | 469 | return db.open().then(() => { 470 | const trans = db.start(); 471 | trans.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 472 | trans.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 473 | trans.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 474 | trans.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 475 | trans.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 476 | return trans.commit().then(() => { 477 | return db.foo.where('[name+ date]').startsWith(['a']).getAll(); 478 | }).then(objs => { 479 | expect(objs).to.deep.equal([ 480 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 481 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 482 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 483 | ]); 484 | 485 | return db.foo.where('[name+date]').startsWith(['b']).reverse().getAll(); 486 | }).then(objs => { 487 | expect(objs).to.deep.equal([ 488 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 489 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 490 | ]); 491 | }); 492 | }); 493 | }); 494 | 495 | }); 496 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "moduleResolution": "node", 6 | "module": "ESNext", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "target": "ES2022", 10 | "allowJs": true, 11 | "declaration": true, 12 | "baseUrl": "src", 13 | "sourceMap": true, 14 | "lib": ["ES2022", "WebWorker"] 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "src/**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | }, 7 | }); 8 | --------------------------------------------------------------------------------