@fireproof/core 0.0.8 → 0.0.10
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/README.md +148 -0
- package/hooks/use-fireproof.ts +14 -8
- package/index.js +2 -1
- package/package.json +2 -1
- package/src/db-index.js +78 -37
- package/src/fireproof.js +18 -30
- package/src/hydrator.js +47 -6
- package/test/db-index.test.js +5 -4
- package/test/fireproof.test.js +2 -2
- package/test/hydrator.test.js +5 -5
- package/test/listener.test.js +2 -1
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/block.js.html +0 -280
- package/coverage/blockstore.js.html +0 -928
- package/coverage/clock.js.html +0 -1078
- package/coverage/db-index.js.html +0 -907
- package/coverage/favicon.png +0 -0
- package/coverage/fireproof.js.html +0 -1042
- package/coverage/index.html +0 -206
- package/coverage/listener.js.html +0 -409
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/prolly.js.html +0 -931
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/tmp/coverage-28490-1680373005621-0.json +0 -1
- package/coverage/tmp/coverage-28494-1680373004502-0.json +0 -1
- package/coverage/tmp/coverage-28500-1680373005593-0.json +0 -1
- package/coverage/tmp/coverage-28504-1680373005559-0.json +0 -1
- package/coverage/valet.js.html +0 -439
package/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# 🔥 Fireproof
|
2
|
+
|
3
|
+
Fireproof is a realtime database for today's interactive applications. It uses immutable data and distributed protocols
|
4
|
+
to offer a new kind of database that:
|
5
|
+
- can be embedded in any page or app, with a flexible data ownership model
|
6
|
+
- scales without incurring developer costs, thanks to Filecoin
|
7
|
+
- uses cryptographically verifiable protocols (what plants crave)
|
8
|
+
|
9
|
+
Learn more about the [concepts and architecture behind Fireproof](https://fireproof.storage/documentation/how-the-database-engine-works/), or jump to the [quick start](#quick-start) for React and server-side examples.
|
10
|
+
|
11
|
+
### Status
|
12
|
+
|
13
|
+
Fireproof is alpha software, you should only use it if you are planning to contribute. For now, [check out our React TodoMVC implementation running in browser-local mode.](https://main--lucky-naiad-5aa507.netlify.app/) It demonstrates document persistence, index queries, and event subscriptions, and uses the [`useFireproof()` React hook.](https://github.com/fireproof-storage/fireproof/blob/main/packages/fireproof/hooks/use-fireproof.tsx)
|
14
|
+
|
15
|
+
[](https://github.com/jchris/fireproof/actions/workflows/test.yml)
|
16
|
+
[](https://standardjs.com)
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
```js
|
21
|
+
import Fireproof from 'fireproof';
|
22
|
+
|
23
|
+
async function main() {
|
24
|
+
const database = Fireproof.storage('my-db');
|
25
|
+
const ok = await database.put({
|
26
|
+
name: 'alice',
|
27
|
+
age: 42
|
28
|
+
});
|
29
|
+
|
30
|
+
const doc = await database.get(ok.id);
|
31
|
+
console.log(doc.name); // 'alice'
|
32
|
+
}
|
33
|
+
|
34
|
+
main();
|
35
|
+
```
|
36
|
+
|
37
|
+
## Features
|
38
|
+
|
39
|
+
### Document Store
|
40
|
+
|
41
|
+
A simple put, get, and delete interface for keeping track of all your JSON documents. Once your data is in Fireproof you can access it from any app or website. Fireproof document store uses MVCC versioning and Merkle clocks so you can always recover the version you are looking for.
|
42
|
+
|
43
|
+
```js
|
44
|
+
const { id, ref } = await database.put({
|
45
|
+
_id: 'three-thousand'
|
46
|
+
name: 'André',
|
47
|
+
age: 47
|
48
|
+
});
|
49
|
+
const doc = await database.get('three-thousand', {mvcc : true}) // mvcc is optional
|
50
|
+
// {
|
51
|
+
// _id : 'three-thousand'
|
52
|
+
// _clock : CID(bafy84...agfw7)
|
53
|
+
// name : 'André',
|
54
|
+
// age : 47
|
55
|
+
// }
|
56
|
+
```
|
57
|
+
|
58
|
+
The `_clock` allows you to query a stable snapshot of that version of the database. Fireproof uses immutable data structures under the hood, so you can always rollback to old data. Files can be embedded anywhere in your document using IPFS links like `{"/":"bafybeih3e3zdiehbqfpxzpppxrb6kaaw4xkbqzyr2f5pwr5refq2te2ape"}`, with API sugar coming soon.
|
59
|
+
|
60
|
+
### Flexible Indexes
|
61
|
+
|
62
|
+
Fireproof indexes are defined by custom JavaScript functions that you write, allowing you to easily index and search your data in the way that works best for your application. Easily handle data variety and schema drift by normalizing any data to the desired index.
|
63
|
+
|
64
|
+
```js
|
65
|
+
const index = new Index(database, function (doc, map) {
|
66
|
+
map(doc.age, doc.name)
|
67
|
+
})
|
68
|
+
const { rows, ref } = await index.query({ range: [40, 52] })
|
69
|
+
// [ { key: 42, value: 'alice', id: 'a1s3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c' },
|
70
|
+
// { key: 47, value: 'André', id: 'three-thousand' } ]
|
71
|
+
```
|
72
|
+
|
73
|
+
### Realtime Updates
|
74
|
+
|
75
|
+
Subscribe to query changes in your application, so your UI updates automatically. Use the supplied React hooks, our Redux connector, or simple function calls to be notified of relevant changes.
|
76
|
+
|
77
|
+
```js
|
78
|
+
const listener = new Listener(database, function(doc, emit) {
|
79
|
+
if (doc.type == 'member') {
|
80
|
+
emit('member')
|
81
|
+
}
|
82
|
+
})
|
83
|
+
listener.on('member', (id) => {
|
84
|
+
const doc = await db.get(id)
|
85
|
+
alert(`Member update ${doc.name}`)
|
86
|
+
})
|
87
|
+
```
|
88
|
+
|
89
|
+
### Self-sovereign Identity
|
90
|
+
|
91
|
+
Fireproof is so easy to integrate with any site or app because you can get started right away, and set up an account later. By default users write to their own database copy, so you can get pretty far before you even have to think about API keys. [Authorization is via non-extractable keypair](https://ucan.xyz), like TouchID / FaceID.
|
92
|
+
|
93
|
+
### Automatic Replication
|
94
|
+
|
95
|
+
Documents changes are persisted to [Filecoin](https://filecoin.io) via [web3.storage](https://web3.storage), and made available over [IPFS] and on a global content delivery network. All you need to do to sync state is send a link to the latest database head, and Fireproof will take care of the rest. [Learn how to enable replication.](#status)
|
96
|
+
|
97
|
+
### Cryptographic Proofs
|
98
|
+
|
99
|
+
The [UCAN protocol](https://ucan.xyz) verifably links Fireproof updates to authorized agents via cryptographic proof chains. These proofs are portable like bearer tokens, but because invocations are signed by end-user device keys, UCAN proofs don't need to be hidden to be secure, allowing for delegation of service capabilities across devices and parties. Additionally, Fireproof's Merkle clocks and hash trees are immutable and self-validating, making merging changes safe and efficient. Fireproof makes cryptographic proofs available for all of it's operations, making it an ideal verfiable document database for smart contracts and other applications running in trustless environments. [Proof chains provide performance benefits as well](https://purrfect-tracker-45c.notion.site/Data-Routing-23c37b269b4c4c3dacb60d0077113bcb), by allowing recipients to skip costly I/O operations and instead cryptographically verify that changes contain all of the required context.
|
100
|
+
|
101
|
+
## Limitations 💣
|
102
|
+
|
103
|
+
### Security
|
104
|
+
|
105
|
+
Until encryption support is enabled, all data written to Fireproof is public. There are no big hurdles for this feature but it's not ready yet.
|
106
|
+
|
107
|
+
### Replication
|
108
|
+
|
109
|
+
Currently Fireproof writes transactions and proofs to [CAR files](https://ipld.io/specs/transport/car/carv2/) which are well suited for peer and cloud replication. They are stored in IndexedDB locally, with cloud replication coming very soon.
|
110
|
+
|
111
|
+
### Pre-beta Software
|
112
|
+
|
113
|
+
While the underlying data structures and libraries Fireproof uses are trusted with billions of dollars worth of data, Fireproof started in February of 2023. Results may vary.
|
114
|
+
|
115
|
+
## Thanks 🙏
|
116
|
+
|
117
|
+
Fireproof is a synthesis of work done by people in the web community over the years. I couldn't even begin to name all the folks who made pivotal contributions. Without npm, React, and VS Code all this would have taken so much longer. Thanks to everyone who supported me getting into database development via Apache CouchDB, one of the original document databases. The distinguishing work on immutable datastructures comes from the years of consideration [IPFS](https://ipfs.tech), [IPLD](https://ipld.io), and the [Filecoin APIs](https://docs.filecoin.io) have enjoyed.
|
118
|
+
|
119
|
+
Thanks to Alan Shaw and Mikeal Rogers without whom this project would have never got started. The core Merkle hash-tree clock is based on [Alan's Pail](https://github.com/alanshaw/pail), and you can see the repository history goes all the way back to work begun as a branch of that repo. Mikeal wrote [the prolly trees implementation](https://github.com/mikeal/prolly-trees).
|
120
|
+
|
121
|
+
## Quick Start
|
122
|
+
|
123
|
+
Look in the `examples/` directory for projects using the database. It's not picky how you use it, but we want to provide convenient jumping off places. Think of the examples as great to fork when starting your next project.
|
124
|
+
|
125
|
+
If are adding Fireproof to an existing page, just install it and try some operations.
|
126
|
+
|
127
|
+
```sh
|
128
|
+
npm install @fireproof/core
|
129
|
+
```
|
130
|
+
|
131
|
+
In your `app.js` or `app.tsx` file:
|
132
|
+
|
133
|
+
```js
|
134
|
+
import { Fireproof } from '@fireproof/core'
|
135
|
+
const fireproof = Fireproof.storage()
|
136
|
+
const ok = await fireproof.put({ hello: 'world' })
|
137
|
+
const doc = await fireproof.get(ok.id)
|
138
|
+
```
|
139
|
+
|
140
|
+
🤫 I like to drop a `window.fireproof = fireproof` in there as a development aid.
|
141
|
+
|
142
|
+
# Contributing
|
143
|
+
|
144
|
+
Feel free to join in. All welcome. [Open an issue](https://github.com/jchris/fireproof/issues)!
|
145
|
+
|
146
|
+
# License
|
147
|
+
|
148
|
+
Dual-licensed under [MIT or Apache 2.0](https://github.com/jchris/fireproof/blob/main/LICENSE.md)
|
package/hooks/use-fireproof.ts
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
3
3
|
// @ts-ignore
|
4
4
|
import { useEffect, useState, createContext } from 'react'
|
5
|
-
import { Fireproof, Listener } from '../index'
|
5
|
+
import { Fireproof, Listener, Hydrator } from '../index'
|
6
|
+
|
6
7
|
|
7
8
|
export interface FireproofCtxValue {
|
8
9
|
addSubscriber: (label: String, fn: Function) => void
|
@@ -18,6 +19,7 @@ export const FireproofCtx = createContext<FireproofCtxValue>({
|
|
18
19
|
const inboundSubscriberQueue = new Map()
|
19
20
|
const database = Fireproof.storage()
|
20
21
|
const listener = new Listener(database)
|
22
|
+
let startedSetup = false;
|
21
23
|
|
22
24
|
/**
|
23
25
|
* @function useFireproof
|
@@ -30,16 +32,15 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
|
|
30
32
|
const [ready, setReady] = useState(false)
|
31
33
|
defineDatabaseFn = defineDatabaseFn || (() => {})
|
32
34
|
setupDatabaseFn = setupDatabaseFn || (() => {})
|
35
|
+
// console.log('useFireproof', database, ready)
|
33
36
|
|
34
|
-
if (!ready) {
|
35
|
-
defineDatabaseFn(database)
|
36
|
-
}
|
37
37
|
|
38
38
|
const addSubscriber = (label: String, fn: Function) => {
|
39
39
|
inboundSubscriberQueue.set(label, fn)
|
40
40
|
}
|
41
41
|
|
42
42
|
const listenerCallback = async () => {
|
43
|
+
// console.log ('listenerCallback', JSON.stringify(database))
|
43
44
|
localSet('fireproof', JSON.stringify(database))
|
44
45
|
for (const [, fn] of inboundSubscriberQueue) fn()
|
45
46
|
}
|
@@ -47,20 +48,25 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
|
|
47
48
|
useEffect(() => {
|
48
49
|
const doSetup = async () => {
|
49
50
|
if (ready) return
|
51
|
+
if (startedSetup) return
|
52
|
+
startedSetup = true
|
53
|
+
defineDatabaseFn(database) // define indexes before querying them
|
50
54
|
const fp = localGet('fireproof')
|
51
55
|
if (fp) {
|
52
|
-
const
|
56
|
+
const serialized = JSON.parse(fp)
|
57
|
+
// console.log('serialized', JSON.stringify(serialized.indexes.map(c => c.clock)))
|
53
58
|
console.log("Loading previous database clock. (localStorage.removeItem('fireproof') to reset)")
|
54
|
-
|
59
|
+
Hydrator.fromJSON(serialized, database)
|
60
|
+
// await database.setClock(clock)
|
55
61
|
try {
|
56
62
|
const changes = await database.changesSince()
|
57
63
|
if (changes.rows.length < 2) {
|
58
|
-
console.log('Resetting database')
|
64
|
+
// console.log('Resetting database')
|
59
65
|
throw new Error('Resetting database')
|
60
66
|
}
|
61
67
|
} catch (e) {
|
62
68
|
console.error(`Error loading previous database clock. ${fp} Resetting.`, e)
|
63
|
-
await database.setClock([])
|
69
|
+
await database.setClock([]) // todo this should be resetClock and also reset the indexes
|
64
70
|
await setupDatabaseFn(database)
|
65
71
|
localSet('fireproof', JSON.stringify(database))
|
66
72
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fireproof/core",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.10",
|
4
4
|
"description": "Realtime database for IPFS",
|
5
5
|
"main": "index.js",
|
6
6
|
"type": "module",
|
@@ -9,6 +9,7 @@
|
|
9
9
|
"test:mocha": "mocha test/*.test.js",
|
10
10
|
"test:watch": "npm run test:mocha -- -w --parallel test/*.test.js",
|
11
11
|
"coverage": "c8 -r html -r text npm test",
|
12
|
+
"prepublishOnly" : "cp ../../README.md .",
|
12
13
|
"lint": "standard",
|
13
14
|
"lint:fix": "standard --fix"
|
14
15
|
},
|
package/src/db-index.js
CHANGED
@@ -1,12 +1,11 @@
|
|
1
|
-
|
2
|
-
import { create, load } from '../../../../prolly-trees/src/db-index.js'
|
1
|
+
import { create, load } from 'prolly-trees/db-index'
|
2
|
+
// import { create, load } from '../../../../prolly-trees/src/db-index.js'
|
3
3
|
|
4
4
|
import { sha256 as hasher } from 'multiformats/hashes/sha2'
|
5
5
|
import { nocache as cache } from 'prolly-trees/cache'
|
6
6
|
import { bf, simpleCompare } from 'prolly-trees/utils'
|
7
7
|
import { makeGetBlock } from './prolly.js'
|
8
8
|
import { cidsToProof } from './fireproof.js'
|
9
|
-
import { CID } from 'multiformats'
|
10
9
|
|
11
10
|
import * as codec from '@ipld/dag-cbor'
|
12
11
|
// import { create as createBlock } from 'multiformats/block'
|
@@ -73,16 +72,16 @@ const makeDoc = ({ key, value }) => ({ _id: key, ...value })
|
|
73
72
|
* Transforms a set of changes to DbIndex entries using a map function.
|
74
73
|
*
|
75
74
|
* @param {ChangeEvent[]} changes
|
76
|
-
* @param {Function}
|
75
|
+
* @param {Function} mapFn
|
77
76
|
* @returns {DbIndexEntry[]} The DbIndex entries generated by the map function.
|
78
77
|
* @private
|
79
78
|
* @memberof DbIndex
|
80
79
|
*/
|
81
|
-
const indexEntriesForChanges = (changes,
|
80
|
+
const indexEntriesForChanges = (changes, mapFn) => {
|
82
81
|
const indexEntries = []
|
83
82
|
changes.forEach(({ key, value, del }) => {
|
84
83
|
if (del || !value) return
|
85
|
-
|
84
|
+
mapFn(makeDoc({ key, value }), (k, v) => {
|
86
85
|
indexEntries.push({
|
87
86
|
key: [charwise.encode(k), key],
|
88
87
|
value: v
|
@@ -99,11 +98,12 @@ const indexEntriesForChanges = (changes, mapFun) => {
|
|
99
98
|
* @classdesc An DbIndex can be used to order and filter the documents in a Fireproof database.
|
100
99
|
*
|
101
100
|
* @param {Fireproof} database - The Fireproof database instance to DbIndex.
|
102
|
-
* @param {Function}
|
101
|
+
* @param {Function} mapFn - The map function to apply to each entry in the database.
|
103
102
|
*
|
104
103
|
*/
|
105
104
|
export default class DbIndex {
|
106
|
-
constructor (database,
|
105
|
+
constructor (database, mapFn, clock) {
|
106
|
+
// console.log('DbIndex constructor', database.constructor.name, typeof mapFn, clock)
|
107
107
|
/**
|
108
108
|
* The database instance to DbIndex.
|
109
109
|
* @type {Fireproof}
|
@@ -113,33 +113,62 @@ export default class DbIndex {
|
|
113
113
|
* The map function to apply to each entry in the database.
|
114
114
|
* @type {Function}
|
115
115
|
*/
|
116
|
-
this.mapFun = mapFun
|
117
|
-
|
118
|
-
this.database.indexes.set(mapFun.toString(), this)
|
119
116
|
|
117
|
+
if (typeof mapFn === 'string') {
|
118
|
+
this.mapFnString = mapFn
|
119
|
+
} else {
|
120
|
+
this.mapFn = mapFn
|
121
|
+
this.mapFnString = mapFn.toString()
|
122
|
+
}
|
120
123
|
this.indexById = { root: null, cid: null }
|
121
124
|
this.indexByKey = { root: null, cid: null }
|
122
|
-
|
123
125
|
this.dbHead = null
|
124
|
-
|
126
|
+
if (clock) {
|
127
|
+
this.indexById.cid = clock.byId
|
128
|
+
this.indexByKey.cid = clock.byKey
|
129
|
+
this.dbHead = clock.db
|
130
|
+
}
|
125
131
|
this.instanceId = this.database.instanceId + `.DbIndex.${Math.random().toString(36).substring(2, 7)}`
|
126
|
-
|
127
132
|
this.updateIndexPromise = null
|
133
|
+
DbIndex.registerWithDatabase(this, this.database)
|
134
|
+
}
|
135
|
+
|
136
|
+
static registerWithDatabase (inIndex, database) {
|
137
|
+
// console.log('.reg > in Index', inIndex.instanceId, { live: !!inIndex.mapFn }, inIndex.indexByKey, inIndex.mapFnString)
|
138
|
+
if (database.indexes.has(inIndex.mapFnString)) {
|
139
|
+
// merge our inIndex code with the inIndex clock or vice versa
|
140
|
+
// keep the code instance, discard the clock instance
|
141
|
+
const existingIndex = database.indexes.get(inIndex.mapFnString)
|
142
|
+
// console.log('.reg - existingIndex', existingIndex.instanceId, { live: !!inIndex.mapFn }, existingIndex.indexByKey)
|
143
|
+
if (existingIndex.mapFn) { // this one also has other config
|
144
|
+
existingIndex.dbHead = inIndex.dbHead
|
145
|
+
existingIndex.indexById.cid = inIndex.indexById.cid
|
146
|
+
existingIndex.indexByKey.cid = inIndex.indexByKey.cid
|
147
|
+
} else {
|
148
|
+
// console.log('.reg use inIndex with existingIndex clock')
|
149
|
+
inIndex.dbHead = existingIndex.dbHead
|
150
|
+
inIndex.indexById.cid = existingIndex.indexById.cid
|
151
|
+
inIndex.indexByKey.cid = existingIndex.indexByKey.cid
|
152
|
+
database.indexes.set(inIndex.mapFnString, inIndex)
|
153
|
+
}
|
154
|
+
} else {
|
155
|
+
// console.log('.reg - fresh')
|
156
|
+
database.indexes.set(inIndex.mapFnString, inIndex)
|
157
|
+
}
|
158
|
+
// console.log('.reg after', JSON.stringify([...database.indexes.values()].map(i => [i.instanceId, typeof i.mapFn, i.indexByKey, i.indexById])))
|
128
159
|
}
|
129
160
|
|
130
161
|
toJSON () {
|
131
|
-
|
162
|
+
const indexJson = { code: this.mapFn?.toString(), clock: { db: null, byId: null, byKey: null } }
|
163
|
+
indexJson.clock.db = this.dbHead?.map(cid => cid.toString())
|
164
|
+
indexJson.clock.byId = this.indexById.cid?.toString()
|
165
|
+
indexJson.clock.byKey = this.indexByKey.cid?.toString()
|
166
|
+
return indexJson
|
132
167
|
}
|
133
168
|
|
134
|
-
static fromJSON (database, { code, clock
|
135
|
-
|
136
|
-
|
137
|
-
eval("mapFun = "+ code)
|
138
|
-
const index = new DbIndex(database, mapFun)
|
139
|
-
index.indexById.cid = CID.parse(byId)
|
140
|
-
index.indexByKey.cid = CID.parse(byKey)
|
141
|
-
index.dbHead = db.map(cid => CID.parse(cid))
|
142
|
-
return index
|
169
|
+
static fromJSON (database, { code, clock }) {
|
170
|
+
// console.log('DbIndex.fromJSON', database.constructor.name, code, clock)
|
171
|
+
return new DbIndex(database, code, clock)
|
143
172
|
}
|
144
173
|
|
145
174
|
/**
|
@@ -166,6 +195,7 @@ export default class DbIndex {
|
|
166
195
|
|
167
196
|
// }
|
168
197
|
// console.time(callId + '.doIndexQuery')
|
198
|
+
// console.log('query', query)
|
169
199
|
const response = await doIndexQuery(this.database.blocks, this.indexByKey, query)
|
170
200
|
// console.timeEnd(callId + '.doIndexQuery')
|
171
201
|
|
@@ -196,12 +226,12 @@ export default class DbIndex {
|
|
196
226
|
|
197
227
|
async #innerUpdateIndex (inBlocks) {
|
198
228
|
// const callTag = Math.random().toString(36).substring(4)
|
199
|
-
// console.log(`#updateIndex ${callTag} >`, this.instanceId, this.dbHead?.toString(), this.
|
229
|
+
// console.log(`#updateIndex ${callTag} >`, this.instanceId, this.dbHead?.toString(), this.indexByKey.cid?.toString(), this.indexById.cid?.toString())
|
200
230
|
// todo remove this hack
|
201
231
|
if (ALWAYS_REBUILD) {
|
202
|
-
this.
|
203
|
-
this.indexByKey = null
|
204
|
-
this.
|
232
|
+
this.indexById = { root: null, cid: null }
|
233
|
+
this.indexByKey = { root: null, cid: null }
|
234
|
+
this.dbHead = null
|
205
235
|
}
|
206
236
|
// console.log('dbHead', this.dbHead)
|
207
237
|
// console.time(callTag + '.changesSince')
|
@@ -210,28 +240,34 @@ export default class DbIndex {
|
|
210
240
|
// console.log('result.rows.length', result.rows.length)
|
211
241
|
|
212
242
|
// console.time(callTag + '.doTransaction#updateIndex')
|
243
|
+
// console.log('#updateIndex changes length', result.rows.length)
|
213
244
|
|
214
245
|
if (result.rows.length === 0) {
|
215
|
-
// console.log('#updateIndex < no changes')
|
246
|
+
// console.log('#updateIndex < no changes', result.clock)
|
216
247
|
this.dbHead = result.clock
|
217
248
|
return
|
218
249
|
}
|
219
250
|
await doTransaction('#updateIndex', inBlocks, async (blocks) => {
|
220
251
|
let oldIndexEntries = []
|
221
252
|
let removeByIdIndexEntries = []
|
222
|
-
|
253
|
+
await loadIndex(blocks, this.indexById, idIndexOpts)
|
254
|
+
await loadIndex(blocks, this.indexByKey, dbIndexOpts)
|
255
|
+
if (this.dbHead) {
|
223
256
|
const oldChangeEntries = await this.indexById.root.getMany(result.rows.map(({ key }) => key))
|
224
257
|
oldIndexEntries = oldChangeEntries.result.map((key) => ({ key, del: true }))
|
225
258
|
removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
|
226
259
|
}
|
227
|
-
|
260
|
+
if (!this.mapFn) {
|
261
|
+
throw new Error('No live map function installed for index, cannot update. Make sure your index definition runs before any queries.' + (this.mapFnString ? ' Your code should match the stored map function source:\n' + this.mapFnString : ''))
|
262
|
+
}
|
263
|
+
const indexEntries = indexEntriesForChanges(result.rows, this.mapFn)
|
228
264
|
const byIdIndexEntries = indexEntries.map(({ key }) => ({ key: key[1], value: key }))
|
229
265
|
this.indexById = await bulkIndex(blocks, this.indexById, removeByIdIndexEntries.concat(byIdIndexEntries), idIndexOpts)
|
230
266
|
this.indexByKey = await bulkIndex(blocks, this.indexByKey, oldIndexEntries.concat(indexEntries), dbIndexOpts)
|
231
267
|
this.dbHead = result.clock
|
232
268
|
})
|
233
269
|
// console.timeEnd(callTag + '.doTransaction#updateIndex')
|
234
|
-
// console.log(`#updateIndex ${callTag} <`, this.instanceId, this.dbHead?.toString(), this.
|
270
|
+
// console.log(`#updateIndex ${callTag} <`, this.instanceId, this.dbHead?.toString(), this.indexByKey.cid?.toString(), this.indexById.cid?.toString())
|
235
271
|
}
|
236
272
|
}
|
237
273
|
|
@@ -271,13 +307,18 @@ async function bulkIndex (blocks, inIndex, indexEntries, opts) {
|
|
271
307
|
return { root: returnNode, cid: returnRootBlock.cid }
|
272
308
|
}
|
273
309
|
|
274
|
-
async function
|
275
|
-
if (!
|
276
|
-
const cid =
|
277
|
-
if (!cid) return
|
310
|
+
async function loadIndex (blocks, index, indexOpts) {
|
311
|
+
if (!index.root) {
|
312
|
+
const cid = index.cid
|
313
|
+
if (!cid) return
|
278
314
|
const { getBlock } = makeGetBlock(blocks)
|
279
|
-
|
315
|
+
index.root = await load({ cid, get: getBlock, ...indexOpts })
|
280
316
|
}
|
317
|
+
return index.root
|
318
|
+
}
|
319
|
+
|
320
|
+
async function doIndexQuery (blocks, indexByKey, query) {
|
321
|
+
await loadIndex(blocks, indexByKey, dbIndexOpts)
|
281
322
|
if (query.range) {
|
282
323
|
const encodedRange = query.range.map((key) => charwise.encode(key))
|
283
324
|
return indexByKey.root.range(...encodedRange)
|
package/src/fireproof.js
CHANGED
@@ -42,22 +42,27 @@ export default class Fireproof {
|
|
42
42
|
}
|
43
43
|
|
44
44
|
/**
|
45
|
-
*
|
46
|
-
* @
|
47
|
-
* @param {CID[]} clock - The Merkle clock head to use for the snapshot.
|
48
|
-
* @returns {Fireproof}
|
49
|
-
* A new Fireproof instance representing the snapshot.
|
45
|
+
* Renders the Fireproof instance as a JSON object.
|
46
|
+
* @returns {Object} - The JSON representation of the Fireproof instance. Includes clock heads for the database and its indexes.
|
50
47
|
* @memberof Fireproof
|
51
48
|
* @instance
|
52
49
|
*/
|
53
|
-
|
54
|
-
//
|
55
|
-
|
56
|
-
|
50
|
+
toJSON () {
|
51
|
+
// todo this also needs to return the index roots...
|
52
|
+
return {
|
53
|
+
clock: this.clock.map(cid => cid.toString()),
|
54
|
+
name: this.name,
|
55
|
+
indexes: [...this.indexes.values()].map(index => index.toJSON())
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
hydrate ({ clock, name }) {
|
60
|
+
this.name = name
|
61
|
+
this.clock = clock
|
57
62
|
}
|
58
63
|
|
59
64
|
/**
|
60
|
-
*
|
65
|
+
* Triggers a notification to all listeners
|
61
66
|
* of the Fireproof instance so they can repaint UI, etc.
|
62
67
|
* @param {CID[] } clock
|
63
68
|
* Clock to use for the snapshot.
|
@@ -65,25 +70,8 @@ export default class Fireproof {
|
|
65
70
|
* @memberof Fireproof
|
66
71
|
* @instance
|
67
72
|
*/
|
68
|
-
async
|
69
|
-
|
70
|
-
this.clock = clock.map((item) => (item['/'] ? item['/'] : item))
|
71
|
-
await this.#notifyListeners({ reset: true, clock })
|
72
|
-
}
|
73
|
-
|
74
|
-
/**
|
75
|
-
* Renders the Fireproof instance as a JSON object.
|
76
|
-
* @returns {Object} - The JSON representation of the Fireproof instance. Includes clock heads for the database and its indexes.
|
77
|
-
* @memberof Fireproof
|
78
|
-
* @instance
|
79
|
-
*/
|
80
|
-
toJSON () {
|
81
|
-
// todo this also needs to return the index roots...
|
82
|
-
return {
|
83
|
-
clock: this.clock.map(cid => cid.toString()),
|
84
|
-
name: this.name,
|
85
|
-
indexes: [...this.indexes.values()].map((index) => index.toJSON())
|
86
|
-
}
|
73
|
+
async notifyReset () {
|
74
|
+
await this.#notifyListeners({ reset: true, clock: this.clock })
|
87
75
|
}
|
88
76
|
|
89
77
|
/**
|
@@ -295,7 +283,7 @@ export default class Fireproof {
|
|
295
283
|
}
|
296
284
|
|
297
285
|
setCarUploader (carUploaderFn) {
|
298
|
-
console.log('registering car uploader')
|
286
|
+
// console.log('registering car uploader')
|
299
287
|
// https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
|
300
288
|
this.blocks.valet.uploadFunction = carUploaderFn
|
301
289
|
}
|
package/src/hydrator.js
CHANGED
@@ -1,10 +1,51 @@
|
|
1
|
-
import Fireproof from './fireproof.js'
|
2
1
|
import DbIndex from './db-index.js'
|
2
|
+
import Fireproof from './fireproof.js'
|
3
|
+
import { CID } from 'multiformats'
|
4
|
+
|
5
|
+
const parseCID = cid => typeof cid === 'string' ? CID.parse(cid) : cid
|
6
|
+
|
7
|
+
export default class Hydrator {
|
8
|
+
static fromJSON (json, database) {
|
9
|
+
database.hydrate({ clock: json.clock.map(c => parseCID(c)), name: json.name })
|
10
|
+
for (const { code, clock: { byId, byKey, db } } of json.indexes) {
|
11
|
+
DbIndex.fromJSON(database, {
|
12
|
+
clock: {
|
13
|
+
byId: byId ? parseCID(byId) : null,
|
14
|
+
byKey: byKey ? parseCID(byKey) : null,
|
15
|
+
db: db ? db.map(c => parseCID(c)) : null
|
16
|
+
},
|
17
|
+
code
|
18
|
+
})
|
19
|
+
}
|
20
|
+
return database
|
21
|
+
}
|
22
|
+
|
23
|
+
static snapshot (database, clock) {
|
24
|
+
const definition = database.toJSON()
|
25
|
+
const withBlocks = new Fireproof(database.blocks)
|
26
|
+
if (clock) {
|
27
|
+
definition.clock = clock.map(c => parseCID(c))
|
28
|
+
definition.indexes.forEach(index => {
|
29
|
+
index.clock.byId = null
|
30
|
+
index.clock.byKey = null
|
31
|
+
index.clock.db = null
|
32
|
+
})
|
33
|
+
}
|
34
|
+
const snappedDb = this.fromJSON(definition, withBlocks)
|
35
|
+
;([...database.indexes.values()]).forEach(index => {
|
36
|
+
snappedDb.indexes.get(index.mapFnString).mapFn = index.mapFn
|
37
|
+
})
|
38
|
+
return snappedDb
|
39
|
+
}
|
3
40
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
41
|
+
static async zoom (database, clock) {
|
42
|
+
;([...database.indexes.values()]).forEach(index => {
|
43
|
+
index.indexById = { root: null, cid: null }
|
44
|
+
index.indexByKey = { root: null, cid: null }
|
45
|
+
index.dbHead = null
|
46
|
+
})
|
47
|
+
database.clock = clock.map(c => parseCID(c))
|
48
|
+
await database.notifyReset()
|
49
|
+
return database
|
8
50
|
}
|
9
|
-
return fp
|
10
51
|
}
|
package/test/db-index.test.js
CHANGED
@@ -3,6 +3,7 @@ import assert from 'node:assert'
|
|
3
3
|
import Blockstore from '../src/blockstore.js'
|
4
4
|
import Fireproof from '../src/fireproof.js'
|
5
5
|
import DbIndex from '../src/db-index.js'
|
6
|
+
import Hydrator from '../src/hydrator.js'
|
6
7
|
console.x = function () {}
|
7
8
|
|
8
9
|
describe('DbIndex query', () => {
|
@@ -77,7 +78,7 @@ describe('DbIndex query', () => {
|
|
77
78
|
// console.x('bresult.rows', bresult.rows)
|
78
79
|
assert.equal(bresult.rows.length, 6, 'all row matched')
|
79
80
|
|
80
|
-
const
|
81
|
+
const snapClock = database.clock
|
81
82
|
|
82
83
|
const notYet = await database.get('xxxx-3c3a-4b5e-9c1c-8c5c0c5c0c5c').catch((e) => e)
|
83
84
|
assert.equal(notYet.message, 'Not found', 'not yet there')
|
@@ -91,7 +92,7 @@ describe('DbIndex query', () => {
|
|
91
92
|
assert(gotX.name === 'Xander', 'got Xander')
|
92
93
|
console.x('got X')
|
93
94
|
|
94
|
-
const snap =
|
95
|
+
const snap = Hydrator.snapshot(database, snapClock)
|
95
96
|
|
96
97
|
const aliceOld = await snap.get('a1s3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')// .catch((e) => e)
|
97
98
|
console.x('aliceOld', aliceOld)
|
@@ -124,7 +125,7 @@ describe('DbIndex query', () => {
|
|
124
125
|
assert.equal(result.rows.length, 1, '1 row matched')
|
125
126
|
assert(result.rows[0].key === 53, 'correct key')
|
126
127
|
|
127
|
-
const snap =
|
128
|
+
const snap = Hydrator.snapshot(database)
|
128
129
|
|
129
130
|
console.x('--- make Xander 63')
|
130
131
|
const response = await database.put({ _id: DOCID, name: 'Xander', age: 63 })
|
@@ -172,7 +173,7 @@ describe('DbIndex query', () => {
|
|
172
173
|
assert.equal(result.rows.length, 1, '1 row matched')
|
173
174
|
assert(result.rows[0].key === 53, 'correct key')
|
174
175
|
|
175
|
-
const snap =
|
176
|
+
const snap = Hydrator.snapshot(database)
|
176
177
|
|
177
178
|
console.x('--- delete Xander 53')
|
178
179
|
const response = await database.del(DOCID)
|
package/test/fireproof.test.js
CHANGED
@@ -2,6 +2,7 @@ import { describe, it, beforeEach } from 'mocha'
|
|
2
2
|
import assert from 'node:assert'
|
3
3
|
import Blockstore from '../src/blockstore.js'
|
4
4
|
import Fireproof from '../src/fireproof.js'
|
5
|
+
import Hydrator from '../src/hydrator.js'
|
5
6
|
// import * as codec from '@ipld/dag-cbor'
|
6
7
|
|
7
8
|
let database, resp0
|
@@ -130,7 +131,7 @@ describe('Fireproof', () => {
|
|
130
131
|
assert(response.id, 'should have id')
|
131
132
|
assert.equal(response.id, dogKey)
|
132
133
|
assert.equal(value._id, dogKey)
|
133
|
-
const
|
134
|
+
const snapshot = Hydrator.snapshot(database)
|
134
135
|
|
135
136
|
const avalue = await database.get(dogKey)
|
136
137
|
assert.equal(avalue.name, value.name)
|
@@ -147,7 +148,6 @@ describe('Fireproof', () => {
|
|
147
148
|
assert.equal(bvalue.age, 3)
|
148
149
|
assert.equal(bvalue._id, dogKey)
|
149
150
|
|
150
|
-
const snapshot = database.snapshot(oldClock)
|
151
151
|
const snapdoc = await snapshot.get(dogKey)
|
152
152
|
// console.log('snapdoc', snapdoc)
|
153
153
|
// assert(snapdoc.id, 'should have id')
|