@live-change/db-store-level 0.9.226 → 0.9.228

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/.node-version ADDED
@@ -0,0 +1 @@
1
+ 20.20.2
package/lib/Store.js CHANGED
@@ -1,7 +1,64 @@
1
1
  import IntervalTreeLib from '@live-change/flatten-interval-tree'
2
- const IntervalTree = IntervalTreeLib.default
2
+ const IntervalTree = IntervalTreeLib.default ?? IntervalTreeLib
3
3
  import ReactiveDao from "@live-change/dao"
4
4
 
5
+ function isNotFound(err) {
6
+ if(!err) return false
7
+ if(err.notFound) return true
8
+ if(err.code === 'LEVEL_NOT_FOUND') return true
9
+ if(err.name === 'NotFoundError') return true
10
+ const text = String(err.message || err)
11
+ return /notfound/i.test(text)
12
+ }
13
+
14
+ function keyToString(key) {
15
+ if(key == null) return ''
16
+ if(typeof key === 'string') return key
17
+ if(Buffer.isBuffer(key)) return key.toString('utf8')
18
+ return String(key)
19
+ }
20
+
21
+ function valueToString(value) {
22
+ if(value == null) return null
23
+ if(typeof value === 'string') return value
24
+ if(Buffer.isBuffer(value)) return value.toString('utf8')
25
+ return String(value)
26
+ }
27
+
28
+ function parseValue(value) {
29
+ const s = valueToString(value)
30
+ if(s == null || s === '') return null
31
+ return JSON.parse(s)
32
+ }
33
+
34
+ function prefixEnd(prefix) {
35
+ if(!prefix) return undefined
36
+ const buf = Buffer.from(prefix, 'utf8')
37
+ for(let i = buf.length - 1; i >= 0; i--) {
38
+ if(buf[i] < 255) {
39
+ buf[i]++
40
+ return buf.slice(0, i + 1).toString('utf8')
41
+ }
42
+ }
43
+ return prefix + '\xFF'
44
+ }
45
+
46
+ function iteratorNext(iterator) {
47
+ return new Promise((resolve, reject) => {
48
+ iterator.next((err, key, value) => {
49
+ if(err) return reject(err)
50
+ if(key === undefined && value === undefined) return resolve(null)
51
+ resolve({ key, value })
52
+ })
53
+ })
54
+ }
55
+
56
+ function iteratorEnd(iterator) {
57
+ return new Promise((resolve, reject) => {
58
+ iterator.end(err => err ? reject(err) : resolve())
59
+ })
60
+ }
61
+
5
62
  class ObjectObservable extends ReactiveDao.ObservableValue {
6
63
  constructor(store, key) {
7
64
  super()
@@ -88,7 +145,6 @@ class RangeObservable extends ReactiveDao.ObservableList {
88
145
 
89
146
  async putObject(object, oldObject) {
90
147
  await this.readPromise
91
- //console.log("OBSERVABLE PUT OBJECT", object)
92
148
  const id = object.id
93
149
  if(this.range.gt && !(id > this.range.gt)) return
94
150
  if(this.range.lt && !(id < this.range.lt)) return
@@ -118,7 +174,7 @@ class RangeObservable extends ReactiveDao.ObservableList {
118
174
  this.fireObservers('putByField', 'id', id, object, true, oldObject)
119
175
  return
120
176
  } else if(this.list[i].id > id) {
121
- if(i == this.list.length - 1) return // last element is bigger, do nothing
177
+ if(i == this.list.length - 1) return
122
178
  this.list.splice(i + 1, 0, object)
123
179
  this.fireObservers('putByField', 'id', id, object, true, oldObject)
124
180
  const popped = this.list.pop()
@@ -160,6 +216,7 @@ class RangeObservable extends ReactiveDao.ObservableList {
160
216
  }
161
217
 
162
218
  async deleteObject(object) {
219
+ if(!object) return
163
220
  await this.readPromise
164
221
  const id = object.id
165
222
  if(this.range.gt && !(id > this.range.gt)) return
@@ -192,7 +249,7 @@ class RangeObservable extends ReactiveDao.ObservableList {
192
249
  this.changesStream = null
193
250
 
194
251
  this.store.rangeObservables.delete(this.rangeKey)
195
- let removed = this.store.rangeObservablesTree.remove(this.rangeDescr, this)
252
+ this.store.rangeObservablesTree.remove(this.rangeDescr, this)
196
253
  }
197
254
 
198
255
  respawn() {
@@ -210,22 +267,194 @@ class RangeObservable extends ReactiveDao.ObservableList {
210
267
  }
211
268
  }
212
269
 
270
+ class CountObservable extends ReactiveDao.ObservableValue {
271
+ constructor(store, range) {
272
+ super()
273
+ this.store = store
274
+ this.range = range
275
+
276
+ this.disposed = false
277
+ this.ready = false
278
+ this.respawnId = 0
279
+ this.refillId = 0
280
+ this.refillPromise = null
281
+
282
+ this.forward = null
283
+
284
+ this.rangeKey = JSON.stringify(this.range)
285
+ this.rangeDescr = [ this.range.gt || this.range.gte || '', this.range.lt || this.range.lte || '\xFF\xFF\xFF\xFF']
286
+
287
+ this.readPromise = this.startReading()
288
+ }
289
+
290
+ async startReading() {
291
+ this.store.countObservables.set(this.rangeKey, this)
292
+ this.store.rangeObservablesTree.insert(this.rangeDescr, this)
293
+ this.set(await this.store.countGet(this.range))
294
+ }
295
+
296
+ async putObject(object, oldObject) {
297
+ const id = object.id
298
+ if(this.range.gt && !(id > this.range.gt)) return
299
+ if(this.range.lt && !(id < this.range.lt)) return
300
+ await this.readPromise
301
+ if(this.range.limit) {
302
+ this.set(await this.store.countGet(this.range))
303
+ } else {
304
+ if(object && !oldObject) {
305
+ this.set(this.value + 1)
306
+ } else if(!object && oldObject) {
307
+ this.set(this.value - 1)
308
+ }
309
+ }
310
+ }
311
+
312
+ async deleteObject(object) {
313
+ if(!object) return
314
+ const id = object.id
315
+ if(this.range.gt && !(id > this.range.gt)) return
316
+ if(this.range.lt && !(id < this.range.lt)) return
317
+ this.set(this.value - 1)
318
+ }
319
+
320
+ dispose() {
321
+ if(this.forward) {
322
+ this.forward.unobserve(this)
323
+ this.forward = null
324
+ return
325
+ }
326
+
327
+ this.disposed = true
328
+ this.respawnId++
329
+ this.changesStream = null
330
+
331
+ this.store.countObservables.delete(this.rangeKey)
332
+ this.store.rangeObservablesTree.remove(this.rangeDescr, this)
333
+ }
334
+
335
+ respawn() {
336
+ const existingObservable = this.store.countObservables.get(JSON.stringify(this.range))
337
+ if(existingObservable) {
338
+ this.forward = existingObservable
339
+ this.forward.observe(this)
340
+ return
341
+ }
342
+
343
+ this.respawnId++
344
+ this.ready = false
345
+ this.disposed = false
346
+ this.startReading()
347
+ }
348
+ }
349
+
213
350
  class Store {
214
- constructor(level, options = {}) {
215
- this.level = level
351
+ constructor(down, options = {}) {
352
+ this.down = down
353
+ this.prefix = options.prefix || ''
354
+ this.prefixEnd = this.prefix ? prefixEnd(this.prefix) : undefined
216
355
  this.objectObservables = new Map()
217
356
  this.rangeObservables = new Map()
357
+ this.countObservables = new Map()
218
358
  this.rangeObservablesTree = new IntervalTree()
219
359
  this.locks = new Map()
220
360
  }
221
361
 
222
- objectGet(key) {
223
- return this.level.get(key).catch(err => {
224
- if(err.name == 'NotFoundError') return null
225
- throw err
362
+ async ready() {
363
+ if(this.down && this.down._opened) await this.down._opened
364
+ }
365
+
366
+ encodeKey(id) {
367
+ return this.prefix + id
368
+ }
369
+
370
+ decodeKey(key) {
371
+ const s = keyToString(key)
372
+ if(this.prefix && s.startsWith(this.prefix)) return s.slice(this.prefix.length)
373
+ return s
374
+ }
375
+
376
+ iteratorRange(range, extra = {}) {
377
+ const opts = {
378
+ reverse: !!range.reverse,
379
+ keyAsBuffer: false,
380
+ valueAsBuffer: false,
381
+ ...extra
382
+ }
383
+ if(range.limit != null) opts.limit = range.limit
384
+
385
+ if(range.gt != null) opts.gt = this.encodeKey(range.gt)
386
+ else if(range.gte != null) opts.gte = this.encodeKey(range.gte)
387
+ else if(this.prefix) opts.gte = this.prefix
388
+
389
+ if(range.lt != null) opts.lt = this.encodeKey(range.lt)
390
+ else if(range.lte != null) opts.lte = this.encodeKey(range.lte)
391
+ else if(this.prefixEnd) opts.lt = this.prefixEnd
392
+
393
+ return opts
394
+ }
395
+
396
+ async iterate(opts, onItem) {
397
+ await this.ready()
398
+ const iterator = this.down.iterator(opts)
399
+ try {
400
+ while(true) {
401
+ const pair = await iteratorNext(iterator)
402
+ if(!pair) break
403
+ await onItem(pair)
404
+ }
405
+ } finally {
406
+ await iteratorEnd(iterator)
407
+ }
408
+ }
409
+
410
+ downGet(key) {
411
+ return new Promise((resolve, reject) => {
412
+ this.down.get(key, (err, value) => {
413
+ if(err) {
414
+ if(isNotFound(err)) return resolve(null)
415
+ return reject(err)
416
+ }
417
+ resolve(value)
418
+ })
419
+ })
420
+ }
421
+
422
+ downPut(key, value) {
423
+ return new Promise((resolve, reject) => {
424
+ this.down.put(key, value, err => err ? reject(err) : resolve())
226
425
  })
227
426
  }
228
427
 
428
+ downDel(key) {
429
+ return new Promise((resolve, reject) => {
430
+ this.down.del(key, err => {
431
+ if(err && !isNotFound(err)) return reject(err)
432
+ resolve()
433
+ })
434
+ })
435
+ }
436
+
437
+ downBatch(ops) {
438
+ if(!ops.length) return Promise.resolve()
439
+ return new Promise((resolve, reject) => {
440
+ this.down.batch(ops, err => err ? reject(err) : resolve())
441
+ })
442
+ }
443
+
444
+ downClear(opts) {
445
+ return new Promise((resolve, reject) => {
446
+ this.down.clear(opts, err => err ? reject(err) : resolve())
447
+ })
448
+ }
449
+
450
+ async objectGet(key) {
451
+ if(!key) throw new Error("key is required")
452
+ await this.ready()
453
+ const raw = await this.downGet(this.encodeKey(key))
454
+ if(raw == null) return null
455
+ return parseValue(raw)
456
+ }
457
+
229
458
  objectObservable(key) {
230
459
  let observable = this.objectObservables.get(key)
231
460
  if(observable) return observable
@@ -233,23 +462,16 @@ class Store {
233
462
  return observable
234
463
  }
235
464
 
236
- rangeGet(range) {
237
- return new Promise((resolve, reject) => {
238
- let data = []
239
- this.level.createReadStream({ ...range, keys: false, values: true })
240
- .on('data', function (value) {
241
- data.push(value)
242
- })
243
- .on('error', function (err) {
244
- reject(err)
245
- })
246
- .on('close', function () {
247
- reject('closed')
248
- })
249
- .on('end', function () {
250
- resolve(data)
251
- })
465
+ async rangeGet(range) {
466
+ if(!range) throw new Error("range not defined")
467
+ const data = []
468
+ await this.iterate(this.iteratorRange(range, { keys: true, values: true }), ({ key, value }) => {
469
+ const id = this.decodeKey(key)
470
+ const object = parseValue(value)
471
+ if(object && object.id == null) object.id = id
472
+ data.push(object)
252
473
  })
474
+ return data
253
475
  }
254
476
 
255
477
  rangeObservable(range) {
@@ -260,96 +482,153 @@ class Store {
260
482
  }
261
483
 
262
484
  async rangeDelete(range, options = {}) {
485
+ if(!range) throw new Error("range not defined")
263
486
  const keysOnly = options.keysOnly === true
264
- const self = this
265
- return new Promise((resolve, reject) => {
266
- let count = 0, last
267
- const streamOpts = keysOnly
268
- ? { ...range, keys: true, values: false }
269
- : { ...range, keys: false, values: true }
270
- this.level.createReadStream(streamOpts)
271
- .on('data', function(data) {
272
- const id = keysOnly ? data : data.id
273
- last = id
274
- self.level.del(id)
275
- count ++
276
- const objectObservable = self.objectObservables.get(id)
277
- if(objectObservable) objectObservable.set(null)
278
- const rangeObservables = self.rangeObservablesTree.search([id, id])
279
- for (const rangeObservable of rangeObservables) {
280
- rangeObservable.deleteObject(keysOnly ? { id } : data)
281
- }
282
- })
283
- .on('error', function(err) {
284
- reject(err)
285
- })
286
- .on('close', function() {
287
- reject('closed')
288
- })
289
- .on('end', function() {
290
- resolve({ count, last })
291
- })
487
+ const rows = []
488
+ await this.iterate(
489
+ this.iteratorRange(range, { keys: true, values: !keysOnly }),
490
+ ({ key, value }) => {
491
+ const id = this.decodeKey(key)
492
+ rows.push({
493
+ id,
494
+ encoded: this.encodeKey(id),
495
+ object: keysOnly ? { id } : parseValue(value)
496
+ })
497
+ }
498
+ )
499
+ await this.downBatch(rows.map(row => ({ type: 'del', key: row.encoded })))
500
+ for(const row of rows) {
501
+ const objectObservable = this.objectObservables.get(row.id)
502
+ if(objectObservable) objectObservable.set(null)
503
+ const rangeObservables = this.rangeObservablesTree.search([row.id, row.id])
504
+ for(const rangeObservable of rangeObservables) {
505
+ await rangeObservable.deleteObject(row.object || { id: row.id })
506
+ }
507
+ }
508
+ return {
509
+ count: rows.length,
510
+ last: rows.length ? rows[rows.length - 1].id : undefined
511
+ }
512
+ }
513
+
514
+ async countGet(range) {
515
+ if(!range) throw new Error("range not defined")
516
+ let count = 0
517
+ await this.iterate(this.iteratorRange(range, { keys: true, values: false }), () => {
518
+ count++
292
519
  })
520
+ return count
521
+ }
522
+
523
+ countObservable(range) {
524
+ let observable = this.countObservables.get(JSON.stringify(range))
525
+ if(observable) return observable
526
+ observable = new CountObservable(this, range)
527
+ return observable
528
+ }
529
+
530
+ async clear() {
531
+ await this.ready()
532
+ const opts = {}
533
+ if(this.prefix) {
534
+ opts.gte = this.prefix
535
+ if(this.prefixEnd) opts.lt = this.prefixEnd
536
+ }
537
+ await this.downClear(opts)
293
538
  }
294
539
 
295
540
  async put(object) {
296
541
  const id = object.id
542
+ if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
543
+ if(!id) throw new Error("ID must not be empty string!")
297
544
  let lock
298
- while(lock = this.locks.get(object.id)) await lock
299
- let updateLock = (async ()=> {
300
- let oldObject = await this.level.get(id).catch(err => {
301
- if(err.name == 'NotFoundError') return null
302
- throw err
303
- })
304
- if (!id) {
545
+ while(lock = this.locks.get(id)) await lock
546
+ const updateLock = (async () => {
547
+ try {
548
+ await this.ready()
549
+ const raw = await this.downGet(this.encodeKey(id))
550
+ const oldObject = raw == null ? null : parseValue(raw)
551
+ await this.downPut(this.encodeKey(id), JSON.stringify(object))
552
+ const objectObservable = this.objectObservables.get(id)
553
+ if(objectObservable) objectObservable.set(object, oldObject)
554
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
555
+ for(const rangeObservable of rangeObservables) {
556
+ if(rangeObservable.rangeDescr[0] > id || rangeObservable.rangeDescr[1] < id) {
557
+ console.error("TREE LEAKING", "ID", id, "IS OUT OF", rangeObservable.rangeDescr)
558
+ continue
559
+ }
560
+ await rangeObservable.putObject(object, oldObject)
561
+ }
562
+ return oldObject
563
+ } finally {
305
564
  this.locks.delete(id)
306
- throw new Error("id must not be empty")
307
565
  }
308
- await this.level.put(id, object)
309
- const objectObservable = this.objectObservables.get(id)
310
- if (objectObservable) objectObservable.set(object, oldObject)
311
- const rangeObservables = this.rangeObservablesTree.search([id, id])
312
- for (const rangeObservable of rangeObservables) {
313
- rangeObservable.putObject(object, oldObject)
314
- }
315
- this.locks.delete(id)
316
- return oldObject
317
566
  })()
318
567
  this.locks.set(id, updateLock)
319
568
  return await updateLock
320
569
  }
321
570
 
322
571
  async delete(id) {
323
- let deleteLock = (async ()=> {
324
- let object = await this.level.get(id).catch(err => {
325
- if(err.name == 'NotFoundError') return null
326
- throw err
327
- })
328
- if(!object) {
572
+ let lock
573
+ while(lock = this.locks.get(id)) await lock
574
+ const deleteLock = (async () => {
575
+ try {
576
+ await this.ready()
577
+ const raw = await this.downGet(this.encodeKey(id))
578
+ const object = raw == null ? null : parseValue(raw)
579
+ if(!object) return null
580
+ await this.downDel(this.encodeKey(id))
581
+ const objectObservable = this.objectObservables.get(id)
582
+ if(objectObservable) objectObservable.set(null)
583
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
584
+ for(const rangeObservable of rangeObservables) {
585
+ await rangeObservable.deleteObject(object)
586
+ }
587
+ return object
588
+ } finally {
329
589
  this.locks.delete(id)
330
- return null
331
590
  }
332
- await this.level.del(id)
333
- const objectObservable = this.objectObservables.get(id)
334
- if(objectObservable) objectObservable.set(null)
335
- const rangeObservables = this.rangeObservablesTree.search([id, id])
336
- for(const rangeObservable of rangeObservables) {
337
- rangeObservable.deleteObject(object)
338
- }
339
- this.locks.delete(id)
340
- return object
341
591
  })()
342
592
  this.locks.set(id, deleteLock)
343
- const result = await deleteLock
344
- return result
593
+ return await deleteLock
345
594
  }
346
595
 
347
596
  stat() {
348
- return {
349
- available: false,
350
- entryCount: null,
351
- usedBytes: null
352
- }
597
+ return new Promise(async (resolve) => {
598
+ try {
599
+ await this.ready()
600
+ if(typeof this.down.approximateSize !== 'function') {
601
+ return resolve({
602
+ available: false,
603
+ entryCount: null,
604
+ usedBytes: null
605
+ })
606
+ }
607
+ const start = this.prefix || '\x00'
608
+ const end = this.prefixEnd || '\xFF'
609
+ this.down.approximateSize(start, end, (err, size) => {
610
+ if(err) {
611
+ resolve({
612
+ available: false,
613
+ entryCount: null,
614
+ usedBytes: null
615
+ })
616
+ return
617
+ }
618
+ resolve({
619
+ available: true,
620
+ entryCount: null,
621
+ usedBytes: size
622
+ })
623
+ })
624
+ } catch(e) {
625
+ resolve({
626
+ available: false,
627
+ entryCount: null,
628
+ usedBytes: null
629
+ })
630
+ }
631
+ })
353
632
  }
354
633
  }
355
634
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@live-change/db-store-level",
3
- "version": "0.9.226",
3
+ "version": "0.9.228",
4
4
  "description": "Database with observable data for live queries",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "NODE_ENV=test tape tests/*"
7
+ "test": "NODE_ENV=test tape tests/*observable.js tests/store-*.js tests/prefix-isolation.js"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -22,17 +22,14 @@
22
22
  "type": "module",
23
23
  "homepage": "https://github.com/live-change/live-change-stack",
24
24
  "devDependencies": {
25
- "encoding-down": "^7.1.0",
26
25
  "leveldown": "^6.1.0",
27
- "levelup": "^5.1.1",
28
- "memdown": "^6.1.1",
26
+ "p-queue": "^8.1.1",
29
27
  "rimraf": "^5.0.5",
30
- "subleveldown": "^6.0.1",
31
28
  "tape": "^5.7.4"
32
29
  },
33
30
  "dependencies": {
34
- "@live-change/dao": "^0.9.226",
35
- "@live-change/flatten-interval-tree": "^0.9.226"
31
+ "@live-change/dao": "^0.9.228",
32
+ "@live-change/flatten-interval-tree": "^0.9.228"
36
33
  },
37
- "gitHead": "8b9b3bae11b5347f01de0cd09d5d8535a0d251cf"
34
+ "gitHead": "fd47aa55100c5a25c3f52c899a79a663421037ff"
38
35
  }
@@ -0,0 +1,138 @@
1
+ import test from 'tape'
2
+ import Store from '../lib/Store.js'
3
+ import { openTestDown, closeTestDown } from './utils.js'
4
+ import PQueue from 'p-queue'
5
+
6
+ const testQueue = new PQueue({ concurrency: 1 })
7
+ function queued(fn) {
8
+ return async t => {
9
+ return await testQueue.add(() => fn(t))
10
+ }
11
+ }
12
+
13
+ const dbPath = `./test.co.db`
14
+
15
+ test("store count observable", t => {
16
+ t.plan(5)
17
+
18
+ let down
19
+ let store
20
+
21
+ t.test("create store", async t => {
22
+ t.plan(1)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
25
+ t.pass('store created')
26
+ })
27
+
28
+ t.test("put objects", async t => {
29
+ t.plan(1)
30
+ await store.put({ v: 1, id: 'a' })
31
+ await store.put({ v: 3, id: 'c' })
32
+ t.pass('objects written')
33
+ })
34
+
35
+ let nextValueResolve
36
+ let gotNextValue
37
+ const getNextValue = () => {
38
+ if(gotNextValue) {
39
+ gotNextValue = false
40
+ return countObservable.value
41
+ }
42
+ return new Promise((resolve, reject) => nextValueResolve = resolve)
43
+ }
44
+ let countObservable
45
+ const countObserver = (signal, value, ...rest) => {
46
+ console.log("SIGNAL", signal, value, ...rest)
47
+ gotNextValue = true
48
+ if(nextValueResolve) nextValueResolve(countObservable.value)
49
+ }
50
+
51
+ t.test("observe count [a,z]", async t => {
52
+ t.plan(6)
53
+ countObservable = store.countObservable({ gte: 'a', lte: 'z' })
54
+ countObservable.observe(countObserver)
55
+ let values = await getNextValue()
56
+ t.deepEqual(values, 2, 'count value' )
57
+
58
+ t.test("remove object 'a' from observed count", queued(async t => {
59
+ t.plan(1)
60
+ await store.delete('a')
61
+ let values = await getNextValue()
62
+ t.deepEqual(values, 1, 'count value' )
63
+ }))
64
+
65
+ t.test("add object 'a' to observed count", queued(async t => {
66
+ t.plan(1)
67
+ await store.put({ id: 'a', v: 4 })
68
+ let values = await getNextValue()
69
+ t.deepEqual(values, 2, 'count value' )
70
+ }))
71
+
72
+ t.test("add object 'b' to observed count", queued(async t => {
73
+ t.plan(1)
74
+ await store.put({ id: 'b', v: 5 })
75
+ let values = await getNextValue()
76
+ t.deepEqual(values, 3, 'count value' )
77
+ }))
78
+
79
+ t.test("add object 'd' to observed count", queued(async t => {
80
+ t.plan(1)
81
+ await store.put({ id: 'd', v: 6 })
82
+ let values = await getNextValue()
83
+ t.deepEqual(values, 4, 'count value' )
84
+ }))
85
+
86
+ t.test("unobserve count", queued(async t => {
87
+ t.plan(1)
88
+ countObservable.unobserve(countObserver)
89
+ t.pass('unobserved')
90
+ }))
91
+ })
92
+
93
+ t.test("observe count (a,d)", async t => {
94
+ t.plan(6)
95
+ countObservable = store.countObservable({ gt: 'a', lt: 'd' })
96
+ countObservable.observe(countObserver)
97
+ let values = await getNextValue()
98
+ t.deepEqual(values, 2, 'count value' )
99
+
100
+ t.test("remove object 'd' outside observed count", queued(async t => {
101
+ t.plan(1)
102
+ await store.delete('d')
103
+ t.pass('deleted')
104
+ }))
105
+
106
+ t.test("remove object 'a' outside observed count", queued(async t => {
107
+ t.plan(1)
108
+ await store.delete('a')
109
+ t.pass('deleted')
110
+ }))
111
+
112
+ t.test("add object 'ab' to observed count", queued(async t => {
113
+ t.plan(1)
114
+ await store.put({ id: 'ab', v: 7 })
115
+ let values = await getNextValue()
116
+ t.deepEqual(values, 3, 'count value' )
117
+ }))
118
+
119
+ t.test("remove object 'ab' from observed count", queued(async t => {
120
+ t.plan(1)
121
+ await store.delete('ab')
122
+ let values = await getNextValue()
123
+ t.deepEqual(values, 2, 'count value' )
124
+ }))
125
+
126
+ t.test("unobserve count", queued(async t => {
127
+ t.plan(1)
128
+ countObservable.unobserve(countObserver)
129
+ t.pass('unobserved')
130
+ }))
131
+ })
132
+
133
+ t.test("close and remove database", queued(async t => {
134
+ t.plan(1)
135
+ await closeTestDown(down, dbPath)
136
+ t.pass('removed')
137
+ }))
138
+ })
@@ -1,15 +1,9 @@
1
1
  import test from 'tape'
2
- import levelup from 'levelup'
3
- import leveldown from 'leveldown'
4
- import encoding from 'encoding-down'
5
- import { rimraf } from 'rimraf'
6
2
  import PQueue from 'p-queue'
7
-
8
3
  import Store from '../lib/Store.js'
4
+ import { openTestDown, closeTestDown } from './utils.js'
9
5
 
10
6
  const dbPath = `./test.lro.db`
11
- rimraf.sync(dbPath)
12
- const level = levelup(encoding(leveldown(dbPath), { keyEncoding: 'ascii', valueEncoding: 'json' }))
13
7
 
14
8
  const testQueue = new PQueue({ concurrency: 1 })
15
9
  function queued(fn) {
@@ -21,11 +15,13 @@ function queued(fn) {
21
15
  test("store range observable", t => {
22
16
  t.plan(5)
23
17
 
18
+ let down
24
19
  let store
25
20
 
26
21
  t.test("create store", async t => {
27
22
  t.plan(1)
28
- store = new Store(level)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
29
25
  t.pass('store created')
30
26
  })
31
27
 
@@ -71,7 +67,6 @@ test("store range observable", t => {
71
67
  console.error("TEST STARTED")
72
68
  await store.delete('b_3')
73
69
  objects.splice(3, 1)
74
- await getNextValue() // one shorter
75
70
  let values = await getNextValue()
76
71
  t.deepEqual(values, objects.slice(0, 5), 'range value' )
77
72
  }))
@@ -106,7 +101,6 @@ test("store range observable", t => {
106
101
  t.plan(1)
107
102
  await store.delete('b_32')
108
103
  objects.splice(4, 1)
109
- await getNextValue()
110
104
  let values = await getNextValue()
111
105
  t.deepEqual(values, objects.slice(0, 5), 'range value' )
112
106
  }))
@@ -157,7 +151,6 @@ test("store range observable", t => {
157
151
  t.plan(1)
158
152
  await store.delete('b_12')
159
153
  objects.splice(2, 1)
160
- await getNextValue()
161
154
  let values = await getNextValue()
162
155
  t.deepEqual(values, objects.slice(1,4) , 'range value' )
163
156
  }))
@@ -169,10 +162,9 @@ test("store range observable", t => {
169
162
  }))
170
163
  })
171
164
 
172
- t.test("close and remove database", async t => {
165
+ t.test("close and remove database", queued(async t => {
173
166
  t.plan(1)
174
- await level.close()
175
- await rimraf(dbPath)
167
+ await closeTestDown(down, dbPath)
176
168
  t.pass('removed')
177
- })
169
+ }))
178
170
  })
@@ -1,16 +1,10 @@
1
1
  import test from 'tape'
2
- import levelup from 'levelup'
3
- import leveldown from 'leveldown'
4
- import encoding from 'encoding-down'
5
- import { rimraf } from 'rimraf'
6
-
7
2
  import Store from '../lib/Store.js'
3
+ import { openTestDown, closeTestDown } from './utils.js'
4
+ import PQueue from 'p-queue'
8
5
 
9
6
  const dbPath = `./test.lrro.db`
10
- rimraf.sync(dbPath)
11
- const level = levelup(encoding(leveldown(dbPath), { keyEncoding: 'ascii', valueEncoding: 'json' }))
12
7
 
13
- import PQueue from 'p-queue'
14
8
  const testQueue = new PQueue({ concurrency: 1 })
15
9
  function queued(fn) {
16
10
  return async t => {
@@ -21,11 +15,13 @@ function queued(fn) {
21
15
  test("store range observable", t => {
22
16
  t.plan(5)
23
17
 
18
+ let down
24
19
  let store
25
20
 
26
21
  t.test("create store", async t => {
27
22
  t.plan(1)
28
- store = new Store(level)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
29
25
  t.pass('store created')
30
26
  })
31
27
 
@@ -70,7 +66,6 @@ test("store range observable", t => {
70
66
  t.plan(1)
71
67
  await store.delete('b_6')
72
68
  objects.splice(3, 1)
73
- await getNextValue() // one shorter
74
69
  let values = await getNextValue()
75
70
  t.deepEqual(values, objects.slice(0, 5), 'range value' )
76
71
  }))
@@ -105,7 +100,6 @@ test("store range observable", t => {
105
100
  t.plan(1)
106
101
  await store.delete('b_58')
107
102
  objects.splice(4, 1)
108
- await getNextValue()
109
103
  let values = await getNextValue()
110
104
  t.deepEqual(values, objects.slice(0, 5), 'range value' )
111
105
  }))
@@ -156,7 +150,6 @@ test("store range observable", t => {
156
150
  t.plan(1)
157
151
  await store.delete('b_48')
158
152
  objects.splice(6, 1)
159
- await getNextValue()
160
153
  let values = await getNextValue()
161
154
  t.deepEqual(values, objects.slice(6,9) , 'range value' )
162
155
  }))
@@ -168,10 +161,9 @@ test("store range observable", t => {
168
161
  }))
169
162
  })
170
163
 
171
- t.test("close and remove database", async t => {
164
+ t.test("close and remove database", queued(async t => {
172
165
  t.plan(1)
173
- await level.close()
174
- await rimraf(dbPath)
166
+ await closeTestDown(down, dbPath)
175
167
  t.pass('removed')
176
- })
168
+ }))
177
169
  })
@@ -1,23 +1,27 @@
1
1
  import test from 'tape'
2
- import levelup from 'levelup'
3
- import leveldown from 'leveldown'
4
- import encoding from 'encoding-down'
5
- import { rimraf } from 'rimraf'
6
-
7
2
  import Store from '../lib/Store.js'
3
+ import { openTestDown, closeTestDown } from './utils.js'
4
+ import PQueue from 'p-queue'
5
+
6
+ const testQueue = new PQueue({ concurrency: 1 })
7
+ function queued(fn) {
8
+ return async t => {
9
+ return await testQueue.add(() => fn(t))
10
+ }
11
+ }
8
12
 
9
13
  const dbPath = `./test.oo.db`
10
- rimraf.sync(dbPath)
11
- const level = levelup(encoding(leveldown(dbPath), { keyEncoding: 'ascii', valueEncoding: 'json' }))
12
14
 
13
15
  test("store object observable", t => {
14
16
  t.plan(3)
15
17
 
18
+ let down
16
19
  let store
17
20
 
18
21
  t.test("create store", async t => {
19
22
  t.plan(1)
20
- store = new Store(level)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
21
25
  t.pass('store created')
22
26
  })
23
27
 
@@ -45,25 +49,24 @@ test("store object observable", t => {
45
49
  let value = await getNextValue()
46
50
  t.deepEqual(value, null, 'found null')
47
51
 
48
- t.test("add object A", async t => {
52
+ t.test("add object A", queued(async t => {
49
53
  t.plan(1)
50
54
  await store.put({ id: 'A', a: 1 })
51
55
  let value = await getNextValue()
52
56
  t.deepEqual(value, { id: 'A', a: 1 } , 'found object' )
53
- })
57
+ }))
54
58
 
55
- t.test("delete object A", async t => {
59
+ t.test("delete object A", queued(async t => {
56
60
  t.plan(1)
57
61
  await store.delete('A')
58
62
  let value = await getNextValue()
59
63
  t.deepEqual(value, null , 'found null' )
60
- })
64
+ }))
61
65
  })
62
66
 
63
- t.test("close and remove database", async t => {
67
+ t.test("close and remove database", queued(async t => {
64
68
  t.plan(1)
65
- await level.close()
66
- await rimraf(dbPath)
69
+ await closeTestDown(down, dbPath)
67
70
  t.pass('removed')
68
- })
69
- })
71
+ }))
72
+ })
@@ -0,0 +1,50 @@
1
+ import test from 'tape'
2
+ import Store from '../lib/Store.js'
3
+ import { openTestDown, closeTestDown } from './utils.js'
4
+ import PQueue from 'p-queue'
5
+
6
+ const testQueue = new PQueue({ concurrency: 1 })
7
+ function queued(fn) {
8
+ return async t => {
9
+ return await testQueue.add(() => fn(t))
10
+ }
11
+ }
12
+
13
+ const dbPath = `./test.px.db`
14
+
15
+ test("store prefix isolation and clear", t => {
16
+ t.plan(3)
17
+
18
+ let down
19
+ let storeA
20
+ let storeB
21
+
22
+ t.test("create stores", queued(async t => {
23
+ t.plan(1)
24
+ down = await openTestDown(dbPath)
25
+ storeA = new Store(down, { prefix: 'a\x00' })
26
+ storeB = new Store(down, { prefix: 'b\x00' })
27
+ t.pass('stores created')
28
+ }))
29
+
30
+ t.test("prefix isolation", queued(async t => {
31
+ t.plan(8)
32
+ await storeA.put({ id: 'k', v: 1 })
33
+ await storeB.put({ id: 'k', v: 2 })
34
+ t.deepEqual(await storeA.objectGet('k'), { id: 'k', v: 1 }, 'store A own value')
35
+ t.deepEqual(await storeB.objectGet('k'), { id: 'k', v: 2 }, 'store B own value')
36
+ t.deepEqual(await storeA.rangeGet({}), [{ id: 'k', v: 1 }], 'store A range')
37
+ t.deepEqual(await storeB.rangeGet({}), [{ id: 'k', v: 2 }], 'store B range')
38
+ t.equal(await storeA.countGet({}), 1, 'store A count')
39
+ t.equal(await storeB.countGet({}), 1, 'store B count')
40
+ await storeA.clear()
41
+ t.equal(await storeA.objectGet('k'), null, 'store A cleared')
42
+ t.deepEqual(await storeB.objectGet('k'), { id: 'k', v: 2 }, 'store B intact after A clear')
43
+ }))
44
+
45
+ t.test("close and remove database", queued(async t => {
46
+ t.plan(1)
47
+ await closeTestDown(down, dbPath)
48
+ t.pass('removed')
49
+ }))
50
+ })
@@ -1,12 +1,8 @@
1
1
  import test from 'tape'
2
- import levelup from 'levelup'
3
- import leveldown from 'leveldown'
4
- import encoding from 'encoding-down'
5
- import { rimraf } from 'rimraf'
6
-
7
2
  import Store from '../lib/Store.js'
8
-
3
+ import { openTestDown, closeTestDown } from './utils.js'
9
4
  import PQueue from 'p-queue'
5
+
10
6
  const testQueue = new PQueue({ concurrency: 1 })
11
7
  function queued(fn) {
12
8
  return async t => {
@@ -15,17 +11,17 @@ function queued(fn) {
15
11
  }
16
12
 
17
13
  const dbPath = `./test.ro.db`
18
- rimraf.sync(dbPath)
19
- const level = levelup(encoding(leveldown(dbPath), { keyEncoding: 'ascii', valueEncoding: 'json' }))
20
14
 
21
15
  test("store range observable", t => {
22
16
  t.plan(5)
23
17
 
18
+ let down
24
19
  let store
25
20
 
26
21
  t.test("create store", async t => {
27
22
  t.plan(1)
28
- store = new Store(level)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
29
25
  t.pass('store created')
30
26
  })
31
27
 
@@ -136,10 +132,9 @@ test("store range observable", t => {
136
132
 
137
133
  })
138
134
 
139
- t.test("close and remove database", async t => {
135
+ t.test("close and remove database", queued(async t => {
140
136
  t.plan(1)
141
- await level.close()
142
- await rimraf(dbPath)
137
+ await closeTestDown(down, dbPath)
143
138
  t.pass('removed')
144
- })
139
+ }))
145
140
  })
@@ -1,12 +1,8 @@
1
1
  import test from 'tape'
2
- import levelup from 'levelup'
3
- import leveldown from 'leveldown'
4
- import encoding from 'encoding-down'
5
- import { rimraf } from 'rimraf'
6
-
7
2
  import Store from '../lib/Store.js'
8
-
3
+ import { openTestDown, closeTestDown } from './utils.js'
9
4
  import PQueue from 'p-queue'
5
+
10
6
  const testQueue = new PQueue({ concurrency: 1 })
11
7
  function queued(fn) {
12
8
  return async t => {
@@ -15,17 +11,17 @@ function queued(fn) {
15
11
  }
16
12
 
17
13
  const dbPath = `./test.rro.db`
18
- rimraf.sync(dbPath)
19
- const level = levelup(encoding(leveldown(dbPath), { keyEncoding: 'ascii', valueEncoding: 'json' }))
20
14
 
21
15
  test("store range observable", t => {
22
16
  t.plan(5)
23
17
 
18
+ let down
24
19
  let store
25
20
 
26
21
  t.test("create store", async t => {
27
22
  t.plan(1)
28
- store = new Store(level)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
29
25
  t.pass('store created')
30
26
  })
31
27
 
@@ -135,10 +131,9 @@ test("store range observable", t => {
135
131
  }))
136
132
  })
137
133
 
138
- t.test("close and remove database", async t => {
134
+ t.test("close and remove database", queued(async t => {
139
135
  t.plan(1)
140
- await level.close()
141
- await rimraf(dbPath)
136
+ await closeTestDown(down, dbPath)
142
137
  t.pass('removed')
143
- })
138
+ }))
144
139
  })
@@ -1,12 +1,8 @@
1
1
  import test from 'tape'
2
- import levelup from 'levelup'
3
- import leveldown from 'leveldown'
4
- import encoding from 'encoding-down'
5
- import { rimraf } from 'rimraf'
6
-
7
2
  import Store from '../lib/Store.js'
8
-
3
+ import { openTestDown, closeTestDown } from './utils.js'
9
4
  import PQueue from 'p-queue'
5
+
10
6
  const testQueue = new PQueue({ concurrency: 1 })
11
7
  function queued(fn) {
12
8
  return async t => {
@@ -15,22 +11,22 @@ function queued(fn) {
15
11
  }
16
12
 
17
13
  const dbPath = `./test.nr.db`
18
- rimraf.sync(dbPath)
19
- const level = levelup(encoding(leveldown(dbPath), { keyEncoding: 'ascii', valueEncoding: 'json' }))
20
14
 
21
15
  test("store non-reactive properties", t => {
22
16
  t.plan(3)
23
17
 
18
+ let down
24
19
  let store
25
20
 
26
21
  t.test("create store", async t => {
27
22
  t.plan(1)
28
- store = new Store(level)
23
+ down = await openTestDown(dbPath)
24
+ store = new Store(down, { prefix: 't\x00' })
29
25
  t.pass('store created')
30
26
  })
31
27
 
32
28
  t.test("non reactive operations", async t => {
33
- t.plan(15)
29
+ t.plan(20)
34
30
 
35
31
  t.test("put value", queued(async t => {
36
32
  t.plan(1)
@@ -57,24 +53,48 @@ test("store non-reactive properties", t => {
57
53
  t.deepEqual(values, [ { v: 1, id: 'a' }, { v: 2, id: 'b' }, { v: 3, id: 'c' } ], 'range read' )
58
54
  }))
59
55
 
56
+ t.test("count range [a,c]", queued(async t => {
57
+ t.plan(1)
58
+ let values = await store.countGet({ gte: 'a', lte: 'c' })
59
+ t.deepEqual(values, 3, 'range count' )
60
+ }))
61
+
60
62
  t.test("get reverse range [c,a]", queued(async t => {
61
63
  t.plan(1)
62
64
  let values = await store.rangeGet({ gte: 'a', lte: 'c', reverse: true })
63
65
  t.deepEqual(values, [ { v: 3, id: 'c' }, { v: 2, id: 'b' }, { v: 1, id: 'a' } ], 'range read' )
64
66
  }))
65
67
 
68
+ t.test("count reverse range [a,c]", queued(async t => {
69
+ t.plan(1)
70
+ let values = await store.countGet({ gte: 'a', lte: 'c', reverse: true })
71
+ t.deepEqual(values, 3, 'range count' )
72
+ }))
73
+
66
74
  t.test("get range [a,c] with limit 2", queued(async t => {
67
75
  t.plan(1)
68
76
  let values = await store.rangeGet({ gte: 'a', lte: 'c', limit: 2 })
69
77
  t.deepEqual(values, [ { v: 1, id: 'a' }, { v: 2, id: 'b' } ], 'range read' )
70
78
  }))
71
79
 
80
+ t.test("count range [a,c] with limit 2", queued(async t => {
81
+ t.plan(1)
82
+ let values = await store.countGet({ gte: 'a', lte: 'c', limit: 2 })
83
+ t.deepEqual(values, 2, 'range read' )
84
+ }))
85
+
72
86
  t.test("get reverse range [c,a] with limit 2", queued(async t => {
73
87
  t.plan(1)
74
88
  let values = await store.rangeGet({ gte: 'a', lte: 'c', reverse: true, limit: 2 })
75
89
  t.deepEqual(values, [ { v: 3, id: 'c' }, { v: 2, id: 'b' } ], 'range read' )
76
90
  }))
77
91
 
92
+ t.test("get reverse count [c,a] with limit 2", queued(async t => {
93
+ t.plan(1)
94
+ let values = await store.countGet({ gte: 'a', lte: 'c', reverse: true, limit: 2 })
95
+ t.deepEqual(values, 2, 'range read' )
96
+ }))
97
+
78
98
  t.test("get range (a,c]", queued(async t => {
79
99
  t.plan(1)
80
100
  let values = await store.rangeGet({ gt: 'a', lte: 'c' })
@@ -122,12 +142,21 @@ test("store non-reactive properties", t => {
122
142
  let values = await store.rangeGet({ gte: 'a', lte: 'c' })
123
143
  t.deepEqual(values, [ { v: 1, id: 'a' }, { v: 3, id: 'c' } ], 'range read' )
124
144
  }))
145
+
146
+ t.test("rangeDelete keysOnly", queued(async t => {
147
+ t.plan(3)
148
+ await store.put({ id: 'd', v: 4 })
149
+ await store.put({ id: 'e', v: 5 })
150
+ const result = await store.rangeDelete({ gte: 'd', lt: 'f' }, { keysOnly: true })
151
+ t.equal(result.count, 2, 'deleted 2 keys without reading values')
152
+ t.equal(await store.objectGet('d'), null, 'd gone')
153
+ t.equal(await store.objectGet('e'), null, 'e gone')
154
+ }))
125
155
  })
126
156
 
127
- t.test("close and remove database", async t => {
157
+ t.test("close and remove database", queued(async t => {
128
158
  t.plan(1)
129
- await level.close()
130
- await rimraf(dbPath)
159
+ await closeTestDown(down, dbPath)
131
160
  t.pass('removed')
132
- })
161
+ }))
133
162
  })
@@ -0,0 +1,33 @@
1
+ import test from 'tape'
2
+ import Store from '../lib/Store.js'
3
+ import { openTestDown, closeTestDown } from './utils.js'
4
+
5
+ const dbPath = `./test.st.db`
6
+
7
+ test("store stat", t => {
8
+ t.plan(3)
9
+
10
+ let down
11
+ let store
12
+
13
+ t.test("create store", async t => {
14
+ t.plan(1)
15
+ down = await openTestDown(dbPath)
16
+ store = new Store(down, { prefix: 't\x00' })
17
+ t.pass('store created')
18
+ })
19
+
20
+ t.test("approximate size after put", async t => {
21
+ t.plan(2)
22
+ await store.put({ id: 'a', v: 1 })
23
+ const s = await store.stat()
24
+ t.equal(s.available, true, 'stat available')
25
+ t.equal(typeof s.usedBytes, 'number', 'usedBytes is a number')
26
+ })
27
+
28
+ t.test("close and remove database", async t => {
29
+ t.plan(1)
30
+ await closeTestDown(down, dbPath)
31
+ t.pass('removed')
32
+ })
33
+ })
package/tests/utils.js ADDED
@@ -0,0 +1,21 @@
1
+ import { createRequire } from 'module'
2
+ import { rimraf } from 'rimraf'
3
+
4
+ const require = createRequire(import.meta.url)
5
+ const leveldown = require('leveldown')
6
+
7
+ export async function openTestDown(dbPath) {
8
+ await rimraf(dbPath)
9
+ const down = leveldown(dbPath)
10
+ await new Promise((resolve, reject) => {
11
+ down.open({ createIfMissing: true }, err => err ? reject(err) : resolve())
12
+ })
13
+ return down
14
+ }
15
+
16
+ export async function closeTestDown(down, dbPath) {
17
+ await new Promise((resolve, reject) => {
18
+ down.close(err => err ? reject(err) : resolve())
19
+ })
20
+ await rimraf(dbPath)
21
+ }