├── 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 | ### Iteration with Async Generators 124 | 125 | Browserbase provides two ways to iterate over database records: 126 | 127 | 1. **Traditional callback-based cursor** (for backward compatibility): 128 | ```js 129 | await db.stores.foo.where('name').startsWith('J').cursor((cursor, transaction) => { 130 | console.log('Found:', cursor.value); 131 | // Return false to stop iteration 132 | }); 133 | ``` 134 | 135 | 2. **New async generator with `entries()`** (recommended for modern code): 136 | ```js 137 | // Iterate using for-await-of loop 138 | for await (const { value, key, cursor, transaction } of db.stores.foo.where('name').startsWith('J').entries()) { 139 | console.log('Found:', value, 'with key:', key); 140 | 141 | // You can break early to stop iteration 142 | if (someCondition) break; 143 | 144 | // Access the underlying cursor if needed 145 | console.log('Cursor direction:', cursor.direction); 146 | } 147 | 148 | // Works with all Where methods (ranges, limits, etc.) 149 | for await (const { value, key } of db.stores.foo.where('age').startsAt(18).limit(10).entries()) { 150 | console.log('Adult:', value); 151 | } 152 | ``` 153 | 154 | The `entries()` method returns an async generator that yields objects with: 155 | - `value`: The database record (already processed through any `revive` function) 156 | - `key`: The record's key 157 | - `cursor`: The underlying IDBCursor for advanced operations 158 | - `transaction`: The IDBTransaction for this operation 159 | 160 | Benefits of the async generator approach: 161 | - More readable with `for-await-of` syntax 162 | - Easy to break early or use control flow 163 | - Integrates well with modern async/await patterns 164 | - Maintains backward compatibility with existing `cursor()` method 165 | 166 | TODO complete documentation 167 | -------------------------------------------------------------------------------- /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.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "browserbase", 9 | "version": "3.1.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "fake-indexeddb": "^6.0.1", 13 | "jsdom": "^26.1.0", 14 | "typescript": "^5.8.3", 15 | "vitest": "^3.2.3" 16 | } 17 | }, 18 | "node_modules/@asamuzakjp/css-color": { 19 | "version": "3.2.0", 20 | "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", 21 | "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", 22 | "dev": true, 23 | "license": "MIT", 24 | "dependencies": { 25 | "@csstools/css-calc": "^2.1.3", 26 | "@csstools/css-color-parser": "^3.0.9", 27 | "@csstools/css-parser-algorithms": "^3.0.4", 28 | "@csstools/css-tokenizer": "^3.0.3", 29 | "lru-cache": "^10.4.3" 30 | } 31 | }, 32 | "node_modules/@csstools/color-helpers": { 33 | "version": "5.0.2", 34 | "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", 35 | "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", 36 | "dev": true, 37 | "funding": [ 38 | { 39 | "type": "github", 40 | "url": "https://github.com/sponsors/csstools" 41 | }, 42 | { 43 | "type": "opencollective", 44 | "url": "https://opencollective.com/csstools" 45 | } 46 | ], 47 | "license": "MIT-0", 48 | "engines": { 49 | "node": ">=18" 50 | } 51 | }, 52 | "node_modules/@csstools/css-calc": { 53 | "version": "2.1.4", 54 | "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", 55 | "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", 56 | "dev": true, 57 | "funding": [ 58 | { 59 | "type": "github", 60 | "url": "https://github.com/sponsors/csstools" 61 | }, 62 | { 63 | "type": "opencollective", 64 | "url": "https://opencollective.com/csstools" 65 | } 66 | ], 67 | "license": "MIT", 68 | "engines": { 69 | "node": ">=18" 70 | }, 71 | "peerDependencies": { 72 | "@csstools/css-parser-algorithms": "^3.0.5", 73 | "@csstools/css-tokenizer": "^3.0.4" 74 | } 75 | }, 76 | "node_modules/@csstools/css-color-parser": { 77 | "version": "3.0.10", 78 | "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", 79 | "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", 80 | "dev": true, 81 | "funding": [ 82 | { 83 | "type": "github", 84 | "url": "https://github.com/sponsors/csstools" 85 | }, 86 | { 87 | "type": "opencollective", 88 | "url": "https://opencollective.com/csstools" 89 | } 90 | ], 91 | "license": "MIT", 92 | "dependencies": { 93 | "@csstools/color-helpers": "^5.0.2", 94 | "@csstools/css-calc": "^2.1.4" 95 | }, 96 | "engines": { 97 | "node": ">=18" 98 | }, 99 | "peerDependencies": { 100 | "@csstools/css-parser-algorithms": "^3.0.5", 101 | "@csstools/css-tokenizer": "^3.0.4" 102 | } 103 | }, 104 | "node_modules/@csstools/css-parser-algorithms": { 105 | "version": "3.0.5", 106 | "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", 107 | "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", 108 | "dev": true, 109 | "funding": [ 110 | { 111 | "type": "github", 112 | "url": "https://github.com/sponsors/csstools" 113 | }, 114 | { 115 | "type": "opencollective", 116 | "url": "https://opencollective.com/csstools" 117 | } 118 | ], 119 | "license": "MIT", 120 | "engines": { 121 | "node": ">=18" 122 | }, 123 | "peerDependencies": { 124 | "@csstools/css-tokenizer": "^3.0.4" 125 | } 126 | }, 127 | "node_modules/@csstools/css-tokenizer": { 128 | "version": "3.0.4", 129 | "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", 130 | "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", 131 | "dev": true, 132 | "funding": [ 133 | { 134 | "type": "github", 135 | "url": "https://github.com/sponsors/csstools" 136 | }, 137 | { 138 | "type": "opencollective", 139 | "url": "https://opencollective.com/csstools" 140 | } 141 | ], 142 | "license": "MIT", 143 | "engines": { 144 | "node": ">=18" 145 | } 146 | }, 147 | "node_modules/@esbuild/aix-ppc64": { 148 | "version": "0.25.5", 149 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", 150 | "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", 151 | "cpu": [ 152 | "ppc64" 153 | ], 154 | "dev": true, 155 | "license": "MIT", 156 | "optional": true, 157 | "os": [ 158 | "aix" 159 | ], 160 | "engines": { 161 | "node": ">=18" 162 | } 163 | }, 164 | "node_modules/@esbuild/android-arm": { 165 | "version": "0.25.5", 166 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", 167 | "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", 168 | "cpu": [ 169 | "arm" 170 | ], 171 | "dev": true, 172 | "license": "MIT", 173 | "optional": true, 174 | "os": [ 175 | "android" 176 | ], 177 | "engines": { 178 | "node": ">=18" 179 | } 180 | }, 181 | "node_modules/@esbuild/android-arm64": { 182 | "version": "0.25.5", 183 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", 184 | "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", 185 | "cpu": [ 186 | "arm64" 187 | ], 188 | "dev": true, 189 | "license": "MIT", 190 | "optional": true, 191 | "os": [ 192 | "android" 193 | ], 194 | "engines": { 195 | "node": ">=18" 196 | } 197 | }, 198 | "node_modules/@esbuild/android-x64": { 199 | "version": "0.25.5", 200 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", 201 | "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", 202 | "cpu": [ 203 | "x64" 204 | ], 205 | "dev": true, 206 | "license": "MIT", 207 | "optional": true, 208 | "os": [ 209 | "android" 210 | ], 211 | "engines": { 212 | "node": ">=18" 213 | } 214 | }, 215 | "node_modules/@esbuild/darwin-arm64": { 216 | "version": "0.25.5", 217 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", 218 | "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", 219 | "cpu": [ 220 | "arm64" 221 | ], 222 | "dev": true, 223 | "license": "MIT", 224 | "optional": true, 225 | "os": [ 226 | "darwin" 227 | ], 228 | "engines": { 229 | "node": ">=18" 230 | } 231 | }, 232 | "node_modules/@esbuild/darwin-x64": { 233 | "version": "0.25.5", 234 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", 235 | "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", 236 | "cpu": [ 237 | "x64" 238 | ], 239 | "dev": true, 240 | "license": "MIT", 241 | "optional": true, 242 | "os": [ 243 | "darwin" 244 | ], 245 | "engines": { 246 | "node": ">=18" 247 | } 248 | }, 249 | "node_modules/@esbuild/freebsd-arm64": { 250 | "version": "0.25.5", 251 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", 252 | "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", 253 | "cpu": [ 254 | "arm64" 255 | ], 256 | "dev": true, 257 | "license": "MIT", 258 | "optional": true, 259 | "os": [ 260 | "freebsd" 261 | ], 262 | "engines": { 263 | "node": ">=18" 264 | } 265 | }, 266 | "node_modules/@esbuild/freebsd-x64": { 267 | "version": "0.25.5", 268 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", 269 | "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", 270 | "cpu": [ 271 | "x64" 272 | ], 273 | "dev": true, 274 | "license": "MIT", 275 | "optional": true, 276 | "os": [ 277 | "freebsd" 278 | ], 279 | "engines": { 280 | "node": ">=18" 281 | } 282 | }, 283 | "node_modules/@esbuild/linux-arm": { 284 | "version": "0.25.5", 285 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", 286 | "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", 287 | "cpu": [ 288 | "arm" 289 | ], 290 | "dev": true, 291 | "license": "MIT", 292 | "optional": true, 293 | "os": [ 294 | "linux" 295 | ], 296 | "engines": { 297 | "node": ">=18" 298 | } 299 | }, 300 | "node_modules/@esbuild/linux-arm64": { 301 | "version": "0.25.5", 302 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", 303 | "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", 304 | "cpu": [ 305 | "arm64" 306 | ], 307 | "dev": true, 308 | "license": "MIT", 309 | "optional": true, 310 | "os": [ 311 | "linux" 312 | ], 313 | "engines": { 314 | "node": ">=18" 315 | } 316 | }, 317 | "node_modules/@esbuild/linux-ia32": { 318 | "version": "0.25.5", 319 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", 320 | "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", 321 | "cpu": [ 322 | "ia32" 323 | ], 324 | "dev": true, 325 | "license": "MIT", 326 | "optional": true, 327 | "os": [ 328 | "linux" 329 | ], 330 | "engines": { 331 | "node": ">=18" 332 | } 333 | }, 334 | "node_modules/@esbuild/linux-loong64": { 335 | "version": "0.25.5", 336 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", 337 | "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", 338 | "cpu": [ 339 | "loong64" 340 | ], 341 | "dev": true, 342 | "license": "MIT", 343 | "optional": true, 344 | "os": [ 345 | "linux" 346 | ], 347 | "engines": { 348 | "node": ">=18" 349 | } 350 | }, 351 | "node_modules/@esbuild/linux-mips64el": { 352 | "version": "0.25.5", 353 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", 354 | "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", 355 | "cpu": [ 356 | "mips64el" 357 | ], 358 | "dev": true, 359 | "license": "MIT", 360 | "optional": true, 361 | "os": [ 362 | "linux" 363 | ], 364 | "engines": { 365 | "node": ">=18" 366 | } 367 | }, 368 | "node_modules/@esbuild/linux-ppc64": { 369 | "version": "0.25.5", 370 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", 371 | "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", 372 | "cpu": [ 373 | "ppc64" 374 | ], 375 | "dev": true, 376 | "license": "MIT", 377 | "optional": true, 378 | "os": [ 379 | "linux" 380 | ], 381 | "engines": { 382 | "node": ">=18" 383 | } 384 | }, 385 | "node_modules/@esbuild/linux-riscv64": { 386 | "version": "0.25.5", 387 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", 388 | "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", 389 | "cpu": [ 390 | "riscv64" 391 | ], 392 | "dev": true, 393 | "license": "MIT", 394 | "optional": true, 395 | "os": [ 396 | "linux" 397 | ], 398 | "engines": { 399 | "node": ">=18" 400 | } 401 | }, 402 | "node_modules/@esbuild/linux-s390x": { 403 | "version": "0.25.5", 404 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", 405 | "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", 406 | "cpu": [ 407 | "s390x" 408 | ], 409 | "dev": true, 410 | "license": "MIT", 411 | "optional": true, 412 | "os": [ 413 | "linux" 414 | ], 415 | "engines": { 416 | "node": ">=18" 417 | } 418 | }, 419 | "node_modules/@esbuild/linux-x64": { 420 | "version": "0.25.5", 421 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", 422 | "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", 423 | "cpu": [ 424 | "x64" 425 | ], 426 | "dev": true, 427 | "license": "MIT", 428 | "optional": true, 429 | "os": [ 430 | "linux" 431 | ], 432 | "engines": { 433 | "node": ">=18" 434 | } 435 | }, 436 | "node_modules/@esbuild/netbsd-arm64": { 437 | "version": "0.25.5", 438 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", 439 | "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", 440 | "cpu": [ 441 | "arm64" 442 | ], 443 | "dev": true, 444 | "license": "MIT", 445 | "optional": true, 446 | "os": [ 447 | "netbsd" 448 | ], 449 | "engines": { 450 | "node": ">=18" 451 | } 452 | }, 453 | "node_modules/@esbuild/netbsd-x64": { 454 | "version": "0.25.5", 455 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", 456 | "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", 457 | "cpu": [ 458 | "x64" 459 | ], 460 | "dev": true, 461 | "license": "MIT", 462 | "optional": true, 463 | "os": [ 464 | "netbsd" 465 | ], 466 | "engines": { 467 | "node": ">=18" 468 | } 469 | }, 470 | "node_modules/@esbuild/openbsd-arm64": { 471 | "version": "0.25.5", 472 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", 473 | "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", 474 | "cpu": [ 475 | "arm64" 476 | ], 477 | "dev": true, 478 | "license": "MIT", 479 | "optional": true, 480 | "os": [ 481 | "openbsd" 482 | ], 483 | "engines": { 484 | "node": ">=18" 485 | } 486 | }, 487 | "node_modules/@esbuild/openbsd-x64": { 488 | "version": "0.25.5", 489 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", 490 | "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", 491 | "cpu": [ 492 | "x64" 493 | ], 494 | "dev": true, 495 | "license": "MIT", 496 | "optional": true, 497 | "os": [ 498 | "openbsd" 499 | ], 500 | "engines": { 501 | "node": ">=18" 502 | } 503 | }, 504 | "node_modules/@esbuild/sunos-x64": { 505 | "version": "0.25.5", 506 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", 507 | "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", 508 | "cpu": [ 509 | "x64" 510 | ], 511 | "dev": true, 512 | "license": "MIT", 513 | "optional": true, 514 | "os": [ 515 | "sunos" 516 | ], 517 | "engines": { 518 | "node": ">=18" 519 | } 520 | }, 521 | "node_modules/@esbuild/win32-arm64": { 522 | "version": "0.25.5", 523 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", 524 | "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", 525 | "cpu": [ 526 | "arm64" 527 | ], 528 | "dev": true, 529 | "license": "MIT", 530 | "optional": true, 531 | "os": [ 532 | "win32" 533 | ], 534 | "engines": { 535 | "node": ">=18" 536 | } 537 | }, 538 | "node_modules/@esbuild/win32-ia32": { 539 | "version": "0.25.5", 540 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", 541 | "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", 542 | "cpu": [ 543 | "ia32" 544 | ], 545 | "dev": true, 546 | "license": "MIT", 547 | "optional": true, 548 | "os": [ 549 | "win32" 550 | ], 551 | "engines": { 552 | "node": ">=18" 553 | } 554 | }, 555 | "node_modules/@esbuild/win32-x64": { 556 | "version": "0.25.5", 557 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", 558 | "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", 559 | "cpu": [ 560 | "x64" 561 | ], 562 | "dev": true, 563 | "license": "MIT", 564 | "optional": true, 565 | "os": [ 566 | "win32" 567 | ], 568 | "engines": { 569 | "node": ">=18" 570 | } 571 | }, 572 | "node_modules/@jridgewell/sourcemap-codec": { 573 | "version": "1.5.0", 574 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 575 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 576 | "dev": true, 577 | "license": "MIT" 578 | }, 579 | "node_modules/@rollup/rollup-android-arm-eabi": { 580 | "version": "4.42.0", 581 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz", 582 | "integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==", 583 | "cpu": [ 584 | "arm" 585 | ], 586 | "dev": true, 587 | "license": "MIT", 588 | "optional": true, 589 | "os": [ 590 | "android" 591 | ] 592 | }, 593 | "node_modules/@rollup/rollup-android-arm64": { 594 | "version": "4.42.0", 595 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz", 596 | "integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==", 597 | "cpu": [ 598 | "arm64" 599 | ], 600 | "dev": true, 601 | "license": "MIT", 602 | "optional": true, 603 | "os": [ 604 | "android" 605 | ] 606 | }, 607 | "node_modules/@rollup/rollup-darwin-arm64": { 608 | "version": "4.42.0", 609 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz", 610 | "integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==", 611 | "cpu": [ 612 | "arm64" 613 | ], 614 | "dev": true, 615 | "license": "MIT", 616 | "optional": true, 617 | "os": [ 618 | "darwin" 619 | ] 620 | }, 621 | "node_modules/@rollup/rollup-darwin-x64": { 622 | "version": "4.42.0", 623 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz", 624 | "integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==", 625 | "cpu": [ 626 | "x64" 627 | ], 628 | "dev": true, 629 | "license": "MIT", 630 | "optional": true, 631 | "os": [ 632 | "darwin" 633 | ] 634 | }, 635 | "node_modules/@rollup/rollup-freebsd-arm64": { 636 | "version": "4.42.0", 637 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz", 638 | "integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==", 639 | "cpu": [ 640 | "arm64" 641 | ], 642 | "dev": true, 643 | "license": "MIT", 644 | "optional": true, 645 | "os": [ 646 | "freebsd" 647 | ] 648 | }, 649 | "node_modules/@rollup/rollup-freebsd-x64": { 650 | "version": "4.42.0", 651 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz", 652 | "integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==", 653 | "cpu": [ 654 | "x64" 655 | ], 656 | "dev": true, 657 | "license": "MIT", 658 | "optional": true, 659 | "os": [ 660 | "freebsd" 661 | ] 662 | }, 663 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 664 | "version": "4.42.0", 665 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz", 666 | "integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==", 667 | "cpu": [ 668 | "arm" 669 | ], 670 | "dev": true, 671 | "license": "MIT", 672 | "optional": true, 673 | "os": [ 674 | "linux" 675 | ] 676 | }, 677 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 678 | "version": "4.42.0", 679 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz", 680 | "integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==", 681 | "cpu": [ 682 | "arm" 683 | ], 684 | "dev": true, 685 | "license": "MIT", 686 | "optional": true, 687 | "os": [ 688 | "linux" 689 | ] 690 | }, 691 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 692 | "version": "4.42.0", 693 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz", 694 | "integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==", 695 | "cpu": [ 696 | "arm64" 697 | ], 698 | "dev": true, 699 | "license": "MIT", 700 | "optional": true, 701 | "os": [ 702 | "linux" 703 | ] 704 | }, 705 | "node_modules/@rollup/rollup-linux-arm64-musl": { 706 | "version": "4.42.0", 707 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz", 708 | "integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==", 709 | "cpu": [ 710 | "arm64" 711 | ], 712 | "dev": true, 713 | "license": "MIT", 714 | "optional": true, 715 | "os": [ 716 | "linux" 717 | ] 718 | }, 719 | "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 720 | "version": "4.42.0", 721 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz", 722 | "integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==", 723 | "cpu": [ 724 | "loong64" 725 | ], 726 | "dev": true, 727 | "license": "MIT", 728 | "optional": true, 729 | "os": [ 730 | "linux" 731 | ] 732 | }, 733 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 734 | "version": "4.42.0", 735 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz", 736 | "integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==", 737 | "cpu": [ 738 | "ppc64" 739 | ], 740 | "dev": true, 741 | "license": "MIT", 742 | "optional": true, 743 | "os": [ 744 | "linux" 745 | ] 746 | }, 747 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 748 | "version": "4.42.0", 749 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz", 750 | "integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==", 751 | "cpu": [ 752 | "riscv64" 753 | ], 754 | "dev": true, 755 | "license": "MIT", 756 | "optional": true, 757 | "os": [ 758 | "linux" 759 | ] 760 | }, 761 | "node_modules/@rollup/rollup-linux-riscv64-musl": { 762 | "version": "4.42.0", 763 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz", 764 | "integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==", 765 | "cpu": [ 766 | "riscv64" 767 | ], 768 | "dev": true, 769 | "license": "MIT", 770 | "optional": true, 771 | "os": [ 772 | "linux" 773 | ] 774 | }, 775 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 776 | "version": "4.42.0", 777 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz", 778 | "integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==", 779 | "cpu": [ 780 | "s390x" 781 | ], 782 | "dev": true, 783 | "license": "MIT", 784 | "optional": true, 785 | "os": [ 786 | "linux" 787 | ] 788 | }, 789 | "node_modules/@rollup/rollup-linux-x64-gnu": { 790 | "version": "4.42.0", 791 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz", 792 | "integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==", 793 | "cpu": [ 794 | "x64" 795 | ], 796 | "dev": true, 797 | "license": "MIT", 798 | "optional": true, 799 | "os": [ 800 | "linux" 801 | ] 802 | }, 803 | "node_modules/@rollup/rollup-linux-x64-musl": { 804 | "version": "4.42.0", 805 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz", 806 | "integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==", 807 | "cpu": [ 808 | "x64" 809 | ], 810 | "dev": true, 811 | "license": "MIT", 812 | "optional": true, 813 | "os": [ 814 | "linux" 815 | ] 816 | }, 817 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 818 | "version": "4.42.0", 819 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz", 820 | "integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==", 821 | "cpu": [ 822 | "arm64" 823 | ], 824 | "dev": true, 825 | "license": "MIT", 826 | "optional": true, 827 | "os": [ 828 | "win32" 829 | ] 830 | }, 831 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 832 | "version": "4.42.0", 833 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz", 834 | "integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==", 835 | "cpu": [ 836 | "ia32" 837 | ], 838 | "dev": true, 839 | "license": "MIT", 840 | "optional": true, 841 | "os": [ 842 | "win32" 843 | ] 844 | }, 845 | "node_modules/@rollup/rollup-win32-x64-msvc": { 846 | "version": "4.42.0", 847 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz", 848 | "integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==", 849 | "cpu": [ 850 | "x64" 851 | ], 852 | "dev": true, 853 | "license": "MIT", 854 | "optional": true, 855 | "os": [ 856 | "win32" 857 | ] 858 | }, 859 | "node_modules/@types/chai": { 860 | "version": "5.2.2", 861 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", 862 | "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", 863 | "dev": true, 864 | "license": "MIT", 865 | "dependencies": { 866 | "@types/deep-eql": "*" 867 | } 868 | }, 869 | "node_modules/@types/deep-eql": { 870 | "version": "4.0.2", 871 | "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 872 | "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 873 | "dev": true, 874 | "license": "MIT" 875 | }, 876 | "node_modules/@types/estree": { 877 | "version": "1.0.8", 878 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 879 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 880 | "dev": true, 881 | "license": "MIT" 882 | }, 883 | "node_modules/@vitest/expect": { 884 | "version": "3.2.3", 885 | "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", 886 | "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", 887 | "dev": true, 888 | "license": "MIT", 889 | "dependencies": { 890 | "@types/chai": "^5.2.2", 891 | "@vitest/spy": "3.2.3", 892 | "@vitest/utils": "3.2.3", 893 | "chai": "^5.2.0", 894 | "tinyrainbow": "^2.0.0" 895 | }, 896 | "funding": { 897 | "url": "https://opencollective.com/vitest" 898 | } 899 | }, 900 | "node_modules/@vitest/mocker": { 901 | "version": "3.2.3", 902 | "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", 903 | "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", 904 | "dev": true, 905 | "license": "MIT", 906 | "dependencies": { 907 | "@vitest/spy": "3.2.3", 908 | "estree-walker": "^3.0.3", 909 | "magic-string": "^0.30.17" 910 | }, 911 | "funding": { 912 | "url": "https://opencollective.com/vitest" 913 | }, 914 | "peerDependencies": { 915 | "msw": "^2.4.9", 916 | "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" 917 | }, 918 | "peerDependenciesMeta": { 919 | "msw": { 920 | "optional": true 921 | }, 922 | "vite": { 923 | "optional": true 924 | } 925 | } 926 | }, 927 | "node_modules/@vitest/pretty-format": { 928 | "version": "3.2.3", 929 | "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", 930 | "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", 931 | "dev": true, 932 | "license": "MIT", 933 | "dependencies": { 934 | "tinyrainbow": "^2.0.0" 935 | }, 936 | "funding": { 937 | "url": "https://opencollective.com/vitest" 938 | } 939 | }, 940 | "node_modules/@vitest/runner": { 941 | "version": "3.2.3", 942 | "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", 943 | "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", 944 | "dev": true, 945 | "license": "MIT", 946 | "dependencies": { 947 | "@vitest/utils": "3.2.3", 948 | "pathe": "^2.0.3", 949 | "strip-literal": "^3.0.0" 950 | }, 951 | "funding": { 952 | "url": "https://opencollective.com/vitest" 953 | } 954 | }, 955 | "node_modules/@vitest/snapshot": { 956 | "version": "3.2.3", 957 | "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", 958 | "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", 959 | "dev": true, 960 | "license": "MIT", 961 | "dependencies": { 962 | "@vitest/pretty-format": "3.2.3", 963 | "magic-string": "^0.30.17", 964 | "pathe": "^2.0.3" 965 | }, 966 | "funding": { 967 | "url": "https://opencollective.com/vitest" 968 | } 969 | }, 970 | "node_modules/@vitest/spy": { 971 | "version": "3.2.3", 972 | "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", 973 | "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", 974 | "dev": true, 975 | "license": "MIT", 976 | "dependencies": { 977 | "tinyspy": "^4.0.3" 978 | }, 979 | "funding": { 980 | "url": "https://opencollective.com/vitest" 981 | } 982 | }, 983 | "node_modules/@vitest/utils": { 984 | "version": "3.2.3", 985 | "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", 986 | "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", 987 | "dev": true, 988 | "license": "MIT", 989 | "dependencies": { 990 | "@vitest/pretty-format": "3.2.3", 991 | "loupe": "^3.1.3", 992 | "tinyrainbow": "^2.0.0" 993 | }, 994 | "funding": { 995 | "url": "https://opencollective.com/vitest" 996 | } 997 | }, 998 | "node_modules/agent-base": { 999 | "version": "7.1.3", 1000 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", 1001 | "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", 1002 | "dev": true, 1003 | "license": "MIT", 1004 | "engines": { 1005 | "node": ">= 14" 1006 | } 1007 | }, 1008 | "node_modules/assertion-error": { 1009 | "version": "2.0.1", 1010 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1011 | "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1012 | "dev": true, 1013 | "license": "MIT", 1014 | "engines": { 1015 | "node": ">=12" 1016 | } 1017 | }, 1018 | "node_modules/cac": { 1019 | "version": "6.7.14", 1020 | "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 1021 | "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 1022 | "dev": true, 1023 | "license": "MIT", 1024 | "engines": { 1025 | "node": ">=8" 1026 | } 1027 | }, 1028 | "node_modules/chai": { 1029 | "version": "5.2.0", 1030 | "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 1031 | "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 1032 | "dev": true, 1033 | "license": "MIT", 1034 | "dependencies": { 1035 | "assertion-error": "^2.0.1", 1036 | "check-error": "^2.1.1", 1037 | "deep-eql": "^5.0.1", 1038 | "loupe": "^3.1.0", 1039 | "pathval": "^2.0.0" 1040 | }, 1041 | "engines": { 1042 | "node": ">=12" 1043 | } 1044 | }, 1045 | "node_modules/check-error": { 1046 | "version": "2.1.1", 1047 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 1048 | "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 1049 | "dev": true, 1050 | "license": "MIT", 1051 | "engines": { 1052 | "node": ">= 16" 1053 | } 1054 | }, 1055 | "node_modules/cssstyle": { 1056 | "version": "4.4.0", 1057 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.4.0.tgz", 1058 | "integrity": "sha512-W0Y2HOXlPkb2yaKrCVRjinYKciu/qSLEmK0K9mcfDei3zwlnHFEHAs/Du3cIRwPqY+J4JsiBzUjoHyc8RsJ03A==", 1059 | "dev": true, 1060 | "license": "MIT", 1061 | "dependencies": { 1062 | "@asamuzakjp/css-color": "^3.2.0", 1063 | "rrweb-cssom": "^0.8.0" 1064 | }, 1065 | "engines": { 1066 | "node": ">=18" 1067 | } 1068 | }, 1069 | "node_modules/data-urls": { 1070 | "version": "5.0.0", 1071 | "dev": true, 1072 | "license": "MIT", 1073 | "dependencies": { 1074 | "whatwg-mimetype": "^4.0.0", 1075 | "whatwg-url": "^14.0.0" 1076 | }, 1077 | "engines": { 1078 | "node": ">=18" 1079 | } 1080 | }, 1081 | "node_modules/debug": { 1082 | "version": "4.4.1", 1083 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 1084 | "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 1085 | "dev": true, 1086 | "license": "MIT", 1087 | "dependencies": { 1088 | "ms": "^2.1.3" 1089 | }, 1090 | "engines": { 1091 | "node": ">=6.0" 1092 | }, 1093 | "peerDependenciesMeta": { 1094 | "supports-color": { 1095 | "optional": true 1096 | } 1097 | } 1098 | }, 1099 | "node_modules/decimal.js": { 1100 | "version": "10.5.0", 1101 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", 1102 | "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", 1103 | "dev": true, 1104 | "license": "MIT" 1105 | }, 1106 | "node_modules/deep-eql": { 1107 | "version": "5.0.2", 1108 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 1109 | "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 1110 | "dev": true, 1111 | "license": "MIT", 1112 | "engines": { 1113 | "node": ">=6" 1114 | } 1115 | }, 1116 | "node_modules/entities": { 1117 | "version": "6.0.1", 1118 | "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", 1119 | "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", 1120 | "dev": true, 1121 | "license": "BSD-2-Clause", 1122 | "engines": { 1123 | "node": ">=0.12" 1124 | }, 1125 | "funding": { 1126 | "url": "https://github.com/fb55/entities?sponsor=1" 1127 | } 1128 | }, 1129 | "node_modules/es-module-lexer": { 1130 | "version": "1.7.0", 1131 | "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1132 | "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1133 | "dev": true, 1134 | "license": "MIT" 1135 | }, 1136 | "node_modules/esbuild": { 1137 | "version": "0.25.5", 1138 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", 1139 | "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", 1140 | "dev": true, 1141 | "hasInstallScript": true, 1142 | "license": "MIT", 1143 | "bin": { 1144 | "esbuild": "bin/esbuild" 1145 | }, 1146 | "engines": { 1147 | "node": ">=18" 1148 | }, 1149 | "optionalDependencies": { 1150 | "@esbuild/aix-ppc64": "0.25.5", 1151 | "@esbuild/android-arm": "0.25.5", 1152 | "@esbuild/android-arm64": "0.25.5", 1153 | "@esbuild/android-x64": "0.25.5", 1154 | "@esbuild/darwin-arm64": "0.25.5", 1155 | "@esbuild/darwin-x64": "0.25.5", 1156 | "@esbuild/freebsd-arm64": "0.25.5", 1157 | "@esbuild/freebsd-x64": "0.25.5", 1158 | "@esbuild/linux-arm": "0.25.5", 1159 | "@esbuild/linux-arm64": "0.25.5", 1160 | "@esbuild/linux-ia32": "0.25.5", 1161 | "@esbuild/linux-loong64": "0.25.5", 1162 | "@esbuild/linux-mips64el": "0.25.5", 1163 | "@esbuild/linux-ppc64": "0.25.5", 1164 | "@esbuild/linux-riscv64": "0.25.5", 1165 | "@esbuild/linux-s390x": "0.25.5", 1166 | "@esbuild/linux-x64": "0.25.5", 1167 | "@esbuild/netbsd-arm64": "0.25.5", 1168 | "@esbuild/netbsd-x64": "0.25.5", 1169 | "@esbuild/openbsd-arm64": "0.25.5", 1170 | "@esbuild/openbsd-x64": "0.25.5", 1171 | "@esbuild/sunos-x64": "0.25.5", 1172 | "@esbuild/win32-arm64": "0.25.5", 1173 | "@esbuild/win32-ia32": "0.25.5", 1174 | "@esbuild/win32-x64": "0.25.5" 1175 | } 1176 | }, 1177 | "node_modules/estree-walker": { 1178 | "version": "3.0.3", 1179 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1180 | "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1181 | "dev": true, 1182 | "license": "MIT", 1183 | "dependencies": { 1184 | "@types/estree": "^1.0.0" 1185 | } 1186 | }, 1187 | "node_modules/expect-type": { 1188 | "version": "1.2.1", 1189 | "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", 1190 | "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", 1191 | "dev": true, 1192 | "license": "Apache-2.0", 1193 | "engines": { 1194 | "node": ">=12.0.0" 1195 | } 1196 | }, 1197 | "node_modules/fake-indexeddb": { 1198 | "version": "6.0.1", 1199 | "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.1.tgz", 1200 | "integrity": "sha512-He2AjQGHe46svIFq5+L2Nx/eHDTI1oKgoevBP+TthnjymXiKkeJQ3+ITeWey99Y5+2OaPFbI1qEsx/5RsGtWnQ==", 1201 | "dev": true, 1202 | "license": "Apache-2.0", 1203 | "engines": { 1204 | "node": ">=18" 1205 | } 1206 | }, 1207 | "node_modules/fdir": { 1208 | "version": "6.4.5", 1209 | "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", 1210 | "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", 1211 | "dev": true, 1212 | "license": "MIT", 1213 | "peerDependencies": { 1214 | "picomatch": "^3 || ^4" 1215 | }, 1216 | "peerDependenciesMeta": { 1217 | "picomatch": { 1218 | "optional": true 1219 | } 1220 | } 1221 | }, 1222 | "node_modules/fsevents": { 1223 | "version": "2.3.3", 1224 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1225 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1226 | "dev": true, 1227 | "hasInstallScript": true, 1228 | "license": "MIT", 1229 | "optional": true, 1230 | "os": [ 1231 | "darwin" 1232 | ], 1233 | "engines": { 1234 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1235 | } 1236 | }, 1237 | "node_modules/html-encoding-sniffer": { 1238 | "version": "4.0.0", 1239 | "dev": true, 1240 | "license": "MIT", 1241 | "dependencies": { 1242 | "whatwg-encoding": "^3.1.1" 1243 | }, 1244 | "engines": { 1245 | "node": ">=18" 1246 | } 1247 | }, 1248 | "node_modules/http-proxy-agent": { 1249 | "version": "7.0.2", 1250 | "dev": true, 1251 | "license": "MIT", 1252 | "dependencies": { 1253 | "agent-base": "^7.1.0", 1254 | "debug": "^4.3.4" 1255 | }, 1256 | "engines": { 1257 | "node": ">= 14" 1258 | } 1259 | }, 1260 | "node_modules/https-proxy-agent": { 1261 | "version": "7.0.6", 1262 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", 1263 | "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", 1264 | "dev": true, 1265 | "license": "MIT", 1266 | "dependencies": { 1267 | "agent-base": "^7.1.2", 1268 | "debug": "4" 1269 | }, 1270 | "engines": { 1271 | "node": ">= 14" 1272 | } 1273 | }, 1274 | "node_modules/iconv-lite": { 1275 | "version": "0.6.3", 1276 | "dev": true, 1277 | "license": "MIT", 1278 | "dependencies": { 1279 | "safer-buffer": ">= 2.1.2 < 3.0.0" 1280 | }, 1281 | "engines": { 1282 | "node": ">=0.10.0" 1283 | } 1284 | }, 1285 | "node_modules/is-potential-custom-element-name": { 1286 | "version": "1.0.1", 1287 | "dev": true, 1288 | "license": "MIT" 1289 | }, 1290 | "node_modules/js-tokens": { 1291 | "version": "9.0.1", 1292 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", 1293 | "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", 1294 | "dev": true, 1295 | "license": "MIT" 1296 | }, 1297 | "node_modules/jsdom": { 1298 | "version": "26.1.0", 1299 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", 1300 | "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", 1301 | "dev": true, 1302 | "license": "MIT", 1303 | "dependencies": { 1304 | "cssstyle": "^4.2.1", 1305 | "data-urls": "^5.0.0", 1306 | "decimal.js": "^10.5.0", 1307 | "html-encoding-sniffer": "^4.0.0", 1308 | "http-proxy-agent": "^7.0.2", 1309 | "https-proxy-agent": "^7.0.6", 1310 | "is-potential-custom-element-name": "^1.0.1", 1311 | "nwsapi": "^2.2.16", 1312 | "parse5": "^7.2.1", 1313 | "rrweb-cssom": "^0.8.0", 1314 | "saxes": "^6.0.0", 1315 | "symbol-tree": "^3.2.4", 1316 | "tough-cookie": "^5.1.1", 1317 | "w3c-xmlserializer": "^5.0.0", 1318 | "webidl-conversions": "^7.0.0", 1319 | "whatwg-encoding": "^3.1.1", 1320 | "whatwg-mimetype": "^4.0.0", 1321 | "whatwg-url": "^14.1.1", 1322 | "ws": "^8.18.0", 1323 | "xml-name-validator": "^5.0.0" 1324 | }, 1325 | "engines": { 1326 | "node": ">=18" 1327 | }, 1328 | "peerDependencies": { 1329 | "canvas": "^3.0.0" 1330 | }, 1331 | "peerDependenciesMeta": { 1332 | "canvas": { 1333 | "optional": true 1334 | } 1335 | } 1336 | }, 1337 | "node_modules/loupe": { 1338 | "version": "3.1.3", 1339 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", 1340 | "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", 1341 | "dev": true, 1342 | "license": "MIT" 1343 | }, 1344 | "node_modules/lru-cache": { 1345 | "version": "10.4.3", 1346 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 1347 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 1348 | "dev": true, 1349 | "license": "ISC" 1350 | }, 1351 | "node_modules/magic-string": { 1352 | "version": "0.30.17", 1353 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 1354 | "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 1355 | "dev": true, 1356 | "license": "MIT", 1357 | "dependencies": { 1358 | "@jridgewell/sourcemap-codec": "^1.5.0" 1359 | } 1360 | }, 1361 | "node_modules/ms": { 1362 | "version": "2.1.3", 1363 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1364 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1365 | "dev": true, 1366 | "license": "MIT" 1367 | }, 1368 | "node_modules/nanoid": { 1369 | "version": "3.3.11", 1370 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1371 | "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1372 | "dev": true, 1373 | "funding": [ 1374 | { 1375 | "type": "github", 1376 | "url": "https://github.com/sponsors/ai" 1377 | } 1378 | ], 1379 | "license": "MIT", 1380 | "bin": { 1381 | "nanoid": "bin/nanoid.cjs" 1382 | }, 1383 | "engines": { 1384 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1385 | } 1386 | }, 1387 | "node_modules/nwsapi": { 1388 | "version": "2.2.20", 1389 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", 1390 | "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", 1391 | "dev": true, 1392 | "license": "MIT" 1393 | }, 1394 | "node_modules/parse5": { 1395 | "version": "7.3.0", 1396 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", 1397 | "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", 1398 | "dev": true, 1399 | "license": "MIT", 1400 | "dependencies": { 1401 | "entities": "^6.0.0" 1402 | }, 1403 | "funding": { 1404 | "url": "https://github.com/inikulin/parse5?sponsor=1" 1405 | } 1406 | }, 1407 | "node_modules/pathe": { 1408 | "version": "2.0.3", 1409 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 1410 | "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 1411 | "dev": true, 1412 | "license": "MIT" 1413 | }, 1414 | "node_modules/pathval": { 1415 | "version": "2.0.0", 1416 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 1417 | "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 1418 | "dev": true, 1419 | "license": "MIT", 1420 | "engines": { 1421 | "node": ">= 14.16" 1422 | } 1423 | }, 1424 | "node_modules/picocolors": { 1425 | "version": "1.1.1", 1426 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1427 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1428 | "dev": true, 1429 | "license": "ISC" 1430 | }, 1431 | "node_modules/picomatch": { 1432 | "version": "4.0.2", 1433 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 1434 | "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 1435 | "dev": true, 1436 | "license": "MIT", 1437 | "engines": { 1438 | "node": ">=12" 1439 | }, 1440 | "funding": { 1441 | "url": "https://github.com/sponsors/jonschlinkert" 1442 | } 1443 | }, 1444 | "node_modules/postcss": { 1445 | "version": "8.5.4", 1446 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", 1447 | "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", 1448 | "dev": true, 1449 | "funding": [ 1450 | { 1451 | "type": "opencollective", 1452 | "url": "https://opencollective.com/postcss/" 1453 | }, 1454 | { 1455 | "type": "tidelift", 1456 | "url": "https://tidelift.com/funding/github/npm/postcss" 1457 | }, 1458 | { 1459 | "type": "github", 1460 | "url": "https://github.com/sponsors/ai" 1461 | } 1462 | ], 1463 | "license": "MIT", 1464 | "dependencies": { 1465 | "nanoid": "^3.3.11", 1466 | "picocolors": "^1.1.1", 1467 | "source-map-js": "^1.2.1" 1468 | }, 1469 | "engines": { 1470 | "node": "^10 || ^12 || >=14" 1471 | } 1472 | }, 1473 | "node_modules/punycode": { 1474 | "version": "2.3.1", 1475 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 1476 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 1477 | "dev": true, 1478 | "license": "MIT", 1479 | "engines": { 1480 | "node": ">=6" 1481 | } 1482 | }, 1483 | "node_modules/rollup": { 1484 | "version": "4.42.0", 1485 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz", 1486 | "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==", 1487 | "dev": true, 1488 | "license": "MIT", 1489 | "dependencies": { 1490 | "@types/estree": "1.0.7" 1491 | }, 1492 | "bin": { 1493 | "rollup": "dist/bin/rollup" 1494 | }, 1495 | "engines": { 1496 | "node": ">=18.0.0", 1497 | "npm": ">=8.0.0" 1498 | }, 1499 | "optionalDependencies": { 1500 | "@rollup/rollup-android-arm-eabi": "4.42.0", 1501 | "@rollup/rollup-android-arm64": "4.42.0", 1502 | "@rollup/rollup-darwin-arm64": "4.42.0", 1503 | "@rollup/rollup-darwin-x64": "4.42.0", 1504 | "@rollup/rollup-freebsd-arm64": "4.42.0", 1505 | "@rollup/rollup-freebsd-x64": "4.42.0", 1506 | "@rollup/rollup-linux-arm-gnueabihf": "4.42.0", 1507 | "@rollup/rollup-linux-arm-musleabihf": "4.42.0", 1508 | "@rollup/rollup-linux-arm64-gnu": "4.42.0", 1509 | "@rollup/rollup-linux-arm64-musl": "4.42.0", 1510 | "@rollup/rollup-linux-loongarch64-gnu": "4.42.0", 1511 | "@rollup/rollup-linux-powerpc64le-gnu": "4.42.0", 1512 | "@rollup/rollup-linux-riscv64-gnu": "4.42.0", 1513 | "@rollup/rollup-linux-riscv64-musl": "4.42.0", 1514 | "@rollup/rollup-linux-s390x-gnu": "4.42.0", 1515 | "@rollup/rollup-linux-x64-gnu": "4.42.0", 1516 | "@rollup/rollup-linux-x64-musl": "4.42.0", 1517 | "@rollup/rollup-win32-arm64-msvc": "4.42.0", 1518 | "@rollup/rollup-win32-ia32-msvc": "4.42.0", 1519 | "@rollup/rollup-win32-x64-msvc": "4.42.0", 1520 | "fsevents": "~2.3.2" 1521 | } 1522 | }, 1523 | "node_modules/rollup/node_modules/@types/estree": { 1524 | "version": "1.0.7", 1525 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 1526 | "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 1527 | "dev": true, 1528 | "license": "MIT" 1529 | }, 1530 | "node_modules/rrweb-cssom": { 1531 | "version": "0.8.0", 1532 | "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", 1533 | "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", 1534 | "dev": true, 1535 | "license": "MIT" 1536 | }, 1537 | "node_modules/safer-buffer": { 1538 | "version": "2.1.2", 1539 | "dev": true, 1540 | "license": "MIT" 1541 | }, 1542 | "node_modules/saxes": { 1543 | "version": "6.0.0", 1544 | "dev": true, 1545 | "license": "ISC", 1546 | "dependencies": { 1547 | "xmlchars": "^2.2.0" 1548 | }, 1549 | "engines": { 1550 | "node": ">=v12.22.7" 1551 | } 1552 | }, 1553 | "node_modules/siginfo": { 1554 | "version": "2.0.0", 1555 | "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 1556 | "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 1557 | "dev": true, 1558 | "license": "ISC" 1559 | }, 1560 | "node_modules/source-map-js": { 1561 | "version": "1.2.1", 1562 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1563 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1564 | "dev": true, 1565 | "license": "BSD-3-Clause", 1566 | "engines": { 1567 | "node": ">=0.10.0" 1568 | } 1569 | }, 1570 | "node_modules/stackback": { 1571 | "version": "0.0.2", 1572 | "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 1573 | "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 1574 | "dev": true, 1575 | "license": "MIT" 1576 | }, 1577 | "node_modules/std-env": { 1578 | "version": "3.9.0", 1579 | "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", 1580 | "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", 1581 | "dev": true, 1582 | "license": "MIT" 1583 | }, 1584 | "node_modules/strip-literal": { 1585 | "version": "3.0.0", 1586 | "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", 1587 | "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", 1588 | "dev": true, 1589 | "license": "MIT", 1590 | "dependencies": { 1591 | "js-tokens": "^9.0.1" 1592 | }, 1593 | "funding": { 1594 | "url": "https://github.com/sponsors/antfu" 1595 | } 1596 | }, 1597 | "node_modules/symbol-tree": { 1598 | "version": "3.2.4", 1599 | "dev": true, 1600 | "license": "MIT" 1601 | }, 1602 | "node_modules/tinybench": { 1603 | "version": "2.9.0", 1604 | "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 1605 | "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 1606 | "dev": true, 1607 | "license": "MIT" 1608 | }, 1609 | "node_modules/tinyexec": { 1610 | "version": "0.3.2", 1611 | "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 1612 | "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 1613 | "dev": true, 1614 | "license": "MIT" 1615 | }, 1616 | "node_modules/tinyglobby": { 1617 | "version": "0.2.14", 1618 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", 1619 | "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", 1620 | "dev": true, 1621 | "license": "MIT", 1622 | "dependencies": { 1623 | "fdir": "^6.4.4", 1624 | "picomatch": "^4.0.2" 1625 | }, 1626 | "engines": { 1627 | "node": ">=12.0.0" 1628 | }, 1629 | "funding": { 1630 | "url": "https://github.com/sponsors/SuperchupuDev" 1631 | } 1632 | }, 1633 | "node_modules/tinypool": { 1634 | "version": "1.1.0", 1635 | "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", 1636 | "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", 1637 | "dev": true, 1638 | "license": "MIT", 1639 | "engines": { 1640 | "node": "^18.0.0 || >=20.0.0" 1641 | } 1642 | }, 1643 | "node_modules/tinyrainbow": { 1644 | "version": "2.0.0", 1645 | "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", 1646 | "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", 1647 | "dev": true, 1648 | "license": "MIT", 1649 | "engines": { 1650 | "node": ">=14.0.0" 1651 | } 1652 | }, 1653 | "node_modules/tinyspy": { 1654 | "version": "4.0.3", 1655 | "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", 1656 | "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", 1657 | "dev": true, 1658 | "license": "MIT", 1659 | "engines": { 1660 | "node": ">=14.0.0" 1661 | } 1662 | }, 1663 | "node_modules/tldts": { 1664 | "version": "6.1.86", 1665 | "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", 1666 | "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", 1667 | "dev": true, 1668 | "license": "MIT", 1669 | "dependencies": { 1670 | "tldts-core": "^6.1.86" 1671 | }, 1672 | "bin": { 1673 | "tldts": "bin/cli.js" 1674 | } 1675 | }, 1676 | "node_modules/tldts-core": { 1677 | "version": "6.1.86", 1678 | "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", 1679 | "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", 1680 | "dev": true, 1681 | "license": "MIT" 1682 | }, 1683 | "node_modules/tough-cookie": { 1684 | "version": "5.1.2", 1685 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", 1686 | "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", 1687 | "dev": true, 1688 | "license": "BSD-3-Clause", 1689 | "dependencies": { 1690 | "tldts": "^6.1.32" 1691 | }, 1692 | "engines": { 1693 | "node": ">=16" 1694 | } 1695 | }, 1696 | "node_modules/tr46": { 1697 | "version": "5.1.1", 1698 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", 1699 | "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", 1700 | "dev": true, 1701 | "license": "MIT", 1702 | "dependencies": { 1703 | "punycode": "^2.3.1" 1704 | }, 1705 | "engines": { 1706 | "node": ">=18" 1707 | } 1708 | }, 1709 | "node_modules/typescript": { 1710 | "version": "5.8.3", 1711 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1712 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1713 | "dev": true, 1714 | "license": "Apache-2.0", 1715 | "bin": { 1716 | "tsc": "bin/tsc", 1717 | "tsserver": "bin/tsserver" 1718 | }, 1719 | "engines": { 1720 | "node": ">=14.17" 1721 | } 1722 | }, 1723 | "node_modules/vite": { 1724 | "version": "6.3.5", 1725 | "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", 1726 | "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", 1727 | "dev": true, 1728 | "license": "MIT", 1729 | "dependencies": { 1730 | "esbuild": "^0.25.0", 1731 | "fdir": "^6.4.4", 1732 | "picomatch": "^4.0.2", 1733 | "postcss": "^8.5.3", 1734 | "rollup": "^4.34.9", 1735 | "tinyglobby": "^0.2.13" 1736 | }, 1737 | "bin": { 1738 | "vite": "bin/vite.js" 1739 | }, 1740 | "engines": { 1741 | "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 1742 | }, 1743 | "funding": { 1744 | "url": "https://github.com/vitejs/vite?sponsor=1" 1745 | }, 1746 | "optionalDependencies": { 1747 | "fsevents": "~2.3.3" 1748 | }, 1749 | "peerDependencies": { 1750 | "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 1751 | "jiti": ">=1.21.0", 1752 | "less": "*", 1753 | "lightningcss": "^1.21.0", 1754 | "sass": "*", 1755 | "sass-embedded": "*", 1756 | "stylus": "*", 1757 | "sugarss": "*", 1758 | "terser": "^5.16.0", 1759 | "tsx": "^4.8.1", 1760 | "yaml": "^2.4.2" 1761 | }, 1762 | "peerDependenciesMeta": { 1763 | "@types/node": { 1764 | "optional": true 1765 | }, 1766 | "jiti": { 1767 | "optional": true 1768 | }, 1769 | "less": { 1770 | "optional": true 1771 | }, 1772 | "lightningcss": { 1773 | "optional": true 1774 | }, 1775 | "sass": { 1776 | "optional": true 1777 | }, 1778 | "sass-embedded": { 1779 | "optional": true 1780 | }, 1781 | "stylus": { 1782 | "optional": true 1783 | }, 1784 | "sugarss": { 1785 | "optional": true 1786 | }, 1787 | "terser": { 1788 | "optional": true 1789 | }, 1790 | "tsx": { 1791 | "optional": true 1792 | }, 1793 | "yaml": { 1794 | "optional": true 1795 | } 1796 | } 1797 | }, 1798 | "node_modules/vite-node": { 1799 | "version": "3.2.3", 1800 | "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", 1801 | "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", 1802 | "dev": true, 1803 | "license": "MIT", 1804 | "dependencies": { 1805 | "cac": "^6.7.14", 1806 | "debug": "^4.4.1", 1807 | "es-module-lexer": "^1.7.0", 1808 | "pathe": "^2.0.3", 1809 | "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" 1810 | }, 1811 | "bin": { 1812 | "vite-node": "vite-node.mjs" 1813 | }, 1814 | "engines": { 1815 | "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 1816 | }, 1817 | "funding": { 1818 | "url": "https://opencollective.com/vitest" 1819 | } 1820 | }, 1821 | "node_modules/vitest": { 1822 | "version": "3.2.3", 1823 | "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", 1824 | "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", 1825 | "dev": true, 1826 | "license": "MIT", 1827 | "dependencies": { 1828 | "@types/chai": "^5.2.2", 1829 | "@vitest/expect": "3.2.3", 1830 | "@vitest/mocker": "3.2.3", 1831 | "@vitest/pretty-format": "^3.2.3", 1832 | "@vitest/runner": "3.2.3", 1833 | "@vitest/snapshot": "3.2.3", 1834 | "@vitest/spy": "3.2.3", 1835 | "@vitest/utils": "3.2.3", 1836 | "chai": "^5.2.0", 1837 | "debug": "^4.4.1", 1838 | "expect-type": "^1.2.1", 1839 | "magic-string": "^0.30.17", 1840 | "pathe": "^2.0.3", 1841 | "picomatch": "^4.0.2", 1842 | "std-env": "^3.9.0", 1843 | "tinybench": "^2.9.0", 1844 | "tinyexec": "^0.3.2", 1845 | "tinyglobby": "^0.2.14", 1846 | "tinypool": "^1.1.0", 1847 | "tinyrainbow": "^2.0.0", 1848 | "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", 1849 | "vite-node": "3.2.3", 1850 | "why-is-node-running": "^2.3.0" 1851 | }, 1852 | "bin": { 1853 | "vitest": "vitest.mjs" 1854 | }, 1855 | "engines": { 1856 | "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 1857 | }, 1858 | "funding": { 1859 | "url": "https://opencollective.com/vitest" 1860 | }, 1861 | "peerDependencies": { 1862 | "@edge-runtime/vm": "*", 1863 | "@types/debug": "^4.1.12", 1864 | "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 1865 | "@vitest/browser": "3.2.3", 1866 | "@vitest/ui": "3.2.3", 1867 | "happy-dom": "*", 1868 | "jsdom": "*" 1869 | }, 1870 | "peerDependenciesMeta": { 1871 | "@edge-runtime/vm": { 1872 | "optional": true 1873 | }, 1874 | "@types/debug": { 1875 | "optional": true 1876 | }, 1877 | "@types/node": { 1878 | "optional": true 1879 | }, 1880 | "@vitest/browser": { 1881 | "optional": true 1882 | }, 1883 | "@vitest/ui": { 1884 | "optional": true 1885 | }, 1886 | "happy-dom": { 1887 | "optional": true 1888 | }, 1889 | "jsdom": { 1890 | "optional": true 1891 | } 1892 | } 1893 | }, 1894 | "node_modules/w3c-xmlserializer": { 1895 | "version": "5.0.0", 1896 | "dev": true, 1897 | "license": "MIT", 1898 | "dependencies": { 1899 | "xml-name-validator": "^5.0.0" 1900 | }, 1901 | "engines": { 1902 | "node": ">=18" 1903 | } 1904 | }, 1905 | "node_modules/webidl-conversions": { 1906 | "version": "7.0.0", 1907 | "dev": true, 1908 | "license": "BSD-2-Clause", 1909 | "engines": { 1910 | "node": ">=12" 1911 | } 1912 | }, 1913 | "node_modules/whatwg-encoding": { 1914 | "version": "3.1.1", 1915 | "dev": true, 1916 | "license": "MIT", 1917 | "dependencies": { 1918 | "iconv-lite": "0.6.3" 1919 | }, 1920 | "engines": { 1921 | "node": ">=18" 1922 | } 1923 | }, 1924 | "node_modules/whatwg-mimetype": { 1925 | "version": "4.0.0", 1926 | "dev": true, 1927 | "license": "MIT", 1928 | "engines": { 1929 | "node": ">=18" 1930 | } 1931 | }, 1932 | "node_modules/whatwg-url": { 1933 | "version": "14.2.0", 1934 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", 1935 | "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", 1936 | "dev": true, 1937 | "license": "MIT", 1938 | "dependencies": { 1939 | "tr46": "^5.1.0", 1940 | "webidl-conversions": "^7.0.0" 1941 | }, 1942 | "engines": { 1943 | "node": ">=18" 1944 | } 1945 | }, 1946 | "node_modules/why-is-node-running": { 1947 | "version": "2.3.0", 1948 | "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 1949 | "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 1950 | "dev": true, 1951 | "license": "MIT", 1952 | "dependencies": { 1953 | "siginfo": "^2.0.0", 1954 | "stackback": "0.0.2" 1955 | }, 1956 | "bin": { 1957 | "why-is-node-running": "cli.js" 1958 | }, 1959 | "engines": { 1960 | "node": ">=8" 1961 | } 1962 | }, 1963 | "node_modules/ws": { 1964 | "version": "8.18.0", 1965 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 1966 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 1967 | "dev": true, 1968 | "engines": { 1969 | "node": ">=10.0.0" 1970 | }, 1971 | "peerDependencies": { 1972 | "bufferutil": "^4.0.1", 1973 | "utf-8-validate": ">=5.0.2" 1974 | }, 1975 | "peerDependenciesMeta": { 1976 | "bufferutil": { 1977 | "optional": true 1978 | }, 1979 | "utf-8-validate": { 1980 | "optional": true 1981 | } 1982 | } 1983 | }, 1984 | "node_modules/xml-name-validator": { 1985 | "version": "5.0.0", 1986 | "dev": true, 1987 | "license": "Apache-2.0", 1988 | "engines": { 1989 | "node": ">=18" 1990 | } 1991 | }, 1992 | "node_modules/xmlchars": { 1993 | "version": "2.2.0", 1994 | "dev": true, 1995 | "license": "MIT" 1996 | } 1997 | } 1998 | } 1999 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserbase", 3 | "version": "3.1.0", 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 run", 10 | "tdd": "vitest", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dabblewriter/browserbase.git" 16 | }, 17 | "author": "Jacob Wright", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/dabblewriter/browserbase/issues" 21 | }, 22 | "homepage": "https://github.com/dabblewriter/browserbase#readme", 23 | "devDependencies": { 24 | "fake-indexeddb": "^6.0.1", 25 | "jsdom": "^26.1.0", 26 | "typescript": "^5.8.3", 27 | "vitest": "^3.2.3" 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 iterate over a range of objects with entries() async generator', 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 | 329 | const objects: any[] = []; 330 | const keys: any[] = []; 331 | for await (const { value, key, cursor } of db.stores.foo 332 | .where('key') 333 | .startsAt('test2') 334 | .endsBefore('test5') 335 | .limit(2) 336 | .entries()) { 337 | objects.push(value); 338 | keys.push(key); 339 | expect(cursor).toBeDefined(); 340 | expect(cursor.key).toEqual(key); 341 | } 342 | 343 | expect(objects).to.eql([{ key: 'test2' }, { key: 'test3' }]); 344 | expect(keys).to.eql(['test2', 'test3']); 345 | }); 346 | 347 | it('should delete a range of objects', async () => { 348 | db.version(1, { foo: 'key' }); 349 | await db.open(); 350 | const trans = db.start(); 351 | trans.stores.foo.put({ key: 'test1' }); 352 | trans.stores.foo.put({ key: 'test2' }); 353 | trans.stores.foo.put({ key: 'test3' }); 354 | trans.stores.foo.put({ key: 'test4' }); 355 | trans.stores.foo.put({ key: 'test5' }); 356 | trans.stores.foo.put({ key: 'test6' }); 357 | await trans.commit(); 358 | await db.stores.foo.where('key').startsAfter('test2').endsAt('test5').deleteAll(); 359 | const objects = await db.stores.foo.getAll(); 360 | expect(objects).to.eql([{ key: 'test1' }, { key: 'test2' }, { key: 'test6' }]); 361 | }); 362 | 363 | it('should update a range of objects', async () => { 364 | db.version(1, { foo: 'key' }); 365 | await db.open(); 366 | const trans = db.start(); 367 | trans.stores.foo.put({ key: 'test1' }); 368 | trans.stores.foo.put({ key: 'test2' }); 369 | trans.stores.foo.put({ key: 'test3' }); 370 | trans.stores.foo.put({ key: 'test4' }); 371 | trans.stores.foo.put({ key: 'test5' }); 372 | trans.stores.foo.put({ key: 'test6' }); 373 | await trans.commit(); 374 | await db.stores.foo 375 | .where('key') 376 | .startsAt('test2') 377 | .endsAt('test5') 378 | .update(obj_1 => { 379 | if (obj_1.key === 'test2') return null; 380 | if (obj_1.key === 'test5') return; 381 | obj_1.name = obj_1.key; 382 | return obj_1; 383 | }); 384 | const objects = await db.stores.foo.getAll(); 385 | expect(objects).to.eql([ 386 | { key: 'test1' }, 387 | { key: 'test3', name: 'test3' }, 388 | { key: 'test4', name: 'test4' }, 389 | { key: 'test5' }, 390 | { key: 'test6' }, 391 | ]); 392 | }); 393 | 394 | it('should handle compound indexes', async () => { 395 | db.version(1, { foo: 'key, [name + date]' }); 396 | 397 | await db.open(); 398 | const trans = db.start(); 399 | trans.stores.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 400 | trans.stores.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 401 | trans.stores.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 402 | trans.stores.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 403 | trans.stores.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 404 | await trans.commit(); 405 | const objs = await db.stores.foo.where('[name+ date]').getAll(); 406 | expect(objs).to.eql([ 407 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 408 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 409 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 410 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 411 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 412 | ]); 413 | const rows = await db.stores.foo 414 | .where('[name+date]') 415 | .startsAt(['a', new Date('2005-01-01')]) 416 | .reverse() 417 | .getAll(); 418 | expect(rows).to.eql([ 419 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 420 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 421 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 422 | ]); 423 | }); 424 | 425 | it('should handle compound primary keys', async () => { 426 | db.version(1, { foo: '[name + date]' }); 427 | 428 | await db.open(); 429 | const trans = db.start(); 430 | trans.stores.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 431 | trans.stores.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 432 | trans.stores.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 433 | trans.stores.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 434 | trans.stores.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 435 | await trans.commit(); 436 | const objs = await db.stores.foo.where().getAll(); 437 | expect(objs).to.eql([ 438 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 439 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 440 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 441 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 442 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 443 | ]); 444 | const objs_1 = await db.stores.foo 445 | .where() 446 | .startsAt(['a', new Date('2005-01-01')]) 447 | .reverse() 448 | .getAll(); 449 | expect(objs_1).to.eql([ 450 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 451 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 452 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 453 | ]); 454 | }); 455 | 456 | it('should handle compound indexes with startsWith', async () => { 457 | db.version(1, { foo: 'key, [name + date]' }); 458 | 459 | await db.open(); 460 | const trans = db.start(); 461 | trans.stores.foo.add({ key: 'test4', name: 'b', date: new Date('2010-01-01') }); 462 | trans.stores.foo.add({ key: 'test1', name: 'a', date: new Date('2004-01-01') }); 463 | trans.stores.foo.add({ key: 'test2', name: 'a', date: new Date('2005-01-01') }); 464 | trans.stores.foo.add({ key: 'test3', name: 'a', date: new Date('2002-01-01') }); 465 | trans.stores.foo.add({ key: 'test5', name: 'b', date: new Date('2000-01-01') }); 466 | await trans.commit(); 467 | const objs = await db.stores.foo.where('[name+ date]').startsWith(['a']).getAll(); 468 | expect(objs).to.eql([ 469 | { key: 'test3', name: 'a', date: new Date('2002-01-01') }, 470 | { key: 'test1', name: 'a', date: new Date('2004-01-01') }, 471 | { key: 'test2', name: 'a', date: new Date('2005-01-01') }, 472 | ]); 473 | const objs_1 = await db.stores.foo.where('[name+date]').startsWith(['b']).reverse().getAll(); 474 | expect(objs_1).to.eql([ 475 | { key: 'test4', name: 'b', date: new Date('2010-01-01') }, 476 | { key: 'test5', name: 'b', date: new Date('2000-01-01') }, 477 | ]); 478 | }); 479 | }); 480 | -------------------------------------------------------------------------------- /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 | * Returns an async generator that yields cursor entries for efficient iteration over objects matching the range. 712 | */ 713 | async* entries(mode: IDBTransactionMode = 'readonly', keyCursor = false): AsyncGenerator<{ cursor: IDBCursor; value: Type; key: Key; transaction: IDBTransaction }> { 714 | let range = this.toRange(); 715 | let store = this.store._transStore(mode); 716 | let source = this.index ? store.index(this.index) : store; 717 | let method: 'openKeyCursor' | 'openCursor' = keyCursor ? 'openKeyCursor' : 'openCursor'; 718 | let request = source[method](range, this._direction); 719 | let count = 0; 720 | 721 | while (true) { 722 | const cursor = await new Promise((resolve, reject) => { 723 | request.onsuccess = () => resolve(request.result); 724 | request.onerror = errorHandler(reject, this); 725 | }); 726 | 727 | if (!cursor || (this._limit !== undefined && count >= this._limit)) { 728 | break; 729 | } 730 | 731 | const value = keyCursor ? undefined : this.store.revive((cursor as any).value as Type); 732 | 733 | yield { 734 | cursor, 735 | value, 736 | key: cursor.key as Key, 737 | transaction: store.transaction 738 | }; 739 | 740 | count++; 741 | cursor.continue(); 742 | } 743 | } 744 | 745 | /** 746 | * Uses a cursor to efficiently iterate over the objects matching the range calling the iterator for each one. 747 | */ 748 | cursor(iterator: CursorIterator, mode: IDBTransactionMode = 'readonly', keyCursor = false) { 749 | return new Promise((resolve, reject) => { 750 | let range = this.toRange(); 751 | let store = this.store._transStore(mode); 752 | let source = this.index ? store.index(this.index) : store; 753 | let method: 'openKeyCursor' | 'openCursor' = keyCursor ? 'openKeyCursor' : 'openCursor'; 754 | let request = source[method](range, this._direction); 755 | let count = 0; 756 | request.onsuccess = () => { 757 | var cursor = request.result; 758 | if (cursor) { 759 | let result = iterator(cursor, store.transaction); 760 | if (this._limit !== undefined && ++count >= this._limit) result = false; 761 | if (result !== false) cursor.continue(); 762 | else resolve(); 763 | } else { 764 | resolve(); 765 | } 766 | }; 767 | request.onerror = errorHandler(reject, this); 768 | }); 769 | } 770 | 771 | /** 772 | * Updates objects using a cursor to update many objects at once matching the range. 773 | */ 774 | async update(iterator: StoreIterator) { 775 | const promises = await this.map(async (object, cursor, trans) => { 776 | let key = cursor.primaryKey as Key; 777 | let newValue = iterator(object, cursor, trans); 778 | if (newValue === null) { 779 | await requestToPromise(cursor.delete(), trans, this); 780 | this.dispatchChange(null, key); 781 | } else if (newValue !== undefined) { 782 | await requestToPromise(cursor.update(this.store.store(newValue)), trans, this); 783 | this.dispatchChange(newValue, key); 784 | } 785 | }, 'readwrite'); 786 | await Promise.all(promises); 787 | } 788 | 789 | /** 790 | * Uses a cursor to efficiently iterate over the objects matching the range calling the iterator for each one. 791 | */ 792 | forEach(iterator: StoreIterator, mode: IDBTransactionMode = 'readonly') { 793 | return this.cursor((cursor, trans) => { 794 | iterator(this.store.revive((cursor as any).value as Type), cursor, trans); 795 | }, mode); 796 | } 797 | 798 | /** 799 | * Uses a cursor to efficiently iterate over the objects matching the range calling the iterator for each one and 800 | * returning the results of the iterator in an array. 801 | */ 802 | async map(iterator: StoreIterator, mode: IDBTransactionMode = 'readonly') { 803 | let results: R[] = []; 804 | await this.forEach((object, cursor, trans) => { 805 | results.push(iterator(object, cursor, trans)); 806 | }, mode); 807 | return results; 808 | } 809 | } 810 | 811 | function requestToPromise( 812 | request: any, 813 | transaction?: IDBTransaction, 814 | errorDispatcher?: ErrorDispatcher 815 | ): Promise { 816 | return new Promise((resolve, reject) => { 817 | if (transaction) { 818 | let promise = transactionPromise.get(transaction); 819 | if (!promise) { 820 | promise = requestToPromise(transaction, null, errorDispatcher); 821 | } 822 | promise = promise.then( 823 | () => resolve(request.result), 824 | err => { 825 | let requestError; 826 | try { 827 | requestError = request.error; 828 | } catch (e) {} 829 | reject(requestError || err); 830 | return Promise.reject(err); 831 | } 832 | ); 833 | transactionPromise.set(transaction, promise); 834 | } else if (request.onsuccess === null) { 835 | request.onsuccess = successHandler(resolve); 836 | } 837 | if (request.oncomplete === null) request.oncomplete = successHandler(resolve); 838 | if (request.onerror === null) request.onerror = errorHandler(reject, errorDispatcher); 839 | if (request.onabort === null) request.onabort = () => reject(new Error('Abort')); 840 | }); 841 | } 842 | 843 | function successHandler(resolve: (result: any) => void) { 844 | return (event: Event) => resolve((event.target as any).result); 845 | } 846 | 847 | function errorHandler(reject: (err: Error) => void, errorDispatcher?: ErrorDispatcher) { 848 | return (event: Event) => { 849 | reject((event.target as any).error); 850 | errorDispatcher && errorDispatcher.dispatchError((event.target as any).error); 851 | }; 852 | } 853 | 854 | function safariMultiStoreFix(storeNames: DOMStringList | string[]) { 855 | return storeNames.length === 1 ? storeNames[0] : Array.from(storeNames); 856 | } 857 | 858 | function upgrade( 859 | oldVersion: number, 860 | transaction: IDBTransaction, 861 | db: IDBDatabase, 862 | versionMap: Record, 863 | versionHandlers: Record, 864 | browserbase: Browserbase 865 | ) { 866 | let versions; 867 | // Optimization for creating a new database. A version 0 may be used as the "latest" version to create a database. 868 | if (oldVersion === 0 && versionMap[0]) { 869 | versions = [0]; 870 | } else { 871 | versions = Object.keys(versionMap) 872 | .map(key => parseInt(key)) 873 | .filter(version => version > oldVersion) 874 | .sort((a, b) => a - b); 875 | } 876 | 877 | versions.forEach(version => { 878 | const stores = versionMap[version]; 879 | Object.keys(stores).forEach(name => { 880 | const indexesString = stores[name]; 881 | upgradeStore(db, transaction, name, indexesString); 882 | }); 883 | 884 | const handler = versionHandlers[version]; 885 | if (handler) { 886 | // Ensure browserbase has the current object stores for working with in the handler 887 | addStores(browserbase, db, transaction); 888 | handler(oldVersion, transaction); 889 | } 890 | }); 891 | } 892 | 893 | function upgradeStore(db: IDBDatabase, transaction: IDBTransaction, storeName: string, indexesString: string) { 894 | const indexes = indexesString && indexesString.split(/\s*,\s*/); 895 | let store; 896 | 897 | if (indexesString === null) { 898 | db.deleteObjectStore(storeName); 899 | return; 900 | } 901 | 902 | if (db.objectStoreNames.contains(storeName)) { 903 | store = transaction.objectStore(storeName); 904 | } else { 905 | store = db.createObjectStore(storeName, getStoreOptions(indexes.shift())); 906 | } 907 | 908 | indexes.forEach(name => { 909 | if (!name) return; 910 | if (name[0] === '-') return store.deleteIndex(name.replace(/^-[&*]?/, '')); 911 | 912 | let options: IDBIndexParameters = {}; 913 | 914 | name = name.replace(/\s/g, ''); 915 | if (name[0] === '&') { 916 | name = name.slice(1); 917 | options.unique = true; 918 | } else if (name[0] === '*') { 919 | name = name.slice(1); 920 | options.multiEntry = true; 921 | } 922 | let keyPath = name[0] === '[' ? name.replace(/^\[|\]$/g, '').split(/\+/) : name; 923 | store.createIndex(name, keyPath, options); 924 | }); 925 | } 926 | 927 | function onOpen(browserbase: Browserbase) { 928 | const db = browserbase.db; 929 | 930 | db.onversionchange = event => { 931 | const versionEvent = new Event('versionchange', { cancelable: true }); 932 | browserbase.dispatchEvent(versionEvent); 933 | if (!versionEvent.defaultPrevented) { 934 | if (event.newVersion > 0) { 935 | console.warn( 936 | `Another connection wants to upgrade database '${browserbase.name}'. Closing db now to resume the upgrade.` 937 | ); 938 | } else { 939 | console.warn( 940 | `Another connection wants to delete database '${browserbase.name}'. Closing db now to resume the delete request.` 941 | ); 942 | } 943 | browserbase.close(); 944 | } 945 | }; 946 | db.onclose = async () => { 947 | if (!browserbase._closed) { 948 | delete browserbase._opening; 949 | await browserbase.open(); 950 | browserbase.dispatchEvent(new Event('recreated')); 951 | } 952 | }; 953 | db.onerror = event => browserbase.dispatchEvent(new ErrorEvent('error', { error: (event.target as any).error })); 954 | if (!browserbase.options.dontDispatch) { 955 | browserbase._channel = createChannel(browserbase); 956 | } 957 | 958 | // Store keyPath's for each store 959 | addStores(browserbase, db, db.transaction(safariMultiStoreFix(db.objectStoreNames), 'readonly')); 960 | } 961 | 962 | function createChannel(browserbase: Browserbase) { 963 | const channel = new BroadcastChannel(`browserbase/${browserbase.name}`); 964 | channel.onmessage = event => { 965 | try { 966 | const { path, obj } = event.data; 967 | const [storeName, key] = path.split('/'); 968 | const store = (browserbase.stores as ObjectStoreMap)[storeName]; 969 | if (store) { 970 | browserbase.dispatchChange(store, obj, key, 'remote'); 971 | } else { 972 | console.warn(`A change event came from another tab for store "${storeName}", but no such store exists.`); 973 | } 974 | } catch (err) { 975 | console.warn('Error parsing object change from browserbase:', err); 976 | } 977 | }; 978 | return browserbase._channel; 979 | } 980 | 981 | function postMessage(browserbase: Browserbase, message: any) { 982 | if (!browserbase._channel) return; 983 | try { 984 | browserbase._channel.postMessage(message); 985 | } catch (e) { 986 | // If the channel is closed, create a new one and try again 987 | if (e.name === 'InvalidStateError') { 988 | browserbase._channel = createChannel(browserbase); 989 | postMessage(browserbase, message); 990 | } 991 | } 992 | } 993 | 994 | function addStores(browserbase: Browserbase, db: IDBDatabase, transaction: IDBTransaction) { 995 | const names = db.objectStoreNames; 996 | for (let i = 0; i < names.length; i++) { 997 | const name = names[i]; 998 | (browserbase.stores as ObjectStoreMap)[name] = new ObjectStore( 999 | browserbase, 1000 | name, 1001 | transaction.objectStore(name).keyPath 1002 | ); 1003 | } 1004 | } 1005 | 1006 | function onClose(browserbase: Browserbase) { 1007 | if (browserbase._channel) browserbase._channel.close(); 1008 | browserbase._channel = null; 1009 | browserbase.db = null; 1010 | browserbase.dispatchEvent(new Event('close')); 1011 | } 1012 | 1013 | function getStoreOptions(keyString: string) { 1014 | let keyPath: string | string[] = keyString.replace(/\s/g, ''); 1015 | let storeOptions: IDBObjectStoreParameters = {}; 1016 | if (keyPath.slice(0, 2) === '++') { 1017 | keyPath = keyPath.replace('++', ''); 1018 | storeOptions.autoIncrement = true; 1019 | } else if (keyPath[0] === '[') { 1020 | keyPath = keyPath.replace(/^\[|\]$/g, '').split(/\+/); 1021 | } 1022 | if (keyPath) storeOptions.keyPath = keyPath; 1023 | return storeOptions; 1024 | } 1025 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------