@fireproof/core 0.0.2 → 0.0.4
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/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/block.js.html +280 -0
- package/coverage/blockstore.js.html +916 -0
- package/coverage/clock.js.html +1141 -0
- package/coverage/db-index.js.html +694 -0
- package/coverage/favicon.png +0 -0
- package/coverage/fireproof.js.html +856 -0
- package/coverage/index.html +221 -0
- package/coverage/listener.js.html +421 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/prolly.js.html +883 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/tmp/coverage-42191-1678146904346-0.json +1 -0
- package/coverage/tmp/coverage-42193-1678146903521-0.json +1 -0
- package/coverage/tmp/coverage-42196-1678146904322-0.json +1 -0
- package/coverage/tmp/coverage-42197-1678146904292-0.json +1 -0
- package/coverage/valet.js.html +589 -0
- package/hooks/use-fireproof.md +149 -0
- package/hooks/use-fireproof.ts +112 -0
- package/package.json +7 -4
- package/src/block.js +0 -10
- package/src/blockstore.js +53 -22
- package/src/db-index.js +71 -46
- package/src/fireproof.js +90 -44
- package/src/listener.js +23 -20
- package/src/prolly.js +53 -22
- package/src/valet.js +119 -93
- package/test/clock.test.js +60 -12
- package/test/db-index.test.js +3 -0
- package/test/listener.test.js +2 -3
- package/test/valet.test.js +59 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
# useFireproof hook for React
|
2
|
+
|
3
|
+
React hook to initialize a Fireproof database, automatically saving and loading the clock.
|
4
|
+
|
5
|
+
The hook takes two optional setup function arguments, `defineDatabaseFn` and `setupDatabaseFn`. See below for examples.
|
6
|
+
|
7
|
+
The return value looks like `{ ready, database, addSubscriber }` where the `database` is your Fireproof instance that you can interact with using `put` and `get`, or via your indexes. The `ready` flag turns true after setup completes, you can use this to activate your UI. The `addSubscriber` function is used to update your app in realtime, see example.
|
8
|
+
|
9
|
+
## Usage Example
|
10
|
+
|
11
|
+
In App.js:
|
12
|
+
|
13
|
+
```js
|
14
|
+
import { FireproofCtx, useFireproof } from '@fireproof/core/hooks/use-fireproof'
|
15
|
+
|
16
|
+
function App() {
|
17
|
+
// establish the Fireproof context value
|
18
|
+
const fpCtxValue = useFireproof()
|
19
|
+
|
20
|
+
// render the rest of the application wrapped in the Fireproof provider
|
21
|
+
return (
|
22
|
+
<FireproofCtx.Provider value={fpCtxValue}>
|
23
|
+
<MyComponent />
|
24
|
+
</FireproofCtx.Provider>
|
25
|
+
)
|
26
|
+
}
|
27
|
+
```
|
28
|
+
|
29
|
+
In your components:
|
30
|
+
|
31
|
+
```js
|
32
|
+
import { FireproofCtx } from '@fireproof/core/hooks/use-fireproof'
|
33
|
+
|
34
|
+
function MyComponent() {
|
35
|
+
// get Fireproof context
|
36
|
+
const { ready, database, addSubscriber } = useContext(FireproofCtx)
|
37
|
+
|
38
|
+
// set a default empty document
|
39
|
+
const [doc, setDoc] = useState({})
|
40
|
+
|
41
|
+
// function to load the document from the database
|
42
|
+
const getDataFn = async () => {
|
43
|
+
setDoc(await database.get("my-doc-id"))
|
44
|
+
}
|
45
|
+
|
46
|
+
// run that function when the database changes
|
47
|
+
addSubscriber('MyComponent', getDataFn)
|
48
|
+
|
49
|
+
// run the loader on first mount
|
50
|
+
useEffect(() => getDataFn(), [])
|
51
|
+
|
52
|
+
// a function to change the value of the document
|
53
|
+
const updateFn = async () => {
|
54
|
+
await database.put({ _id : "my-doc-id", hello: "world", updated_at: new Date()})
|
55
|
+
}
|
56
|
+
|
57
|
+
// render the document with a click handler to update it
|
58
|
+
return <pre onclick={updateFn}>JSON.stringify(doc)</pre>
|
59
|
+
}
|
60
|
+
```
|
61
|
+
|
62
|
+
This should result in a tiny application that updates the document when you click it. In a real appliction you'd probably query an index to present eg. all of the photos in a gallery.
|
63
|
+
|
64
|
+
## Setup Functions
|
65
|
+
|
66
|
+
### defineDatabaseFn
|
67
|
+
|
68
|
+
Synchronous function that defines the database, run this before any async calls. You can use it to do stuff like set up Indexes. Here's an example:
|
69
|
+
|
70
|
+
```js
|
71
|
+
const defineIndexes = (database) => {
|
72
|
+
database.allLists = new Index(database, function (doc, map) {
|
73
|
+
if (doc.type === 'list') map(doc.type, doc)
|
74
|
+
})
|
75
|
+
database.todosByList = new Index(database, function (doc, map) {
|
76
|
+
if (doc.type === 'todo' && doc.listId) {
|
77
|
+
map([doc.listId, doc.createdAt], doc)
|
78
|
+
}
|
79
|
+
})
|
80
|
+
window.fireproof = database // 🤫 for dev
|
81
|
+
return database
|
82
|
+
}
|
83
|
+
```
|
84
|
+
|
85
|
+
### setupDatabaseFn
|
86
|
+
|
87
|
+
Asynchronous function that uses the database when it's ready, run this to load fixture data, insert a dataset from somewhere else, etc. Here's a simple example:
|
88
|
+
|
89
|
+
|
90
|
+
```
|
91
|
+
async function setupDatabase(database)) {
|
92
|
+
const apiData = await (await fetch('https://dummyjson.com/products')).json()
|
93
|
+
for (const product of apiData.products) {
|
94
|
+
await database.put(product)
|
95
|
+
}
|
96
|
+
}
|
97
|
+
```
|
98
|
+
|
99
|
+
Note there are no protections against you running the same thing over and over again, so you probably want to put some logic in there to do the right thing.
|
100
|
+
|
101
|
+
Here is an example of generating deterministic fixtures, using `mulberry32` for determinstic randomness so re-runs give the same CID, avoiding unnecessary bloat at development time, taken from the TodoMVC demo app.
|
102
|
+
|
103
|
+
```js
|
104
|
+
function mulberry32(a) {
|
105
|
+
return function () {
|
106
|
+
let t = (a += 0x6d2b79f5)
|
107
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
108
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
109
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
110
|
+
}
|
111
|
+
}
|
112
|
+
const rand = mulberry32(1) // determinstic fixtures
|
113
|
+
|
114
|
+
export default async function loadFixtures(database) {
|
115
|
+
const nextId = (prefix = '') => prefix + rand().toString(32).slice(2)
|
116
|
+
const listTitles = ['Building Apps', 'Having Fun', 'Getting Groceries']
|
117
|
+
const todoTitles = [
|
118
|
+
[
|
119
|
+
'In the browser',
|
120
|
+
'On the phone',
|
121
|
+
'With or without Redux',
|
122
|
+
'Login components',
|
123
|
+
'GraphQL queries',
|
124
|
+
'Automatic replication and versioning',
|
125
|
+
],
|
126
|
+
['Rollerskating meetup', 'Motorcycle ride', 'Write a sci-fi story with ChatGPT'],
|
127
|
+
['Macadamia nut milk', 'Avocado toast', 'Coffee', 'Bacon', 'Sourdough bread', 'Fruit salad'],
|
128
|
+
]
|
129
|
+
let ok
|
130
|
+
for (let j = 0; j < 3; j++) {
|
131
|
+
ok = await database.put({
|
132
|
+
title: listTitles[j],
|
133
|
+
type: 'list',
|
134
|
+
_id: nextId('' + j)
|
135
|
+
})
|
136
|
+
for (let i = 0; i < todoTitles[j].length; i++) {
|
137
|
+
await database.put({
|
138
|
+
_id: nextId(),
|
139
|
+
title: todoTitles[j][i],
|
140
|
+
listId: ok.id,
|
141
|
+
completed: rand() > 0.75,
|
142
|
+
type: 'todo',
|
143
|
+
})
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
```
|
148
|
+
|
149
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
/* global localStorage */
|
2
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
3
|
+
// @ts-ignore
|
4
|
+
import { useEffect, useState, createContext } from 'react'
|
5
|
+
import { Fireproof, Listener } from '@fireproof/core'
|
6
|
+
|
7
|
+
export interface FireproofCtxValue {
|
8
|
+
addSubscriber: (label: String, fn: Function) => void
|
9
|
+
database: Fireproof
|
10
|
+
ready: boolean
|
11
|
+
}
|
12
|
+
export const FireproofCtx = createContext<FireproofCtxValue>({
|
13
|
+
addSubscriber: () => {},
|
14
|
+
database: null,
|
15
|
+
ready: false,
|
16
|
+
})
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
const inboundSubscriberQueue = new Map()
|
21
|
+
const database = Fireproof.storage()
|
22
|
+
const listener = new Listener(database)
|
23
|
+
|
24
|
+
/**
|
25
|
+
* @function useFireproof
|
26
|
+
* React hook to initialize a Fireproof database, automatically saving and loading the clock.
|
27
|
+
* @param [defineDatabaseFn] Synchronous function that defines the database, run this before any async calls
|
28
|
+
* @param [setupDatabaseFn] Asynchronous function that sets up the database, run this to load fixture data etc
|
29
|
+
* @returns {FireproofCtxValue} { addSubscriber, database, ready }
|
30
|
+
*/
|
31
|
+
export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Function): FireproofCtxValue {
|
32
|
+
const [ready, setReady] = useState(false)
|
33
|
+
defineDatabaseFn = defineDatabaseFn || (() => {})
|
34
|
+
setupDatabaseFn = setupDatabaseFn || (() => {})
|
35
|
+
|
36
|
+
if (!ready) {
|
37
|
+
defineDatabaseFn(database)
|
38
|
+
}
|
39
|
+
|
40
|
+
const addSubscriber = (label: String, fn: Function) => {
|
41
|
+
inboundSubscriberQueue.set(label, fn)
|
42
|
+
}
|
43
|
+
|
44
|
+
const listenerCallback = async () => {
|
45
|
+
localSet('fireproof', JSON.stringify(database))
|
46
|
+
for (const [, fn] of inboundSubscriberQueue) fn()
|
47
|
+
}
|
48
|
+
|
49
|
+
useEffect(() => {
|
50
|
+
const doSetup = async () => {
|
51
|
+
if (ready) return
|
52
|
+
const fp = localGet('fireproof')
|
53
|
+
if (fp) {
|
54
|
+
const { clock } = JSON.parse(fp)
|
55
|
+
console.log("Loading previous database clock. (localStorage.removeItem('fireproof') to reset)")
|
56
|
+
await database.setClock(clock)
|
57
|
+
try {
|
58
|
+
await database.changesSince()
|
59
|
+
} catch (e) {
|
60
|
+
console.error('Error loading previous database clock.', e)
|
61
|
+
await database.setClock([])
|
62
|
+
await setupDatabaseFn(database)
|
63
|
+
localSet('fireproof', JSON.stringify(database))
|
64
|
+
}
|
65
|
+
} else {
|
66
|
+
await setupDatabaseFn(database)
|
67
|
+
localSet('fireproof', JSON.stringify(database))
|
68
|
+
}
|
69
|
+
setReady(true)
|
70
|
+
listener.on('*', hushed('*', listenerCallback, 250))
|
71
|
+
}
|
72
|
+
doSetup()
|
73
|
+
}, [ready])
|
74
|
+
|
75
|
+
return {
|
76
|
+
addSubscriber,
|
77
|
+
database,
|
78
|
+
ready,
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
const husherMap = new Map()
|
83
|
+
const husher = (id: string, workFn: { (): Promise<any> }, ms: number) => {
|
84
|
+
if (!husherMap.has(id)) {
|
85
|
+
husherMap.set(
|
86
|
+
id,
|
87
|
+
workFn().finally(() => setTimeout(() => husherMap.delete(id), ms))
|
88
|
+
)
|
89
|
+
}
|
90
|
+
return husherMap.get(id)
|
91
|
+
}
|
92
|
+
const hushed = (id: string, workFn: { (): Promise<any> }, ms: number) => () => husher(id, workFn, ms)
|
93
|
+
|
94
|
+
let storageSupported = false
|
95
|
+
try {
|
96
|
+
storageSupported = window.localStorage && true
|
97
|
+
} catch (e) {}
|
98
|
+
export function localGet(key: string) {
|
99
|
+
if (storageSupported) {
|
100
|
+
return localStorage && localStorage.getItem(key)
|
101
|
+
}
|
102
|
+
}
|
103
|
+
function localSet(key: string, value: string) {
|
104
|
+
if (storageSupported) {
|
105
|
+
return localStorage && localStorage.setItem(key, value)
|
106
|
+
}
|
107
|
+
}
|
108
|
+
// function localRemove(key) {
|
109
|
+
// if (storageSupported) {
|
110
|
+
// return localStorage && localStorage.removeItem(key)
|
111
|
+
// }
|
112
|
+
// }
|
package/package.json
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fireproof/core",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.4",
|
4
4
|
"description": "Realtime database for IPFS",
|
5
5
|
"main": "index.js",
|
6
6
|
"type": "module",
|
7
7
|
"scripts": {
|
8
|
-
"test": "standard &&
|
8
|
+
"test": "standard && npm run test:mocha",
|
9
|
+
"test:mocha": "mocha test/*.test.js",
|
10
|
+
"test:watch": "npm run test:mocha -- -w --parallel test/*.test.js",
|
9
11
|
"coverage": "c8 -r html -r text npm test",
|
10
12
|
"lint": "standard",
|
11
13
|
"lint:fix": "standard --fix"
|
@@ -29,6 +31,7 @@
|
|
29
31
|
"@ipld/car": "^5.1.0",
|
30
32
|
"@ipld/dag-cbor": "^9.0.0",
|
31
33
|
"archy": "^1.0.0",
|
34
|
+
"async": "^3.2.4",
|
32
35
|
"car-transaction": "^1.0.1",
|
33
36
|
"charwise": "^3.0.1",
|
34
37
|
"cli-color": "^2.0.3",
|
@@ -52,7 +55,8 @@
|
|
52
55
|
"standard": {
|
53
56
|
"ignore": [
|
54
57
|
"examples/**/*.tsx",
|
55
|
-
"examples/**/dist"
|
58
|
+
"examples/**/dist",
|
59
|
+
"out/**"
|
56
60
|
]
|
57
61
|
},
|
58
62
|
"repository": {
|
@@ -66,5 +70,4 @@
|
|
66
70
|
"workspaces": [
|
67
71
|
"examples/todomvc"
|
68
72
|
]
|
69
|
-
|
70
73
|
}
|
package/src/block.js
CHANGED
@@ -37,16 +37,6 @@ export class MemoryBlockstore {
|
|
37
37
|
this.#blocks.set(cid.toString(), bytes)
|
38
38
|
}
|
39
39
|
|
40
|
-
/** @param {import('./link').AnyLink} cid */
|
41
|
-
async delete (cid) {
|
42
|
-
this.#blocks.delete(cid.toString())
|
43
|
-
}
|
44
|
-
|
45
|
-
/** @param {import('./link').AnyLink} cid */
|
46
|
-
deleteSync (cid) {
|
47
|
-
this.#blocks.delete(cid.toString())
|
48
|
-
}
|
49
|
-
|
50
40
|
* entries () {
|
51
41
|
for (const [str, bytes] of this.#blocks) {
|
52
42
|
yield { cid: parse(str), bytes }
|
package/src/blockstore.js
CHANGED
@@ -3,11 +3,23 @@ import * as raw from 'multiformats/codecs/raw'
|
|
3
3
|
import { sha256 } from 'multiformats/hashes/sha2'
|
4
4
|
import * as Block from 'multiformats/block'
|
5
5
|
import * as CBW from '@ipld/car/buffer-writer'
|
6
|
+
import { CID } from 'multiformats'
|
6
7
|
|
7
8
|
import Valet from './valet.js'
|
8
9
|
|
9
10
|
// const sleep = ms => new Promise(r => setTimeout(r, ms))
|
10
11
|
|
12
|
+
const husherMap = new Map()
|
13
|
+
const husher = (id, workFn) => {
|
14
|
+
if (!husherMap.has(id)) {
|
15
|
+
husherMap.set(
|
16
|
+
id,
|
17
|
+
workFn().finally(() => setTimeout(() => husherMap.delete(id), 100))
|
18
|
+
)
|
19
|
+
}
|
20
|
+
return husherMap.get(id)
|
21
|
+
}
|
22
|
+
|
11
23
|
/**
|
12
24
|
* @typedef {Object} AnyBlock
|
13
25
|
* @property {import('./link').AnyLink} cid - The CID of the block
|
@@ -24,7 +36,7 @@ export default class TransactionBlockstore {
|
|
24
36
|
/** @type {Map<string, Uint8Array>} */
|
25
37
|
#oldBlocks = new Map()
|
26
38
|
|
27
|
-
|
39
|
+
valet = new Valet() // cars by cid
|
28
40
|
|
29
41
|
#instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4)
|
30
42
|
#inflightTransactions = new Set()
|
@@ -38,10 +50,10 @@ export default class TransactionBlockstore {
|
|
38
50
|
async get (cid) {
|
39
51
|
const key = cid.toString()
|
40
52
|
// it is safe to read from the in-flight transactions becauase they are immutable
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
53
|
+
const bytes = await Promise.any([this.#transactionsGet(key), this.commitedGet(key)]).catch((e) => {
|
54
|
+
console.log('networkGet', cid.toString(), e)
|
55
|
+
return this.networkGet(key)
|
56
|
+
})
|
45
57
|
if (!bytes) throw new Error('Missing block: ' + key)
|
46
58
|
return { cid, bytes }
|
47
59
|
}
|
@@ -53,11 +65,30 @@ export default class TransactionBlockstore {
|
|
53
65
|
const got = await transaction.get(key)
|
54
66
|
if (got && got.bytes) return got.bytes
|
55
67
|
}
|
68
|
+
throw new Error('Missing block: ' + key)
|
56
69
|
}
|
57
70
|
|
58
71
|
async commitedGet (key) {
|
59
|
-
|
60
|
-
|
72
|
+
const old = this.#oldBlocks.get(key)
|
73
|
+
if (old) return old
|
74
|
+
return await this.valet.getBlock(key)
|
75
|
+
}
|
76
|
+
|
77
|
+
async networkGet (key) {
|
78
|
+
if (this.valet.remoteBlockFunction) {
|
79
|
+
const value = await husher(key, async () => await this.valet.remoteBlockFunction(key))
|
80
|
+
if (value) {
|
81
|
+
// console.log('networkGot: ' + key, value.length)
|
82
|
+
// dont turn this on until the Nan thing is fixed
|
83
|
+
// it keep the network blocks in indexedb but lets get the basics solid first
|
84
|
+
doTransaction('networkGot: ' + key, this, async (innerBlockstore) => {
|
85
|
+
await innerBlockstore.put(CID.parse(key), value)
|
86
|
+
})
|
87
|
+
return value
|
88
|
+
}
|
89
|
+
} else {
|
90
|
+
throw new Error('No remoteBlockFunction')
|
91
|
+
}
|
61
92
|
}
|
62
93
|
|
63
94
|
/**
|
@@ -91,11 +122,11 @@ export default class TransactionBlockstore {
|
|
91
122
|
}
|
92
123
|
|
93
124
|
/**
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
125
|
+
* Begin a transaction. Ensures the uncommited blocks are empty at the begining.
|
126
|
+
* Returns the blocks to read and write during the transaction.
|
127
|
+
* @returns {InnerBlockstore}
|
128
|
+
* @memberof TransactionBlockstore
|
129
|
+
*/
|
99
130
|
begin (label = '') {
|
100
131
|
const innerTransactionBlockstore = new InnerBlockstore(label, this)
|
101
132
|
this.#inflightTransactions.add(innerTransactionBlockstore)
|
@@ -103,10 +134,10 @@ export default class TransactionBlockstore {
|
|
103
134
|
}
|
104
135
|
|
105
136
|
/**
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
137
|
+
* Commit the transaction. Writes the blocks to the store.
|
138
|
+
* @returns {Promise<void>}
|
139
|
+
* @memberof TransactionBlockstore
|
140
|
+
*/
|
110
141
|
async commit (innerBlockstore) {
|
111
142
|
await this.#doCommit(innerBlockstore)
|
112
143
|
}
|
@@ -128,7 +159,7 @@ export default class TransactionBlockstore {
|
|
128
159
|
}
|
129
160
|
}
|
130
161
|
if (cids.size > 0) {
|
131
|
-
console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
|
162
|
+
// console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
|
132
163
|
await this.#valetWriteTransaction(innerBlockstore, cids)
|
133
164
|
}
|
134
165
|
}
|
@@ -144,15 +175,15 @@ export default class TransactionBlockstore {
|
|
144
175
|
#valetWriteTransaction = async (innerBlockstore, cids) => {
|
145
176
|
if (innerBlockstore.lastCid) {
|
146
177
|
const newCar = await blocksToCarBlock(innerBlockstore.lastCid, innerBlockstore)
|
147
|
-
await this
|
178
|
+
await this.valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
|
148
179
|
}
|
149
180
|
}
|
150
181
|
|
151
182
|
/**
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
183
|
+
* Retire the transaction. Clears the uncommited blocks.
|
184
|
+
* @returns {void}
|
185
|
+
* @memberof TransactionBlockstore
|
186
|
+
*/
|
156
187
|
retire (innerBlockstore) {
|
157
188
|
this.#inflightTransactions.delete(innerBlockstore)
|
158
189
|
}
|
package/src/db-index.js
CHANGED
@@ -16,16 +16,33 @@ const makeGetBlock = (blocks) => async (address) => {
|
|
16
16
|
}
|
17
17
|
const makeDoc = ({ key, value }) => ({ _id: key, ...value })
|
18
18
|
|
19
|
-
|
19
|
+
/**
|
20
|
+
* JDoc for the result row type.
|
21
|
+
* @typedef {Object} ChangeEvent
|
22
|
+
* @property {string} key - The key of the document.
|
23
|
+
* @property {Object} value - The new value of the document.
|
24
|
+
* @property {boolean} [del] - Is the row deleted?
|
25
|
+
* @memberof DbIndex
|
26
|
+
*/
|
20
27
|
|
21
28
|
/**
|
22
|
-
*
|
29
|
+
* JDoc for the result row type.
|
30
|
+
* @typedef {Object} DbIndexEntry
|
31
|
+
* @property {string[]} key - The key for the DbIndex entry.
|
32
|
+
* @property {Object} value - The value of the document.
|
33
|
+
* @property {boolean} [del] - Is the row deleted?
|
34
|
+
* @memberof DbIndex
|
35
|
+
*/
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Transforms a set of changes to DbIndex entries using a map function.
|
23
39
|
*
|
24
|
-
* @param {
|
40
|
+
* @param {ChangeEvent[]} changes
|
25
41
|
* @param {Function} mapFun
|
26
|
-
* @returns {
|
42
|
+
* @returns {DbIndexEntry[]} The DbIndex entries generated by the map function.
|
43
|
+
* @private
|
44
|
+
* @memberof DbIndex
|
27
45
|
*/
|
28
|
-
|
29
46
|
const indexEntriesForChanges = (changes, mapFun) => {
|
30
47
|
const indexEntries = []
|
31
48
|
changes.forEach(({ key, value, del }) => {
|
@@ -49,25 +66,20 @@ const indexEntriesForOldChanges = async (blocks, byIDindexRoot, ids, mapFun) =>
|
|
49
66
|
}
|
50
67
|
|
51
68
|
/**
|
52
|
-
* Represents an
|
69
|
+
* Represents an DbIndex for a Fireproof database.
|
53
70
|
*
|
54
|
-
* @class
|
55
|
-
* @classdesc An
|
71
|
+
* @class DbIndex
|
72
|
+
* @classdesc An DbIndex can be used to order and filter the documents in a Fireproof database.
|
56
73
|
*
|
57
|
-
* @param {
|
74
|
+
* @param {Fireproof} database - The Fireproof database instance to DbIndex.
|
58
75
|
* @param {Function} mapFun - The map function to apply to each entry in the database.
|
59
76
|
*
|
60
77
|
*/
|
61
|
-
export default class
|
62
|
-
/**
|
63
|
-
* Creates a new index with the given map function and database.
|
64
|
-
* @param {import('./fireproof').Fireproof} database - The Fireproof database instance to index.
|
65
|
-
* @param {Function} mapFun - The map function to apply to each entry in the database.
|
66
|
-
*/
|
78
|
+
export default class DbIndex {
|
67
79
|
constructor (database, mapFun) {
|
68
80
|
/**
|
69
|
-
* The database instance to
|
70
|
-
* @type {
|
81
|
+
* The database instance to DbIndex.
|
82
|
+
* @type {Fireproof}
|
71
83
|
*/
|
72
84
|
this.database = database
|
73
85
|
/**
|
@@ -80,26 +92,38 @@ export default class Index {
|
|
80
92
|
this.dbHead = null
|
81
93
|
}
|
82
94
|
|
95
|
+
/**
|
96
|
+
* JSDoc for Query type.
|
97
|
+
* @typedef {Object} DbQuery
|
98
|
+
* @property {string[]} [range] - The range to query.
|
99
|
+
* @memberof DbIndex
|
100
|
+
*/
|
101
|
+
|
83
102
|
/**
|
84
103
|
* Query object can have {range}
|
85
|
-
*
|
104
|
+
* @param {DbQuery} query - the query range to use
|
105
|
+
* @param {CID} [root] - an optional root to query a snapshot
|
106
|
+
* @returns {Promise<{rows: Array<{id: string, key: string, value: any}>}>}
|
107
|
+
* @memberof DbIndex
|
108
|
+
* @instance
|
86
109
|
*/
|
87
110
|
async query (query, root = null) {
|
88
|
-
if (!root) {
|
111
|
+
if (!root) {
|
112
|
+
// pass a root to query a snapshot
|
89
113
|
await doTransaction('#updateIndex', this.database.blocks, async (blocks) => {
|
90
114
|
await this.#updateIndex(blocks)
|
91
115
|
})
|
92
116
|
}
|
93
117
|
const response = await doIndexQuery(this.database.blocks, root || this.indexRoot, query)
|
94
118
|
return {
|
95
|
-
// TODO fix this naming upstream in prolly/db-
|
119
|
+
// TODO fix this naming upstream in prolly/db-DbIndex
|
96
120
|
// todo maybe this is a hint about why deletes arent working?
|
97
121
|
rows: response.result.map(({ id, key, row }) => ({ id: key, key: charwise.decode(id), value: row }))
|
98
122
|
}
|
99
123
|
}
|
100
124
|
|
101
125
|
/**
|
102
|
-
* Update the
|
126
|
+
* Update the DbIndex with the latest changes
|
103
127
|
* @private
|
104
128
|
* @returns {Promise<void>}
|
105
129
|
*/
|
@@ -111,16 +135,23 @@ export default class Index {
|
|
111
135
|
}
|
112
136
|
const result = await this.database.changesSince(this.dbHead) // {key, value, del}
|
113
137
|
if (this.dbHead) {
|
114
|
-
const oldIndexEntries = (
|
138
|
+
const oldIndexEntries = (
|
139
|
+
await indexEntriesForOldChanges(
|
140
|
+
blocks,
|
141
|
+
this.byIDindexRoot,
|
142
|
+
result.rows.map(({ key }) => key),
|
143
|
+
this.mapFun
|
144
|
+
)
|
145
|
+
)
|
115
146
|
// .map((key) => ({ key, value: null })) // tombstone just adds more rows...
|
116
147
|
.map((key) => ({ key, del: true })) // should be this
|
117
|
-
|
148
|
+
// .map((key) => ({ key: undefined, del: true })) // todo why does this work?
|
118
149
|
|
119
150
|
this.indexRoot = await bulkIndex(blocks, this.indexRoot, oldIndexEntries, opts)
|
120
151
|
// console.x('oldIndexEntries', oldIndexEntries)
|
121
152
|
// [ { key: ['b', 1], del: true } ]
|
122
153
|
// [ { key: [ 5, 'x' ], del: true } ]
|
123
|
-
// for now we just let the by id
|
154
|
+
// for now we just let the by id DbIndex grow and then don't use the results...
|
124
155
|
// const removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
|
125
156
|
// this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, removeByIdIndexEntries, opts)
|
126
157
|
}
|
@@ -130,67 +161,61 @@ export default class Index {
|
|
130
161
|
this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, byIdIndexEntries, opts)
|
131
162
|
// console.log('indexEntries', indexEntries)
|
132
163
|
this.indexRoot = await bulkIndex(blocks, this.indexRoot, indexEntries, opts)
|
133
|
-
// console.log('did
|
164
|
+
// console.log('did DbIndex', this.indexRoot)
|
134
165
|
this.dbHead = result.clock
|
135
166
|
}
|
136
167
|
|
137
|
-
// todo use the
|
168
|
+
// todo use the DbIndex from other peers?
|
138
169
|
// we might need to add CRDT logic to it for that
|
139
170
|
// it would only be a performance improvement, but might add a lot of complexity
|
140
171
|
// advanceIndex ()) {}
|
141
172
|
}
|
142
173
|
|
143
174
|
/**
|
144
|
-
* Update the
|
175
|
+
* Update the DbIndex with the given entries
|
145
176
|
* @param {Blockstore} blocks
|
146
|
-
* @param {
|
147
|
-
* @param {
|
177
|
+
* @param {Block} inRoot
|
178
|
+
* @param {DbIndexEntry[]} indexEntries
|
179
|
+
* @private
|
148
180
|
*/
|
149
181
|
async function bulkIndex (blocks, inRoot, indexEntries) {
|
150
182
|
if (!indexEntries.length) return inRoot
|
151
183
|
const putBlock = blocks.put.bind(blocks)
|
152
184
|
const getBlock = makeGetBlock(blocks)
|
153
185
|
if (!inRoot) {
|
154
|
-
// make a new
|
186
|
+
// make a new DbIndex
|
155
187
|
|
156
188
|
for await (const node of await create({ get: getBlock, list: indexEntries, ...opts })) {
|
157
189
|
const block = await node.block
|
158
190
|
await putBlock(block.cid, block.bytes)
|
159
191
|
inRoot = block
|
160
192
|
}
|
161
|
-
// console.x('created
|
193
|
+
// console.x('created DbIndex', inRoot.cid)
|
162
194
|
return inRoot
|
163
195
|
} else {
|
164
|
-
// load existing
|
165
|
-
// console.x('loading
|
166
|
-
const
|
196
|
+
// load existing DbIndex
|
197
|
+
// console.x('loading DbIndex', inRoot.cid)
|
198
|
+
const DbIndex = await load({ cid: inRoot.cid, get: getBlock, ...opts })
|
167
199
|
// console.log('new indexEntries', indexEntries)
|
168
|
-
const { root, blocks } = await
|
200
|
+
const { root, blocks } = await DbIndex.bulk(indexEntries)
|
169
201
|
for await (const block of blocks) {
|
170
202
|
await putBlock(block.cid, block.bytes)
|
171
203
|
}
|
172
|
-
// console.x('updated
|
204
|
+
// console.x('updated DbIndex', root.block.cid)
|
173
205
|
return await root.block // if we hold the root we won't have to load every time
|
174
206
|
}
|
175
207
|
}
|
176
208
|
|
177
|
-
/**
|
178
|
-
* Query the index for the given range
|
179
|
-
* @param {Blockstore} blocks
|
180
|
-
* @param {import('multiformats/block').Block} inRoot
|
181
|
-
* @param {import('prolly-trees/db-index').Query} query
|
182
|
-
* @returns {Promise<import('prolly-trees/db-index').QueryResult>}
|
183
|
-
**/
|
184
209
|
async function doIndexQuery (blocks, root, query) {
|
185
210
|
const cid = root && root.cid
|
186
211
|
if (!cid) return { result: [] }
|
187
212
|
const getBlock = makeGetBlock(blocks)
|
188
|
-
const
|
213
|
+
const DbIndex = await load({ cid, get: getBlock, ...opts })
|
189
214
|
if (query.range) {
|
190
215
|
const encodedRange = query.range.map((key) => charwise.encode(key))
|
191
|
-
return
|
216
|
+
return DbIndex.range(...encodedRange)
|
192
217
|
} else if (query.key) {
|
193
218
|
const encodedKey = charwise.encode(query.key)
|
194
|
-
return
|
219
|
+
return DbIndex.get(encodedKey)
|
195
220
|
}
|
196
221
|
}
|