@fireproof/core 0.6.5 → 0.7.0-alpha.0

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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/{src/blockstore.js → blockstore.js} +7 -3
  3. package/dist/{src/database.js → database.js} +74 -48
  4. package/dist/{src/db-index.js → db-index.js} +2 -1
  5. package/dist/fireproof.js +92 -0
  6. package/dist/{src/loader.js → loader.js} +2 -3
  7. package/dist/{src/prolly.js → prolly.js} +1 -0
  8. package/dist/src/fireproof.d.ts +137 -136
  9. package/dist/src/fireproof.js +18179 -11484
  10. package/dist/src/fireproof.js.map +1 -1
  11. package/dist/src/fireproof.mjs +18180 -11484
  12. package/dist/src/fireproof.mjs.map +1 -1
  13. package/dist/{src/storage → storage}/filesystem.js +2 -2
  14. package/dist/{src/sync.js → sync.js} +1 -1
  15. package/dist/valet.js +200 -0
  16. package/package.json +4 -5
  17. package/src/blockstore.js +6 -3
  18. package/src/database.js +80 -52
  19. package/src/db-index.js +2 -1
  20. package/src/fireproof.js +41 -30
  21. package/src/import.js +34 -0
  22. package/src/loader.js +26 -0
  23. package/src/prolly.js +2 -0
  24. package/src/storage/base.js +371 -0
  25. package/src/storage/browser.js +67 -0
  26. package/src/storage/filesystem.js +70 -0
  27. package/src/storage/rest.js +60 -0
  28. package/src/storage/ucan.js +0 -0
  29. package/src/sync.js +1 -1
  30. package/src/valet.js +57 -359
  31. package/dist/hooks/use-fireproof.js +0 -150
  32. package/dist/src/crypto-poly.js +0 -4
  33. package/dist/src/link.js +0 -1
  34. package/dist/src/valet.js +0 -476
  35. package/hooks/use-fireproof.js +0 -173
  36. package/src/listener.js +0 -119
  37. package/src/utils.js +0 -16
  38. /package/dist/{src/clock.js → clock.js} +0 -0
  39. /package/dist/{src/crypto.js → crypto.js} +0 -0
  40. /package/dist/{src/import.js → import.js} +0 -0
  41. /package/dist/{src/listener.js → listener.js} +0 -0
  42. /package/dist/{src/sha1.js → sha1.js} +0 -0
  43. /package/dist/{src/storage → storage}/base.js +0 -0
  44. /package/dist/{src/storage → storage}/browser.js +0 -0
  45. /package/dist/{src/storage → storage}/rest.js +0 -0
  46. /package/dist/{src/storage → storage}/ucan.js +0 -0
  47. /package/dist/{src/utils.js → utils.js} +0 -0
package/README.md CHANGED
@@ -38,14 +38,14 @@ With Fireproof, you **build first** and connect it to your cloud of choice when
38
38
  ```js
39
39
  const completedTodos = useLiveQuery((doc) => doc.completed, { key: true })
40
40
  ```
