@fireproof/core 0.0.3 → 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/hooks/use-fireproof.md +149 -0
- package/hooks/use-fireproof.ts +112 -0
- package/package.json +1 -1
- package/src/db-index.js +33 -15
- package/src/fireproof.js +3 -11
- package/src/listener.js +5 -3
- package/src/prolly.js +4 -4
@@ -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
package/src/db-index.js
CHANGED
@@ -16,13 +16,32 @@ const makeGetBlock = (blocks) => async (address) => {
|
|
16
16
|
}
|
17
17
|
const makeDoc = ({ key, value }) => ({ _id: key, ...value })
|
18
18
|
|
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
|
+
*/
|
27
|
+
|
28
|
+
/**
|
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
|
+
|
19
37
|
/**
|
20
38
|
* Transforms a set of changes to DbIndex entries using a map function.
|
21
39
|
*
|
22
|
-
* @param {
|
40
|
+
* @param {ChangeEvent[]} changes
|
23
41
|
* @param {Function} mapFun
|
24
|
-
* @returns {
|
42
|
+
* @returns {DbIndexEntry[]} The DbIndex entries generated by the map function.
|
25
43
|
* @private
|
44
|
+
* @memberof DbIndex
|
26
45
|
*/
|
27
46
|
const indexEntriesForChanges = (changes, mapFun) => {
|
28
47
|
const indexEntries = []
|
@@ -52,7 +71,7 @@ const indexEntriesForOldChanges = async (blocks, byIDindexRoot, ids, mapFun) =>
|
|
52
71
|
* @class DbIndex
|
53
72
|
* @classdesc An DbIndex can be used to order and filter the documents in a Fireproof database.
|
54
73
|
*
|
55
|
-
* @param {
|
74
|
+
* @param {Fireproof} database - The Fireproof database instance to DbIndex.
|
56
75
|
* @param {Function} mapFun - The map function to apply to each entry in the database.
|
57
76
|
*
|
58
77
|
*/
|
@@ -60,7 +79,7 @@ export default class DbIndex {
|
|
60
79
|
constructor (database, mapFun) {
|
61
80
|
/**
|
62
81
|
* The database instance to DbIndex.
|
63
|
-
* @type {
|
82
|
+
* @type {Fireproof}
|
64
83
|
*/
|
65
84
|
this.database = database
|
66
85
|
/**
|
@@ -73,9 +92,16 @@ export default class DbIndex {
|
|
73
92
|
this.dbHead = null
|
74
93
|
}
|
75
94
|
|
95
|
+
/**
|
96
|
+
* JSDoc for Query type.
|
97
|
+
* @typedef {Object} DbQuery
|
98
|
+
* @property {string[]} [range] - The range to query.
|
99
|
+
* @memberof DbIndex
|
100
|
+
*/
|
101
|
+
|
76
102
|
/**
|
77
103
|
* Query object can have {range}
|
78
|
-
* @param {
|
104
|
+
* @param {DbQuery} query - the query range to use
|
79
105
|
* @param {CID} [root] - an optional root to query a snapshot
|
80
106
|
* @returns {Promise<{rows: Array<{id: string, key: string, value: any}>}>}
|
81
107
|
* @memberof DbIndex
|
@@ -148,8 +174,8 @@ export default class DbIndex {
|
|
148
174
|
/**
|
149
175
|
* Update the DbIndex with the given entries
|
150
176
|
* @param {Blockstore} blocks
|
151
|
-
* @param {
|
152
|
-
* @param {
|
177
|
+
* @param {Block} inRoot
|
178
|
+
* @param {DbIndexEntry[]} indexEntries
|
153
179
|
* @private
|
154
180
|
*/
|
155
181
|
async function bulkIndex (blocks, inRoot, indexEntries) {
|
@@ -180,14 +206,6 @@ async function bulkIndex (blocks, inRoot, indexEntries) {
|
|
180
206
|
}
|
181
207
|
}
|
182
208
|
|
183
|
-
/**
|
184
|
-
* Query the DbIndex for the given range
|
185
|
-
* @param {Blockstore} blocks
|
186
|
-
* @param {import('multiformats/block').Block} inRoot
|
187
|
-
* @param {import('prolly-trees/db-DbIndex').Query} query
|
188
|
-
* @returns {Promise<import('prolly-trees/db-DbIndex').QueryResult>}
|
189
|
-
* @private
|
190
|
-
**/
|
191
209
|
async function doIndexQuery (blocks, root, query) {
|
192
210
|
const cid = root && root.cid
|
193
211
|
if (!cid) return { result: [] }
|
package/src/fireproof.js
CHANGED
@@ -35,7 +35,7 @@ export default class Fireproof {
|
|
35
35
|
this.clock = clock
|
36
36
|
this.config = config
|
37
37
|
this.authCtx = authCtx
|
38
|
-
this.instanceId = '
|
38
|
+
this.instanceId = 'db.' + Math.random().toString(36).substring(2, 7)
|
39
39
|
}
|
40
40
|
|
41
41
|
/**
|
@@ -181,8 +181,8 @@ export default class Fireproof {
|
|
181
181
|
/**
|
182
182
|
* Updates the underlying storage with the specified event.
|
183
183
|
* @private
|
184
|
-
* @param {
|
185
|
-
* @returns {Object<{ id: string, clock:
|
184
|
+
* @param {CID[]} event - the event to add
|
185
|
+
* @returns {Object<{ id: string, clock: CID[] }>} - The result of adding the event to storage
|
186
186
|
*/
|
187
187
|
async #putToProllyTree (event) {
|
188
188
|
const result = await doTransaction(
|
@@ -203,8 +203,6 @@ export default class Fireproof {
|
|
203
203
|
// /**
|
204
204
|
// * Advances the clock to the specified event and updates the root CID
|
205
205
|
// * Will be used by replication
|
206
|
-
// * @param {import('../clock').EventLink<import('../crdt').EventData>} event - the event to advance to
|
207
|
-
// * @returns {import('../clock').EventLink<import('../crdt').EventData>[]} - the new clock after advancing
|
208
206
|
// */
|
209
207
|
// async advance (event) {
|
210
208
|
// this.clock = await advance(this.blocks, this.clock, event)
|
@@ -250,12 +248,6 @@ export default class Fireproof {
|
|
250
248
|
this.blocks.valet.uploadFunction = carUploaderFn
|
251
249
|
}
|
252
250
|
|
253
|
-
/**
|
254
|
-
* Sets the function that will be used to read blocks from a remote peer.
|
255
|
-
* @param {Function} remoteBlockReaderFn - the function that will be used to read blocks from a remote peer
|
256
|
-
* @memberof Fireproof
|
257
|
-
* @instance
|
258
|
-
*/
|
259
251
|
setRemoteBlockReader (remoteBlockReaderFn) {
|
260
252
|
// console.log('registering remote block reader')
|
261
253
|
this.blocks.valet.remoteBlockFunction = remoteBlockReaderFn
|
package/src/listener.js
CHANGED
@@ -4,9 +4,11 @@
|
|
4
4
|
* @class Listener
|
5
5
|
* @classdesc An listener attaches to a Fireproof database and runs a routing function on each change, sending the results to subscribers.
|
6
6
|
*
|
7
|
-
* @param {
|
7
|
+
* @param {Fireproof} database - The Fireproof database instance to index.
|
8
8
|
* @param {Function} routingFn - The routing function to apply to each entry in the database.
|
9
9
|
*/
|
10
|
+
// import { ChangeEvent } from './db-index'
|
11
|
+
|
10
12
|
export default class Listener {
|
11
13
|
#subcribers = new Map()
|
12
14
|
|
@@ -20,7 +22,7 @@ export default class Listener {
|
|
20
22
|
constructor (database, routingFn) {
|
21
23
|
/** routingFn
|
22
24
|
* The database instance to index.
|
23
|
-
* @type {
|
25
|
+
* @type {Fireproof}
|
24
26
|
*/
|
25
27
|
this.database = database
|
26
28
|
this.#doStopListening = database.registerListener((changes) => this.#onChanges(changes))
|
@@ -94,7 +96,7 @@ const makeDoc = ({ key, value }) => ({ _id: key, ...value })
|
|
94
96
|
/**
|
95
97
|
* Transforms a set of changes to events using an emitter function.
|
96
98
|
*
|
97
|
-
* @param {
|
99
|
+
* @param {ChangeEvent[]} changes
|
98
100
|
* @param {Function} routingFn
|
99
101
|
* @returns {Array<string>} The topics emmitted by the event function.
|
100
102
|
* @private
|
package/src/prolly.js
CHANGED
@@ -35,15 +35,15 @@ const makeGetBlock = (blocks) => async (address) => {
|
|
35
35
|
* @param {Function} bigPut - A function that puts a block.
|
36
36
|
* @param {import('prolly-trees/map').Root} root - The root node.
|
37
37
|
* @param {Object<{ key: string, value: any, del: boolean }>} event - The update event.
|
38
|
-
* @param {
|
38
|
+
* @param {CID[]} head - The head of the event chain.
|
39
39
|
* @param {Array<import('multiformats/block').Block>} additions - A array of additions.
|
40
40
|
* @param {Array<mport('multiformats/block').Block>>} removals - An array of removals.
|
41
41
|
* @returns {Promise<{
|
42
42
|
* root: import('prolly-trees/map').Root,
|
43
43
|
* additions: Map<string, import('multiformats/block').Block>,
|
44
44
|
* removals: Array<string>,
|
45
|
-
* head:
|
46
|
-
* event:
|
45
|
+
* head: CID[],
|
46
|
+
* event: CID[]
|
47
47
|
* }>}
|
48
48
|
*/
|
49
49
|
async function createAndSaveNewEvent (
|
@@ -135,7 +135,7 @@ const prollyRootFromAncestor = async (events, ancestor, getBlock) => {
|
|
135
135
|
* @param {import('./block').BlockFetcher} blocks Bucket block storage.
|
136
136
|
* @param {import('./clock').EventLink<EventData>[]} head Merkle clock head.
|
137
137
|
* @param {string} key The key of the value to put.
|
138
|
-
* @param {
|
138
|
+
* @param {CID} value The value to put.
|
139
139
|
* @param {object} [options]
|
140
140
|
* @returns {Promise<Result>}
|
141
141
|
*/
|