@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.
@@ -0,0 +1,379 @@
1
+ // @ts-nocheck
2
+ import { visMerkleClock, visMerkleTree, vis, put, get, getAll, eventsSince } from './prolly';
3
+ import { doTransaction } from './blockstore';
4
+ import charwise from 'charwise';
5
+ import { localSet } from './utils';
6
+ import { CID } from 'multiformats';
7
+ // TypeScript Types
8
+ // eslint-disable-next-line no-unused-vars
9
+ // import { CID } from 'multiformats/dist/types/src/cid'
10
+ // eslint-disable-next-line no-unused-vars
11
+ class Proof {
12
+ }
13
+ export const parseCID = cid => (typeof cid === 'string' ? CID.parse(cid) : cid);
14
+ /**
15
+ * @class Fireproof
16
+ * @classdesc Fireproof stores data in IndexedDB and provides a Merkle clock.
17
+ * This is the main class for saving and loading JSON and other documents with the database. You can find additional examples and
18
+ * usage guides in the repository README.
19
+ *
20
+ * @param {import('./blockstore').TransactionBlockstore} blocks - The block storage instance to use documents and indexes
21
+ * @param {CID[]} clock - The Merkle clock head to use for the Fireproof instance.
22
+ * @param {object} [config] - Optional configuration options for the Fireproof instance.
23
+ * @param {object} [authCtx] - Optional authorization context object to use for any authentication checks.
24
+ *
25
+ */
26
+ export class Database {
27
+ listeners = new Set();
28
+ indexes = new Map();
29
+ rootCache = null;
30
+ eventsCache = new Map();
31
+ blocks;
32
+ clock;
33
+ constructor(blocks, clock, config = {}) {
34
+ this.name = config.name;
35
+ this.instanceId = `fp.${this.name}.${Math.random().toString(36).substring(2, 7)}`;
36
+ this.blocks = blocks;
37
+ this.clock = clock;
38
+ this.config = config;
39
+ }
40
+ /**
41
+ * Renders the Fireproof instance as a JSON object.
42
+ * @returns {Object} - The JSON representation of the Fireproof instance. Includes clock heads for the database and its indexes.
43
+ * @memberof Fireproof
44
+ * @instance
45
+ */
46
+ toJSON() {
47
+ // todo this also needs to return the index roots...
48
+ return {
49
+ clock: this.clockToJSON(),
50
+ name: this.name,
51
+ key: this.blocks.valet?.getKeyMaterial(),
52
+ indexes: [...this.indexes.values()].map(index => index.toJSON())
53
+ };
54
+ }
55
+ /**
56
+ * Returns the Merkle clock heads for the Fireproof instance.
57
+ * @returns {string[]} - The Merkle clock heads for the Fireproof instance.
58
+ * @memberof Fireproof
59
+ * @instance
60
+ */
61
+ clockToJSON() {
62
+ return this.clock.map(cid => cid.toString());
63
+ }
64
+ hydrate({ clock, name, key }) {
65
+ this.name = name;
66
+ this.clock = clock;
67
+ this.blocks.valet?.setKeyMaterial(key);
68
+ this.indexBlocks = null;
69
+ }
70
+ maybeSaveClock() {
71
+ if (this.name && this.blocks.valet) {
72
+ localSet('fp.' + this.name, JSON.stringify(this));
73
+ }
74
+ }
75
+ /**
76
+ * Triggers a notification to all listeners
77
+ * of the Fireproof instance so they can repaint UI, etc.
78
+ * @returns {Promise<void>}
79
+ * @memberof Fireproof
80
+ * @instance
81
+ */
82
+ async notifyReset() {
83
+ await this.notifyListeners({ _reset: true, _clock: this.clockToJSON() });
84
+ }
85
+ // used be indexes etc to notify database listeners of new availability
86
+ // async notifyExternal (source = 'unknown') {
87
+ // // await this.notifyListeners({ _external: source, _clock: this.clockToJSON() })
88
+ // }
89
+ /**
90
+ * Returns the changes made to the Fireproof instance since the specified event.
91
+ * @function changesSince
92
+ * @param {CID[]} [event] - The clock head to retrieve changes since. If null or undefined, retrieves all changes.
93
+ * @returns {Promise<{rows : Object[], clock: CID[], proof: {}}>} An object containing the rows and the head of the instance's clock.
94
+ * @memberof Fireproof
95
+ * @instance
96
+ */
97
+ async changesSince(event) {
98
+ // console.log('events for', this.instanceId, event.constructor.name)
99
+ // console.log('changesSince', this.instanceId, event, this.clock)
100
+ let rows, dataCIDs, clockCIDs;
101
+ // if (!event) event = []
102
+ if (event) {
103
+ event = event.map((cid) => cid.toString());
104
+ const eventKey = JSON.stringify([...event, ...this.clockToJSON()]);
105
+ let resp;
106
+ if (this.eventsCache.has(eventKey)) {
107
+ console.log('events from cache');
108
+ resp = this.eventsCache.get(eventKey);
109
+ }
110
+ else {
111
+ resp = await eventsSince(this.blocks, this.clock, event);
112
+ this.eventsCache.set(eventKey, resp);
113
+ }
114
+ const docsMap = new Map();
115
+ for (const { key, type, value } of resp.result.map(decodeEvent)) {
116
+ if (type === 'del') {
117
+ docsMap.set(key, { key, del: true });
118
+ }
119
+ else {
120
+ docsMap.set(key, { key, value });
121
+ }
122
+ }
123
+ rows = Array.from(docsMap.values());
124
+ clockCIDs = resp.clockCIDs;
125
+ // console.log('change rows', this.instanceId, rows)
126
+ }
127
+ else {
128
+ const allResp = await getAll(this.blocks, this.clock, this.rootCache);
129
+ this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs };
130
+ rows = allResp.result.map(({ key, value }) => decodeEvent({ key, value }));
131
+ dataCIDs = allResp.cids;
132
+ // console.log('dbdoc rows', this.instanceId, rows)
133
+ }
134
+ return {
135
+ rows,
136
+ clock: this.clockToJSON(),
137
+ proof: { data: await cidsToProof(dataCIDs), clock: await cidsToProof(clockCIDs) }
138
+ };
139
+ }
140
+ async allDocuments() {
141
+ const allResp = await getAll(this.blocks, this.clock, this.rootCache);
142
+ this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs };
143
+ const rows = allResp.result
144
+ .map(({ key, value }) => decodeEvent({ key, value }))
145
+ .map(({ key, value }) => ({ key, value: { _id: key, ...value } }));
146
+ return {
147
+ rows,
148
+ clock: this.clockToJSON(),
149
+ proof: await cidsToProof(allResp.cids)
150
+ };
151
+ }
152
+ async allCIDs() {
153
+ const allResp = await getAll(this.blocks, this.clock, this.rootCache, true);
154
+ this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs };
155
+ // console.log('allcids', allResp.cids, allResp.clockCIDs)
156
+ const cids = await cidsToProof(allResp.cids);
157
+ const clockCids = await cidsToProof(allResp.clockCIDs);
158
+ // console.log('allcids', cids, clockCids)
159
+ // todo we need to put the clock head as the last block in the encrypted car
160
+ return [...cids, ...clockCids]; // need a single block version of clock head, maybe an encoded block for it
161
+ }
162
+ async allStoredCIDs() {
163
+ const allCIDs = [];
164
+ for await (const { cid } of this.blocks.entries()) {
165
+ allCIDs.push(cid);
166
+ }
167
+ return allCIDs;
168
+ }
169
+ /**
170
+ * Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
171
+ *
172
+ * @param {Object} doc - The document to validate.
173
+ * @returns {Promise<void>}
174
+ * @throws {Error} - Throws an error if the document is invalid.
175
+ * @memberof Fireproof
176
+ * @instance
177
+ */
178
+ async runValidation(doc) {
179
+ if (this.config && this.config.validateChange) {
180
+ const oldDoc = await this.get(doc._id)
181
+ .then(doc => doc)
182
+ .catch(() => ({}));
183
+ this.config.validateChange(doc, oldDoc, this.authCtx);
184
+ }
185
+ }
186
+ /**
187
+ * Retrieves the document with the specified ID from the database
188
+ *
189
+ * @param {string} key - the ID of the document to retrieve
190
+ * @param {Object} [opts] - options
191
+ * @returns {Promise<{_id: string}>} - the document with the specified ID
192
+ * @memberof Fireproof
193
+ * @instance
194
+ */
195
+ async get(key, opts = {}) {
196
+ const clock = opts.clock || this.clock;
197
+ const resp = await get(this.blocks, clock, charwise.encode(key), this.rootCache);
198
+ this.rootCache = { root: resp.root, clockCIDs: resp.clockCIDs };
199
+ // this tombstone is temporary until we can get the prolly tree to delete
200
+ if (!resp || resp.result === null) {
201
+ throw new Error('Not found');
202
+ }
203
+ const doc = { ...resp.result };
204
+ if (opts.mvcc === true) {
205
+ doc._clock = this.clockToJSON();
206
+ }
207
+ doc._proof = {
208
+ data: await cidsToProof(resp.cids),
209
+ clock: this.clockToJSON()
210
+ };
211
+ doc._id = key;
212
+ return doc;
213
+ }
214
+ /**
215
+ * @typedef {Object} Document
216
+ * @property {string} _id - The ID of the document (required)
217
+ * @property {string} [_proof] - The proof of the document (optional)
218
+ * @property {string} [_clock] - The clock of the document (optional)
219
+ * @property {any} [key: string] - Index signature notation to allow any other unknown fields
220
+ * * @property {Object.<string, any>} [otherProperties] - Any other unknown properties (optional)
221
+ */
222
+ /**
223
+ * Adds a new document to the database, or updates an existing document. Returns the ID of the document and the new clock head.
224
+ *
225
+ * @param {Document} doc - the document to be added
226
+ * @returns {Promise<{ id: string, clock: CID[] }>} - The result of adding the document to the database
227
+ * @memberof Fireproof
228
+ * @instance
229
+ */
230
+ async put({ _id, _proof, ...doc }) {
231
+ const id = _id || 'f' + Math.random().toString(36).slice(2);
232
+ await this.runValidation({ _id: id, ...doc });
233
+ return await this.putToProllyTree({ key: id, value: doc }, doc._clock);
234
+ }
235
+ /**
236
+ * Deletes a document from the database
237
+ * @param {string | any} docOrId - the document ID
238
+ * @returns {Promise<{ id: string, clock: CID[] }>} - The result of deleting the document from the database
239
+ * @memberof Fireproof
240
+ * @instance
241
+ */
242
+ async del(docOrId) {
243
+ let id;
244
+ let clock = null;
245
+ if (docOrId._id) {
246
+ id = docOrId._id;
247
+ clock = docOrId._clock;
248
+ }
249
+ else {
250
+ id = docOrId;
251
+ }
252
+ await this.runValidation({ _id: id, _deleted: true });
253
+ return await this.putToProllyTree({ key: id, del: true }, clock); // not working at prolly tree layer?
254
+ // this tombstone is temporary until we can get the prolly tree to delete
255
+ // return await this.putToProllyTree({ key: id, value: null }, clock)
256
+ }
257
+ /**
258
+ * Updates the underlying storage with the specified event.
259
+ * @private
260
+ * @param {{del?: true, key : string, value?: any}} decodedEvent - the event to add
261
+ * @returns {Promise<{ proof:{}, id: string, clock: CID[] }>} - The result of adding the event to storage
262
+ */
263
+ async putToProllyTree(decodedEvent, clock = null) {
264
+ const event = encodeEvent(decodedEvent);
265
+ if (clock && JSON.stringify(clock) !== JSON.stringify(this.clockToJSON())) {
266
+ // we need to check and see what version of the document exists at the clock specified
267
+ // if it is the same as the one we are trying to put, then we can proceed
268
+ const resp = await eventsSince(this.blocks, this.clock, event.value._clock);
269
+ const missedChange = resp.result.find(({ key }) => key === event.key);
270
+ if (missedChange) {
271
+ throw new Error('MVCC conflict, document is changed, please reload the document and try again.');
272
+ }
273
+ }
274
+ const prevClock = [...this.clock];
275
+ const result = await doTransaction('putToProllyTree', this.blocks, async (blocks) => await put(blocks, this.clock, event));
276
+ if (!result) {
277
+ console.error('failed', event);
278
+ throw new Error('failed to put at storage layer');
279
+ }
280
+ this.applyClock(prevClock, result.head);
281
+ await this.notifyListeners([decodedEvent]); // this type is odd
282
+ return {
283
+ id: decodedEvent.key,
284
+ clock: this.clockToJSON(),
285
+ proof: { data: await cidsToProof(result.cids), clock: await cidsToProof(result.clockCIDs) }
286
+ };
287
+ // todo should include additions (or split clock)
288
+ }
289
+ applyClock(prevClock, newClock) {
290
+ // console.log('prevClock', prevClock.length, prevClock.map((cid) => cid.toString()))
291
+ // console.log('newClock', newClock.length, newClock.map((cid) => cid.toString()))
292
+ // console.log('this.clock', this.clock.length, this.clockToJSON())
293
+ const stPrev = prevClock.map(cid => cid.toString());
294
+ const keptPrevClock = this.clock.filter(cid => stPrev.indexOf(cid.toString()) === -1);
295
+ const merged = keptPrevClock.concat(newClock);
296
+ const uniquebyCid = new Map();
297
+ for (const cid of merged) {
298
+ uniquebyCid.set(cid.toString(), cid);
299
+ }
300
+ this.clock = Array.from(uniquebyCid.values()).sort((a, b) => a.toString().localeCompare(b.toString()));
301
+ this.rootCache = null;
302
+ this.eventsCache.clear();
303
+ // console.log('afterClock', this.clock.length, this.clockToJSON())
304
+ }
305
+ // /**
306
+ // * Advances the clock to the specified event and updates the root CID
307
+ // * Will be used by replication
308
+ // */
309
+ // async advance (event) {
310
+ // this.clock = await advance(this.blocks, this.clock, event)
311
+ // this.rootCid = await root(this.blocks, this.clock)
312
+ // return this.clock
313
+ // }
314
+ async *vis() {
315
+ return yield* vis(this.blocks, this.clock);
316
+ }
317
+ async visTree() {
318
+ return await visMerkleTree(this.blocks, this.clock);
319
+ }
320
+ async visClock() {
321
+ return await visMerkleClock(this.blocks, this.clock);
322
+ }
323
+ /**
324
+ * Registers a Listener to be called when the Fireproof instance's clock is updated.
325
+ * Recieves live changes from the database after they are committed.
326
+ * @param {Function} listener - The listener to be called when the clock is updated.
327
+ * @returns {Function} - A function that can be called to unregister the listener.
328
+ * @memberof Fireproof
329
+ */
330
+ subscribe(listener) {
331
+ this.listeners.add(listener);
332
+ return () => {
333
+ this.listeners.delete(listener);
334
+ };
335
+ }
336
+ /**
337
+ * @deprecated 0.7.0 - renamed subscribe(listener)
338
+ * @param {Function} listener - The listener to be called when the clock is updated.
339
+ * @returns {Function} - A function that can be called to unregister the listener.
340
+ * @memberof Fireproof
341
+ */
342
+ registerListener(listener) {
343
+ return this.subscribe(listener);
344
+ }
345
+ async notifyListeners(changes) {
346
+ // await sleep(10)
347
+ await this.maybeSaveClock();
348
+ for (const listener of this.listeners) {
349
+ await listener(changes);
350
+ }
351
+ }
352
+ setCarUploader(carUploaderFn) {
353
+ // console.log('registering car uploader')
354
+ // https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
355
+ this.blocks.valet.uploadFunction = carUploaderFn;
356
+ }
357
+ setRemoteBlockReader(remoteBlockReaderFn) {
358
+ this.blocks.remoteBlockFunction = remoteBlockReaderFn;
359
+ }
360
+ }
361
+ export async function cidsToProof(cids) {
362
+ if (!cids)
363
+ return [];
364
+ if (!cids.all) {
365
+ return [...cids];
366
+ }
367
+ const all = await cids.all();
368
+ return [...all].map(cid => cid.toString());
369
+ }
370
+ function decodeEvent(event) {
371
+ const decodedKey = charwise.decode(event.key);
372
+ return { ...event, key: decodedKey };
373
+ }
374
+ function encodeEvent(event) {
375
+ if (!(event && event.key))
376
+ return;
377
+ const encodedKey = charwise.encode(event.key);
378
+ return { ...event, key: encodedKey };
379
+ }