deepbase-indexeddb 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 mclasen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # 🌐 DeepBase IndexedDB Driver
2
+
3
+ **Browser-based persistence using IndexedDB**
4
+
5
+ The IndexedDB driver allows you to use DeepBase in browser environments with full offline capabilities. Perfect for Progressive Web Apps (PWAs), single-page applications, and any web app that needs reliable client-side storage.
6
+
7
+ ## ✨ Features
8
+
9
+ - 🌐 **Browser-native**: Uses IndexedDB API built into modern browsers
10
+ - 📴 **Offline-first**: Works without network connectivity
11
+ - 💾 **Large storage**: Can store much more data than localStorage (typically hundreds of MBs)
12
+ - 🔒 **Concurrency-safe**: Built-in operation queuing prevents race conditions
13
+ - 🚀 **Fast**: Asynchronous operations with IndexedDB transactions
14
+ - 🔄 **PWA-ready**: Perfect for Progressive Web Apps
15
+ - 🛡️ **Type-safe**: Full support for nested objects and arrays
16
+ - 🎯 **Same API**: Use the exact same DeepBase API in browser and Node.js
17
+
18
+ ## 📦 Installation
19
+
20
+ ```bash
21
+ npm install deepbase deepbase-indexeddb
22
+ ```
23
+
24
+ Or via CDN:
25
+
26
+ ```html
27
+ <script type="module">
28
+ import DeepBase from 'https://cdn.skypack.dev/deepbase';
29
+ import IndexedDBDriver from 'https://cdn.skypack.dev/deepbase-indexeddb';
30
+
31
+ // Your code here
32
+ </script>
33
+ ```
34
+
35
+ ## 🚀 Quick Start
36
+
37
+ ### Basic Usage (Production)
38
+
39
+ When using the published NPM packages:
40
+
41
+ ```javascript
42
+ import DeepBase from 'deepbase';
43
+ import IndexedDBDriver from 'deepbase-indexeddb';
44
+
45
+ // Create database instance
46
+ const db = new DeepBase(new IndexedDBDriver({
47
+ name: 'myapp', // Database name
48
+ version: 1, // Database version
49
+ storeName: 'store' // Object store name (optional)
50
+ }));
51
+
52
+ // Connect to database
53
+ await db.connect();
54
+
55
+ // Store data
56
+ await db.set('users', 'alice', {
57
+ name: 'Alice',
58
+ email: 'alice@example.com',
59
+ settings: {
60
+ theme: 'dark',
61
+ notifications: true
62
+ }
63
+ });
64
+
65
+ // Retrieve data
66
+ const alice = await db.get('users', 'alice');
67
+ console.log(alice.settings.theme); // 'dark'
68
+
69
+ // Update nested values
70
+ await db.set('users', 'alice', 'settings', 'theme', 'light');
71
+
72
+ // Delete data
73
+ await db.del('users', 'alice');
74
+
75
+ // Disconnect when done
76
+ await db.disconnect();
77
+ ```
78
+
79
+ ### Using with a Bundler (Webpack, Vite, etc.)
80
+
81
+ For browser applications using a bundler, you can import normally:
82
+
83
+ ```javascript
84
+ import DeepBase from 'deepbase';
85
+ import IndexedDBDriver from 'deepbase-indexeddb';
86
+
87
+ const db = new DeepBase(new IndexedDBDriver({
88
+ name: 'myapp',
89
+ version: 1
90
+ }));
91
+
92
+ await db.connect();
93
+ // ... your code
94
+ ```
95
+
96
+ ### Using without a Bundler (Plain HTML)
97
+
98
+ **Good news!** DeepBase now works directly in the browser thanks to dynamic imports:
99
+
100
+ ```html
101
+ <script type="module">
102
+ // Import directly from local files
103
+ import DeepBase from './path/to/packages/core/src/index.js';
104
+ import { IndexedDBDriver } from './path/to/packages/driver-indexeddb/src/IndexedDBDriver.js';
105
+
106
+ // Use the full DeepBase class with all features!
107
+ const db = new DeepBase(new IndexedDBDriver({
108
+ name: 'myapp',
109
+ version: 1
110
+ }));
111
+
112
+ await db.connect();
113
+ await db.set('users', 'alice', { name: 'Alice' });
114
+ const alice = await db.get('users', 'alice');
115
+ console.log(alice);
116
+ </script>
117
+ ```
118
+
119
+ Or via CDN (once published):
120
+
121
+ ```html
122
+ <script type="module">
123
+ import DeepBase from 'https://cdn.skypack.dev/deepbase';
124
+ import IndexedDBDriver from 'https://cdn.skypack.dev/deepbase-indexeddb';
125
+
126
+ const db = new DeepBase(new IndexedDBDriver({
127
+ name: 'myapp',
128
+ version: 1
129
+ }));
130
+
131
+ await db.connect();
132
+ // Your code here
133
+ </script>
134
+ ```
135
+
136
+ **Note:** The full DeepBase class now works in browsers! You get all features including multi-driver support, timeouts, and advanced operations.
137
+
138
+ ### Progressive Web App Example
139
+
140
+ ```javascript
141
+ import DeepBase from 'deepbase';
142
+ import IndexedDBDriver from 'deepbase-indexeddb';
143
+
144
+ class TodoApp {
145
+ constructor() {
146
+ this.db = new DeepBase(new IndexedDBDriver({
147
+ name: 'todoapp',
148
+ version: 1
149
+ }));
150
+ }
151
+
152
+ async init() {
153
+ await this.db.connect();
154
+
155
+ // Initialize default data if needed
156
+ const todos = await this.db.get('todos');
157
+ if (!todos) {
158
+ await this.db.set('todos', {});
159
+ }
160
+ }
161
+
162
+ async addTodo(text) {
163
+ const todoPath = await this.db.add('todos', {
164
+ text,
165
+ completed: false,
166
+ createdAt: Date.now()
167
+ });
168
+ return todoPath[1]; // Return the auto-generated ID
169
+ }
170
+
171
+ async toggleTodo(id) {
172
+ await this.db.upd('todos', id, 'completed', val => !val);
173
+ }
174
+
175
+ async getTodos() {
176
+ return await this.db.get('todos');
177
+ }
178
+
179
+ async deleteTodo(id) {
180
+ await this.db.del('todos', id);
181
+ }
182
+ }
183
+
184
+ // Usage
185
+ const app = new TodoApp();
186
+ await app.init();
187
+
188
+ const id = await app.addTodo('Learn DeepBase');
189
+ console.log('Created todo:', id);
190
+
191
+ await app.toggleTodo(id);
192
+ const todos = await app.getTodos();
193
+ console.log('All todos:', todos);
194
+ ```
195
+
196
+ ## 🎯 Configuration Options
197
+
198
+ ```javascript
199
+ new IndexedDBDriver({
200
+ name: 'mydb', // Database name (default: 'deepbase')
201
+ version: 1, // Database version (default: 1)
202
+ storeName: 'store', // Object store name (default: 'store')
203
+
204
+ // Inherited from DeepBaseDriver:
205
+ nidAlphabet: 'ABC...', // Custom alphabet for auto-generated IDs
206
+ nidLength: 10 // Length of auto-generated IDs
207
+ })
208
+ ```
209
+
210
+ ## 📚 API Reference
211
+
212
+ The IndexedDB driver supports all standard DeepBase operations:
213
+
214
+ ### Basic Operations
215
+
216
+ ```javascript
217
+ // Get value at path
218
+ const value = await db.get('path', 'to', 'value');
219
+
220
+ // Set value at path
221
+ await db.set('path', 'to', 'value', 'hello');
222
+
223
+ // Delete value at path
224
+ await db.del('path', 'to', 'value');
225
+ ```
226
+
227
+ ### Array Operations
228
+
229
+ ```javascript
230
+ // Add item with auto-generated ID
231
+ const path = await db.add('items', { name: 'Item 1' });
232
+ // Returns: ['items', 'aB3xK9mL2n']
233
+
234
+ // Pop last item from array
235
+ const item = await db.pop('myArray');
236
+
237
+ // Shift first item from array
238
+ const first = await db.shift('myArray');
239
+ ```
240
+
241
+ ### Numeric Operations
242
+
243
+ ```javascript
244
+ // Increment number
245
+ await db.inc('counter', 1);
246
+
247
+ // Decrement number
248
+ await db.dec('counter', 1);
249
+ ```
250
+
251
+ ### Update with Functions
252
+
253
+ ```javascript
254
+ // Update value using a function
255
+ await db.upd('user', 'name', name => name.toUpperCase());
256
+ ```
257
+
258
+ ### Object Operations
259
+
260
+ ```javascript
261
+ // Get keys
262
+ const keys = await db.keys('users');
263
+
264
+ // Get values
265
+ const values = await db.values('users');
266
+
267
+ // Get entries
268
+ const entries = await db.entries('users');
269
+ ```
270
+
271
+ ## 🔒 Concurrency Safety
272
+
273
+ The IndexedDB driver includes built-in operation queuing to prevent race conditions:
274
+
275
+ ```javascript
276
+ // These operations are safely serialized
277
+ await Promise.all([
278
+ db.inc('counter', 1),
279
+ db.inc('counter', 1),
280
+ db.inc('counter', 1)
281
+ ]);
282
+
283
+ const counter = await db.get('counter');
284
+ console.log(counter); // Always 3, never less
285
+ ```
286
+
287
+ ## 🌐 Browser Compatibility
288
+
289
+ The IndexedDB driver works in all modern browsers that support IndexedDB:
290
+
291
+ - ✅ Chrome 24+
292
+ - ✅ Firefox 16+
293
+ - ✅ Safari 10+
294
+ - ✅ Edge (all versions)
295
+ - ✅ Opera 15+
296
+ - ✅ Mobile browsers (iOS Safari, Chrome Mobile, etc.)
297
+
298
+ ## 💡 Use Cases
299
+
300
+ ### Progressive Web Apps (PWAs)
301
+
302
+ ```javascript
303
+ // Store user preferences
304
+ await db.set('preferences', {
305
+ theme: 'dark',
306
+ language: 'en',
307
+ notifications: true
308
+ });
309
+
310
+ // Cache API responses
311
+ await db.set('cache', 'users', apiResponse);
312
+ ```
313
+
314
+ ### Offline-First Applications
315
+
316
+ ```javascript
317
+ // Queue operations while offline
318
+ if (!navigator.onLine) {
319
+ await db.add('syncQueue', {
320
+ action: 'updateUser',
321
+ data: userData,
322
+ timestamp: Date.now()
323
+ });
324
+ }
325
+
326
+ // Sync when back online
327
+ window.addEventListener('online', async () => {
328
+ const queue = await db.get('syncQueue');
329
+ // Process queue...
330
+ await db.del('syncQueue');
331
+ });
332
+ ```
333
+
334
+ ### Client-Side State Management
335
+
336
+ ```javascript
337
+ // Store application state
338
+ await db.set('app', 'state', {
339
+ currentUser: userId,
340
+ openModals: ['settings'],
341
+ cart: [item1, item2]
342
+ });
343
+
344
+ // Restore state on page load
345
+ const state = await db.get('app', 'state');
346
+ ```
347
+
348
+ ### Form Data Persistence
349
+
350
+ ```javascript
351
+ // Auto-save form data
352
+ document.querySelector('#myForm').addEventListener('input', async (e) => {
353
+ await db.set('forms', 'contact', e.target.form.id, e.target.value);
354
+ });
355
+
356
+ // Restore form data
357
+ const savedData = await db.get('forms', 'contact');
358
+ ```
359
+
360
+ ## 🔄 Multi-Driver Setup
361
+
362
+ Combine IndexedDB with other drivers for advanced scenarios:
363
+
364
+ ```javascript
365
+ import DeepBase from 'deepbase';
366
+ import IndexedDBDriver from 'deepbase-indexeddb';
367
+ import JsonDriver from 'deepbase-json';
368
+
369
+ // Browser: Use IndexedDB
370
+ // Node.js: Use JSON files
371
+ const db = new DeepBase(
372
+ typeof window !== 'undefined'
373
+ ? new IndexedDBDriver({ name: 'myapp' })
374
+ : new JsonDriver({ path: './data', name: 'myapp' })
375
+ );
376
+
377
+ await db.connect();
378
+ // Same code works in both environments!
379
+ ```
380
+
381
+ ## 🛠️ Advanced Features
382
+
383
+ ### Database Versioning
384
+
385
+ ```javascript
386
+ // Upgrade database version when schema changes
387
+ const db = new DeepBase(new IndexedDBDriver({
388
+ name: 'myapp',
389
+ version: 2 // Increment version number
390
+ }));
391
+
392
+ // IndexedDB will automatically handle the upgrade
393
+ await db.connect();
394
+ ```
395
+
396
+ ### Multiple Object Stores
397
+
398
+ ```javascript
399
+ // Create separate stores for different data types
400
+ const usersDB = new DeepBase(new IndexedDBDriver({
401
+ name: 'myapp',
402
+ storeName: 'users'
403
+ }));
404
+
405
+ const postsDB = new DeepBase(new IndexedDBDriver({
406
+ name: 'myapp',
407
+ storeName: 'posts'
408
+ }));
409
+
410
+ await usersDB.connect();
411
+ await postsDB.connect();
412
+ ```
413
+
414
+ ### Custom ID Generation
415
+
416
+ ```javascript
417
+ const db = new DeepBase(new IndexedDBDriver({
418
+ name: 'myapp',
419
+ nidAlphabet: '0123456789', // Numbers only
420
+ nidLength: 6 // 6 digits
421
+ }));
422
+
423
+ const path = await db.add('items', { name: 'Item' });
424
+ // ID will be something like: ['items', '123456']
425
+ ```
426
+
427
+ ## ⚠️ Limitations
428
+
429
+ - **Browser-only**: This driver only works in browser environments with IndexedDB support
430
+ - **Storage limits**: Browser-dependent (typically 50-100MB, but can be more)
431
+ - **Same-origin policy**: Data is isolated per domain
432
+ - **Not suitable for**: Node.js, server-side rendering (SSR) initial render
433
+
434
+ For Node.js environments, use:
435
+ - `deepbase-json` for development
436
+ - `deepbase-sqlite` for production
437
+ - `deepbase-mongodb` for scalability
438
+ - `deepbase-redis` for caching
439
+
440
+ ## 🔍 Debugging
441
+
442
+ ### Check Database in Browser DevTools
443
+
444
+ 1. Open Chrome DevTools
445
+ 2. Go to "Application" tab
446
+ 3. Expand "IndexedDB" in the sidebar
447
+ 4. Find your database name
448
+ 5. Inspect stored data
449
+
450
+ ### Common Issues
451
+
452
+ **Error: "IndexedDB is not available"**
453
+ - This driver requires a browser environment
454
+ - Check that you're not running in Node.js
455
+ - Ensure browser supports IndexedDB
456
+
457
+ **Data not persisting**
458
+ - Make sure to call `await db.connect()` before operations
459
+ - Check browser storage settings/permissions
460
+ - Verify you're not in private/incognito mode (some browsers restrict storage)
461
+
462
+ ## 📝 TypeScript Support
463
+
464
+ ```typescript
465
+ import DeepBase from 'deepbase';
466
+ import IndexedDBDriver from 'deepbase-indexeddb';
467
+
468
+ interface User {
469
+ name: string;
470
+ email: string;
471
+ settings: {
472
+ theme: 'light' | 'dark';
473
+ notifications: boolean;
474
+ };
475
+ }
476
+
477
+ const db = new DeepBase(new IndexedDBDriver({
478
+ name: 'myapp'
479
+ }));
480
+
481
+ await db.connect();
482
+
483
+ // TypeScript will infer types
484
+ await db.set('users', 'alice', {
485
+ name: 'Alice',
486
+ email: 'alice@example.com',
487
+ settings: {
488
+ theme: 'dark',
489
+ notifications: true
490
+ }
491
+ } as User);
492
+
493
+ const alice = await db.get('users', 'alice') as User;
494
+ ```
495
+
496
+ ## 🤝 Contributing
497
+
498
+ Found a bug or want to contribute? Check out the [main repository](https://github.com/clasen/DeepBase).
499
+
500
+ ## 📄 License
501
+
502
+ MIT License - Copyright (c) Martin Clasen
503
+
504
+ ## 🔗 Links
505
+
506
+ - [Main Documentation](https://github.com/clasen/DeepBase)
507
+ - [GitHub Repository](https://github.com/clasen/DeepBase)
508
+ - [Report Issues](https://github.com/clasen/DeepBase/issues)
509
+ - [Other Drivers](https://github.com/clasen/DeepBase/tree/main/packages)
510
+
511
+ ---
512
+
513
+ 🚀 **Build amazing offline-first web apps with DeepBase + IndexedDB!**
514
+
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "deepbase-indexeddb",
3
+ "version": "3.4.0",
4
+ "description": "⚡ DeepBase IndexedDB - browser storage driver",
5
+ "type": "module",
6
+ "main": "src/index.cjs",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./src/index.cjs",
11
+ "import": "./src/index.js"
12
+ }
13
+ },
14
+ "dependencies": {},
15
+ "peerDependencies": {
16
+ "deepbase": "^3.4.0"
17
+ },
18
+ "scripts": {
19
+ "test": "echo \"IndexedDB tests require browser environment. See test/test.html\""
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/clasen/DeepBase.git"
24
+ },
25
+ "keywords": [
26
+ "deepbase",
27
+ "indexeddb",
28
+ "browser",
29
+ "driver",
30
+ "database",
31
+ "persist",
32
+ "nested",
33
+ "objects",
34
+ "offline",
35
+ "pwa"
36
+ ],
37
+ "author": "Martin Clasen",
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/clasen/DeepBase/issues"
41
+ },
42
+ "homepage": "https://github.com/clasen/DeepBase/tree/main/packages/driver-indexeddb"
43
+ }
@@ -0,0 +1,331 @@
1
+ import { DeepBaseDriver } from 'deepbase';
2
+
3
+ export class IndexedDBDriver extends DeepBaseDriver {
4
+ static _instances = {};
5
+
6
+ constructor({name, version, storeName, ...opts} = {}) {
7
+ super(opts);
8
+
9
+ this.name = name || "deepbase";
10
+ this.version = version || 1;
11
+ this.storeName = storeName || "store";
12
+
13
+ this.db = null;
14
+ this._rootKey = '__root__';
15
+
16
+ // Queue for serializing concurrent operations
17
+ this._operationQueue = Promise.resolve();
18
+
19
+ // Singleton pattern per database name
20
+ const instanceKey = `${this.name}:${this.storeName}`;
21
+ if (IndexedDBDriver._instances[instanceKey]) {
22
+ return IndexedDBDriver._instances[instanceKey];
23
+ }
24
+
25
+ IndexedDBDriver._instances[instanceKey] = this;
26
+ }
27
+
28
+ async connect() {
29
+ if (this._connected) return;
30
+
31
+ // Check if we're in a browser environment
32
+ if (typeof window === 'undefined' || !window.indexedDB) {
33
+ throw new Error('IndexedDB is not available. This driver only works in browser environments.');
34
+ }
35
+
36
+ return new Promise((resolve, reject) => {
37
+ const request = indexedDB.open(this.name, this.version);
38
+
39
+ request.onerror = () => {
40
+ reject(new Error(`Failed to open IndexedDB: ${request.error}`));
41
+ };
42
+
43
+ request.onsuccess = () => {
44
+ this.db = request.result;
45
+ this._connected = true;
46
+ resolve();
47
+ };
48
+
49
+ request.onupgradeneeded = (event) => {
50
+ const db = event.target.result;
51
+
52
+ // Create object store if it doesn't exist
53
+ if (!db.objectStoreNames.contains(this.storeName)) {
54
+ db.createObjectStore(this.storeName);
55
+ }
56
+ };
57
+ });
58
+ }
59
+
60
+ async disconnect() {
61
+ if (this.db) {
62
+ this.db.close();
63
+ this.db = null;
64
+ this._connected = false;
65
+ }
66
+ }
67
+
68
+ async get(...args) {
69
+ if (!this._connected) {
70
+ throw new Error('Database not connected. Call connect() first.');
71
+ }
72
+
73
+ // Get root object
74
+ const rootObj = await this._getRoot();
75
+
76
+ // Navigate to the requested path
77
+ const value = this._getRecursive(rootObj, args.slice());
78
+
79
+ // Return deep copy to prevent mutations
80
+ return typeof value === 'object' && value !== null
81
+ ? JSON.parse(JSON.stringify(value))
82
+ : value;
83
+ }
84
+
85
+ async set(...args) {
86
+ return this._queueOperation(async () => {
87
+ if (!this._connected) {
88
+ throw new Error('Database not connected. Call connect() first.');
89
+ }
90
+
91
+ // Get root object
92
+ const rootObj = await this._getRoot();
93
+
94
+ if (args.length < 2) {
95
+ // Setting the entire root
96
+ await this._setRoot(args[0]);
97
+ return [];
98
+ }
99
+
100
+ const keys = args.slice(0, -1);
101
+ const value = args[args.length - 1];
102
+
103
+ // Set value at path
104
+ this._setRecursive(rootObj, keys.slice(), value);
105
+
106
+ // Save back to IndexedDB
107
+ await this._setRoot(rootObj);
108
+
109
+ return keys;
110
+ });
111
+ }
112
+
113
+ async del(...keys) {
114
+ return this._queueOperation(async () => {
115
+ if (!this._connected) {
116
+ throw new Error('Database not connected. Call connect() first.');
117
+ }
118
+
119
+ if (keys.length === 0) {
120
+ // Delete entire root
121
+ await this._deleteRoot();
122
+ return;
123
+ }
124
+
125
+ const rootObj = await this._getRoot();
126
+ const key = keys.pop();
127
+ const parentObj = this._getRecursive(rootObj, keys.slice());
128
+
129
+ if (parentObj && parentObj.hasOwnProperty(key)) {
130
+ delete parentObj[key];
131
+ await this._setRoot(rootObj);
132
+ }
133
+ });
134
+ }
135
+
136
+ async inc(...args) {
137
+ const i = args.pop();
138
+ return this.upd(...args, n => n + i);
139
+ }
140
+
141
+ async dec(...args) {
142
+ const i = args.pop();
143
+ return this.upd(...args, n => n - i);
144
+ }
145
+
146
+ async add(...keys) {
147
+ return this._queueOperation(async () => {
148
+ const value = keys.pop();
149
+ const id = this.nanoid();
150
+ await this._setInternal(...[...keys, id], value);
151
+ return [...keys, id];
152
+ });
153
+ }
154
+
155
+ async upd(...args) {
156
+ return this._queueOperation(async () => {
157
+ if (!this._connected) {
158
+ throw new Error('Database not connected. Call connect() first.');
159
+ }
160
+
161
+ const func = args.pop();
162
+ const rootObj = await this._getRoot();
163
+ const currentValue = this._getRecursive(rootObj, args.slice());
164
+ const newValue = func(currentValue);
165
+ await this._setInternal(...args, newValue);
166
+ return args;
167
+ });
168
+ }
169
+
170
+ async pop(...args) {
171
+ return this._queueOperation(async () => {
172
+ if (!this._connected) {
173
+ throw new Error('Database not connected. Call connect() first.');
174
+ }
175
+
176
+ const rootObj = await this._getRoot();
177
+ const arr = this._getRecursive(rootObj, args.slice());
178
+
179
+ if (!Array.isArray(arr)) {
180
+ throw new Error('pop() can only be used on arrays');
181
+ }
182
+
183
+ const poppedValue = arr.pop();
184
+ await this._setRoot(rootObj);
185
+
186
+ return poppedValue;
187
+ });
188
+ }
189
+
190
+ async shift(...args) {
191
+ return this._queueOperation(async () => {
192
+ if (!this._connected) {
193
+ throw new Error('Database not connected. Call connect() first.');
194
+ }
195
+
196
+ const rootObj = await this._getRoot();
197
+ const arr = this._getRecursive(rootObj, args.slice());
198
+
199
+ if (!Array.isArray(arr)) {
200
+ throw new Error('shift() can only be used on arrays');
201
+ }
202
+
203
+ const shiftedValue = arr.shift();
204
+ await this._setRoot(rootObj);
205
+
206
+ return shiftedValue;
207
+ });
208
+ }
209
+
210
+ // Internal method to set without queuing
211
+ async _setInternal(...args) {
212
+ if (!this._connected) {
213
+ throw new Error('Database not connected. Call connect() first.');
214
+ }
215
+
216
+ const rootObj = await this._getRoot();
217
+
218
+ if (args.length < 2) {
219
+ await this._setRoot(args[0]);
220
+ return [];
221
+ }
222
+
223
+ const keys = args.slice(0, -1);
224
+ const value = args[args.length - 1];
225
+
226
+ this._setRecursive(rootObj, keys.slice(), value);
227
+ await this._setRoot(rootObj);
228
+
229
+ return keys;
230
+ }
231
+
232
+ // Queue operations to prevent race conditions
233
+ async _queueOperation(operation) {
234
+ const previousOperation = this._operationQueue;
235
+
236
+ let resolver;
237
+ this._operationQueue = new Promise(resolve => {
238
+ resolver = resolve;
239
+ });
240
+
241
+ try {
242
+ await previousOperation;
243
+ const result = await operation();
244
+ resolver();
245
+ return result;
246
+ } catch (error) {
247
+ resolver();
248
+ throw error;
249
+ }
250
+ }
251
+
252
+ // Get root object from IndexedDB
253
+ async _getRoot() {
254
+ return new Promise((resolve, reject) => {
255
+ const transaction = this.db.transaction([this.storeName], 'readonly');
256
+ const store = transaction.objectStore(this.storeName);
257
+ const request = store.get(this._rootKey);
258
+
259
+ request.onsuccess = () => {
260
+ resolve(request.result || {});
261
+ };
262
+
263
+ request.onerror = () => {
264
+ reject(new Error(`Failed to get root: ${request.error}`));
265
+ };
266
+ });
267
+ }
268
+
269
+ // Set root object in IndexedDB
270
+ async _setRoot(obj) {
271
+ return new Promise((resolve, reject) => {
272
+ const transaction = this.db.transaction([this.storeName], 'readwrite');
273
+ const store = transaction.objectStore(this.storeName);
274
+ const request = store.put(obj, this._rootKey);
275
+
276
+ request.onsuccess = () => {
277
+ resolve();
278
+ };
279
+
280
+ request.onerror = () => {
281
+ reject(new Error(`Failed to set root: ${request.error}`));
282
+ };
283
+ });
284
+ }
285
+
286
+ // Delete root object from IndexedDB
287
+ async _deleteRoot() {
288
+ return new Promise((resolve, reject) => {
289
+ const transaction = this.db.transaction([this.storeName], 'readwrite');
290
+ const store = transaction.objectStore(this.storeName);
291
+ const request = store.delete(this._rootKey);
292
+
293
+ request.onsuccess = () => {
294
+ resolve();
295
+ };
296
+
297
+ request.onerror = () => {
298
+ reject(new Error(`Failed to delete root: ${request.error}`));
299
+ };
300
+ });
301
+ }
302
+
303
+ _setRecursive(obj, keys, value) {
304
+ if (keys.length === 1) {
305
+ obj[keys[0]] = value;
306
+ return;
307
+ }
308
+
309
+ const key = keys.shift();
310
+ if (!obj.hasOwnProperty(key) || typeof obj[key] !== "object") {
311
+ obj[key] = {};
312
+ }
313
+
314
+ this._setRecursive(obj[key], keys, value);
315
+ }
316
+
317
+ _getRecursive(obj, keys) {
318
+ if (keys.length === 0) return obj;
319
+ if (keys.length === 1) {
320
+ return obj === null || obj[keys[0]] === undefined ? null : obj[keys[0]];
321
+ }
322
+
323
+ const key = keys.shift();
324
+ if (!obj.hasOwnProperty(key)) return null;
325
+
326
+ return this._getRecursive(obj[key], keys);
327
+ }
328
+ }
329
+
330
+ export default IndexedDBDriver;
331
+
package/src/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ module.exports = require('./index.js').default;
2
+
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { IndexedDBDriver } from './IndexedDBDriver.js';
2
+ export { IndexedDBDriver } from './IndexedDBDriver.js';
3
+ export default IndexedDBDriver;
4
+
package/test/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Testing IndexedDB Driver
2
+
3
+ Since IndexedDB is a browser API, tests must be run in a browser environment.
4
+
5
+ ## Running Tests
6
+
7
+ 1. Open `test.html` in a web browser
8
+ 2. Tests will automatically run when the page loads
9
+ 3. You can also click "Run All Tests" button to re-run tests
10
+ 4. Use "Clear Database" to reset the test database
11
+
12
+ ## Test Files
13
+
14
+ - `test.html` - Interactive browser-based test suite with visual feedback
15
+
16
+ ## Manual Testing
17
+
18
+ You can also test manually using the browser console:
19
+
20
+ ```javascript
21
+ import DeepBase from 'deepbase';
22
+ import IndexedDBDriver from 'deepbase-indexeddb';
23
+
24
+ const db = new DeepBase(new IndexedDBDriver({
25
+ name: 'manual-test',
26
+ version: 1
27
+ }));
28
+
29
+ await db.connect();
30
+
31
+ // Test basic operations
32
+ await db.set('test', 'value', 'hello');
33
+ console.log(await db.get('test', 'value')); // 'hello'
34
+
35
+ // Test nested operations
36
+ await db.set('users', 'alice', { name: 'Alice', age: 30 });
37
+ console.log(await db.get('users', 'alice'));
38
+
39
+ await db.disconnect();
40
+ ```
41
+
42
+ ## Debugging
43
+
44
+ Open Chrome DevTools:
45
+ 1. Go to "Application" tab
46
+ 2. Expand "IndexedDB" in the sidebar
47
+ 3. Find your database
48
+ 4. Inspect stored data
49
+
50
+ ## Browser Compatibility
51
+
52
+ Tests should work in:
53
+ - Chrome 24+
54
+ - Firefox 16+
55
+ - Safari 10+
56
+ - Edge (all versions)
57
+ - Opera 15+
58
+
package/test/test.html ADDED
@@ -0,0 +1,382 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DeepBase IndexedDB Driver - Tests</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+ max-width: 1200px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ h1 {
16
+ color: #333;
17
+ border-bottom: 3px solid #4CAF50;
18
+ padding-bottom: 10px;
19
+ }
20
+ .test {
21
+ background: white;
22
+ margin: 10px 0;
23
+ padding: 15px;
24
+ border-radius: 5px;
25
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
26
+ }
27
+ .test.running {
28
+ background: #fff9c4;
29
+ border-left: 4px solid #FFC107;
30
+ }
31
+ .test.passed {
32
+ background: #e8f5e9;
33
+ border-left: 4px solid #4CAF50;
34
+ }
35
+ .test.failed {
36
+ background: #ffebee;
37
+ border-left: 4px solid #f44336;
38
+ }
39
+ .test-name {
40
+ font-weight: bold;
41
+ margin-bottom: 5px;
42
+ }
43
+ .test-result {
44
+ font-family: 'Courier New', monospace;
45
+ font-size: 12px;
46
+ color: #666;
47
+ margin-top: 5px;
48
+ }
49
+ .summary {
50
+ background: #333;
51
+ color: white;
52
+ padding: 20px;
53
+ border-radius: 5px;
54
+ margin-top: 20px;
55
+ font-size: 18px;
56
+ }
57
+ .summary.success {
58
+ background: #4CAF50;
59
+ }
60
+ .summary.failure {
61
+ background: #f44336;
62
+ }
63
+ .error {
64
+ color: #f44336;
65
+ font-family: 'Courier New', monospace;
66
+ font-size: 12px;
67
+ margin-top: 5px;
68
+ white-space: pre-wrap;
69
+ }
70
+ button {
71
+ background: #4CAF50;
72
+ color: white;
73
+ border: none;
74
+ padding: 10px 20px;
75
+ font-size: 16px;
76
+ border-radius: 5px;
77
+ cursor: pointer;
78
+ margin: 10px 5px;
79
+ }
80
+ button:hover {
81
+ background: #45a049;
82
+ }
83
+ button.danger {
84
+ background: #f44336;
85
+ }
86
+ button.danger:hover {
87
+ background: #da190b;
88
+ }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <h1>🧪 DeepBase IndexedDB Driver - Test Suite</h1>
93
+
94
+ <div>
95
+ <button onclick="runTests()">Run All Tests</button>
96
+ <button onclick="clearDatabase()" class="danger">Clear Database</button>
97
+ </div>
98
+
99
+ <div id="tests"></div>
100
+ <div id="summary"></div>
101
+
102
+ <script type="module">
103
+ // Import DeepBase (now works in browser without bundler thanks to dynamic imports!)
104
+ import DeepBase from '../../core/src/index.js';
105
+ import { IndexedDBDriver } from '../src/IndexedDBDriver.js';
106
+
107
+ // Make available globally
108
+ window.DeepBase = DeepBase;
109
+ window.IndexedDBDriver = IndexedDBDriver;
110
+
111
+ console.log('Modules loaded successfully');
112
+ </script>
113
+
114
+ <script>
115
+ let testResults = [];
116
+ let db;
117
+
118
+ async function clearDatabase() {
119
+ if (confirm('Are you sure you want to clear the test database?')) {
120
+ try {
121
+ if (db) {
122
+ await db.disconnect();
123
+ }
124
+
125
+ // Delete the database
126
+ const request = indexedDB.deleteDatabase('deepbase-test');
127
+
128
+ request.onsuccess = () => {
129
+ alert('Database cleared successfully');
130
+ document.getElementById('tests').innerHTML = '';
131
+ document.getElementById('summary').innerHTML = '';
132
+ testResults = [];
133
+ };
134
+
135
+ request.onerror = () => {
136
+ alert('Failed to clear database');
137
+ };
138
+ } catch (error) {
139
+ alert('Error: ' + error.message);
140
+ }
141
+ }
142
+ }
143
+
144
+ async function runTests() {
145
+ testResults = [];
146
+ document.getElementById('tests').innerHTML = '';
147
+ document.getElementById('summary').innerHTML = '';
148
+
149
+ const tests = [
150
+ { name: 'Initialize database', fn: testInit },
151
+ { name: 'Set and get simple value', fn: testSetGet },
152
+ { name: 'Set and get nested value', fn: testNestedSetGet },
153
+ { name: 'Delete value', fn: testDelete },
154
+ { name: 'Increment number', fn: testIncrement },
155
+ { name: 'Decrement number', fn: testDecrement },
156
+ { name: 'Add with auto-generated ID', fn: testAdd },
157
+ { name: 'Update with function', fn: testUpdate },
158
+ { name: 'Get keys', fn: testKeys },
159
+ { name: 'Get values', fn: testValues },
160
+ { name: 'Get entries', fn: testEntries },
161
+ { name: 'Pop from array', fn: testPop },
162
+ { name: 'Shift from array', fn: testShift },
163
+ { name: 'Concurrent operations', fn: testConcurrency },
164
+ { name: 'Complex nested objects', fn: testComplexObjects },
165
+ { name: 'Disconnect', fn: testDisconnect }
166
+ ];
167
+
168
+ for (const test of tests) {
169
+ await runTest(test.name, test.fn);
170
+ }
171
+
172
+ displaySummary();
173
+ }
174
+
175
+ async function runTest(name, fn) {
176
+ const testDiv = document.createElement('div');
177
+ testDiv.className = 'test running';
178
+ testDiv.innerHTML = `
179
+ <div class="test-name">${name}</div>
180
+ <div class="test-result">Running...</div>
181
+ `;
182
+ document.getElementById('tests').appendChild(testDiv);
183
+
184
+ try {
185
+ const result = await fn();
186
+ testDiv.className = 'test passed';
187
+ testDiv.innerHTML = `
188
+ <div class="test-name">✓ ${name}</div>
189
+ <div class="test-result">${result || 'Passed'}</div>
190
+ `;
191
+ testResults.push({ name, passed: true });
192
+ } catch (error) {
193
+ testDiv.className = 'test failed';
194
+ testDiv.innerHTML = `
195
+ <div class="test-name">✗ ${name}</div>
196
+ <div class="error">${error.message}\n${error.stack}</div>
197
+ `;
198
+ testResults.push({ name, passed: false, error: error.message });
199
+ }
200
+ }
201
+
202
+ function displaySummary() {
203
+ const passed = testResults.filter(r => r.passed).length;
204
+ const total = testResults.length;
205
+ const summaryDiv = document.getElementById('summary');
206
+ summaryDiv.className = passed === total ? 'summary success' : 'summary failure';
207
+ summaryDiv.innerHTML = `
208
+ <strong>Test Results:</strong> ${passed}/${total} passed
209
+ ${passed < total ? '<br><br>Some tests failed. See details above.' : '<br><br>All tests passed! 🎉'}
210
+ `;
211
+ }
212
+
213
+ async function testInit() {
214
+ db = new window.DeepBase(new window.IndexedDBDriver({
215
+ name: 'deepbase-test',
216
+ version: 1
217
+ }));
218
+ await db.connect();
219
+ return 'Database initialized';
220
+ }
221
+
222
+ async function testSetGet() {
223
+ await db.set('test', 'value', 'hello');
224
+ const value = await db.get('test', 'value');
225
+ if (value !== 'hello') throw new Error(`Expected 'hello', got '${value}'`);
226
+ return `Value: ${value}`;
227
+ }
228
+
229
+ async function testNestedSetGet() {
230
+ await db.set('users', 'alice', { name: 'Alice', age: 30 });
231
+ const user = await db.get('users', 'alice');
232
+ if (user.name !== 'Alice' || user.age !== 30) {
233
+ throw new Error(`Unexpected user data: ${JSON.stringify(user)}`);
234
+ }
235
+ return `User: ${JSON.stringify(user)}`;
236
+ }
237
+
238
+ async function testDelete() {
239
+ await db.set('temp', 'data', 'delete me');
240
+ await db.del('temp', 'data');
241
+ const value = await db.get('temp', 'data');
242
+ if (value !== null) throw new Error(`Expected null, got '${value}'`);
243
+ return 'Value deleted successfully';
244
+ }
245
+
246
+ async function testIncrement() {
247
+ await db.set('counter', 10);
248
+ await db.inc('counter', 5);
249
+ const value = await db.get('counter');
250
+ if (value !== 15) throw new Error(`Expected 15, got ${value}`);
251
+ return `Counter: ${value}`;
252
+ }
253
+
254
+ async function testDecrement() {
255
+ await db.set('counter', 20);
256
+ await db.dec('counter', 7);
257
+ const value = await db.get('counter');
258
+ if (value !== 13) throw new Error(`Expected 13, got ${value}`);
259
+ return `Counter: ${value}`;
260
+ }
261
+
262
+ async function testAdd() {
263
+ const path = await db.add('items', { name: 'Item 1', price: 99 });
264
+ if (path.length !== 2 || path[0] !== 'items') {
265
+ throw new Error(`Unexpected path: ${JSON.stringify(path)}`);
266
+ }
267
+ const item = await db.get(...path);
268
+ if (item.name !== 'Item 1') throw new Error('Item not found');
269
+ return `Added item with ID: ${path[1]}`;
270
+ }
271
+
272
+ async function testUpdate() {
273
+ await db.set('user', 'name', 'john');
274
+ await db.upd('user', 'name', name => name.toUpperCase());
275
+ const value = await db.get('user', 'name');
276
+ if (value !== 'JOHN') throw new Error(`Expected 'JOHN', got '${value}'`);
277
+ return `Updated name: ${value}`;
278
+ }
279
+
280
+ async function testKeys() {
281
+ await db.set('products', 'laptop', { price: 999 });
282
+ await db.set('products', 'mouse', { price: 29 });
283
+ const keys = await db.keys('products');
284
+ if (!keys.includes('laptop') || !keys.includes('mouse')) {
285
+ throw new Error(`Unexpected keys: ${JSON.stringify(keys)}`);
286
+ }
287
+ return `Keys: ${JSON.stringify(keys)}`;
288
+ }
289
+
290
+ async function testValues() {
291
+ await db.set('products', 'laptop', { price: 999 });
292
+ await db.set('products', 'mouse', { price: 29 });
293
+ const values = await db.values('products');
294
+ if (values.length !== 2) throw new Error('Expected 2 values');
295
+ return `Values: ${JSON.stringify(values)}`;
296
+ }
297
+
298
+ async function testEntries() {
299
+ await db.set('products', 'laptop', { price: 999 });
300
+ await db.set('products', 'mouse', { price: 29 });
301
+ const entries = await db.entries('products');
302
+ if (entries.length !== 2) throw new Error('Expected 2 entries');
303
+ return `Entries: ${JSON.stringify(entries)}`;
304
+ }
305
+
306
+ async function testPop() {
307
+ await db.set('myArray', [1, 2, 3, 4, 5]);
308
+ const popped = await db.pop('myArray');
309
+ if (popped !== 5) throw new Error(`Expected 5, got ${popped}`);
310
+ const arr = await db.get('myArray');
311
+ if (arr.length !== 4) throw new Error(`Expected length 4, got ${arr.length}`);
312
+ return `Popped: ${popped}, Array: ${JSON.stringify(arr)}`;
313
+ }
314
+
315
+ async function testShift() {
316
+ await db.set('myArray', [1, 2, 3, 4, 5]);
317
+ const shifted = await db.shift('myArray');
318
+ if (shifted !== 1) throw new Error(`Expected 1, got ${shifted}`);
319
+ const arr = await db.get('myArray');
320
+ if (arr.length !== 4) throw new Error(`Expected length 4, got ${arr.length}`);
321
+ return `Shifted: ${shifted}, Array: ${JSON.stringify(arr)}`;
322
+ }
323
+
324
+ async function testConcurrency() {
325
+ await db.set('concurrentCounter', 0);
326
+
327
+ // Run 10 increments concurrently
328
+ await Promise.all(
329
+ Array.from({ length: 10 }, () => db.inc('concurrentCounter', 1))
330
+ );
331
+
332
+ const value = await db.get('concurrentCounter');
333
+ if (value !== 10) throw new Error(`Expected 10, got ${value} - race condition detected!`);
334
+ return `Counter after 10 concurrent increments: ${value}`;
335
+ }
336
+
337
+ async function testComplexObjects() {
338
+ const complexObj = {
339
+ user: {
340
+ id: 123,
341
+ profile: {
342
+ name: 'Alice',
343
+ email: 'alice@example.com',
344
+ settings: {
345
+ theme: 'dark',
346
+ notifications: {
347
+ email: true,
348
+ push: false
349
+ }
350
+ }
351
+ },
352
+ posts: [
353
+ { id: 1, title: 'First Post', tags: ['intro', 'hello'] },
354
+ { id: 2, title: 'Second Post', tags: ['update'] }
355
+ ]
356
+ }
357
+ };
358
+
359
+ await db.set('complex', complexObj);
360
+ const retrieved = await db.get('complex');
361
+
362
+ if (JSON.stringify(retrieved) !== JSON.stringify(complexObj)) {
363
+ throw new Error('Complex object not preserved correctly');
364
+ }
365
+
366
+ return `Complex object stored and retrieved successfully`;
367
+ }
368
+
369
+ async function testDisconnect() {
370
+ await db.disconnect();
371
+ return 'Database disconnected';
372
+ }
373
+
374
+ // Auto-run tests when page loads
375
+ window.addEventListener('load', () => {
376
+ console.log('Page loaded, waiting 1 second before running tests...');
377
+ setTimeout(runTests, 1000);
378
+ });
379
+ </script>
380
+ </body>
381
+ </html>
382
+