@live-change/db-store-indexeddb 0.6.1 → 0.6.3

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.
Files changed (2) hide show
  1. package/lib/Store.js +267 -90
  2. package/package.json +3 -3
package/lib/Store.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const IntervalTree = require('@live-change/interval-tree').default
2
2
  const ReactiveDao = require("@live-change/dao")
3
3
  const { BroadcastChannel, createLeaderElection } = require('broadcast-channel')
4
- const idb = require('idb')
4
+
5
5
 
6
6
 
7
7
  class ObjectObservable extends ReactiveDao.ObservableValue {
@@ -221,13 +221,119 @@ class RangeObservable extends ReactiveDao.ObservableList {
221
221
  }
222
222
  }
223
223
 
224
+ class CountObservable extends ReactiveDao.ObservableValue {
225
+ constructor(store, range) {
226
+ super()
227
+ this.store = store
228
+ this.range = range
229
+
230
+ this.disposed = false
231
+ this.ready = false
232
+ this.respawnId = 0
233
+ this.refillId = 0
234
+ this.refillPromise = null
235
+
236
+ this.forward = null
237
+
238
+ this.rangeKey = JSON.stringify(this.range)
239
+ this.rangeDescr = [ this.range.gt || this.range.gte || '', this.range.lt || this.range.lte || '\xFF\xFF\xFF\xFF']
240
+
241
+ this.readPromise = this.startReading()
242
+ }
243
+
244
+ async startReading() {
245
+ this.store.rangeObservables.set(this.rangeKey, this)
246
+ const treeInsert = this.rangeDescr
247
+ this.store.rangeObservablesTree.insert(treeInsert, this)
248
+ this.set(await this.store.countGet(this.range))
249
+ }
250
+
251
+ async putObject(object, oldObject) {
252
+ const id = object.id
253
+ if(this.range.gt && !(id > this.range.gt)) return
254
+ if(this.range.lt && !(id < this.range.lt)) return
255
+ await this.readPromise
256
+ if(this.range.limit) {
257
+ this.set(await this.store.countGet(this.range))
258
+ } else {
259
+ if(object && !oldObject) {
260
+ this.set(this.value + 1)
261
+ } else if(!object && oldObject) {
262
+ this.set(this.value - 1)
263
+ }
264
+ }
265
+ }
266
+
267
+ async deleteObject(object) {
268
+ const id = object.id
269
+ if(this.range.gt && !(id > this.range.gt)) return
270
+ if(this.range.lt && !(id < this.range.lt)) return
271
+ this.set(this.value - 1)
272
+ }
273
+
274
+ dispose() {
275
+ if(this.forward) {
276
+ this.forward.unobserve(this)
277
+ this.forward = null
278
+ return
279
+ }
280
+
281
+ this.disposed = true
282
+ this.respawnId++
283
+ this.changesStream = null
284
+
285
+ this.store.rangeObservables.delete(this.rangeKey)
286
+ let removed = this.store.rangeObservablesTree.remove(this.rangeDescr, this)
287
+ }
288
+
289
+ respawn() {
290
+ const existingObservable = this.store.rangeObservables.get(JSON.stringify(this.range))
291
+ if(existingObservable) {
292
+ this.forward = existingObservable
293
+ this.forward.observe(this)
294
+ return
295
+ }
296
+
297
+ this.respawnId++
298
+ this.ready = false
299
+ this.disposed = false
300
+ this.startReading()
301
+ }
302
+ }
303
+
304
+ async function handleRequest(request, onUpgrade = ()=>{}) {
305
+ return new Promise((resolve, reject) => {
306
+ request.onerror = (event) => {
307
+ reject(request.error)
308
+ }
309
+ request.onsuccess = (event) => {
310
+ resolve(request.result)
311
+ }
312
+ request.onupgradeneeded = onUpgrade
313
+ })
314
+ }
315
+
224
316
  class Store {
225
- constructor(dbName, storeName) {
317
+ constructor(dbName, storeName, options = {}) {
226
318
  if(!dbName) throw new Error("dbName argument is required")
227
319
  if(!storeName) throw new Error("storeName argument is required")
228
320
 
229
321
  this.dbName = dbName
230
322
  this.storeName = storeName
323
+ this.idbName = dbName + '.' + storeName
324
+
325
+ if(options.noSerialization) {
326
+ this.serialization = {
327
+ stringify: x => x,
328
+ parse: x => x
329
+ }
330
+ } else {
331
+ const serialization = options.serialization ?? JSON
332
+ this.serialization = {
333
+ stringify: x => x ? ({ id: x.id, data: serialization.stringify(x) }) : null,
334
+ parse: x => x?.data ? serialization.parse(x.data) : null
335
+ }
336
+ }
231
337
 
232
338
  this.finished = false
233
339
 
@@ -235,30 +341,28 @@ class Store {
235
341
  this.dbPromise = null
236
342
  this.channel = null
237
343
 
344
+ this.openPromise = null
345
+
238
346
  this.objectObservables = new Map()
239
347
  this.rangeObservables = new Map()
348
+ this.countObservables = new Map()
240
349
  this.rangeObservablesTree = new IntervalTree()
241
350
  }
242
351
 
243
352
  async openDb() {
244
- this.dbPromise = idb.openDB(`lc-db-${this.dbName}-${this.storeName}`, 1, {
245
- upgrade: (db) => {
246
- const store = db.createObjectStore(this.storeName, { keyPath: 'id' })
247
- },
248
- terminated: async () => {
249
- console.log("IndexedDB terminated!")
250
- if(this.finished) return
251
- for(const [key, value] of this.objectObservables) value.dispose()
252
- for(const [key, value] of this.rangeObservables) value.dispose()
253
- await this.openDb()
254
- for(const [key, value] of this.objectObservables) value.respawn()
255
- for(const [key, value] of this.rangeObservables) value.respawn()
256
- }
353
+ //console.log("Opening db", this.dbName, this.storeName)
354
+ const openRequest = globalThis.indexedDB.open(this.idbName, 1)
355
+ globalThis.lastOpenRequest = openRequest
356
+ this.dbPromise = handleRequest(openRequest, (event) => {
357
+ //console.error("Upgrading db", this.dbName, this.storeName)
358
+ const db = event.target.result
359
+ const store = db.createObjectStore(this.storeName, { keyPath: 'id' })
257
360
  })
258
361
  this.db = await this.dbPromise
362
+ //console.log("Opened db", this.dbName, this.storeName)
259
363
  }
260
364
  async openChannel() {
261
- this.channel = new BroadcastChannel('lc-db-channel' + this.dbName, {
365
+ this.channel = new BroadcastChannel('lc-db-channel' + this.dbName + '-' + this.storeName, {
262
366
  idb: {
263
367
  onclose: () => {
264
368
  if(this.finished) return
@@ -278,16 +382,22 @@ class Store {
278
382
  await this.channel.close()
279
383
  ;(await this.dbPromise).close()
280
384
  }
385
+ async ensureOpen() {
386
+ if(!this.openPromise) this.openPromise = this.open()
387
+ await this.openPromise
388
+ }
281
389
 
282
390
  async deleteDb() {
391
+ ;(await this.dbPromise).deleteObjectStore(this.storeName)
283
392
  if(!this.finished) await this.close()
284
- await idb.deleteDB('lc-db-' + this.dbName)
285
393
  }
286
394
 
287
395
  async handleChannelMessage(message) {
288
396
  switch(message.type) {
289
397
  case 'put' : {
290
- const { object, oldObject } = message
398
+ const { object: objectJson, oldObjectJson } = message
399
+ const object = this.serialization.parse(objectJson)
400
+ const oldObject = this.serialization.parse(oldObjectJson)
291
401
  const id = object?.id || oldObject?.id
292
402
  if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
293
403
  const objectObservable = this.objectObservables.get(id)
@@ -298,7 +408,8 @@ class Store {
298
408
  }
299
409
  } break
300
410
  case 'delete' : {
301
- const { object } = message
411
+ const { object: objectJson } = message
412
+ const object = this.serialization.parse(objectJson)
302
413
  const id = object?.id
303
414
  if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
304
415
  const objectObservable = this.objectObservables.get(id)
@@ -314,9 +425,13 @@ class Store {
314
425
  }
315
426
 
316
427
  async objectGet(id) {
428
+ await this.ensureOpen()
317
429
  if(!id) throw new Error("key is required")
318
430
  if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
319
- return await this.db.get(this.storeName, id) || null
431
+ const transaction = this.db.transaction([this.storeName], 'readonly')
432
+ const store = transaction.objectStore(this.storeName)
433
+ const json = await handleRequest(store.get(id) || null)
434
+ return json ? this.serialization.parse(json) : null
320
435
  }
321
436
 
322
437
  objectObservable(key) {
@@ -328,9 +443,7 @@ class Store {
328
443
 
329
444
  async rangeGet(range) {
330
445
  if(!range) throw new Error("range not defined")
331
- console.log("RANGE GET!")
332
-
333
- const txn = this.db.transaction(this.storeName, 'readonly')
446
+ await this.ensureOpen()
334
447
  let data = []
335
448
  const min = range.gt || range.gte
336
449
  const max = range.lt || range.lte
@@ -340,32 +453,53 @@ class Store {
340
453
  } else if(min) {
341
454
  keyRange = IDBKeyRange.lowerBound(min, !!range.gt)
342
455
  } else if(max) {
343
- keyRange = IDBKeyRange.upperBound(min, !!range.gt)
456
+ keyRange = IDBKeyRange.upperBound(max, !!range.gt)
344
457
  }
345
458
 
459
+ const txn = this.db.transaction([this.storeName], 'readonly')
460
+ const store = txn.objectStore(this.storeName)
346
461
  if(range.reverse) {
347
- let cursor = await txn.store.openCursor(keyRange, 'prev')
348
- while ((!range.limit || data.length < range.limit) && cursor) {
349
- if (range.gt && cursor.key <= range.gt) break
350
- if (range.gte && cursor.key < range.gte) break
351
- if ((!range.lt || cursor.key < range.lt) && (!range.lte || cursor.key <= range.lte)) {
352
- data.push(cursor.value)
462
+ await new Promise((resolve, reject) => {
463
+ const cursorRequest = store.openCursor(keyRange, 'prev')
464
+ cursorRequest.onsuccess = (event) => {
465
+ const cursor = event.target.result
466
+ if ((!range.limit || data.length < range.limit) && cursor) {
467
+ if (range.gt && cursor.key <= range.gt) return resolve()
468
+ if (range.gte && cursor.key < range.gte) return resolve()
469
+ if ((!range.lt || cursor.key < range.lt) && (!range.lte || cursor.key <= range.lte)) {
470
+ const json = cursor.value
471
+ data.push(this.serialization.parse(json))
472
+ }
473
+ cursor.continue()
474
+ } else {
475
+ return resolve()
476
+ }
353
477
  }
354
- cursor = await cursor.continue()
355
- }
478
+ cursorRequest.onerror = (event) => {
479
+ reject(event.target.error)
480
+ }
481
+ })
356
482
  } else {
357
- let cursor = await txn.store.openCursor(keyRange, 'next')
358
- //console.log("CURSOR", cursor)
359
- while((!range.limit || data.length < range.limit) && cursor) {
360
- if(range.lt && cursor.key >= range.lt) break
361
- if(range.lte && cursor.key > range.lte) break
362
- if((!range.gt || cursor.key > range.gt) && (!range.gte || cursor.key >= range.gte)) {
363
- data.push(cursor.value)
483
+ await new Promise((resolve, reject) => {
484
+ const cursorRequest = store.openCursor(keyRange, 'next')
485
+ cursorRequest.onsuccess = (event) => {
486
+ const cursor = event.target.result
487
+ if ((!range.limit || data.length < range.limit) && cursor) {
488
+ if(range.lt && cursor.key >= range.lt) return resolve()
489
+ if(range.lte && cursor.key > range.lte) return resolve()
490
+ if((!range.gt || cursor.key > range.gt) && (!range.gte || cursor.key >= range.gte)) {
491
+ const json = cursor.value
492
+ data.push(this.serialization.parse(json))
493
+ }
494
+ cursor.continue()
495
+ } else {
496
+ return resolve()
497
+ }
364
498
  }
365
- cursor = await cursor.continue()
366
- //console.log("CURSOR C", cursor)
367
- }
368
- //console.log("CUR READ END", cursor)
499
+ cursorRequest.onerror = (event) => {
500
+ reject(event.target.error)
501
+ }
502
+ })
369
503
  }
370
504
  return data
371
505
  }
@@ -379,6 +513,7 @@ class Store {
379
513
 
380
514
  async countGet(range) {
381
515
  if(!range) throw new Error("range not defined")
516
+ await this.ensureOpen()
382
517
  const min = range.gt || range.gte
383
518
  const max = range.lt || range.lte
384
519
  let keyRange = undefined
@@ -389,7 +524,9 @@ class Store {
389
524
  } else if(max) {
390
525
  keyRange = IDBKeyRange.upperBound(min, !!range.gt)
391
526
  }
392
- const count = await this.count(this.storeName, keyRange)
527
+ const txn = this.db.transaction([this.storeName], 'readonly')
528
+ const store = txn.objectStore(this.storeName)
529
+ const count = await handleRequest(store.count(keyRange))
393
530
  if(range.limit && count > range.limit) return range.limit
394
531
  return count
395
532
  }
@@ -403,8 +540,8 @@ class Store {
403
540
 
404
541
  async rangeDelete(range) {
405
542
  if(!range) throw new Error("range not defined")
543
+ await this.ensureOpen()
406
544
 
407
- const txn = this.db.transaction(this.storeName)
408
545
  let count = 0, last
409
546
  const min = range.gt || range.gte
410
547
  const max = range.lt || range.lte
@@ -417,50 +554,74 @@ class Store {
417
554
  keyRange = IDBKeyRange.upperBound(min, !!range.gt)
418
555
  }
419
556
 
557
+ const txn = this.db.transaction([this.storeName], 'readonly')
558
+ const store = txn.objectStore(this.storeName)
420
559
  if(range.reverse) {
421
- let cursor = await txn.store.openCursor(keyRange, 'prev')
422
- while ((!range.limit || data.length < range.limit) && cursor) {
423
- if (range.gt && cursor.key <= range.gt) break
424
- if (range.gte && cursor.key < range.gte) break
425
- if ((!range.lt || cursor.key < range.lt) && (!range.lte || cursor.key <= range.lte)) {
426
- count++
427
- const id = cursor.key
428
- const object = cursor.value
429
- last = id
430
- await cursor.delete()
431
-
432
- const objectObservable = this.objectObservables.get(id)
433
- if(objectObservable) objectObservable.set(null)
434
- const rangeObservables = this.rangeObservablesTree.search([id, id])
435
- for(const rangeObservable of rangeObservables) {
436
- rangeObservable.deleteObject(object)
560
+ await new Promise((resolve, reject) => {
561
+ const cursorRequest = store.openCursor(keyRange, 'prev')
562
+ cursorRequest.onsuccess = async (event) => {
563
+ const cursor = event.target.result
564
+ if ((!range.limit || count < range.limit) && cursor) {
565
+ if (range.gt && cursor.key <= range.gt) return resolve()
566
+ if (range.gte && cursor.key < range.gte) return resolve()
567
+ if ((!range.lt || cursor.key < range.lt) && (!range.lte || cursor.key <= range.lte)) {
568
+ count++
569
+ const id = cursor.key
570
+ const json = cursor.value
571
+ const object = this.serialization.parse(json)
572
+ last = id
573
+ await handleRequest(cursor.delete())
574
+
575
+ const objectObservable = this.objectObservables.get(id)
576
+ if(objectObservable) objectObservable.set(null)
577
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
578
+ for(const rangeObservable of rangeObservables) {
579
+ rangeObservable.deleteObject(object)
580
+ }
581
+ this.channel.postMessage({ type: "delete", object: this.serialization.stringify(object) })
582
+ }
583
+ cursor.continue()
584
+ } else {
585
+ return resolve()
437
586
  }
438
- this.channel.postMessage({ type: "delete", object })
439
587
  }
440
- cursor = await cursor.continue()
441
- }
588
+ cursorRequest.onerror = (event) => {
589
+ reject(event.target.error)
590
+ }
591
+ })
442
592
  } else {
443
- let cursor = await txn.store.openCursor(keyRange, 'prev')
444
- while((!range.limit || data.length < range.limit) && cursor) {
445
- if(range.lt && cursor.key >= range.lt) break
446
- if(range.lte && cursor.key > range.lte) break
447
- if((!range.gt || cursor.key > range.gt) && (!range.gte || cursor.key >= range.gte)) {
448
- count++
449
- const id = cursor.key
450
- const object = cursor.value
451
- last = id
452
- await cursor.delete()
453
-
454
- const objectObservable = this.objectObservables.get(id)
455
- if(objectObservable) objectObservable.set(null)
456
- const rangeObservables = this.rangeObservablesTree.search([id, id])
457
- for(const rangeObservable of rangeObservables) {
458
- rangeObservable.deleteObject(object)
593
+ await new Promise((resolve, reject) => {
594
+ const cursorRequest = store.openCursor(keyRange, 'next')
595
+ cursorRequest.onsuccess = async (event) => {
596
+ const cursor = event.target.result
597
+ if ((!range.limit || count < range.limit) && cursor) {
598
+ if(range.lt && cursor.key >= range.lt) return resolve()
599
+ if(range.lte && cursor.key > range.lte) return resolve()
600
+ if((!range.gt || cursor.key > range.gt) && (!range.gte || cursor.key >= range.gte)) {
601
+ count++
602
+ const id = cursor.key
603
+ const json = cursor.value
604
+ const object = this.serialization.parse(json)
605
+ last = id
606
+ await handleRequest(cursor.delete())
607
+
608
+ const objectObservable = this.objectObservables.get(id)
609
+ if(objectObservable) objectObservable.set(null)
610
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
611
+ for(const rangeObservable of rangeObservables) {
612
+ rangeObservable.deleteObject(object)
613
+ }
614
+ this.channel.postMessage({ type: "delete", object: this.serialization.stringify(object) })
615
+ }
616
+ cursor.continue()
617
+ } else {
618
+ return resolve()
459
619
  }
460
- this.channel.postMessage({ type: "delete", object })
461
620
  }
462
- cursor = await cursor.continue()
463
- }
621
+ cursorRequest.onerror = (event) => {
622
+ reject(event.target.error)
623
+ }
624
+ })
464
625
  }
465
626
  return { count, last }
466
627
  }
@@ -468,30 +629,46 @@ class Store {
468
629
  async put(object) {
469
630
  const id = object.id
470
631
  if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
471
- const oldObject = await this.db.get(this.storeName, id)
472
- console.log("PUT", object)
473
- await this.db.put(this.storeName, object)
632
+ await this.ensureOpen()
633
+ //console.error("put", id, object, 'in', this.storeName)
634
+ //console.error("storeNames", this.db.objectStoreNames)
635
+ const transaction = this.db.transaction([this.storeName], 'readwrite')
636
+ const store = transaction.objectStore(this.storeName)
637
+ const oldObjectJson = await handleRequest(store.get(id))
638
+ const oldObject = oldObjectJson ? this.serialization.parse(oldObjectJson) : null
639
+ //console.log("PUT", object)
640
+ const json = this.serialization.stringify(object)
641
+ await handleRequest(store.put(json))
474
642
  const objectObservable = this.objectObservables.get(id)
475
643
  if(objectObservable) objectObservable.set(object, oldObject)
476
644
  const rangeObservables = this.rangeObservablesTree.search([id, id])
477
645
  for(const rangeObservable of rangeObservables) {
478
646
  rangeObservable.putObject(object, oldObject)
479
647
  }
480
- this.channel.postMessage({ type: "put", object, oldObject })
648
+ this.channel.postMessage({ type: "put",
649
+ object: this.serialization.stringify(object),
650
+ oldObject: this.serialization.stringify(oldObject)
651
+ })
481
652
  return oldObject
482
653
  }
483
654
 
484
655
  async delete(id) {
485
656
  if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
486
- const object = await this.db.get(this.storeName, id)
487
- await this.db.delete(this.storeName, id)
657
+ await this.ensureOpen()
658
+
659
+ const transaction = this.db.transaction([this.storeName], 'readwrite')
660
+ const store = transaction.objectStore(this.storeName)
661
+
662
+ const json = await handleRequest(store.get(id))
663
+ const object = json ? this.serialization.parse(json) : null
664
+ await handleRequest(store.delete(id))
488
665
  const objectObservable = this.objectObservables.get(id)
489
666
  if(objectObservable) objectObservable.set(null)
490
667
  const rangeObservables = this.rangeObservablesTree.search([id, id])
491
668
  for(const rangeObservable of rangeObservables) {
492
669
  rangeObservable.deleteObject(object)
493
670
  }
494
- this.channel.postMessage({ type: "delete", object })
671
+ this.channel.postMessage({ type: "delete", object: this.serialization.stringify(object) })
495
672
  return object
496
673
  }
497
674
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/db-store-indexeddb",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Database with observable data for live queries",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -26,9 +26,9 @@
26
26
  "tape": "^5.3.2"
27
27
  },
28
28
  "dependencies": {
29
- "@live-change/dao": "0.5.14",
29
+ "@live-change/dao": "0.5.15",
30
30
  "@live-change/interval-tree": "^1.0.12",
31
31
  "broadcast-channel": "^4.2.0"
32
32
  },
33
- "gitHead": "f458425c609deeb9d1f38c15a3aa37759629a67e"
33
+ "gitHead": "8dc4ac726243970c9f1431bf67b4390f7845ce76"
34
34
  }