41
- This is the README for the core database technology. [The `useFireproof` hook documentation has features and a getting started guide](https://github.com/fireproof-storage/fireproof/blob/main/packages/react/README.md), like `useLiveDocument` and database setup helpers. Don't forget to star 🌟 this repo on the way over.
41
+ This is the README for the core database technology. [The `useFireproof` hook documentation has features and a getting started guide](https://github.com/fireproof-storage/fireproof/blob/main/packages/react/README.md), like `useDocument` and database setup helpers. Don't forget to star 🌟 this repo on the way over.
42
42
 
43
43
  ## AI Assistant Quick Start
44
44
 
45
45
  Because Fireproof is designed around the mantra of build-first, it's ideal for AI-assisted app development as you can get an app up and running before even considering the cloud. If you are using GPT-3.5, GPT-4, or Bard, you can [easily enable the AI to write React apps using Fireproof](https://hackernoon.com/get-chatgpt-to-focus-on-coding-on-the-right-apis-with-gptdoc-strings). Paste this line of code into your prompt, along with a detailed description of the app to build:
46
46
 
47
47
  ```
48
- Fireproof/React/Usage: import { useFireproof } from '@fireproof/react'; function App() { const { useLiveQuery, useLiveDocument, database } = useFireproof(); const result = useLiveQuery(doc => doc.word, { limit: 10 }); const [{ count }, saveCountDocument] = useLiveDocument({_id: 'count', count: 0}); return (<><p>{count} changes</p><input type='text' onChange={() => saveCountDocument({count: count + 1})} onSubmit={e => database.put({word: e.target.value})} /><ul>{result.map(row => (<li key={row.id}>{row.key}</li>))}</ul></>)}
48
+ Fireproof/React/Usage: import { useFireproof } from '@fireproof/react'; function App() { const { useLiveQuery, useDocument, database } = useFireproof(); const result = useLiveQuery(doc => doc.word, { limit: 10 }); const [{ count }, saveCountDocument] = useDocument({_id: 'count', count: 0}); return (<><p>{count} changes</p><input type='text' onChange={() => saveCountDocument({count: count + 1})} onSubmit={e => database.put({word: e.target.value})} /><ul>{result.map(row => (<li key={row.id}>{row.key}</li>))}</ul></>)}
49
49
  ```
50
50
 
51
51
  Here are some examples that worked for us:
@@ -31,9 +31,13 @@ export class TransactionBlockstore {
31
31
  instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4);
32
32
  inflightTransactions = new Set();
33
33
  syncs = new Set();
34
- constructor(name, encryptionKey) {
34
+ constructor(name, config) {
35
35
  if (name) {
36
- this.valet = new Valet(name, encryptionKey);
36
+ this.valet = new Valet(name, config);
37
+ this.ready = this.valet.ready;
38
+ }
39
+ else {
40
+ this.ready = Promise.resolve();
37
41
  }
38
42
  this.remoteBlockFunction = null;
39
43
  }
@@ -47,7 +51,7 @@ export class TransactionBlockstore {
47
51
  const key = cid.toString();
48
52
  // it is safe to read from the in-flight transactions becauase they are immutable
49
53
  const bytes = await Promise.any([this.transactionsGet(key), this.committedGet(key)]).catch(e => {
50
- // console.log('networkGet', cid.toString(), e)
54
+ console.log('get error', cid.toString(), e);
51
55
  return this.networkGet(key);
52
56
  });
53
57
  if (!bytes)
@@ -2,8 +2,8 @@
2
2
  import { visMerkleClock, visMerkleTree, vis, put, get, getAll, eventsSince } from './prolly.js';
3
3
  import { doTransaction, TransactionBlockstore } from './blockstore.js';
4
4
  import charwise from 'charwise';
5
- import { localSet } from './utils.js';
6
5
  import { CID } from 'multiformats';
6
+ import { DbIndex as Index } from './db-index.js';
7
7
  // TypeScript Types
8
8
  // eslint-disable-next-line no-unused-vars
9
9
  // import { CID } from 'multiformats/dist/types/src/cid.js'
@@ -27,13 +27,49 @@ export class Database {
27
27
  indexes = new Map();
28
28
  rootCache = null;
29
29
  eventsCache = new Map();
30
- constructor(name, clock, config = {}) {
30
+ constructor(name, config = {}) {
31
31
  this.name = name;
32
+ this.clock = [];
32
33
  this.instanceId = `fp.${this.name}.${Math.random().toString(36).substring(2, 7)}`;
33
- this.blocks = new TransactionBlockstore(name, config.key);
34
- this.indexBlocks = new TransactionBlockstore(name + '.indexes', config.key);
35
- this.clock = clock;
34
+ this.blocks = new TransactionBlockstore(name, config);
35
+ this.indexBlocks = new TransactionBlockstore(name ? name + '.indexes' : null, { primary: config.index });
36
36
  this.config = config;
37
+ // todo we can wait for index blocks elsewhere
38
+ this.ready = Promise.all([this.blocks.ready, this.indexBlocks.ready]).then(([blocksReady, indexReady]) => {
39
+ const clock = new Set();
40
+ // console.log('blocksReady', blocksReady)
41
+ if (!blocksReady) {
42
+ return;
43
+ }
44
+ for (const headers of blocksReady) {
45
+ for (const [, header] of Object.entries(headers)) {
46
+ if (!header)
47
+ continue;
48
+ for (const cid of header.clock) {
49
+ clock.add(cid);
50
+ }
51
+ if (header.index) {
52
+ this.indexBlocks.valet.primary.setCarCidMapCarCid(header.index.car);
53
+ this.indexBlocks.valet.primary.setKeyMaterial(header.index.key);
54
+ }
55
+ if (header.indexes) {
56
+ for (const { name, code, clock: { byId, byKey, db } } of header.indexes) {
57
+ // console.log('index', name, code, { byId, byKey }, db, header.indexes)
58
+ Index.fromJSON(this, {
59
+ clock: {
60
+ byId: byId ? parseCID(byId) : null,
61
+ byKey: byKey ? parseCID(byKey) : null,
62
+ db: (db && db.length > 0) ? db.map(c => parseCID(c)) : null
63
+ },
64
+ code,
65
+ name
66
+ });
67
+ }
68
+ }
69
+ }
70
+ }
71
+ this.clock = [...clock];
72
+ });
37
73
  }
38
74
  /**
39
75
  * Renders the Fireproof instance as a JSON object.
@@ -42,12 +78,16 @@ export class Database {
42
78
  * @instance
43
79
  */
44
80
  toJSON() {
81
+ return this.blocks.valet ? this.blocks.valet.primary.prepareHeader(this.toHeader(), false) : this.toHeader(); // omg
82
+ }
83
+ toHeader() {
45
84
  return {
46
85
  clock: this.clockToJSON(),
47
86
  name: this.name,
48
- key: this.blocks.valet?.getKeyMaterial(),
49
- car: this.blocks.valet?.valetRootCarCid.toString(),
50
- indexCar: this.indexBlocks.valet?.valetRootCarCid?.toString(),
87
+ index: {
88
+ key: this.indexBlocks.valet?.primary.keyMaterial,
89
+ car: this.indexBlocks.valet?.primary.valetRootCarCid?.toString()
90
+ },
51
91
  indexes: [...this.indexes.values()].map(index => index.toJSON())
52
92
  };
53
93
  }
@@ -60,25 +100,12 @@ export class Database {
60
100
  clockToJSON(clock = null) {
61
101
  return (clock || this.clock).map(cid => cid.toString());
62
102
  }
63
- hydrate({ clock, name, key, car, indexCar }) {
64
- this.name = name;
65
- this.clock = clock;
66
- this.blocks.valet?.setKeyMaterial(key);
67
- this.blocks.valet?.setRootCarCid(car); // maybe
68
- this.indexBlocks.valet?.setKeyMaterial(key);
69
- this.indexBlocks.valet?.setRootCarCid(indexCar); // maybe
70
- // this.indexBlocks = null
71
- }
72
103
  maybeSaveClock() {
73
104
  if (this.name && this.blocks.valet) {
74
- localSet('fp.' + this.name, JSON.stringify(this));
105
+ this.blocks.valet.saveHeader(this.toHeader());
75
106
  }
76
107
  }
77
108
  index(name) {
78
- // iterate over the indexes and gather any with the same name
79
- // if there are more than one, throw an error
80
- // if there is one, return it
81
- // if there are none, return null
82
109
  const indexes = [...this.indexes.values()].filter(index => index.name === name);
83
110
  if (indexes.length > 1) {
84
111
  throw new Error(`Multiple indexes found with name ${name}`);
@@ -93,12 +120,9 @@ export class Database {
93
120
  * @instance
94
121
  */
95
122
  async notifyReset() {
123
+ await this.ready;
96
124
  await this.notifyListeners({ _reset: true, _clock: this.clockToJSON() });
97
125
  }
98
- // used be indexes etc to notify database listeners of new availability
99
- // async notifyExternal (source = 'unknown') {
100
- // // await this.notifyListeners({ _external: source, _clock: this.clockToJSON() })
101
- // }
102
126
  /**
103
127
  * Returns the changes made to the Fireproof instance since the specified event.
104
128
  * @function changesSince
@@ -108,6 +132,7 @@ export class Database {
108
132
  * @instance
109
133
  */
110
134
  async changesSince(aClock) {
135
+ await this.ready;
111
136
  // console.log('events for', this.instanceId, aClock?.constructor.name)
112
137
  // console.log('changesSince', this.instanceId, this.clockToJSON(aClock), this.clockToJSON())
113
138
  let rows, dataCIDs, clockCIDs;
@@ -151,6 +176,7 @@ export class Database {
151
176
  };
152
177
  }
153
178
  async allDocuments() {
179
+ await this.ready;
154
180
  const allResp = await getAll(this.blocks, this.clock, this.rootCache);
155
181
  this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs };
156
182
  const rows = allResp.result
@@ -163,6 +189,7 @@ export class Database {
163
189
  };
164
190
  }
165
191
  async allCIDs() {
192
+ await this.ready;
166
193
  const allResp = await getAll(this.blocks, this.clock, this.rootCache, true);
167
194
  this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs };
168
195
  // console.log('allcids', allResp.cids, allResp.clockCIDs)
@@ -172,29 +199,13 @@ export class Database {
172
199
  return [...cids, ...clockCids]; // clock CID last -- need to handle multiple entry clocks
173
200
  }
174
201
  async allStoredCIDs() {
202
+ await this.ready;
175
203
  const allCIDs = [];
176
204
  for await (const { cid } of this.blocks.entries()) {
177
205
  allCIDs.push(cid);
178
206
  }
179
207
  return allCIDs;
180
208
  }
181
- /**
182
- * Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
183
- *
184
- * @param {Object} doc - The document to validate.
185
- * @returns {Promise<void>}
186
- * @throws {Error} - Throws an error if the document is invalid.
187
- * @memberof Fireproof
188
- * @instance
189
- */
190
- async runValidation(doc) {
191
- if (this.config && this.config.validateChange) {
192
- const oldDoc = await this.get(doc._id)
193
- .then(doc => doc)
194
- .catch(() => ({}));
195
- this.config.validateChange(doc, oldDoc, this.authCtx);
196
- }
197
- }
198
209
  /**
199
210
  * Retrieves the document with the specified ID from the database
200
211
  *
@@ -205,6 +216,7 @@ export class Database {
205
216
  * @instance
206
217
  */
207
218
  async get(key, opts = {}) {
219
+ await this.ready;
208
220
  const clock = opts.clock || this.clock;
209
221
  const resp = await get(this.blocks, clock, charwise.encode(key), this.rootCache);
210
222
  this.rootCache = { root: resp.root, clockCIDs: resp.clockCIDs };
@@ -239,6 +251,7 @@ export class Database {
239
251
  * @instance
240
252
  */
241
253
  async put({ _id, _proof, ...doc }) {
254
+ await this.ready;
242
255
  const id = _id || 'f' + Math.random().toString(36).slice(2);
243
256
  await this.runValidation({ _id: id, ...doc });
244
257
  return await this.putToProllyTree({ key: id, value: doc }, doc._clock);
@@ -251,6 +264,7 @@ export class Database {
251
264
  * @instance
252
265
  */
253
266
  async del(docOrId) {
267
+ await this.ready;
254
268
  let id;
255
269
  let clock = null;
256
270
  if (docOrId._id) {
@@ -265,6 +279,23 @@ export class Database {
265
279
  // this tombstone is temporary until we can get the prolly tree to delete
266
280
  // return await this.putToProllyTree({ key: id, value: null }, clock)
267
281
  }
282
+ /**
283
+ * Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
284
+ *
285
+ * @param {Object} doc - The document to validate.
286
+ * @returns {Promise<void>}
287
+ * @throws {Error} - Throws an error if the document is invalid.
288
+ * @memberof Fireproof
289
+ * @instance
290
+ */
291
+ async runValidation(doc) {
292
+ if (this.config && this.config.validateChange) {
293
+ const oldDoc = await this.get(doc._id)
294
+ .then(doc => doc)
295
+ .catch(() => ({}));
296
+ this.config.validateChange(doc, oldDoc, this.authCtx);
297
+ }
298
+ }
268
299
  /**
269
300
  * Updates the underlying storage with the specified event.
270
301
  * @private
@@ -364,11 +395,6 @@ export class Database {
364
395
  await listener(changes);
365
396
  }
366
397
  }
367
- setCarUploader(carUploaderFn) {
368
- // console.log('registering car uploader')
369
- // https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
370
- this.blocks.valet.uploadFunction = carUploaderFn;
371
- }
372
398
  setRemoteBlockReader(remoteBlockReaderFn) {
373
399
  this.blocks.remoteBlockFunction = remoteBlockReaderFn;
374
400
  }
@@ -236,6 +236,7 @@ export class DbIndex {
236
236
  * @returns
237
237
  */
238
238
  async applyQuery(resp, query) {
239
+ // console.log('applyQuery', resp, query)
239
240
  if (query.descending) {
240
241
  resp.result = resp.result.reverse();
241
242
  }
@@ -271,7 +272,7 @@ export class DbIndex {
271
272
  }
272
273
  else if (query.key) {
273
274
  const encodedKey = charwise.encode(query.key);
274
- return await this.applyQuery(this.indexByKey.root.get(encodedKey), query);
275
+ return await this.applyQuery(await this.indexByKey.root.get(encodedKey), query);
275
276
  }
276
277
  else {
277
278
  const { result, ...all } = await this.indexByKey.root.getAllEntries();
@@ -0,0 +1,92 @@
1
+ import { Database, parseCID } from './database.js';
2
+ import { DbIndex as Index } from './db-index.js';
3
+ import { Sync } from './sync.js';
4
+ export { Index, Database, Sync };
5
+ class Fireproof {
6
+ /**
7
+ * @function storage
8
+ * @memberof Fireproof
9
+ * Creates a new Fireproof instance with default storage settings
10
+ * Most apps should use this and not worry about the details.
11
+ * @static
12
+ * @returns {Database|Promise<Database>} - a new Fireproof instance or a promise for remote loaders
13
+ */
14
+ static storage = (name = null, opts = {}) => {
15
+ if (!name) {
16
+ return new Database(null, opts);
17
+ }
18
+ else {
19
+ // const primaryLoader = Loader.appropriate(name, opts.primary, { key: null })
20
+ // const secondaryLoader = opts.secondary ? Loader.appropriate(name, opts.secondary, { key: null }) : null
21
+ const db = new Database(name, opts);
22
+ return db;
23
+ // const loaders = [pr]
24
+ // todo we need branch names here
25
+ // console.log('storage', name, opts, primaryLoader, secondaryLoader)
26
+ }
27
+ };
28
+ // static fromConfig (name, primary, secondary, opts = {}) {
29
+ // console.log('fromConfig', name, primary, secondary, opts)
30
+ // let clock = []
31
+ // if (primary && primary.clock) {
32
+ // clock = clock.concat(primary.clock)
33
+ // }
34
+ // if (secondary && secondary.clock) {
35
+ // clock = clock.concat(secondary.clock)
36
+ // }
37
+ // const mergedClock = [...new Set(clock)].map(c => parseCID(c))
38
+ // opts.primaryHeader = primary
39
+ // opts.secondaryHeader = secondary
40
+ // opts.index = primary ? primary.index : {}
41
+ // const fp = new Database(name, mergedClock, opts)
42
+ // return Fireproof.fromJSON(primary, secondary, fp)
43
+ // }
44
+ static fromJSON(primary, secondary, database) {
45
+ const json = primary && primary.indexes ? primary : secondary;
46
+ if (json.indexes) {
47
+ for (const { name, code, clock: { byId, byKey, db } } of json.indexes) {
48
+ Index.fromJSON(database, {
49
+ clock: {
50
+ byId: byId ? parseCID(byId) : null,
51
+ byKey: byKey ? parseCID(byKey) : null,
52
+ db: (db && db.length > 0) ? db.map(c => parseCID(c)) : null
53
+ },
54
+ code,
55
+ name
56
+ });
57
+ }
58
+ }
59
+ return database;
60
+ }
61
+ static snapshot(database, clock) {
62
+ const definition = database.toJSON();
63
+ if (clock) {
64
+ definition.clock = clock.map(c => parseCID(c));
65
+ definition.indexes.forEach(index => {
66
+ index.clock.byId = null;
67
+ index.clock.byKey = null;
68
+ index.clock.db = null;
69
+ });
70
+ }
71
+ const withBlocks = new Database(database.name);
72
+ withBlocks.blocks = database.blocks;
73
+ withBlocks.clock = definition.clock;
74
+ const snappedDb = Fireproof.fromJSON(definition, null, withBlocks);
75
+ [...database.indexes.values()].forEach(index => {
76
+ snappedDb.indexes.get(index.mapFnString).mapFn = index.mapFn;
77
+ });
78
+ return snappedDb;
79
+ }
80
+ static async zoom(database, clock) {
81
+ ;
82
+ [...database.indexes.values()].forEach(index => {
83
+ index.indexById = { root: null, cid: null };
84
+ index.indexByKey = { root: null, cid: null };
85
+ index.dbHead = null;
86
+ });
87
+ database.clock = clock.map(c => parseCID(c));
88
+ await database.notifyReset(); // hmm... indexes should listen to this? might be more complex than worth it. so far this is the only caller
89
+ return database;
90
+ }
91
+ }
92
+ export { Fireproof };
@@ -1,5 +1,5 @@
1
1
  import { Browser } from './storage/browser.js';
2
- // import { Filesystem } from './storage/filesystem.js'
2
+ import { Filesystem } from './storage/filesystem.js';
3
3
  import { Rest } from './storage/rest.js';
4
4
  const FORCE_IDB = typeof process !== 'undefined' && !!process.env?.FORCE_IDB;
5
5
  /* global window */
@@ -17,8 +17,7 @@ export const Loader = {
17
17
  return new Browser(name, config);
18
18
  }
19
19
  else {
20
- return new Browser(name, config);
21
- // return new Filesystem(name, config)
20
+ return new Filesystem(name, config);
22
21
  }
23
22
  }
24
23
  };
@@ -8,6 +8,7 @@ import { nocache as cache } from 'prolly-trees/cache';
8
8
  import { CIDCounter, bf, simpleCompare as compare } from 'prolly-trees/utils';
9
9
  import * as codec from '@ipld/dag-cbor';
10
10
  import { sha256 as hasher } from 'multiformats/hashes/sha2';
11
+ // import { blake2b256 as hasher } from '@multiformats/blake2/blake2b'
11
12
  import { doTransaction } from './blockstore.js';
12
13
  import { create as createBlock } from 'multiformats/block';
13
14
  const blockOpts = { cache, chunker: bf(30), codec, hasher, compare };