@fireproof/core 0.2.0 → 0.3.1
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/hooks/{use-fireproof.ts → use-fireproof.js} +32 -33
- package/package.json +2 -2
- package/src/db-index.js +24 -41
- package/src/fireproof.js +52 -66
- package/src/hydrator.js +3 -2
- package/src/listener.js +1 -4
- package/src/prolly.js +105 -52
- package/test/db-index.test.js +15 -1
- package/test/fireproof.test.js +34 -1
- package/test/hydrator.test.js +5 -1
@@ -1,16 +1,15 @@
|
|
1
1
|
/* global localStorage */
|
2
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
3
2
|
// @ts-ignore
|
4
3
|
import { useEffect, useState, createContext } from 'react'
|
5
4
|
import { Fireproof, Listener, Hydrator } from '../index'
|
6
5
|
|
7
|
-
export interface FireproofCtxValue {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
}
|
13
|
-
export const FireproofCtx = createContext
|
6
|
+
// export interface FireproofCtxValue {
|
7
|
+
// addSubscriber: (label: String, fn: Function) => void
|
8
|
+
// database: Fireproof
|
9
|
+
// ready: boolean
|
10
|
+
// persist: () => void
|
11
|
+
// }
|
12
|
+
export const FireproofCtx = createContext({
|
14
13
|
addSubscriber: () => {},
|
15
14
|
database: null,
|
16
15
|
ready: false
|
@@ -35,21 +34,21 @@ const initializeDatabase = name => {
|
|
35
34
|
* @param [setupDatabaseFn] Asynchronous function that sets up the database, run this to load fixture data etc
|
36
35
|
* @returns {FireproofCtxValue} { addSubscriber, database, ready }
|
37
36
|
*/
|
38
|
-
export function useFireproof(
|
39
|
-
defineDatabaseFn = (
|
40
|
-
setupDatabaseFn = async (
|
41
|
-
name
|
42
|
-
)
|
37
|
+
export function useFireproof (
|
38
|
+
defineDatabaseFn = () => {},
|
39
|
+
setupDatabaseFn = async () => {},
|
40
|
+
name
|
41
|
+
) {
|
43
42
|
const [ready, setReady] = useState(false)
|
44
43
|
initializeDatabase(name || 'useFireproof')
|
45
44
|
const localStorageKey = 'fp.' + database.name
|
46
45
|
|
47
|
-
const addSubscriber = (label
|
46
|
+
const addSubscriber = (label, fn) => {
|
48
47
|
inboundSubscriberQueue.set(label, fn)
|
49
48
|
}
|
50
49
|
|
51
50
|
const listenerCallback = async event => {
|
52
|
-
|
51
|
+
localSet(localStorageKey, JSON.stringify(database))
|
53
52
|
if (event._external) return
|
54
53
|
for (const [, fn] of inboundSubscriberQueue) fn()
|
55
54
|
}
|
@@ -84,7 +83,7 @@ export function useFireproof(
|
|
84
83
|
localSet(localStorageKey, JSON.stringify(database))
|
85
84
|
}
|
86
85
|
setReady(true)
|
87
|
-
listener.on('*', listenerCallback)//hushed('*', listenerCallback, 250))
|
86
|
+
listener.on('*', listenerCallback)// hushed('*', listenerCallback, 250))
|
88
87
|
}
|
89
88
|
doSetup()
|
90
89
|
}, [ready])
|
@@ -99,32 +98,32 @@ export function useFireproof(
|
|
99
98
|
}
|
100
99
|
}
|
101
100
|
|
102
|
-
const husherMap = new Map()
|
103
|
-
const husher = (id
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
}
|
113
|
-
const hushed =
|
114
|
-
|
115
|
-
|
116
|
-
|
101
|
+
// const husherMap = new Map()
|
102
|
+
// const husher = (id, workFn, ms) => {
|
103
|
+
// if (!husherMap.has(id)) {
|
104
|
+
// const start = Date.now()
|
105
|
+
// husherMap.set(
|
106
|
+
// id,
|
107
|
+
// workFn().finally(() => setTimeout(() => husherMap.delete(id), ms - (Date.now() - start)))
|
108
|
+
// )
|
109
|
+
// }
|
110
|
+
// return husherMap.get(id)
|
111
|
+
// }
|
112
|
+
// const hushed =
|
113
|
+
// (id, workFn, ms) =>
|
114
|
+
// (...args) =>
|
115
|
+
// husher(id, () => workFn(...args), ms)
|
117
116
|
|
118
117
|
let storageSupported = false
|
119
118
|
try {
|
120
119
|
storageSupported = window.localStorage && true
|
121
120
|
} catch (e) {}
|
122
|
-
export function localGet(key
|
121
|
+
export function localGet (key) {
|
123
122
|
if (storageSupported) {
|
124
123
|
return localStorage && localStorage.getItem(key)
|
125
124
|
}
|
126
125
|
}
|
127
|
-
function localSet(key
|
126
|
+
function localSet (key, value) {
|
128
127
|
if (storageSupported) {
|
129
128
|
return localStorage && localStorage.setItem(key, value)
|
130
129
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fireproof/core",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.3.1",
|
4
4
|
"description": "Realtime database for IPFS",
|
5
5
|
"main": "index.js",
|
6
6
|
"type": "module",
|
@@ -32,6 +32,7 @@
|
|
32
32
|
],
|
33
33
|
"license": "Apache-2.0 OR MIT",
|
34
34
|
"dependencies": {
|
35
|
+
"prolly-trees": "1.0.4",
|
35
36
|
"@ipld/car": "^5.1.0",
|
36
37
|
"@ipld/dag-cbor": "^9.0.0",
|
37
38
|
"archy": "^1.0.0",
|
@@ -42,7 +43,6 @@
|
|
42
43
|
"encrypted-block": "^0.0.3",
|
43
44
|
"idb": "^7.1.1",
|
44
45
|
"multiformats": "^11.0.1",
|
45
|
-
"prolly-trees": "1.0.3",
|
46
46
|
"sade": "^1.8.1"
|
47
47
|
},
|
48
48
|
"devDependencies": {
|
package/src/db-index.js
CHANGED
@@ -14,21 +14,6 @@ import charwise from 'charwise'
|
|
14
14
|
|
15
15
|
const ALWAYS_REBUILD = false // todo: make false
|
16
16
|
|
17
|
-
// const arrayCompare = (a, b) => {
|
18
|
-
// if (Array.isArray(a) && Array.isArray(b)) {
|
19
|
-
// const len = Math.min(a.length, b.length)
|
20
|
-
// for (let i = 0; i < len; i++) {
|
21
|
-
// const comp = simpleCompare(a[i], b[i])
|
22
|
-
// if (comp !== 0) {
|
23
|
-
// return comp
|
24
|
-
// }
|
25
|
-
// }
|
26
|
-
// return simpleCompare(a.length, b.length)
|
27
|
-
// } else {
|
28
|
-
// return simpleCompare(a, b)
|
29
|
-
// }
|
30
|
-
// }
|
31
|
-
|
32
17
|
const compare = (a, b) => {
|
33
18
|
const [aKey, aRef] = a
|
34
19
|
const [bKey, bRef] = b
|
@@ -124,6 +109,7 @@ export default class DbIndex {
|
|
124
109
|
this.mapFn = mapFn
|
125
110
|
this.mapFnString = mapFn.toString()
|
126
111
|
}
|
112
|
+
this.name = opts.name || this.makeName()
|
127
113
|
this.indexById = { root: null, cid: null }
|
128
114
|
this.indexByKey = { root: null, cid: null }
|
129
115
|
this.dbHead = null
|
@@ -137,42 +123,43 @@ export default class DbIndex {
|
|
137
123
|
if (!opts.temporary) { DbIndex.registerWithDatabase(this, this.database) }
|
138
124
|
}
|
139
125
|
|
126
|
+
makeName () {
|
127
|
+
const regex = /\(([^,()]+,\s*[^,()]+|\[[^\]]+\],\s*[^,()]+)\)/g
|
128
|
+
const matches = Array.from(this.mapFnString.matchAll(regex), match => match[1].trim())
|
129
|
+
return matches[1]
|
130
|
+
}
|
131
|
+
|
140
132
|
static registerWithDatabase (inIndex, database) {
|
141
|
-
|
142
|
-
|
133
|
+
if (!database.indexes.has(inIndex.mapFnString)) {
|
134
|
+
database.indexes.set(inIndex.mapFnString, inIndex)
|
135
|
+
} else {
|
143
136
|
// merge our inIndex code with the inIndex clock or vice versa
|
144
|
-
// keep the code instance, discard the clock instance
|
145
137
|
const existingIndex = database.indexes.get(inIndex.mapFnString)
|
146
|
-
//
|
138
|
+
// keep the code instance, discard the clock instance
|
147
139
|
if (existingIndex.mapFn) { // this one also has other config
|
148
140
|
existingIndex.dbHead = inIndex.dbHead
|
149
141
|
existingIndex.indexById.cid = inIndex.indexById.cid
|
150
142
|
existingIndex.indexByKey.cid = inIndex.indexByKey.cid
|
151
143
|
} else {
|
152
|
-
// console.log('.reg use inIndex with existingIndex clock')
|
153
144
|
inIndex.dbHead = existingIndex.dbHead
|
154
145
|
inIndex.indexById.cid = existingIndex.indexById.cid
|
155
146
|
inIndex.indexByKey.cid = existingIndex.indexByKey.cid
|
156
147
|
database.indexes.set(inIndex.mapFnString, inIndex)
|
157
148
|
}
|
158
|
-
} else {
|
159
|
-
// console.log('.reg - fresh')
|
160
|
-
database.indexes.set(inIndex.mapFnString, inIndex)
|
161
149
|
}
|
162
|
-
// console.log('.reg after', JSON.stringify([...database.indexes.values()].map(i => [i.instanceId, typeof i.mapFn, i.indexByKey, i.indexById])))
|
163
150
|
}
|
164
151
|
|
165
152
|
toJSON () {
|
166
|
-
const indexJson = { code: this.mapFnString, clock: { db: null, byId: null, byKey: null } }
|
153
|
+
const indexJson = { name: this.name, code: this.mapFnString, clock: { db: null, byId: null, byKey: null } }
|
167
154
|
indexJson.clock.db = this.dbHead?.map(cid => cid.toString())
|
168
155
|
indexJson.clock.byId = this.indexById.cid?.toString()
|
169
156
|
indexJson.clock.byKey = this.indexByKey.cid?.toString()
|
170
157
|
return indexJson
|
171
158
|
}
|
172
159
|
|
173
|
-
static fromJSON (database, { code, clock }) {
|
160
|
+
static fromJSON (database, { code, clock, name }) {
|
174
161
|
// console.log('DbIndex.fromJSON', database.constructor.name, code, clock)
|
175
|
-
return new DbIndex(database, code, clock)
|
162
|
+
return new DbIndex(database, code, clock, { name })
|
176
163
|
}
|
177
164
|
|
178
165
|
/**
|
@@ -191,23 +178,17 @@ export default class DbIndex {
|
|
191
178
|
*/
|
192
179
|
async query (query, update = true) {
|
193
180
|
// const callId = Math.random().toString(36).substring(2, 7)
|
194
|
-
//
|
195
|
-
// pass a root to query a snapshot
|
181
|
+
// todo pass a root to query a snapshot
|
196
182
|
// console.time(callId + '.#updateIndex')
|
197
183
|
update && await this.#updateIndex(this.database.indexBlocks)
|
198
184
|
// console.timeEnd(callId + '.#updateIndex')
|
199
|
-
|
200
|
-
// }
|
201
185
|
// console.time(callId + '.doIndexQuery')
|
202
186
|
// console.log('query', query)
|
203
187
|
const response = await doIndexQuery(this.database.indexBlocks, this.indexByKey, query)
|
204
188
|
// console.timeEnd(callId + '.doIndexQuery')
|
205
|
-
|
206
189
|
return {
|
207
190
|
proof: { index: await cidsToProof(response.cids) },
|
208
|
-
// TODO fix this naming upstream in prolly/db-DbIndex?
|
209
191
|
rows: response.result.map(({ id, key, row }) => {
|
210
|
-
// console.log('query', id, key, row)
|
211
192
|
return ({ id, key: charwise.decode(key), value: row })
|
212
193
|
})
|
213
194
|
}
|
@@ -322,20 +303,22 @@ async function loadIndex (blocks, index, indexOpts) {
|
|
322
303
|
return index.root
|
323
304
|
}
|
324
305
|
|
306
|
+
async function applyLimit (results, limit) {
|
307
|
+
results.result = results.result.slice(0, limit)
|
308
|
+
return results
|
309
|
+
}
|
310
|
+
|
325
311
|
async function doIndexQuery (blocks, indexByKey, query = {}) {
|
326
312
|
await loadIndex(blocks, indexByKey, dbIndexOpts)
|
313
|
+
if (!indexByKey.root) return { result: [] }
|
327
314
|
if (query.range) {
|
328
315
|
const encodedRange = query.range.map((key) => charwise.encode(key))
|
329
|
-
return indexByKey.root.range(...encodedRange)
|
316
|
+
return applyLimit(await indexByKey.root.range(...encodedRange), query.limit)
|
330
317
|
} else if (query.key) {
|
331
318
|
const encodedKey = charwise.encode(query.key)
|
332
319
|
return indexByKey.root.get(encodedKey)
|
333
320
|
} else {
|
334
|
-
|
335
|
-
|
336
|
-
return { result: result.map(({ key: [k, id], value }) => ({ key: k, id, row: value })), ...all }
|
337
|
-
} else {
|
338
|
-
return { result: [] }
|
339
|
-
}
|
321
|
+
const { result, ...all } = await indexByKey.root.getAllEntries()
|
322
|
+
return applyLimit({ result: result.map(({ key: [k, id], value }) => ({ key: k, id, row: value })), ...all }, query.limit)
|
340
323
|
}
|
341
324
|
}
|
package/src/fireproof.js
CHANGED
@@ -138,27 +138,6 @@ export default class Fireproof {
|
|
138
138
|
}
|
139
139
|
}
|
140
140
|
|
141
|
-
/**
|
142
|
-
* Registers a Listener to be called when the Fireproof instance's clock is updated.
|
143
|
-
* Recieves live changes from the database after they are committed.
|
144
|
-
* @param {Function} listener - The listener to be called when the clock is updated.
|
145
|
-
* @returns {Function} - A function that can be called to unregister the listener.
|
146
|
-
* @memberof Fireproof
|
147
|
-
*/
|
148
|
-
registerListener (listener) {
|
149
|
-
this.#listeners.add(listener)
|
150
|
-
return () => {
|
151
|
-
this.#listeners.delete(listener)
|
152
|
-
}
|
153
|
-
}
|
154
|
-
|
155
|
-
async #notifyListeners (changes) {
|
156
|
-
// await sleep(10)
|
157
|
-
for (const listener of this.#listeners) {
|
158
|
-
await listener(changes)
|
159
|
-
}
|
160
|
-
}
|
161
|
-
|
162
141
|
/**
|
163
142
|
* Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
|
164
143
|
*
|
@@ -177,6 +156,35 @@ export default class Fireproof {
|
|
177
156
|
}
|
178
157
|
}
|
179
158
|
|
159
|
+
/**
|
160
|
+
* Retrieves the document with the specified ID from the database
|
161
|
+
*
|
162
|
+
* @param {string} key - the ID of the document to retrieve
|
163
|
+
* @param {Object} [opts] - options
|
164
|
+
* @returns {Object<{_id: string, ...doc: Object}>} - the document with the specified ID
|
165
|
+
* @memberof Fireproof
|
166
|
+
* @instance
|
167
|
+
*/
|
168
|
+
async get (key, opts = {}) {
|
169
|
+
const clock = opts.clock || this.clock
|
170
|
+
const resp = await get(this.blocks, clock, charwise.encode(key))
|
171
|
+
|
172
|
+
// this tombstone is temporary until we can get the prolly tree to delete
|
173
|
+
if (!resp || resp.result === null) {
|
174
|
+
throw new Error('Not found')
|
175
|
+
}
|
176
|
+
const doc = resp.result
|
177
|
+
if (opts.mvcc === true) {
|
178
|
+
doc._clock = this.clockToJSON()
|
179
|
+
}
|
180
|
+
doc._proof = {
|
181
|
+
data: await cidsToProof(resp.cids),
|
182
|
+
clock: this.clockToJSON()
|
183
|
+
}
|
184
|
+
doc._id = key
|
185
|
+
return doc
|
186
|
+
}
|
187
|
+
|
180
188
|
/**
|
181
189
|
* Adds a new document to the database, or updates an existing document. Returns the ID of the document and the new clock head.
|
182
190
|
*
|
@@ -210,9 +218,9 @@ export default class Fireproof {
|
|
210
218
|
id = docOrId
|
211
219
|
}
|
212
220
|
await this.#runValidation({ _id: id, _deleted: true })
|
213
|
-
|
221
|
+
return await this.#putToProllyTree({ key: id, del: true }, clock) // not working at prolly tree layer?
|
214
222
|
// this tombstone is temporary until we can get the prolly tree to delete
|
215
|
-
return await this.#putToProllyTree({ key: id, value: null }, clock)
|
223
|
+
// return await this.#putToProllyTree({ key: id, value: null }, clock)
|
216
224
|
}
|
217
225
|
|
218
226
|
/**
|
@@ -262,49 +270,6 @@ export default class Fireproof {
|
|
262
270
|
// return this.clock
|
263
271
|
// }
|
264
272
|
|
265
|
-
/**
|
266
|
-
* Displays a visualization of the current clock in the console
|
267
|
-
*/
|
268
|
-
// async visClock () {
|
269
|
-
// const shortLink = (l) => `${String(l).slice(0, 4)}..${String(l).slice(-4)}`
|
270
|
-
// const renderNodeLabel = (event) => {
|
271
|
-
// return event.value.data.type === 'put'
|
272
|
-
// ? `${shortLink(event.cid)}\\nput(${shortLink(event.value.data.key)},
|
273
|
-
// {${Object.values(event.value.data.value)}})`
|
274
|
-
// : `${shortLink(event.cid)}\\ndel(${event.value.data.key})`
|
275
|
-
// }
|
276
|
-
// for await (const line of vis(this.blocks, this.clock, { renderNodeLabel })) console.log(line)
|
277
|
-
// }
|
278
|
-
|
279
|
-
/**
|
280
|
-
* Retrieves the document with the specified ID from the database
|
281
|
-
*
|
282
|
-
* @param {string} key - the ID of the document to retrieve
|
283
|
-
* @param {Object} [opts] - options
|
284
|
-
* @returns {Object<{_id: string, ...doc: Object}>} - the document with the specified ID
|
285
|
-
* @memberof Fireproof
|
286
|
-
* @instance
|
287
|
-
*/
|
288
|
-
async get (key, opts = {}) {
|
289
|
-
const clock = opts.clock || this.clock
|
290
|
-
const resp = await get(this.blocks, clock, charwise.encode(key))
|
291
|
-
|
292
|
-
// this tombstone is temporary until we can get the prolly tree to delete
|
293
|
-
if (!resp || resp.result === null) {
|
294
|
-
throw new Error('Not found')
|
295
|
-
}
|
296
|
-
const doc = resp.result
|
297
|
-
if (opts.mvcc === true) {
|
298
|
-
doc._clock = this.clockToJSON()
|
299
|
-
}
|
300
|
-
doc._proof = {
|
301
|
-
data: await cidsToProof(resp.cids),
|
302
|
-
clock: this.clockToJSON()
|
303
|
-
}
|
304
|
-
doc._id = key
|
305
|
-
return doc
|
306
|
-
}
|
307
|
-
|
308
273
|
async * vis () {
|
309
274
|
return yield * vis(this.blocks, this.clock)
|
310
275
|
}
|
@@ -317,6 +282,27 @@ export default class Fireproof {
|
|
317
282
|
return await visMerkleClock(this.blocks, this.clock)
|
318
283
|
}
|
319
284
|
|
285
|
+
/**
|
286
|
+
* Registers a Listener to be called when the Fireproof instance's clock is updated.
|
287
|
+
* Recieves live changes from the database after they are committed.
|
288
|
+
* @param {Function} listener - The listener to be called when the clock is updated.
|
289
|
+
* @returns {Function} - A function that can be called to unregister the listener.
|
290
|
+
* @memberof Fireproof
|
291
|
+
*/
|
292
|
+
registerListener (listener) {
|
293
|
+
this.#listeners.add(listener)
|
294
|
+
return () => {
|
295
|
+
this.#listeners.delete(listener)
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
async #notifyListeners (changes) {
|
300
|
+
// await sleep(10)
|
301
|
+
for (const listener of this.#listeners) {
|
302
|
+
await listener(changes)
|
303
|
+
}
|
304
|
+
}
|
305
|
+
|
320
306
|
setCarUploader (carUploaderFn) {
|
321
307
|
// console.log('registering car uploader')
|
322
308
|
// https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
|
package/src/hydrator.js
CHANGED
@@ -8,14 +8,15 @@ export default class Hydrator {
|
|
8
8
|
static fromJSON (json, database) {
|
9
9
|
database.hydrate({ clock: json.clock.map(c => parseCID(c)), name: json.name, key: json.key })
|
10
10
|
if (json.indexes) {
|
11
|
-
for (const { code, clock: { byId, byKey, db } } of json.indexes) {
|
11
|
+
for (const { name, code, clock: { byId, byKey, db } } of json.indexes) {
|
12
12
|
DbIndex.fromJSON(database, {
|
13
13
|
clock: {
|
14
14
|
byId: byId ? parseCID(byId) : null,
|
15
15
|
byKey: byKey ? parseCID(byKey) : null,
|
16
16
|
db: db ? db.map(c => parseCID(c)) : null
|
17
17
|
},
|
18
|
-
code
|
18
|
+
code,
|
19
|
+
name
|
19
20
|
})
|
20
21
|
}
|
21
22
|
}
|
package/src/listener.js
CHANGED
@@ -80,9 +80,6 @@ function getTopicList (subscribersMap, name) {
|
|
80
80
|
return topicList
|
81
81
|
}
|
82
82
|
|
83
|
-
// copied from src/db-index.js
|
84
|
-
const makeDoc = ({ key, value }) => ({ _id: key, ...value })
|
85
|
-
|
86
83
|
/**
|
87
84
|
* Transforms a set of changes to events using an emitter function.
|
88
85
|
*
|
@@ -95,7 +92,7 @@ const topicsForChanges = (changes, routingFn) => {
|
|
95
92
|
const seenTopics = new Map()
|
96
93
|
changes.forEach(({ key, value, del }) => {
|
97
94
|
if (del || !value) value = { _deleted: true }
|
98
|
-
routingFn(
|
95
|
+
routingFn(({ _id: key, ...value }), t => {
|
99
96
|
const topicList = getTopicList(seenTopics, t)
|
100
97
|
topicList.push(key)
|
101
98
|
})
|
package/src/prolly.js
CHANGED
@@ -67,12 +67,13 @@ async function createAndSaveNewEvent ({
|
|
67
67
|
let cids
|
68
68
|
const { key, value, del } = inEvent
|
69
69
|
const data = {
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
70
|
+
root: (root
|
71
|
+
? {
|
72
|
+
cid: root.cid,
|
73
|
+
bytes: root.bytes, // can we remove this?
|
74
|
+
value: root.value // can we remove this?
|
75
|
+
}
|
76
|
+
: null),
|
76
77
|
key
|
77
78
|
}
|
78
79
|
|
@@ -81,6 +82,7 @@ async function createAndSaveNewEvent ({
|
|
81
82
|
data.type = 'del'
|
82
83
|
} else {
|
83
84
|
data.value = value
|
85
|
+
data.type = 'put'
|
84
86
|
}
|
85
87
|
/** @type {EventData} */
|
86
88
|
|
@@ -115,13 +117,27 @@ const makeGetAndPutBlock = (inBlocks) => {
|
|
115
117
|
return { getBlock, bigPut, blocks: inBlocks, cids }
|
116
118
|
}
|
117
119
|
|
118
|
-
const bulkFromEvents = (sorted) =>
|
119
|
-
|
120
|
+
const bulkFromEvents = (sorted, event) => {
|
121
|
+
if (event) {
|
122
|
+
const update = { value: { data: { key: event.key } } }
|
123
|
+
if (event.del) {
|
124
|
+
update.value.data.type = 'del'
|
125
|
+
} else {
|
126
|
+
update.value.data.type = 'put'
|
127
|
+
update.value.data.value = event.value
|
128
|
+
}
|
129
|
+
sorted.push(update)
|
130
|
+
}
|
131
|
+
const bulk = new Map()
|
132
|
+
for (const { value: event } of sorted) {
|
120
133
|
const {
|
121
134
|
data: { type, value, key }
|
122
135
|
} = event
|
123
|
-
|
124
|
-
|
136
|
+
const bulkEvent = type === 'put' ? { key, value } : { key, del: true }
|
137
|
+
bulk.set(bulkEvent.key, bulkEvent) // last wins
|
138
|
+
}
|
139
|
+
return Array.from(bulk.values())
|
140
|
+
}
|
125
141
|
|
126
142
|
// Get the value of the root from the ancestor event
|
127
143
|
/**
|
@@ -136,7 +152,47 @@ const prollyRootFromAncestor = async (events, ancestor, getBlock) => {
|
|
136
152
|
const event = await events.get(ancestor)
|
137
153
|
const { root } = event.value.data
|
138
154
|
// console.log('prollyRootFromAncestor', root.cid, JSON.stringify(root.value))
|
139
|
-
|
155
|
+
if (root) {
|
156
|
+
return load({ cid: root.cid, get: getBlock, ...blockOpts })
|
157
|
+
} else {
|
158
|
+
return null
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
const doProllyBulk = async (inBlocks, head, event) => {
|
163
|
+
const { getBlock, blocks } = makeGetAndPutBlock(inBlocks)
|
164
|
+
let bulkSorted = []
|
165
|
+
let prollyRootNode = null
|
166
|
+
if (head.length) {
|
167
|
+
// Otherwise, we find the common ancestor and update the root and other blocks
|
168
|
+
const events = new EventFetcher(blocks)
|
169
|
+
// todo this is returning more events than necessary, lets define the desired semantics from the top down
|
170
|
+
// good semantics mean we can cache the results of this call
|
171
|
+
const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
|
172
|
+
bulkSorted = sorted
|
173
|
+
// console.log('sorted', JSON.stringify(sorted.map(({ value: { data: { key, value } } }) => ({ key, value }))))
|
174
|
+
prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
|
175
|
+
// console.log('event', event)
|
176
|
+
}
|
177
|
+
|
178
|
+
const bulkOperations = bulkFromEvents(bulkSorted, event)
|
179
|
+
|
180
|
+
// if prolly root node is null, we need to create a new one
|
181
|
+
if (!prollyRootNode) {
|
182
|
+
let root
|
183
|
+
const newBlocks = []
|
184
|
+
// if all operations are deletes, we can just return an empty root
|
185
|
+
if (bulkOperations.every((op) => op.del)) {
|
186
|
+
return { root: null, blocks: [] }
|
187
|
+
}
|
188
|
+
for await (const node of create({ get: getBlock, list: bulkOperations, ...blockOpts })) {
|
189
|
+
root = await node.block
|
190
|
+
newBlocks.push(root)
|
191
|
+
}
|
192
|
+
return { root, blocks: newBlocks }
|
193
|
+
} else {
|
194
|
+
return await prollyRootNode.bulk(bulkOperations) // { root: newProllyRootNode, blocks: newBlocks }
|
195
|
+
}
|
140
196
|
}
|
141
197
|
|
142
198
|
/**
|
@@ -150,44 +206,45 @@ const prollyRootFromAncestor = async (events, ancestor, getBlock) => {
|
|
150
206
|
* @returns {Promise<Result>}
|
151
207
|
*/
|
152
208
|
export async function put (inBlocks, head, event, options) {
|
153
|
-
const {
|
209
|
+
const { bigPut } = makeGetAndPutBlock(inBlocks)
|
154
210
|
|
155
211
|
// If the head is empty, we create a new event and return the root and addition blocks
|
156
212
|
if (!head.length) {
|
157
213
|
const additions = new Map()
|
158
|
-
|
159
|
-
for
|
160
|
-
|
161
|
-
bigPut(root, additions)
|
214
|
+
const { root, blocks } = await doProllyBulk(inBlocks, head, event)
|
215
|
+
for (const b of blocks) {
|
216
|
+
bigPut(b, additions)
|
162
217
|
}
|
163
218
|
return createAndSaveNewEvent({ inBlocks, bigPut, root, event, head, additions: Array.from(additions.values()) })
|
164
219
|
}
|
220
|
+
const { root: newProllyRootNode, blocks: newBlocks } = await doProllyBulk(inBlocks, head, event)
|
165
221
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
222
|
+
if (!newProllyRootNode) {
|
223
|
+
return createAndSaveNewEvent({
|
224
|
+
inBlocks,
|
225
|
+
bigPut,
|
226
|
+
root: null,
|
227
|
+
event,
|
228
|
+
head,
|
229
|
+
additions: []
|
230
|
+
})
|
231
|
+
} else {
|
232
|
+
const prollyRootBlock = await newProllyRootNode.block
|
233
|
+
const additions = new Map() // ; const removals = new Map()
|
234
|
+
bigPut(prollyRootBlock, additions)
|
235
|
+
for (const nb of newBlocks) {
|
236
|
+
bigPut(nb, additions)
|
237
|
+
}
|
238
|
+
// additions are new blocks
|
239
|
+
return createAndSaveNewEvent({
|
240
|
+
inBlocks,
|
241
|
+
bigPut,
|
242
|
+
root: prollyRootBlock,
|
243
|
+
event,
|
244
|
+
head,
|
245
|
+
additions: Array.from(additions.values()) /*, todo? Array.from(removals.values()) */
|
246
|
+
})
|
181
247
|
}
|
182
|
-
// additions are new blocks
|
183
|
-
return createAndSaveNewEvent({
|
184
|
-
inBlocks,
|
185
|
-
bigPut,
|
186
|
-
root: prollyRootBlock,
|
187
|
-
event,
|
188
|
-
head,
|
189
|
-
additions: Array.from(additions.values()) /*, todo? Array.from(removals.values()) */
|
190
|
-
})
|
191
248
|
}
|
192
249
|
|
193
250
|
/**
|
@@ -200,25 +257,15 @@ export async function root (inBlocks, head) {
|
|
200
257
|
if (!head.length) {
|
201
258
|
throw new Error('no head')
|
202
259
|
}
|
203
|
-
const {
|
204
|
-
const events = new EventFetcher(blocks)
|
205
|
-
const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
|
206
|
-
const prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
|
207
|
-
|
208
|
-
// Perform bulk operations (put or delete) for each event in the sorted array
|
209
|
-
const bulkOperations = bulkFromEvents(sorted)
|
210
|
-
const { root: newProllyRootNode, blocks: newBlocks } = await prollyRootNode.bulk(bulkOperations)
|
211
|
-
// const prollyRootBlock = await newProllyRootNode.block
|
212
|
-
// console.log('newBlocks', newBlocks.map((nb) => nb.cid.toString()))
|
260
|
+
const { root: newProllyRootNode, blocks: newBlocks, cids } = await doProllyBulk(inBlocks, head)
|
213
261
|
// todo maybe these should go to a temp blockstore?
|
214
262
|
await doTransaction('root', inBlocks, async (transactionBlockstore) => {
|
215
263
|
const { bigPut } = makeGetAndPutBlock(transactionBlockstore)
|
216
264
|
for (const nb of newBlocks) {
|
217
265
|
bigPut(nb)
|
218
266
|
}
|
219
|
-
// bigPut(prollyRootBlock)
|
220
267
|
})
|
221
|
-
return { cids
|
268
|
+
return { cids, node: newProllyRootNode }
|
222
269
|
}
|
223
270
|
|
224
271
|
/**
|
@@ -252,6 +299,9 @@ export async function getAll (blocks, head) {
|
|
252
299
|
return { clockCIDs: new CIDCounter(), cids: new CIDCounter(), result: [] }
|
253
300
|
}
|
254
301
|
const { node: prollyRootNode, cids: clockCIDs } = await root(blocks, head)
|
302
|
+
if (!prollyRootNode) {
|
303
|
+
return { clockCIDs, cids: new CIDCounter(), result: [] }
|
304
|
+
}
|
255
305
|
const { result, cids } = await prollyRootNode.getAllEntries() // todo params
|
256
306
|
return { clockCIDs, cids, result: result.map(({ key, value }) => ({ key, value })) }
|
257
307
|
}
|
@@ -267,6 +317,9 @@ export async function get (blocks, head, key) {
|
|
267
317
|
return { cids: new CIDCounter(), result: null }
|
268
318
|
}
|
269
319
|
const { node: prollyRootNode, cids: clockCIDs } = await root(blocks, head)
|
320
|
+
if (!prollyRootNode) {
|
321
|
+
return { clockCIDs, cids: new CIDCounter(), result: null }
|
322
|
+
}
|
270
323
|
const { result, cids } = await prollyRootNode.get(key)
|
271
324
|
return { result, cids, clockCIDs }
|
272
325
|
}
|
package/test/db-index.test.js
CHANGED
@@ -27,7 +27,10 @@ describe('DbIndex query', () => {
|
|
27
27
|
}
|
28
28
|
index = new DbIndex(database, function (doc, map) {
|
29
29
|
map(doc.age, doc.name)
|
30
|
-
})
|
30
|
+
}, null, { name: 'namesByAge' })
|
31
|
+
})
|
32
|
+
it('has a name', () => {
|
33
|
+
assert.equal(index.name, 'namesByAge')
|
31
34
|
})
|
32
35
|
it('query index range', async () => {
|
33
36
|
const result = await index.query({ range: [41, 49] })
|
@@ -56,6 +59,14 @@ describe('DbIndex query', () => {
|
|
56
59
|
assert.equal(result.rows[0].value, 'emily')
|
57
60
|
assert.equal(result.rows[result.rows.length - 1].value, 'dave')
|
58
61
|
})
|
62
|
+
it('query index limit', async () => {
|
63
|
+
const result = await index.query({ limit: 3 })
|
64
|
+
assert(result, 'did return result')
|
65
|
+
assert(result.rows)
|
66
|
+
assert.equal(result.rows.length, 3, 'six row matched')
|
67
|
+
assert.equal(result.rows[0].key, 4)
|
68
|
+
assert.equal(result.rows[0].value, 'emily')
|
69
|
+
})
|
59
70
|
it('query index NaN', async () => {
|
60
71
|
const result = await index.query({ range: [NaN, 44] })
|
61
72
|
assert(result, 'did return result')
|
@@ -236,6 +247,9 @@ describe('DbIndex query with bad index definition', () => {
|
|
236
247
|
map(doc.oops.missingField, doc.name)
|
237
248
|
})
|
238
249
|
})
|
250
|
+
it('has a default name', () => {
|
251
|
+
assert.equal(index.name, 'doc.oops.missingField, doc.name')
|
252
|
+
})
|
239
253
|
it('query index range', async () => {
|
240
254
|
const oldErrFn = console.error
|
241
255
|
console.error = () => {}
|
package/test/fireproof.test.js
CHANGED
@@ -124,6 +124,14 @@ describe('Fireproof', () => {
|
|
124
124
|
const changes = await db.changesSince()
|
125
125
|
assert.equal(changes.rows.length, 0)
|
126
126
|
})
|
127
|
+
it('delete on an empty database', async () => {
|
128
|
+
const db = Fireproof.storage()
|
129
|
+
assert(db instanceof Fireproof)
|
130
|
+
const e = await db.del('8c5c0c5c0c5c').catch((err) => err)
|
131
|
+
assert.equal(e.id, '8c5c0c5c0c5c')
|
132
|
+
const changes = await db.changesSince()
|
133
|
+
assert.equal(changes.rows.length, 0)
|
134
|
+
})
|
127
135
|
it('update existing document', async () => {
|
128
136
|
// const alice = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
|
129
137
|
// assert.equal(alice.name, 'alice')
|
@@ -194,7 +202,7 @@ describe('Fireproof', () => {
|
|
194
202
|
const e = await database.get('missing').catch((e) => e)
|
195
203
|
assert.equal(e.message, 'Not found')
|
196
204
|
})
|
197
|
-
it('delete
|
205
|
+
it('delete the only document', async () => {
|
198
206
|
const id = '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c'
|
199
207
|
const found = await database.get(id)
|
200
208
|
assert.equal(found._id, id)
|
@@ -212,6 +220,31 @@ describe('Fireproof', () => {
|
|
212
220
|
assert.equal(e.message, 'Not found')
|
213
221
|
})
|
214
222
|
|
223
|
+
it('delete not last document', async () => {
|
224
|
+
const resp1 = await database.put({
|
225
|
+
_id: 'second',
|
226
|
+
name: 'bob',
|
227
|
+
age: 39
|
228
|
+
})
|
229
|
+
|
230
|
+
// const id = '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c'
|
231
|
+
const id = resp1.id
|
232
|
+
const found = await database.get(id)
|
233
|
+
assert.equal(found._id, id)
|
234
|
+
const deleted = await database.del(id)
|
235
|
+
assert.equal(deleted.id, id)
|
236
|
+
const e = await database
|
237
|
+
.get(id)
|
238
|
+
.then((doc) => assert.equal('should be deleted', JSON.stringify(doc)))
|
239
|
+
.catch((e) => {
|
240
|
+
if (e.message !== 'Not found') {
|
241
|
+
throw e
|
242
|
+
}
|
243
|
+
return e
|
244
|
+
})
|
245
|
+
assert.equal(e.message, 'Not found')
|
246
|
+
})
|
247
|
+
|
215
248
|
it("delete a document with validation function that doesn't allow it", async () => {
|
216
249
|
const validationDatabase = new Fireproof(new Blockstore(), [], {
|
217
250
|
validateChange: (newDoc, oldDoc, authCtx) => {
|
package/test/hydrator.test.js
CHANGED
@@ -25,7 +25,7 @@ describe('DbIndex query', () => {
|
|
25
25
|
}
|
26
26
|
index = new DbIndex(database, function (doc, map) {
|
27
27
|
map(doc.age, doc.name)
|
28
|
-
})
|
28
|
+
}, null, { name: 'names_by_age' })
|
29
29
|
})
|
30
30
|
it('serialize database with index', async () => {
|
31
31
|
await database.put({ _id: 'rehy', name: 'drate', age: 1 })
|
@@ -44,6 +44,8 @@ describe('DbIndex query', () => {
|
|
44
44
|
assert.equal(serialized.indexes[0].code, `function (doc, map) {
|
45
45
|
map(doc.age, doc.name)
|
46
46
|
}`)
|
47
|
+
assert.equal(serialized.indexes[0].name, 'names_by_age')
|
48
|
+
|
47
49
|
assert.equal(serialized.indexes[0].clock.byId.constructor.name, 'String')
|
48
50
|
assert.equal(serialized.indexes[0].clock.byKey.constructor.name, 'String')
|
49
51
|
assert.equal(serialized.indexes[0].clock.db[0].constructor.name, 'String')
|
@@ -71,6 +73,8 @@ describe('DbIndex query', () => {
|
|
71
73
|
assert.equal(newIndex.indexByKey.cid, 'bafyreicr5rpvsxnqchcwk5rxlmdvd3fah2vexmbsp2dvr4cfdxd2q2ycgu')
|
72
74
|
// assert.equal(newIndex.indexByKey.root, null)
|
73
75
|
|
76
|
+
assert.equal(newIndex.name, 'names_by_age')
|
77
|
+
|
74
78
|
const newResult = await newIndex.query({ range: [0, 54] })
|
75
79
|
assert.equal(newResult.rows[0].value, 'drate')
|
76
80
|
})
|