@live-change/db-store-localstorage 0.6.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/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./lib/Store.js')
package/lib/Store.js ADDED
@@ -0,0 +1,416 @@
1
+ const IntervalTree = require('@live-change/interval-tree').default
2
+ const ReactiveDao = require("@live-change/dao")
3
+ const { BroadcastChannel, createLeaderElection } = require('broadcast-channel')
4
+ const storage = require('./storage.js')
5
+
6
+ class ObjectObservable extends ReactiveDao.ObservableValue {
7
+ constructor(store, key) {
8
+ super()
9
+ this.store = store
10
+ this.key = key
11
+
12
+ this.disposed = false
13
+ this.ready = false
14
+ this.respawnId = 0
15
+
16
+ this.forward = null
17
+
18
+ this.readPromise = this.startReading()
19
+ }
20
+
21
+ async startReading() {
22
+ this.store.objectObservables.set(this.key, this)
23
+ this.value = await this.store.objectGet(this.key)
24
+ this.fireObservers('set', this.value)
25
+ }
26
+
27
+ async set(value) {
28
+ await this.readPromise
29
+ this.value = value
30
+ this.fireObservers('set', this.value)
31
+ }
32
+
33
+ dispose() {
34
+ if(this.forward) {
35
+ this.forward.unobserve(this)
36
+ this.forward = null
37
+ return
38
+ }
39
+
40
+ this.disposed = true
41
+ this.respawnId++
42
+ if(this.changesStream) this.changesStream.close()
43
+ this.changesStream = null
44
+
45
+ this.store.objectObservables.delete(this.key)
46
+ }
47
+
48
+ respawn() {
49
+ const existingObservable = this.store.objectObservables.get(this.key)
50
+ if(existingObservable) {
51
+ this.forward = existingObservable
52
+ this.forward.observe(this)
53
+ return
54
+ }
55
+
56
+ this.respawnId++
57
+ if(this.changesStream) this.changesStream.close()
58
+ this.ready = false
59
+ this.disposed = false
60
+ this.startReading()
61
+ }
62
+ }
63
+
64
+ class RangeObservable extends ReactiveDao.ObservableList {
65
+ constructor(store, range) {
66
+ super()
67
+ this.store = store
68
+ this.range = range
69
+
70
+ this.disposed = false
71
+ this.ready = false
72
+ this.respawnId = 0
73
+ this.refillId = 0
74
+ this.refillPromise = null
75
+
76
+ this.forward = null
77
+
78
+ this.rangeKey = JSON.stringify(this.range)
79
+ this.rangeDescr = [ this.range.gt || this.range.gte || '', this.range.lt || this.range.lte || '\xFF\xFF\xFF\xFF']
80
+
81
+ this.readPromise = this.startReading()
82
+ }
83
+
84
+ async startReading() {
85
+ this.store.rangeObservables.set(this.rangeKey, this)
86
+ const treeInsert = this.rangeDescr
87
+ const inserted = this.store.rangeObservablesTree.insert(treeInsert, this)
88
+ if(this.store.rangeObservablesTree.search([this.low, this.high]).length == 0) {
89
+ console.error("TREE NOT WORKING")
90
+ console.log("INSERTED", JSON.stringify(treeInsert),
91
+ "TO TREE", this.store.rangeObservablesTree)
92
+ console.log("FOUND", this.store.rangeObservablesTree.search(this.rangeDescr))
93
+ console.log("ALL RECORDS", this.store.rangeObservablesTree.search(['', '\xFF\xFF\xFF\xFF']))
94
+ process.exit(1)
95
+ }
96
+ this.set(await this.store.rangeGet(this.range))
97
+ }
98
+
99
+ async putObject(object, oldObject) {
100
+ await this.readPromise
101
+ const id = object.id
102
+ if(this.range.gt && !(id > this.range.gt)) return
103
+ if(this.range.lt && !(id < this.range.lt)) return
104
+ if(!this.range.reverse) {
105
+ if(this.range.limit && this.list.length == this.range.limit) {
106
+ for(let i = 0, l = this.list.length; i < l; i++) {
107
+ if(this.list[i].id == id) {
108
+ this.list.splice(i, 1, object)
109
+ this.fireObservers('putByField', 'id', id, object, false, oldObject)
110
+ return
111
+ } else if(this.list[i].id > id) {
112
+ this.list.splice(i, 0, object)
113
+ this.fireObservers('putByField', 'id', id, object, false, oldObject)
114
+ const popped = this.list.pop()
115
+ this.fireObservers('removeByField', 'id', popped.id, popped)
116
+ return
117
+ }
118
+ }
119
+ } else {
120
+ this.putByField('id', object.id, object, false, oldObject)
121
+ }
122
+ } else {
123
+ if(this.range.limit && this.list.length == this.range.limit) {
124
+ for(let i = this.list.length-1; i >= 0; i--) {
125
+ if(this.list[i].id == id) {
126
+ this.list.splice(i, 1, object)
127
+ this.fireObservers('putByField', 'id', id, object, true, oldObject)
128
+ return
129
+ } else if(this.list[i].id > id) {
130
+ if(i == this.list.length - 1) return // last element is bigger, do nothing
131
+ this.list.splice(i + 1, 0, object)
132
+ this.fireObservers('putByField', 'id', id, object, true, oldObject)
133
+ const popped = this.list.pop()
134
+ this.fireObservers('removeByField', 'id', popped.id, popped)
135
+ return
136
+ }
137
+ }
138
+ this.list.splice(0, 0, object)
139
+ this.fireObservers('putByField', 'id', id, object, true)
140
+ const popped = this.list.pop()
141
+ this.fireObservers('removeByField', 'id', popped.id, popped)
142
+ } else {
143
+ this.putByField('id', id, object, true, oldObject)
144
+ }
145
+ }
146
+ }
147
+
148
+ refillDeleted(from, limit) {
149
+ this.refillId ++
150
+ const refillId = this.refillId
151
+ let promise = (async () => {
152
+ let req
153
+ if(!this.range.reverse) {
154
+ req = { gt: from, limit }
155
+ if(this.range.lt) req.lt = this.range.lt
156
+ if(this.range.lte) req.lte = this.range.lte
157
+ } else {
158
+ req = { lt: from, limit, reverse: true }
159
+ if(this.range.gt) req.gt = this.range.gt
160
+ if(this.range.gte) req.gte = this.range.gte
161
+ }
162
+ const objects = await this.store.rangeGet(req)
163
+ if(this.refillId != refillId) return this.refillPromise
164
+ for(let object of objects) this.push(object)
165
+ this.refillPromise = null
166
+ })()
167
+ this.refillPromise = promise
168
+ return promise
169
+ }
170
+
171
+ async deleteObject(object) {
172
+ if(!object) return;
173
+ await this.readPromise
174
+ const id = object.id
175
+ if(this.range.gt && !(id > this.range.gt)) return
176
+ if(this.range.lt && !(id < this.range.lt)) return
177
+ if(this.range.limit && (this.list.length == this.range.limit || this.refillPromise)) {
178
+ let exists
179
+ let last
180
+ for(let obj of this.list) {
181
+ if(obj.id == id) exists = obj
182
+ else last = obj
183
+ }
184
+ this.removeByField('id', id, object)
185
+ if(exists) await this.refillDeleted(
186
+ last && last.id || (this.reverse ? this.range.lt || this.range.lte : this.range.gt || this.range.gte),
187
+ this.range.limit - this.list.length)
188
+ } else {
189
+ this.removeByField('id', id, object)
190
+ }
191
+ }
192
+
193
+ dispose() {
194
+ if(this.forward) {
195
+ this.forward.unobserve(this)
196
+ this.forward = null
197
+ return
198
+ }
199
+
200
+ this.disposed = true
201
+ this.respawnId++
202
+ this.changesStream = null
203
+
204
+ this.store.rangeObservables.delete(this.rangeKey)
205
+ let removed = this.store.rangeObservablesTree.remove(this.rangeDescr, this)
206
+ }
207
+
208
+ respawn() {
209
+ const existingObservable = this.store.rangeObservables.get(JSON.stringify(this.range))
210
+ if(existingObservable) {
211
+ this.forward = existingObservable
212
+ this.forward.observe(this)
213
+ return
214
+ }
215
+
216
+ this.respawnId++
217
+ this.ready = false
218
+ this.disposed = false
219
+ this.startReading()
220
+ }
221
+ }
222
+
223
+ class Store {
224
+ constructor(dbName, storeName, type) {
225
+ if(!dbName) throw new Error("dbName argument is required")
226
+ if(!storeName) throw new Error("storeName argument is required")
227
+ if(!type) throw new Error("type argument is required")
228
+
229
+ this.dbName = dbName
230
+ this.storeName = storeName
231
+
232
+ this.prefix = `lcdb/${dbName}/${storeName}/`
233
+
234
+ this.finished = false
235
+
236
+ this.storage = storage[type]
237
+ if(!this.storage) throw new Error("Unknown storage type: " + type)
238
+
239
+ this.channel = null
240
+
241
+ this.objectObservables = new Map()
242
+ this.rangeObservables = new Map()
243
+ this.rangeObservablesTree = new IntervalTree()
244
+ }
245
+
246
+ async openChannel() {
247
+ this.channel = new BroadcastChannel(`lc-db-${this.dbName}-${this.storeName}`, {
248
+ idb: {
249
+ onclose: () => {
250
+ if(this.finished) return
251
+ this.channel.close()
252
+ this.openChannel()
253
+ }
254
+ }
255
+ })
256
+ this.channel.onmessage = message => this.handleChannelMessage(message)
257
+ }
258
+ async open() {
259
+ await this.openChannel()
260
+ }
261
+ async close() {
262
+ this.finished = true
263
+ await this.channel.close()
264
+ }
265
+
266
+ async deleteDb() {
267
+ if(!this.finished) await this.close()
268
+ const all = await this.storage.getKeys()
269
+ const own = all.filter(key => key.startsWith(this.prefix))
270
+ await this.storage.remove(own)
271
+ }
272
+
273
+ async handleChannelMessage(message) {
274
+ console.log("handleChannelMessage", message)
275
+ switch(message.type) {
276
+ case 'put' : {
277
+ const { object, oldObject } = message
278
+ const id = object?.id || oldObject?.id
279
+ if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
280
+ const objectObservable = this.objectObservables.get(id)
281
+ if(objectObservable) objectObservable.set(object, oldObject)
282
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
283
+ for(const rangeObservable of rangeObservables) {
284
+ rangeObservable.putObject(object, oldObject)
285
+ }
286
+ } break
287
+ case 'delete' : {
288
+ const { object } = message
289
+ const id = object?.id
290
+ if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
291
+ const objectObservable = this.objectObservables.get(id)
292
+ if(objectObservable) objectObservable.set(null)
293
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
294
+ for(const rangeObservable of rangeObservables) {
295
+ rangeObservable.deleteObject(object)
296
+ }
297
+ } break
298
+ default:
299
+ throw new Error("unknown message type " + message.type + ' in message ' + JSON.stringify(message))
300
+ }
301
+ }
302
+
303
+ async objectGet(id) {
304
+ if(!id) throw new Error("key is required")
305
+ if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
306
+ return JSON.parse(await this.storage.getItem(this.prefix + id) || 'null')
307
+ }
308
+
309
+ objectObservable(key) {
310
+ let observable = this.objectObservables.get(key)
311
+ if(observable) return observable
312
+ observable = new ObjectObservable(this, key)
313
+ return observable
314
+ }
315
+
316
+ async rangeGet(range) {
317
+ if(!range) throw new Error("range not defined")
318
+ const { gt, gte, lt, lte, limit, reverse } = range
319
+ const all = (await this.storage.getKeys())
320
+ .filter(key => key.startsWith(this.prefix))
321
+ .sort()
322
+ if(range.reverse) all.reverse()
323
+ const keys = all.filter(key => {
324
+ const id = key.slice(this.prefix.length)
325
+ if(gt && !(id > gt)) return false
326
+ if(gte && !(id >= gte)) return false
327
+ if(lt && !(id < lt)) return false
328
+ if(lte && !(id <= lte)) return false
329
+ return true
330
+ }).slice(0, limit)
331
+ const objects = (await this.storage.getValues(keys))
332
+ .map(json => JSON.parse(json))
333
+ .sort((a, b) => (a.id > b.id) ? 1 : ((b.id > a.id) ? -1 : 0))
334
+ if(range.reverse) objects.reverse()
335
+ return objects
336
+ }
337
+
338
+ rangeObservable(range) {
339
+ let observable = this.rangeObservables.get(JSON.stringify(range))
340
+ if(observable) return observable
341
+ observable = new RangeObservable(this, range)
342
+ return observable
343
+ }
344
+
345
+ async countGet(range) {
346
+ if(!range) throw new Error("range not defined")
347
+ const { gt, gte, lt, lte, limit, reverse } = range
348
+ const all = await this.storage.getKeys()
349
+ const keys = all.filter(key => {
350
+ if(!key.startsWith(this.prefix)) return false
351
+ const id = key.slice(this.prefix.length)
352
+ if(gt && !(id > gt)) return false
353
+ if(gte && !(id >= gte)) return false
354
+ if(lt && !(id < lt)) return false
355
+ if(lte && !(id <= lte)) return false
356
+ return true
357
+ }).slice(0, limit)
358
+ return keys.length
359
+ }
360
+
361
+ countObservable(range) {
362
+ let observable = this.countObservables.get(JSON.stringify(range))
363
+ if(observable) return observable
364
+ observable = new CountObservable(this, range)
365
+ return observable
366
+ }
367
+
368
+ async rangeDelete(range) {
369
+ if(!range) throw new Error("range not defined")
370
+ const { gt, gte, lt, lte, limit, reverse } = range
371
+ const all = await this.storage.getKeys()
372
+ const keys = all.filter(key => {
373
+ if(!key.startsWith(this.prefix)) return false
374
+ const id = key.slice(this.prefix.length)
375
+ if(gt && !(id > gt)) return false
376
+ if(gte && !(id >= gte)) return false
377
+ if(lt && !(id < lt)) return false
378
+ if(lte && !(id <= lte)) return false
379
+ return true
380
+ }).slice(0, limit)
381
+ await this.storage.delete(keys)
382
+ return { count: keys.length, last: keys[keys.length - 1] }
383
+ }
384
+
385
+ async put(object) {
386
+ const id = object.id
387
+ if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
388
+ const oldObject = JSON.parse(await this.storage.getItem(this.prefix + id) || 'null')
389
+ await this.storage.setItem(this.prefix + id, JSON.stringify(object))
390
+ const objectObservable = this.objectObservables.get(id)
391
+ if(objectObservable) objectObservable.set(object, oldObject)
392
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
393
+ for(const rangeObservable of rangeObservables) {
394
+ rangeObservable.putObject(object, oldObject)
395
+ }
396
+ this.channel.postMessage({ type: "put", object, oldObject })
397
+ return oldObject
398
+ }
399
+
400
+ async delete(id) {
401
+ if(typeof id != 'string') throw new Error(`ID is not string: ${JSON.stringify(id)}`)
402
+ const object = JSON.parse(await this.storage.getItem(this.prefix + id))
403
+ await this.storage.removeItem(this.prefix + id)
404
+ const objectObservable = this.objectObservables.get(id)
405
+ if(objectObservable) objectObservable.set(null)
406
+ const rangeObservables = this.rangeObservablesTree.search([id, id])
407
+ for(const rangeObservable of rangeObservables) {
408
+ rangeObservable.deleteObject(object)
409
+ }
410
+ this.channel.postMessage({ type: "delete", object })
411
+ return object
412
+ }
413
+
414
+ }
415
+
416
+ module.exports = Store
package/lib/storage.js ADDED
@@ -0,0 +1,158 @@
1
+ let local
2
+ let session
3
+
4
+ function useWebStorage(storage) {
5
+ return {
6
+ setItem: async (key, value) => Promise.resolve(storage.setItem(key, JSON.stringify(value))),
7
+ getItem: async (key) => {
8
+ const json = storage.getItem(key)
9
+ return Promise.resolve(json && JSON.parse(json))
10
+ },
11
+ removeItem: async (key) => Promise.resolve(storage.removeItem(key)),
12
+ clear: async () => Promise.resolve(storage.clear()),
13
+ set: async (keys) => {
14
+ for(let key in keys) {
15
+ storage.setItem(key, JSON.stringify(keys[key]))
16
+ }
17
+ return Promise.resolve()
18
+ },
19
+ get: async (keys) => {
20
+ if(!keys) {
21
+ const result = {}
22
+ for(let i = 0; i<storage.length; i++) {
23
+ const key = storage.key(i)
24
+ result[key] = JSON.parse(storage.getItem(key))
25
+ }
26
+ return Promise.resolve(result)
27
+ }
28
+ const result = {}
29
+ const keysList = typeof keys == 'string' ? keys : (Array.isArray(keys) ? keys : Object.keys(keys))
30
+ for(let key of keysList) {
31
+ const item = storage.getItem(key)
32
+ result[key] = (item && JSON.parse(item)) ?? (typeof keys == 'object' ? keys[key] : undefined)
33
+ }
34
+ return Promise.resolve(result)
35
+ },
36
+ getValues: async (keys) => {
37
+ if(!keys) {
38
+ const result = new Array(storage.length)
39
+ for(let i = 0; i<storage.length; i++) {
40
+ const key = storage.key(i)
41
+ result[i] = JSON.parse(storage.getItem(key))
42
+ }
43
+ return Promise.resolve(result)
44
+ }
45
+ const result = new Array(keys.length)
46
+ for(let i = 0; i<keys.length; i++) {
47
+ const key = keys[i]
48
+ const item = storage.getItem(key)
49
+ result[i] = (item && JSON.parse(item)) ?? (typeof keys == 'object' ? keys[key] : undefined)
50
+ }
51
+ return Promise.resolve(result)
52
+ },
53
+ getKeys: async () => {
54
+ const result = []
55
+ for(let i = 0; i<storage.length; i++) {
56
+ result.push(storage.key(i))
57
+ }
58
+ return Promise.resolve(result)
59
+ },
60
+ remove: async (keys) => {
61
+ for(let key of keys) {
62
+ storage.removeItem(key)
63
+ }
64
+ return Promise.resolve()
65
+ }
66
+ }
67
+ }
68
+
69
+ function useExtensionStorage(storage) {
70
+ return {
71
+ set: async (keys) => storage.set(keys),
72
+ get: async (keys) => storage.get(keys),
73
+ getValues: async (keys) => Object.values(await storage.get(keys)),
74
+ getKeys: async () => Object.keys(await storage.get()),
75
+ remove: async (keys) => storage.remove(keys),
76
+ clear: async () => storage.clear(),
77
+ setItem: async (key, value) => storage.set({ [key]: value }),
78
+ getItem: async (key) => storage.get(key)[key],
79
+ removeItem: async (key) => storage.remove(key),
80
+ }
81
+ }
82
+
83
+ function useTestStorage() {
84
+ const storage = new Map()
85
+ return {
86
+ setItem: async (key, value) => Promise.resolve(storage.set(key, value)),
87
+ getItem: async (key) => Promise.resolve(storage.get(key)),
88
+ removeItem: async (key) => Promise.resolve(storage.delete(key)),
89
+ clear: async () => Promise.resolve(storage.clear()),
90
+ set: async (keys) => {
91
+ for(let key in keys) {
92
+ storage.set(key, keys[key])
93
+ }
94
+ return Promise.resolve()
95
+ },
96
+ get: async (keys) => {
97
+ if(!keys) {
98
+ const result = {}
99
+ for(let [key, value] of storage) {
100
+ result[key] = value
101
+ }
102
+ return Promise.resolve(result)
103
+ }
104
+ const result = {}
105
+ const keysList = typeof keys == 'string' ? keys : (Array.isArray(keys) ? keys : Object.keys(keys))
106
+ for(let key of keysList) {
107
+ result[key] = storage.get(key) ?? (typeof keys == 'object' ? keys[key] : undefined)
108
+ }
109
+ return Promise.resolve(result)
110
+ },
111
+ getValues: async (keys) => {
112
+ if(!keys) {
113
+ const result = new Array(storage.size)
114
+ let i = 0
115
+ for(let value of storage.values()) {
116
+ result[i++] = value
117
+ }
118
+ return Promise.resolve(result)
119
+ }
120
+ const result = new Array(keys.length)
121
+ for(let i = 0; i<keys.length; i++) {
122
+ const key = keys[i]
123
+ result[i] = storage.get(key) ?? (typeof keys == 'object' ? keys[key] : undefined)
124
+ }
125
+ return Promise.resolve(result)
126
+ },
127
+ getKeys: async () => Promise.resolve(Array.from(storage.keys())),
128
+ remove: async (keys) => {
129
+ for(let key of keys) {
130
+ storage.delete(key)
131
+ }
132
+ return Promise.resolve()
133
+ }
134
+ }
135
+ }
136
+
137
+ if(typeof window == 'undefined') {
138
+ local = useTestStorage()
139
+ session = useTestStorage()
140
+ } else {
141
+ let isExtension = false
142
+ if (typeof browser !== "undefined") {
143
+ isExtension = browser.extension
144
+ } {
145
+ if (typeof chrome !== "undefined") {
146
+ window.browser = chrome.extension
147
+ }
148
+ }
149
+ if (isExtension) {
150
+ local = useExtensionStorage(browser.storage.local)
151
+ session = useExtensionStorage(browser.storage.session)
152
+ } else {
153
+ local = useWebStorage(window.localStorage)
154
+ session = useWebStorage(window.sessionStorage)
155
+ }
156
+ }
157
+
158
+ module.exports = { local, session }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@live-change/db-store-localstorage",
3
+ "version": "0.6.0",
4
+ "description": "Database with observable data for live queries",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "NODE_ENV=test tape tests/*"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/live-change/live-change-db.git"
12
+ },
13
+ "author": {
14
+ "email": "michal@laszczewski.com",
15
+ "name": "Michał Łaszczewski",
16
+ "url": "https://www.viamage.com/"
17
+ },
18
+ "license": "MIT",
19
+ "bugs": {
20
+ "url": "https://github.com/live-change/live-change-db/issues"
21
+ },
22
+ "homepage": "https://github.com/live-change/live-change-db",
23
+ "devDependencies": {
24
+ "fake-indexeddb": "^3.1.3",
25
+ "idb": "^6.1.4",
26
+ "tape": "^5.3.2"
27
+ },
28
+ "dependencies": {
29
+ "@live-change/dao": "0.5.14",
30
+ "@live-change/interval-tree": "^1.0.12",
31
+ "broadcast-channel": "^4.2.0"
32
+ },
33
+ "gitHead": "358bc9b508a6e446b9286a01e0ceb0e77c1dec28"
34
+ }
@@ -0,0 +1,80 @@
1
+ const test = require('tape')
2
+
3
+ require('fake-indexeddb/auto.js')
4
+ const idb = require('idb')
5
+ const Store = require('../lib/Store.js')
6
+
7
+ test("store broadcast object changes", t => {
8
+ t.plan(3)
9
+
10
+ let writeStore
11
+ let readStore
12
+
13
+ t.test("create stores", async t => {
14
+ t.plan(2)
15
+ readStore = new Store('test-broadcast-object-changes', 'test', 'local')
16
+ await readStore.open()
17
+ t.pass('read store created')
18
+ writeStore = new Store('test-broadcast-object-changes', 'test', 'local')
19
+ await writeStore.open()
20
+ t.pass('write store created')
21
+ })
22
+
23
+ let nextValueResolve
24
+ let gotNextValue
25
+ const getNextValue = () => {
26
+ if(gotNextValue) {
27
+ gotNextValue = false
28
+ return objectObservable.value
29
+ }
30
+ return new Promise((resolve, reject) => nextValueResolve = resolve)
31
+ }
32
+
33
+ let objectObservable
34
+ const objectObserver = (signal, value, ...rest) => {
35
+ console.log("SIGNAL", signal, value, ...rest)
36
+ if(nextValueResolve) {
37
+ nextValueResolve(value)
38
+ } else {
39
+ gotNextValue = true
40
+ }
41
+ }
42
+
43
+ t.test('observe object A', t => {
44
+ t.plan(3)
45
+ objectObservable = readStore.objectObservable('A')
46
+ objectObservable.observe(objectObserver)
47
+
48
+ let value
49
+
50
+ t.test('get value', async t => {
51
+ t.plan(1)
52
+ value = await getNextValue()
53
+ t.deepEqual(value, null, 'found null')
54
+ })
55
+
56
+ t.test("add object A", async t => {
57
+ t.plan(1)
58
+ await writeStore.put({ id: 'A', a: 1 })
59
+ let value = await getNextValue()
60
+ t.deepEqual(value, { id: 'A', a: 1 } , 'found object' )
61
+ })
62
+
63
+ t.test("delete object A", async t => {
64
+ t.plan(1)
65
+ await writeStore.delete('A')
66
+ let value = await getNextValue()
67
+ t.deepEqual(value, null , 'found null' )
68
+ })
69
+ })
70
+
71
+ t.test("close database", async t => {
72
+ t.plan(2)
73
+ await readStore.close()
74
+ t.pass('read store closed')
75
+ await writeStore.close()
76
+ t.pass('write store closed')
77
+ t.end()
78
+ })
79
+
80
+ })