@fireproof/core 0.5.9 → 0.5.11

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.
@@ -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
+ }