@fireproof/core 0.1.1 → 0.3.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/hooks/use-fireproof.ts +44 -27
- package/package.json +9 -6
- package/scripts/keygen.js +3 -0
- package/src/blockstore.js +27 -56
- package/src/clock.js +0 -1
- package/src/crypto.js +65 -0
- package/src/db-index.js +34 -45
- package/src/fireproof.js +87 -66
- package/src/hydrator.js +14 -11
- package/src/listener.js +7 -14
- package/src/prolly.js +130 -54
- package/src/sha1.js +82 -0
- package/src/valet.js +169 -11
- package/test/db-index.test.js +15 -1
- package/test/fireproof.test.js +83 -4
- package/test/hydrator.test.js +8 -2
package/hooks/use-fireproof.ts
CHANGED
@@ -4,44 +4,53 @@
|
|
4
4
|
import { useEffect, useState, createContext } from 'react'
|
5
5
|
import { Fireproof, Listener, Hydrator } from '../index'
|
6
6
|
|
7
|
-
|
8
7
|
export interface FireproofCtxValue {
|
9
8
|
addSubscriber: (label: String, fn: Function) => void
|
10
9
|
database: Fireproof
|
11
10
|
ready: boolean
|
11
|
+
persist: () => void
|
12
12
|
}
|
13
13
|
export const FireproofCtx = createContext<FireproofCtxValue>({
|
14
14
|
addSubscriber: () => {},
|
15
15
|
database: null,
|
16
|
-
ready: false
|
16
|
+
ready: false
|
17
17
|
})
|
18
18
|
|
19
19
|
const inboundSubscriberQueue = new Map()
|
20
|
-
|
21
|
-
|
22
|
-
let
|
20
|
+
|
21
|
+
let startedSetup = false
|
22
|
+
let database
|
23
|
+
let listener
|
24
|
+
const initializeDatabase = name => {
|
25
|
+
if (database) return
|
26
|
+
database = Fireproof.storage(name)
|
27
|
+
listener = new Listener(database)
|
28
|
+
}
|
23
29
|
|
24
30
|
/**
|
25
31
|
* @function useFireproof
|
26
32
|
* React hook to initialize a Fireproof database, automatically saving and loading the clock.
|
33
|
+
* You might need to `import { nodePolyfills } from 'vite-plugin-node-polyfills'` in your vite.config.ts
|
27
34
|
* @param [defineDatabaseFn] Synchronous function that defines the database, run this before any async calls
|
28
35
|
* @param [setupDatabaseFn] Asynchronous function that sets up the database, run this to load fixture data etc
|
29
36
|
* @returns {FireproofCtxValue} { addSubscriber, database, ready }
|
30
37
|
*/
|
31
|
-
export function useFireproof(
|
38
|
+
export function useFireproof(
|
39
|
+
defineDatabaseFn = (database: Fireproof) => {},
|
40
|
+
setupDatabaseFn = async (database: Fireproof) => {},
|
41
|
+
name: string
|
42
|
+
): FireproofCtxValue {
|
32
43
|
const [ready, setReady] = useState(false)
|
33
|
-
|
34
|
-
|
35
|
-
// console.log('useFireproof', database, ready)
|
36
|
-
|
44
|
+
initializeDatabase(name || 'useFireproof')
|
45
|
+
const localStorageKey = 'fp.' + database.name
|
37
46
|
|
38
47
|
const addSubscriber = (label: String, fn: Function) => {
|
39
48
|
inboundSubscriberQueue.set(label, fn)
|
40
49
|
}
|
41
50
|
|
42
|
-
const listenerCallback = async
|
43
|
-
|
44
|
-
|
51
|
+
const listenerCallback = async event => {
|
52
|
+
localSet(localStorageKey, JSON.stringify(database))
|
53
|
+
if (event._external) return
|
45
54
|
for (const [, fn] of inboundSubscriberQueue) fn()
|
46
55
|
}
|
47
56
|
|
@@ -51,14 +60,14 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
|
|
51
60
|
if (startedSetup) return
|
52
61
|
startedSetup = true
|
53
62
|
defineDatabaseFn(database) // define indexes before querying them
|
54
|
-
|
63
|
+
console.log('Initializing database', database.name)
|
64
|
+
const fp = localGet(localStorageKey) // todo use db.name
|
55
65
|
if (fp) {
|
56
|
-
const serialized = JSON.parse(fp)
|
57
|
-
// console.log('serialized', JSON.stringify(serialized.indexes.map(c => c.clock)))
|
58
|
-
console.log("Loading previous database clock. (localStorage.removeItem('fireproof') to reset)")
|
59
|
-
Hydrator.fromJSON(serialized, database)
|
60
|
-
// await database.setClock(clock)
|
61
66
|
try {
|
67
|
+
const serialized = JSON.parse(fp)
|
68
|
+
// console.log('serialized', JSON.stringify(serialized.indexes.map(c => c.clock)))
|
69
|
+
console.log(`Loading previous database clock. (localStorage.removeItem('${localStorageKey}') to reset)`)
|
70
|
+
await Hydrator.fromJSON(serialized, database)
|
62
71
|
const changes = await database.changesSince()
|
63
72
|
if (changes.rows.length < 2) {
|
64
73
|
// console.log('Resetting database')
|
@@ -66,16 +75,16 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
|
|
66
75
|
}
|
67
76
|
} catch (e) {
|
68
77
|
console.error(`Error loading previous database clock. ${fp} Resetting.`, e)
|
69
|
-
await
|
78
|
+
await Hydrator.zoom(database, [])
|
70
79
|
await setupDatabaseFn(database)
|
71
|
-
localSet(
|
80
|
+
localSet(localStorageKey, JSON.stringify(database))
|
72
81
|
}
|
73
82
|
} else {
|
74
83
|
await setupDatabaseFn(database)
|
75
|
-
localSet(
|
84
|
+
localSet(localStorageKey, JSON.stringify(database))
|
76
85
|
}
|
77
86
|
setReady(true)
|
78
|
-
listener.on('*', hushed('*', listenerCallback, 250))
|
87
|
+
listener.on('*', listenerCallback)//hushed('*', listenerCallback, 250))
|
79
88
|
}
|
80
89
|
doSetup()
|
81
90
|
}, [ready])
|
@@ -84,6 +93,9 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
|
|
84
93
|
addSubscriber,
|
85
94
|
database,
|
86
95
|
ready,
|
96
|
+
persist: () => {
|
97
|
+
localSet(localStorageKey, JSON.stringify(database))
|
98
|
+
}
|
87
99
|
}
|
88
100
|
}
|
89
101
|
|
@@ -91,12 +103,17 @@ const husherMap = new Map()
|
|
91
103
|
const husher = (id: string, workFn: { (): Promise<any> }, ms: number) => {
|
92
104
|
if (!husherMap.has(id)) {
|
93
105
|
const start: number = Date.now()
|
94
|
-
husherMap.set(
|
95
|
-
|
106
|
+
husherMap.set(
|
107
|
+
id,
|
108
|
+
workFn().finally(() => setTimeout(() => husherMap.delete(id), ms - (Date.now() - start)))
|
109
|
+
)
|
96
110
|
}
|
97
111
|
return husherMap.get(id)
|
98
112
|
}
|
99
|
-
const hushed =
|
113
|
+
const hushed =
|
114
|
+
(id: string, workFn: { (...args): Promise<any> }, ms: number) =>
|
115
|
+
(...args) =>
|
116
|
+
husher(id, () => workFn(...args), ms)
|
100
117
|
|
101
118
|
let storageSupported = false
|
102
119
|
try {
|
@@ -116,4 +133,4 @@ function localSet(key: string, value: string) {
|
|
116
133
|
// if (storageSupported) {
|
117
134
|
// return localStorage && localStorage.removeItem(key)
|
118
135
|
// }
|
119
|
-
// }
|
136
|
+
// }
|
package/package.json
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fireproof/core",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.3.0",
|
4
4
|
"description": "Realtime database for IPFS",
|
5
5
|
"main": "index.js",
|
6
6
|
"type": "module",
|
7
7
|
"scripts": {
|
8
|
-
"
|
9
|
-
"test
|
10
|
-
"test:
|
8
|
+
"keygen": "node scripts/keygen.js",
|
9
|
+
"test": "standard && npm run test:unencrypted && npm run test:mocha",
|
10
|
+
"test:unencrypted": "NO_ENCRYPT=true npm run test:mocha",
|
11
|
+
"test:mocha": "mocha --reporter list test/*.test.js",
|
12
|
+
"test:watch": "npm run test:mocha -- -w --parallel",
|
11
13
|
"coverage": "c8 -r html -r text npm test",
|
12
|
-
"prepublishOnly"
|
14
|
+
"prepublishOnly": "cp ../../README.md .",
|
13
15
|
"postpublish": "rm README.md",
|
14
16
|
"lint": "standard",
|
15
17
|
"lint:fix": "standard --fix"
|
@@ -30,6 +32,7 @@
|
|
30
32
|
],
|
31
33
|
"license": "Apache-2.0 OR MIT",
|
32
34
|
"dependencies": {
|
35
|
+
"prolly-trees": "1.0.4",
|
33
36
|
"@ipld/car": "^5.1.0",
|
34
37
|
"@ipld/dag-cbor": "^9.0.0",
|
35
38
|
"archy": "^1.0.0",
|
@@ -37,9 +40,9 @@
|
|
37
40
|
"car-transaction": "^1.0.1",
|
38
41
|
"charwise": "^3.0.1",
|
39
42
|
"cli-color": "^2.0.3",
|
43
|
+
"encrypted-block": "^0.0.3",
|
40
44
|
"idb": "^7.1.1",
|
41
45
|
"multiformats": "^11.0.1",
|
42
|
-
"prolly-trees": "1.0.3",
|
43
46
|
"sade": "^1.8.1"
|
44
47
|
},
|
45
48
|
"devDependencies": {
|
package/src/blockstore.js
CHANGED
@@ -1,10 +1,5 @@
|
|
1
1
|
import { parse } from 'multiformats/link'
|
2
|
-
import * as raw from 'multiformats/codecs/raw'
|
3
|
-
import { sha256 } from 'multiformats/hashes/sha2'
|
4
|
-
import * as Block from 'multiformats/block'
|
5
|
-
import * as CBW from '@ipld/car/buffer-writer'
|
6
2
|
import { CID } from 'multiformats'
|
7
|
-
|
8
3
|
import Valet from './valet.js'
|
9
4
|
|
10
5
|
// const sleep = ms => new Promise(r => setTimeout(r, ms))
|
@@ -34,15 +29,15 @@ const husher = (id, workFn) => {
|
|
34
29
|
*/
|
35
30
|
export default class TransactionBlockstore {
|
36
31
|
/** @type {Map<string, Uint8Array>} */
|
37
|
-
#
|
32
|
+
#committedBlocks = new Map()
|
38
33
|
|
39
34
|
valet = null
|
40
35
|
|
41
36
|
#instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4)
|
42
37
|
#inflightTransactions = new Set()
|
43
38
|
|
44
|
-
constructor (name) {
|
45
|
-
this.valet = new Valet(name)
|
39
|
+
constructor (name, encryptionKey) {
|
40
|
+
this.valet = new Valet(name, encryptionKey)
|
46
41
|
}
|
47
42
|
|
48
43
|
/**
|
@@ -54,7 +49,7 @@ export default class TransactionBlockstore {
|
|
54
49
|
async get (cid) {
|
55
50
|
const key = cid.toString()
|
56
51
|
// it is safe to read from the in-flight transactions becauase they are immutable
|
57
|
-
const bytes = await Promise.any([this.#transactionsGet(key), this.
|
52
|
+
const bytes = await Promise.any([this.#transactionsGet(key), this.committedGet(key)]).catch(e => {
|
58
53
|
// console.log('networkGet', cid.toString(), e)
|
59
54
|
return this.networkGet(key)
|
60
55
|
})
|
@@ -72,18 +67,26 @@ export default class TransactionBlockstore {
|
|
72
67
|
throw new Error('Missing block: ' + key)
|
73
68
|
}
|
74
69
|
|
75
|
-
async
|
76
|
-
const old = this.#
|
70
|
+
async committedGet (key) {
|
71
|
+
const old = this.#committedBlocks.get(key)
|
77
72
|
if (old) return old
|
78
|
-
|
73
|
+
const got = await this.valet.getBlock(key)
|
74
|
+
// console.log('committedGet: ' + key)
|
75
|
+
this.#committedBlocks.set(key, got)
|
76
|
+
return got
|
77
|
+
}
|
78
|
+
|
79
|
+
async clearCommittedCache () {
|
80
|
+
this.#committedBlocks.clear()
|
79
81
|
}
|
80
82
|
|
81
83
|
async networkGet (key) {
|
82
84
|
if (this.valet.remoteBlockFunction) {
|
85
|
+
// todo why is this on valet?
|
83
86
|
const value = await husher(key, async () => await this.valet.remoteBlockFunction(key))
|
84
87
|
if (value) {
|
85
88
|
// console.log('networkGot: ' + key, value.length)
|
86
|
-
doTransaction('networkGot: ' + key, this, async
|
89
|
+
doTransaction('networkGot: ' + key, this, async innerBlockstore => {
|
87
90
|
await innerBlockstore.put(CID.parse(key), value)
|
88
91
|
})
|
89
92
|
return value
|
@@ -118,7 +121,7 @@ export default class TransactionBlockstore {
|
|
118
121
|
// // for (const [str, bytes] of this.#blocks) {
|
119
122
|
// // yield { cid: parse(str), bytes }
|
120
123
|
// // }
|
121
|
-
// for (const [str, bytes] of this.#
|
124
|
+
// for (const [str, bytes] of this.#committedBlocks) {
|
122
125
|
// yield { cid: parse(str), bytes }
|
123
126
|
// }
|
124
127
|
// }
|
@@ -145,39 +148,24 @@ export default class TransactionBlockstore {
|
|
145
148
|
}
|
146
149
|
|
147
150
|
// first get the transaction blockstore from the map of transaction blockstores
|
148
|
-
// then copy it to
|
151
|
+
// then copy it to committedBlocks
|
149
152
|
// then write the transaction blockstore to a car
|
150
153
|
// then write the car to the valet
|
151
154
|
// then remove the transaction blockstore from the map of transaction blockstores
|
152
|
-
#doCommit = async
|
155
|
+
#doCommit = async innerBlockstore => {
|
153
156
|
const cids = new Set()
|
154
157
|
for (const { cid, bytes } of innerBlockstore.entries()) {
|
155
158
|
const stringCid = cid.toString() // unnecessary string conversion, can we fix upstream?
|
156
|
-
if (this.#
|
157
|
-
// console.log('Duplicate block: ' + stringCid)
|
159
|
+
if (this.#committedBlocks.has(stringCid)) {
|
160
|
+
// console.log('Duplicate block: ' + stringCid) // todo some of this can be avoided, cost is extra size on car files
|
158
161
|
} else {
|
159
|
-
this.#
|
162
|
+
this.#committedBlocks.set(stringCid, bytes)
|
160
163
|
cids.add(stringCid)
|
161
164
|
}
|
162
165
|
}
|
163
166
|
if (cids.size > 0) {
|
164
167
|
// console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
|
165
|
-
await this
|
166
|
-
}
|
167
|
-
}
|
168
|
-
|
169
|
-
/**
|
170
|
-
* Group the blocks into a car and write it to the valet.
|
171
|
-
* @param {InnerBlockstore} innerBlockstore
|
172
|
-
* @param {Set<string>} cids
|
173
|
-
* @returns {Promise<void>}
|
174
|
-
* @memberof TransactionBlockstore
|
175
|
-
* @private
|
176
|
-
*/
|
177
|
-
#valetWriteTransaction = async (innerBlockstore, cids) => {
|
178
|
-
if (innerBlockstore.lastCid) {
|
179
|
-
const newCar = await blocksToCarBlock(innerBlockstore.lastCid, innerBlockstore)
|
180
|
-
await this.valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
|
168
|
+
await this.valet.writeTransaction(innerBlockstore, cids)
|
181
169
|
}
|
182
170
|
}
|
183
171
|
|
@@ -215,25 +203,6 @@ export const doTransaction = async (label, blockstore, doFun) => {
|
|
215
203
|
}
|
216
204
|
}
|
217
205
|
|
218
|
-
const blocksToCarBlock = async (lastCid, blocks) => {
|
219
|
-
let size = 0
|
220
|
-
const headerSize = CBW.headerLength({ roots: [lastCid] })
|
221
|
-
size += headerSize
|
222
|
-
for (const { cid, bytes } of blocks.entries()) {
|
223
|
-
size += CBW.blockLength({ cid, bytes })
|
224
|
-
}
|
225
|
-
const buffer = new Uint8Array(size)
|
226
|
-
const writer = await CBW.createWriter(buffer, { headerSize })
|
227
|
-
|
228
|
-
writer.addRoot(lastCid)
|
229
|
-
|
230
|
-
for (const { cid, bytes } of blocks.entries()) {
|
231
|
-
writer.write({ cid, bytes })
|
232
|
-
}
|
233
|
-
await writer.close()
|
234
|
-
return await Block.encode({ value: writer.bytes, hasher: sha256, codec: raw })
|
235
|
-
}
|
236
|
-
|
237
206
|
/** @implements {BlockFetcher} */
|
238
207
|
export class InnerBlockstore {
|
239
208
|
/** @type {Map<string, Uint8Array>} */
|
@@ -254,8 +223,10 @@ export class InnerBlockstore {
|
|
254
223
|
async get (cid) {
|
255
224
|
const key = cid.toString()
|
256
225
|
let bytes = this.#blocks.get(key)
|
257
|
-
if (bytes) {
|
258
|
-
|
226
|
+
if (bytes) {
|
227
|
+
return { cid, bytes }
|
228
|
+
}
|
229
|
+
bytes = await this.parentBlockstore.committedGet(key)
|
259
230
|
if (bytes) {
|
260
231
|
return { cid, bytes }
|
261
232
|
}
|
package/src/clock.js
CHANGED
@@ -220,7 +220,6 @@ export async function findEventsToSync (blocks, head) {
|
|
220
220
|
}
|
221
221
|
|
222
222
|
const asyncFilter = async (arr, predicate) =>
|
223
|
-
|
224
223
|
Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index]))
|
225
224
|
|
226
225
|
export async function findCommonAncestorWithSortedEvents (events, children) {
|
package/src/crypto.js
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
import * as codec from 'encrypted-block'
|
2
|
+
import {
|
3
|
+
create,
|
4
|
+
load
|
5
|
+
} from 'prolly-trees/cid-set'
|
6
|
+
import { CID } from 'multiformats'
|
7
|
+
import { encode, decode, create as mfCreate } from 'multiformats/block'
|
8
|
+
import * as dagcbor from '@ipld/dag-cbor'
|
9
|
+
import { sha256 as hasher } from 'multiformats/hashes/sha2'
|
10
|
+
|
11
|
+
const createBlock = (bytes, cid) => mfCreate({ cid, bytes, hasher, codec })
|
12
|
+
|
13
|
+
const encrypt = async function * ({ get, cids, hasher, key, cache, chunker, root }) {
|
14
|
+
const set = new Set()
|
15
|
+
let eroot
|
16
|
+
for (const string of cids) {
|
17
|
+
const cid = CID.parse(string)
|
18
|
+
const unencrypted = await get(cid)
|
19
|
+
const block = await encode({ ...await codec.encrypt({ ...unencrypted, key }), codec, hasher })
|
20
|
+
// console.log(`encrypting ${string} as ${block.cid}`)
|
21
|
+
yield block
|
22
|
+
set.add(block.cid.toString())
|
23
|
+
if (unencrypted.cid.equals(root)) eroot = block.cid
|
24
|
+
}
|
25
|
+
if (!eroot) throw new Error('cids does not include root')
|
26
|
+
const list = [...set].map(s => CID.parse(s))
|
27
|
+
let last
|
28
|
+
for await (const node of create({ list, get, cache, chunker, hasher, codec: dagcbor })) {
|
29
|
+
const block = await node.block
|
30
|
+
yield block
|
31
|
+
last = block
|
32
|
+
}
|
33
|
+
const head = [eroot, last.cid]
|
34
|
+
const block = await encode({ value: head, codec: dagcbor, hasher })
|
35
|
+
yield block
|
36
|
+
}
|
37
|
+
|
38
|
+
const decrypt = async function * ({ root, get, key, cache, chunker, hasher }) {
|
39
|
+
const o = { ...await get(root), codec: dagcbor, hasher }
|
40
|
+
const decodedRoot = await decode(o)
|
41
|
+
// console.log('decodedRoot', decodedRoot)
|
42
|
+
const { value: [eroot, tree] } = decodedRoot
|
43
|
+
const rootBlock = await get(eroot) // should I decrypt?
|
44
|
+
const cidset = await load({ cid: tree, get, cache, chunker, codec, hasher })
|
45
|
+
const { result: nodes } = await cidset.getAllEntries()
|
46
|
+
const unwrap = async (eblock) => {
|
47
|
+
const { bytes, cid } = await codec.decrypt({ ...eblock, key }).catch(e => {
|
48
|
+
console.log('ekey', e)
|
49
|
+
throw new Error('bad key: ' + key.toString('hex'))
|
50
|
+
})
|
51
|
+
const block = await createBlock(bytes, cid)
|
52
|
+
return block
|
53
|
+
}
|
54
|
+
const promises = []
|
55
|
+
for (const { cid } of nodes) {
|
56
|
+
if (!rootBlock.cid.equals(cid)) promises.push(get(cid).then(unwrap))
|
57
|
+
}
|
58
|
+
yield * promises
|
59
|
+
yield unwrap(rootBlock)
|
60
|
+
}
|
61
|
+
|
62
|
+
export {
|
63
|
+
encrypt,
|
64
|
+
decrypt
|
65
|
+
}
|
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
|
@@ -82,6 +67,7 @@ const indexEntriesForChanges = (changes, mapFn) => {
|
|
82
67
|
changes.forEach(({ key, value, del }) => {
|
83
68
|
if (del || !value) return
|
84
69
|
mapFn(makeDoc({ key, value }), (k, v) => {
|
70
|
+
if (typeof v === 'undefined' || typeof k === 'undefined') return
|
85
71
|
indexEntries.push({
|
86
72
|
key: [charwise.encode(k), key],
|
87
73
|
value: v
|
@@ -102,7 +88,7 @@ const indexEntriesForChanges = (changes, mapFn) => {
|
|
102
88
|
*
|
103
89
|
*/
|
104
90
|
export default class DbIndex {
|
105
|
-
constructor (database, mapFn, clock) {
|
91
|
+
constructor (database, mapFn, clock, opts = {}) {
|
106
92
|
// console.log('DbIndex constructor', database.constructor.name, typeof mapFn, clock)
|
107
93
|
/**
|
108
94
|
* The database instance to DbIndex.
|
@@ -110,7 +96,7 @@ export default class DbIndex {
|
|
110
96
|
*/
|
111
97
|
this.database = database
|
112
98
|
if (!database.indexBlocks) {
|
113
|
-
database.indexBlocks = new TransactionBlockstore(database.name + '.indexes')
|
99
|
+
database.indexBlocks = new TransactionBlockstore(database.name + '.indexes', database.blocks.valet.getKeyMaterial())
|
114
100
|
}
|
115
101
|
/**
|
116
102
|
* The map function to apply to each entry in the database.
|
@@ -123,6 +109,7 @@ export default class DbIndex {
|
|
123
109
|
this.mapFn = mapFn
|
124
110
|
this.mapFnString = mapFn.toString()
|
125
111
|
}
|
112
|
+
this.name = opts.name || this.makeName()
|
126
113
|
this.indexById = { root: null, cid: null }
|
127
114
|
this.indexByKey = { root: null, cid: null }
|
128
115
|
this.dbHead = null
|
@@ -133,45 +120,46 @@ export default class DbIndex {
|
|
133
120
|
}
|
134
121
|
this.instanceId = this.database.instanceId + `.DbIndex.${Math.random().toString(36).substring(2, 7)}`
|
135
122
|
this.updateIndexPromise = null
|
136
|
-
DbIndex.registerWithDatabase(this, this.database)
|
123
|
+
if (!opts.temporary) { DbIndex.registerWithDatabase(this, this.database) }
|
124
|
+
}
|
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]
|
137
130
|
}
|
138
131
|
|
139
132
|
static registerWithDatabase (inIndex, database) {
|
140
|
-
|
141
|
-
|
133
|
+
if (!database.indexes.has(inIndex.mapFnString)) {
|
134
|
+
database.indexes.set(inIndex.mapFnString, inIndex)
|
135
|
+
} else {
|
142
136
|
// merge our inIndex code with the inIndex clock or vice versa
|
143
|
-
// keep the code instance, discard the clock instance
|
144
137
|
const existingIndex = database.indexes.get(inIndex.mapFnString)
|
145
|
-
//
|
138
|
+
// keep the code instance, discard the clock instance
|
146
139
|
if (existingIndex.mapFn) { // this one also has other config
|
147
140
|
existingIndex.dbHead = inIndex.dbHead
|
148
141
|
existingIndex.indexById.cid = inIndex.indexById.cid
|
149
142
|
existingIndex.indexByKey.cid = inIndex.indexByKey.cid
|
150
143
|
} else {
|
151
|
-
// console.log('.reg use inIndex with existingIndex clock')
|
152
144
|
inIndex.dbHead = existingIndex.dbHead
|
153
145
|
inIndex.indexById.cid = existingIndex.indexById.cid
|
154
146
|
inIndex.indexByKey.cid = existingIndex.indexByKey.cid
|
155
147
|
database.indexes.set(inIndex.mapFnString, inIndex)
|
156
148
|
}
|
157
|
-
} else {
|
158
|
-
// console.log('.reg - fresh')
|
159
|
-
database.indexes.set(inIndex.mapFnString, inIndex)
|
160
149
|
}
|
161
|
-
// console.log('.reg after', JSON.stringify([...database.indexes.values()].map(i => [i.instanceId, typeof i.mapFn, i.indexByKey, i.indexById])))
|
162
150
|
}
|
163
151
|
|
164
152
|
toJSON () {
|
165
|
-
const indexJson = { code: this.
|
153
|
+
const indexJson = { name: this.name, code: this.mapFnString, clock: { db: null, byId: null, byKey: null } }
|
166
154
|
indexJson.clock.db = this.dbHead?.map(cid => cid.toString())
|
167
155
|
indexJson.clock.byId = this.indexById.cid?.toString()
|
168
156
|
indexJson.clock.byKey = this.indexByKey.cid?.toString()
|
169
157
|
return indexJson
|
170
158
|
}
|
171
159
|
|
172
|
-
static fromJSON (database, { code, clock }) {
|
160
|
+
static fromJSON (database, { code, clock, name }) {
|
173
161
|
// console.log('DbIndex.fromJSON', database.constructor.name, code, clock)
|
174
|
-
return new DbIndex(database, code, clock)
|
162
|
+
return new DbIndex(database, code, clock, { name })
|
175
163
|
}
|
176
164
|
|
177
165
|
/**
|
@@ -188,25 +176,19 @@ export default class DbIndex {
|
|
188
176
|
* @memberof DbIndex
|
189
177
|
* @instance
|
190
178
|
*/
|
191
|
-
async query (query) {
|
179
|
+
async query (query, update = true) {
|
192
180
|
// const callId = Math.random().toString(36).substring(2, 7)
|
193
|
-
//
|
194
|
-
// pass a root to query a snapshot
|
181
|
+
// todo pass a root to query a snapshot
|
195
182
|
// console.time(callId + '.#updateIndex')
|
196
|
-
await this.#updateIndex(this.database.indexBlocks)
|
183
|
+
update && await this.#updateIndex(this.database.indexBlocks)
|
197
184
|
// console.timeEnd(callId + '.#updateIndex')
|
198
|
-
|
199
|
-
// }
|
200
185
|
// console.time(callId + '.doIndexQuery')
|
201
186
|
// console.log('query', query)
|
202
187
|
const response = await doIndexQuery(this.database.indexBlocks, this.indexByKey, query)
|
203
188
|
// console.timeEnd(callId + '.doIndexQuery')
|
204
|
-
|
205
189
|
return {
|
206
190
|
proof: { index: await cidsToProof(response.cids) },
|
207
|
-
// TODO fix this naming upstream in prolly/db-DbIndex?
|
208
191
|
rows: response.result.map(({ id, key, row }) => {
|
209
|
-
// console.log('query', id, key, row)
|
210
192
|
return ({ id, key: charwise.decode(key), value: row })
|
211
193
|
})
|
212
194
|
}
|
@@ -269,6 +251,7 @@ export default class DbIndex {
|
|
269
251
|
this.indexByKey = await bulkIndex(blocks, this.indexByKey, oldIndexEntries.concat(indexEntries), dbIndexOpts)
|
270
252
|
this.dbHead = result.clock
|
271
253
|
})
|
254
|
+
this.database.notifyExternal('dbIndex')
|
272
255
|
// console.timeEnd(callTag + '.doTransaction#updateIndex')
|
273
256
|
// console.log(`#updateIndex ${callTag} <`, this.instanceId, this.dbHead?.toString(), this.indexByKey.cid?.toString(), this.indexById.cid?.toString())
|
274
257
|
}
|
@@ -320,16 +303,22 @@ async function loadIndex (blocks, index, indexOpts) {
|
|
320
303
|
return index.root
|
321
304
|
}
|
322
305
|
|
323
|
-
async function
|
306
|
+
async function applyLimit (results, limit) {
|
307
|
+
results.result = results.result.slice(0, limit)
|
308
|
+
return results
|
309
|
+
}
|
310
|
+
|
311
|
+
async function doIndexQuery (blocks, indexByKey, query = {}) {
|
324
312
|
await loadIndex(blocks, indexByKey, dbIndexOpts)
|
325
|
-
if (!
|
326
|
-
|
327
|
-
return { result: result.map(({ key: [k, id], value }) => ({ key: k, id, row: value })), ...all }
|
328
|
-
} else if (query.range) {
|
313
|
+
if (!indexByKey.root) return { result: [] }
|
314
|
+
if (query.range) {
|
329
315
|
const encodedRange = query.range.map((key) => charwise.encode(key))
|
330
|
-
return indexByKey.root.range(...encodedRange)
|
316
|
+
return applyLimit(await indexByKey.root.range(...encodedRange), query.limit)
|
331
317
|
} else if (query.key) {
|
332
318
|
const encodedKey = charwise.encode(query.key)
|
333
319
|
return indexByKey.root.get(encodedKey)
|
320
|
+
} else {
|
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)
|
334
323
|
}
|
335
324
|
}
|