@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 +1 -0
- package/lib/Store.js +416 -0
- package/lib/storage.js +158 -0
- package/package.json +34 -0
- package/tests/broadcast-object-changes.js +80 -0
- package/tests/broadcast-range-changes.js +168 -0
- package/tests/limited-range-observable.js +176 -0
- package/tests/limited-reverse-range-observable.js +175 -0
- package/tests/object-observable.js +68 -0
- package/tests/range-observable.js +140 -0
- package/tests/reverse-range-observable.js +139 -0
- package/tests/store-non-reactive.js +120 -0
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
|
+
})
|