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 +22 -0
- package/README.md +514 -0
- package/package.json +43 -0
- package/src/IndexedDBDriver.js +331 -0
- package/src/index.cjs +2 -0
- package/src/index.js +4 -0
- package/test/README.md +58 -0
- package/test/test.html +382 -0
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
package/src/index.js
ADDED
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
|
+
|