@fireproof/core 0.5.9 → 0.5.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/dist/hooks/use-fireproof.js +149 -0
- package/dist/src/blockstore.js +259 -0
- package/dist/src/clock.js +385 -0
- package/dist/src/crypto.js +59 -0
- package/dist/src/database.js +379 -0
- package/dist/src/db-index.js +387 -0
- package/dist/src/fireproof.d.ts +25 -5
- package/dist/src/fireproof.js +272 -163
- package/dist/src/fireproof.js.map +1 -1
- package/dist/src/fireproof.mjs +272 -163
- package/dist/src/fireproof.mjs.map +1 -1
- package/dist/src/link.js +1 -0
- package/dist/src/listener.js +112 -0
- package/dist/src/prolly.js +360 -0
- package/dist/src/sha1.js +73 -0
- package/dist/src/sync.js +198 -0
- package/dist/src/utils.js +16 -0
- package/dist/src/valet.js +291 -0
- package/hooks/use-fireproof.js +77 -14
- package/package.json +2 -1
- package/src/database.js +15 -4
- package/src/db-index.js +107 -44
- package/src/listener.js +3 -1
@@ -0,0 +1,149 @@
|
|
1
|
+
// @ts-ignore
|
2
|
+
import { useEffect, useState, useCallback, createContext } from 'react';
|
3
|
+
import { Fireproof, Index } from '@fireproof/core';
|
4
|
+
/**
|
5
|
+
@typedef {Object} FireproofCtxValue
|
6
|
+
@property {Function} addSubscriber - A function to add a subscriber with a label and function.
|
7
|
+
@property {Fireproof} database - An instance of the Fireproof class.
|
8
|
+
@property {Function} useLiveQuery - A hook to return a query result
|
9
|
+
@property {Function} useLiveDocument - A hook to return a live document
|
10
|
+
@property {boolean} ready - A boolean indicating whether the database is ready.
|
11
|
+
@param {string} label - A label for the subscriber.
|
12
|
+
@param {Function} fn - A function to be added as a subscriber.
|
13
|
+
@returns {void}
|
14
|
+
*/
|
15
|
+
export const FireproofCtx = createContext({
|
16
|
+
addSubscriber: () => { },
|
17
|
+
database: null,
|
18
|
+
ready: false
|
19
|
+
});
|
20
|
+
// const inboundSubscriberQueue = new Map()
|
21
|
+
let startedSetup = false;
|
22
|
+
let database;
|
23
|
+
const initializeDatabase = name => {
|
24
|
+
if (database)
|
25
|
+
return;
|
26
|
+
database = Fireproof.storage(name);
|
27
|
+
};
|
28
|
+
/**
|
29
|
+
@function useFireproof
|
30
|
+
React hook to initialize a Fireproof database, automatically saving and loading the clock.
|
31
|
+
You might need to import { nodePolyfills } from 'vite-plugin-node-polyfills' in your vite.config.ts
|
32
|
+
@param {string} name - The path to the database file
|
33
|
+
@param {function(database): void} [defineDatabaseFn] - Synchronous function that defines the database, run this before any async calls
|
34
|
+
@param {function(database): Promise<void>} [setupDatabaseFn] - Asynchronous function that sets up the database, run this to load fixture data etc
|
35
|
+
@returns {FireproofCtxValue} { useLiveQuery, useLiveDocument, database, ready }
|
36
|
+
*/
|
37
|
+
export function useFireproof(name = 'useFireproof', defineDatabaseFn = () => { }, setupDatabaseFn = async () => { }) {
|
38
|
+
const [ready, setReady] = useState(false);
|
39
|
+
initializeDatabase(name);
|
40
|
+
/**
|
41
|
+
* @deprecated - use database.subscribe instead
|
42
|
+
*/
|
43
|
+
const addSubscriber = (label, fn) => {
|
44
|
+
// todo test that the label is not needed
|
45
|
+
return database.subscribe(fn);
|
46
|
+
// inboundSubscriberQueue.set(label, fn)
|
47
|
+
};
|
48
|
+
useEffect(() => {
|
49
|
+
const doSetup = async () => {
|
50
|
+
if (ready)
|
51
|
+
return;
|
52
|
+
if (startedSetup)
|
53
|
+
return;
|
54
|
+
startedSetup = true;
|
55
|
+
defineDatabaseFn(database); // define indexes before querying them
|
56
|
+
if (database.clock.length === 0) {
|
57
|
+
await setupDatabaseFn(database);
|
58
|
+
}
|
59
|
+
setReady(true);
|
60
|
+
};
|
61
|
+
doSetup();
|
62
|
+
}, [ready]);
|
63
|
+
function useLiveDocument(initialDoc) {
|
64
|
+
const id = initialDoc._id;
|
65
|
+
const [doc, setDoc] = useState(initialDoc);
|
66
|
+
const saveDoc = async (newDoc) => {
|
67
|
+
await database.put({ _id: id, ...newDoc });
|
68
|
+
};
|
69
|
+
const refreshDoc = useCallback(async () => {
|
70
|
+
// todo add option for mvcc checks
|
71
|
+
const got = await database.get(id).catch(() => initialDoc);
|
72
|
+
setDoc(got);
|
73
|
+
}, [id, initialDoc]);
|
74
|
+
useEffect(() => database.subscribe(change => {
|
75
|
+
if (change.find(c => c.key === id)) {
|
76
|
+
refreshDoc(); // todo use change.value
|
77
|
+
}
|
78
|
+
}), [id, refreshDoc]);
|
79
|
+
useEffect(() => {
|
80
|
+
refreshDoc();
|
81
|
+
}, []);
|
82
|
+
return [doc, saveDoc];
|
83
|
+
}
|
84
|
+
function useLiveQuery(mapFn, query = null, initialRows = []) {
|
85
|
+
const [rows, setRows] = useState({ rows: initialRows, proof: {} });
|
86
|
+
const [index, setIndex] = useState(null);
|
87
|
+
const refreshRows = useCallback(async () => {
|
88
|
+
if (!index)
|
89
|
+
return;
|
90
|
+
const got = await index.query(query || {});
|
91
|
+
setRows(got);
|
92
|
+
}, [index, JSON.stringify(query)]);
|
93
|
+
useEffect(() => {
|
94
|
+
// todo listen to index changes
|
95
|
+
return database.subscribe(() => {
|
96
|
+
refreshRows();
|
97
|
+
});
|
98
|
+
}, [refreshRows]);
|
99
|
+
useEffect(() => {
|
100
|
+
refreshRows();
|
101
|
+
}, [index]);
|
102
|
+
useEffect(() => {
|
103
|
+
const index = new Index(database, null, mapFn); // this should only be created once
|
104
|
+
setIndex(index);
|
105
|
+
}, [mapFn.toString()]);
|
106
|
+
return rows;
|
107
|
+
}
|
108
|
+
return {
|
109
|
+
addSubscriber,
|
110
|
+
useLiveQuery,
|
111
|
+
useLiveDocument,
|
112
|
+
database,
|
113
|
+
ready
|
114
|
+
};
|
115
|
+
}
|
116
|
+
// const husherMap = new Map()
|
117
|
+
// const husher = (id, workFn, ms) => {
|
118
|
+
// if (!husherMap.has(id)) {
|
119
|
+
// const start = Date.now()
|
120
|
+
// husherMap.set(
|
121
|
+
// id,
|
122
|
+
// workFn().finally(() => setTimeout(() => husherMap.delete(id), ms - (Date.now() - start)))
|
123
|
+
// )
|
124
|
+
// }
|
125
|
+
// return husherMap.get(id)
|
126
|
+
// }
|
127
|
+
// const hushed =
|
128
|
+
// (id, workFn, ms) =>
|
129
|
+
// (...args) =>
|
130
|
+
// husher(id, () => workFn(...args), ms)
|
131
|
+
// let storageSupported = false
|
132
|
+
// try {
|
133
|
+
// storageSupported = window.localStorage && true
|
134
|
+
// } catch (e) {}
|
135
|
+
// export function localGet (key) {
|
136
|
+
// if (storageSupported) {
|
137
|
+
// return localStorage && localStorage.getItem(key)
|
138
|
+
// }
|
139
|
+
// }
|
140
|
+
// function localSet (key, value) {
|
141
|
+
// if (storageSupported) {
|
142
|
+
// return localStorage && localStorage.setItem(key, value)
|
143
|
+
// }
|
144
|
+
// }
|
145
|
+
// function localRemove(key) {
|
146
|
+
// if (storageSupported) {
|
147
|
+
// return localStorage && localStorage.removeItem(key)
|
148
|
+
// }
|
149
|
+
// }
|
@@ -0,0 +1,259 @@
|
|
1
|
+
// @ts-nocheck
|
2
|
+
import { parse } from 'multiformats/link';
|
3
|
+
import { CID } from 'multiformats';
|
4
|
+
import { Valet } from './valet';
|
5
|
+
// const sleep = ms => new Promise(r => setTimeout(r, ms))
|
6
|
+
const husherMap = new Map();
|
7
|
+
const husher = (id, workFn) => {
|
8
|
+
if (!husherMap.has(id)) {
|
9
|
+
husherMap.set(id, workFn().finally(() => setTimeout(() => husherMap.delete(id), 100)));
|
10
|
+
}
|
11
|
+
return husherMap.get(id);
|
12
|
+
};
|
13
|
+
/**
|
14
|
+
* @typedef {{ get: (link: import('../src/link').AnyLink) => Promise<AnyBlock | undefined> }} BlockFetcher
|
15
|
+
*/
|
16
|
+
/**
|
17
|
+
* @typedef {Object} AnyBlock
|
18
|
+
* @property {import('./link').AnyLink} cid - The CID of the block
|
19
|
+
* @property {Uint8Array} bytes - The block's data
|
20
|
+
*
|
21
|
+
* @typedef {Object} Blockstore
|
22
|
+
* @property {function(import('./link').AnyLink): Promise<AnyBlock|undefined>} get - A function to retrieve a block by CID
|
23
|
+
* @property {function(import('./link').AnyLink, Uint8Array): Promise<void>} put - A function to store a block's data and CID
|
24
|
+
*
|
25
|
+
* A blockstore that caches writes to a transaction and only persists them when committed.
|
26
|
+
*/
|
27
|
+
export class TransactionBlockstore {
|
28
|
+
/** @type {Map<string, Uint8Array>} */
|
29
|
+
committedBlocks = new Map();
|
30
|
+
/** @type {Valet} */
|
31
|
+
valet = null;
|
32
|
+
instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4);
|
33
|
+
inflightTransactions = new Set();
|
34
|
+
syncs = new Set();
|
35
|
+
constructor(name, encryptionKey) {
|
36
|
+
if (name) {
|
37
|
+
this.valet = new Valet(name, encryptionKey);
|
38
|
+
}
|
39
|
+
this.remoteBlockFunction = null;
|
40
|
+
}
|
41
|
+
/**
|
42
|
+
* Get a block from the store.
|
43
|
+
*
|
44
|
+
* @param {import('./link').AnyLink} cid
|
45
|
+
* @returns {Promise<AnyBlock | undefined>}
|
46
|
+
*/
|
47
|
+
async get(cid) {
|
48
|
+
const key = cid.toString();
|
49
|
+
// it is safe to read from the in-flight transactions becauase they are immutable
|
50
|
+
const bytes = await Promise.any([this.transactionsGet(key), this.committedGet(key)]).catch(e => {
|
51
|
+
// console.log('networkGet', cid.toString(), e)
|
52
|
+
return this.networkGet(key);
|
53
|
+
});
|
54
|
+
if (!bytes)
|
55
|
+
throw new Error('Missing block: ' + key);
|
56
|
+
return { cid, bytes };
|
57
|
+
}
|
58
|
+
// this iterates over the in-flight transactions
|
59
|
+
// and returns the first matching block it finds
|
60
|
+
async transactionsGet(key) {
|
61
|
+
for (const transaction of this.inflightTransactions) {
|
62
|
+
const got = await transaction.get(key);
|
63
|
+
if (got && got.bytes)
|
64
|
+
return got.bytes;
|
65
|
+
}
|
66
|
+
throw new Error('Missing block: ' + key);
|
67
|
+
}
|
68
|
+
async committedGet(key) {
|
69
|
+
const old = this.committedBlocks.get(key);
|
70
|
+
// console.log('committedGet: ' + key + ' ' + this.instanceId, old.length)
|
71
|
+
if (old)
|
72
|
+
return old;
|
73
|
+
if (!this.valet)
|
74
|
+
throw new Error('Missing block: ' + key);
|
75
|
+
const got = await this.valet.getBlock(key);
|
76
|
+
this.committedBlocks.set(key, got);
|
77
|
+
return got;
|
78
|
+
}
|
79
|
+
async clearCommittedCache() {
|
80
|
+
this.committedBlocks.clear();
|
81
|
+
}
|
82
|
+
async networkGet(key) {
|
83
|
+
if (this.remoteBlockFunction) {
|
84
|
+
// todo why is this on valet?
|
85
|
+
const value = await husher(key, async () => await this.remoteBlockFunction(key));
|
86
|
+
if (value) {
|
87
|
+
// console.log('networkGot: ' + key, value.length)
|
88
|
+
doTransaction('networkGot: ' + key, this, async (innerBlockstore) => {
|
89
|
+
await innerBlockstore.put(CID.parse(key), value);
|
90
|
+
});
|
91
|
+
return value;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
else {
|
95
|
+
return false;
|
96
|
+
}
|
97
|
+
}
|
98
|
+
/**
|
99
|
+
* Add a block to the store. Usually bound to a transaction by a closure.
|
100
|
+
* It sets the lastCid property to the CID of the block that was put.
|
101
|
+
* This is used by the transaction as the head of the car when written to the valet.
|
102
|
+
* We don't have to worry about which transaction we are when we are here because
|
103
|
+
* we are the transactionBlockstore.
|
104
|
+
*
|
105
|
+
* @param {import('./link').AnyLink} cid
|
106
|
+
* @param {Uint8Array} bytes
|
107
|
+
*/
|
108
|
+
put(cid, bytes) {
|
109
|
+
throw new Error('use a transaction to put');
|
110
|
+
}
|
111
|
+
/**
|
112
|
+
* Iterate over all blocks in the store.
|
113
|
+
*
|
114
|
+
* @yields {{cid: string, bytes: Uint8Array}}
|
115
|
+
* @returns {AsyncGenerator<any, any, any>}
|
116
|
+
*/
|
117
|
+
async *entries() {
|
118
|
+
for (const transaction of this.inflightTransactions) {
|
119
|
+
for (const [str, bytes] of transaction) {
|
120
|
+
yield { cid: str, bytes };
|
121
|
+
}
|
122
|
+
}
|
123
|
+
for (const [str, bytes] of this.committedBlocks) {
|
124
|
+
yield { cid: str, bytes };
|
125
|
+
}
|
126
|
+
if (this.valet) {
|
127
|
+
for await (const { cid } of this.valet.cids()) {
|
128
|
+
yield { cid };
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
/**
|
133
|
+
* Begin a transaction. Ensures the uncommited blocks are empty at the begining.
|
134
|
+
* Returns the blocks to read and write during the transaction.
|
135
|
+
* @returns {InnerBlockstore}
|
136
|
+
* @memberof TransactionBlockstore
|
137
|
+
*/
|
138
|
+
begin(label = '') {
|
139
|
+
const innerTransactionBlockstore = new InnerBlockstore(label, this);
|
140
|
+
this.inflightTransactions.add(innerTransactionBlockstore);
|
141
|
+
return innerTransactionBlockstore;
|
142
|
+
}
|
143
|
+
/**
|
144
|
+
* Commit the transaction. Writes the blocks to the store.
|
145
|
+
* @returns {Promise<void>}
|
146
|
+
* @memberof TransactionBlockstore
|
147
|
+
*/
|
148
|
+
async commit(innerBlockstore, doSync = true) {
|
149
|
+
// console.log('commit', doSync, innerBlockstore.label)
|
150
|
+
await this.doCommit(innerBlockstore);
|
151
|
+
if (doSync) {
|
152
|
+
// const all =
|
153
|
+
await Promise.all([...this.syncs].map(async (sync) => sync.sendUpdate(innerBlockstore).catch(e => {
|
154
|
+
console.error('sync error', e);
|
155
|
+
this.syncs.delete(sync);
|
156
|
+
})));
|
157
|
+
}
|
158
|
+
}
|
159
|
+
// first get the transaction blockstore from the map of transaction blockstores
|
160
|
+
// then copy it to committedBlocks
|
161
|
+
// then write the transaction blockstore to a car
|
162
|
+
// then write the car to the valet
|
163
|
+
// then remove the transaction blockstore from the map of transaction blockstores
|
164
|
+
doCommit = async (innerBlockstore) => {
|
165
|
+
const cids = new Set();
|
166
|
+
for (const { cid, bytes } of innerBlockstore.entries()) {
|
167
|
+
const stringCid = cid.toString(); // unnecessary string conversion, can we fix upstream?
|
168
|
+
if (this.committedBlocks.has(stringCid)) {
|
169
|
+
// console.log('Duplicate block: ' + stringCid) // todo some of this can be avoided, cost is extra size on car files
|
170
|
+
}
|
171
|
+
else {
|
172
|
+
this.committedBlocks.set(stringCid, bytes);
|
173
|
+
cids.add(stringCid);
|
174
|
+
}
|
175
|
+
}
|
176
|
+
// console.log(innerBlockstore.label, 'committing', cids.size, 'blocks', [...cids].map(cid => cid.toString()), this.valet)
|
177
|
+
if (cids.size > 0 && this.valet) {
|
178
|
+
await this.valet.writeTransaction(innerBlockstore, cids);
|
179
|
+
}
|
180
|
+
};
|
181
|
+
/**
|
182
|
+
* Retire the transaction. Clears the uncommited blocks.
|
183
|
+
* @returns {void}
|
184
|
+
* @memberof TransactionBlockstore
|
185
|
+
*/
|
186
|
+
retire(innerBlockstore) {
|
187
|
+
this.inflightTransactions.delete(innerBlockstore);
|
188
|
+
}
|
189
|
+
}
|
190
|
+
/**
|
191
|
+
* Runs a function on an inner blockstore, then persists the change to a car writer
|
192
|
+
* or other outer blockstore.
|
193
|
+
* @param {string} label
|
194
|
+
* @param {TransactionBlockstore} blockstore
|
195
|
+
* @param {(innerBlockstore: Blockstore) => Promise<any>} doFun
|
196
|
+
* @returns {Promise<any>}
|
197
|
+
* @memberof TransactionBlockstore
|
198
|
+
*/
|
199
|
+
export const doTransaction = async (label, blockstore, doFun, doSync = true) => {
|
200
|
+
// @ts-ignore
|
201
|
+
if (!blockstore.commit)
|
202
|
+
return await doFun(blockstore);
|
203
|
+
// @ts-ignore
|
204
|
+
const innerBlockstore = blockstore.begin(label);
|
205
|
+
try {
|
206
|
+
const result = await doFun(innerBlockstore);
|
207
|
+
// @ts-ignore
|
208
|
+
await blockstore.commit(innerBlockstore, doSync);
|
209
|
+
return result;
|
210
|
+
}
|
211
|
+
catch (e) {
|
212
|
+
console.error(`Transaction ${label} failed`, e, e.stack);
|
213
|
+
throw e;
|
214
|
+
}
|
215
|
+
finally {
|
216
|
+
// @ts-ignore
|
217
|
+
blockstore.retire(innerBlockstore);
|
218
|
+
}
|
219
|
+
};
|
220
|
+
export class InnerBlockstore {
|
221
|
+
/** @type {Map<string, Uint8Array>} */
|
222
|
+
blocks = new Map();
|
223
|
+
lastCid = null;
|
224
|
+
label = '';
|
225
|
+
parentBlockstore = null;
|
226
|
+
constructor(label, parentBlockstore) {
|
227
|
+
this.label = label;
|
228
|
+
this.parentBlockstore = parentBlockstore;
|
229
|
+
}
|
230
|
+
/**
|
231
|
+
* @param {import('./link').AnyLink} cid
|
232
|
+
* @returns {Promise<AnyBlock | undefined>}
|
233
|
+
*/
|
234
|
+
async get(cid) {
|
235
|
+
const key = cid.toString();
|
236
|
+
let bytes = this.blocks.get(key);
|
237
|
+
if (bytes) {
|
238
|
+
return { cid, bytes };
|
239
|
+
}
|
240
|
+
bytes = await this.parentBlockstore.committedGet(key);
|
241
|
+
if (bytes) {
|
242
|
+
return { cid, bytes };
|
243
|
+
}
|
244
|
+
}
|
245
|
+
/**
|
246
|
+
* @param {import('./link').AnyLink} cid
|
247
|
+
* @param {Uint8Array} bytes
|
248
|
+
*/
|
249
|
+
async put(cid, bytes) {
|
250
|
+
// console.log('put', cid)
|
251
|
+
this.blocks.set(cid.toString(), bytes);
|
252
|
+
this.lastCid = cid;
|
253
|
+
}
|
254
|
+
*entries() {
|
255
|
+
for (const [str, bytes] of this.blocks) {
|
256
|
+
yield { cid: parse(str), bytes };
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|