@fireproof/core 0.0.2 → 0.0.3

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/src/blockstore.js CHANGED
@@ -3,11 +3,23 @@ import * as raw from 'multiformats/codecs/raw'
3
3
  import { sha256 } from 'multiformats/hashes/sha2'
4
4
  import * as Block from 'multiformats/block'
5
5
  import * as CBW from '@ipld/car/buffer-writer'
6
+ import { CID } from 'multiformats'
6
7
 
7
8
  import Valet from './valet.js'
8
9
 
9
10
  // const sleep = ms => new Promise(r => setTimeout(r, ms))
10
11
 
12
+ const husherMap = new Map()
13
+ const husher = (id, workFn) => {
14
+ if (!husherMap.has(id)) {
15
+ husherMap.set(
16
+ id,
17
+ workFn().finally(() => setTimeout(() => husherMap.delete(id), 100))
18
+ )
19
+ }
20
+ return husherMap.get(id)
21
+ }
22
+
11
23
  /**
12
24
  * @typedef {Object} AnyBlock
13
25
  * @property {import('./link').AnyLink} cid - The CID of the block
@@ -24,7 +36,7 @@ export default class TransactionBlockstore {
24
36
  /** @type {Map<string, Uint8Array>} */
25
37
  #oldBlocks = new Map()
26
38
 
27
- #valet = new Valet() // cars by cid
39
+ valet = new Valet() // cars by cid
28
40
 
29
41
  #instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4)
30
42
  #inflightTransactions = new Set()
@@ -38,10 +50,10 @@ export default class TransactionBlockstore {
38
50
  async get (cid) {
39
51
  const key = cid.toString()
40
52
  // it is safe to read from the in-flight transactions becauase they are immutable
41
- // const bytes = this.#oldBlocks.get(key) || await this.#valet.getBlock(key)
42
- const bytes = await this.#transactionsGet(key) || await this.commitedGet(key)
43
- // const bytes = this.#blocks.get(key) || await this.#valet.getBlock(key)
44
- // console.log('bytes', typeof bytes)
53
+ const bytes = await Promise.any([this.#transactionsGet(key), this.commitedGet(key)]).catch((e) => {
54
+ console.log('networkGet', cid.toString(), e)
55
+ return this.networkGet(key)
56
+ })
45
57
  if (!bytes) throw new Error('Missing block: ' + key)
46
58
  return { cid, bytes }
47
59
  }
@@ -53,11 +65,30 @@ export default class TransactionBlockstore {
53
65
  const got = await transaction.get(key)
54
66
  if (got && got.bytes) return got.bytes
55
67
  }
68
+ throw new Error('Missing block: ' + key)
56
69
  }
57
70
 
58
71
  async commitedGet (key) {
59
- return this.#oldBlocks.get(key) || await this.#valet.getBlock(key)
60
- // return await this.#valet.getBlock(key) // todo this is just for testing
72
+ const old = this.#oldBlocks.get(key)
73
+ if (old) return old
74
+ return await this.valet.getBlock(key)
75
+ }
76
+
77
+ async networkGet (key) {
78
+ if (this.valet.remoteBlockFunction) {
79
+ const value = await husher(key, async () => await this.valet.remoteBlockFunction(key))
80
+ if (value) {
81
+ // console.log('networkGot: ' + key, value.length)
82
+ // dont turn this on until the Nan thing is fixed
83
+ // it keep the network blocks in indexedb but lets get the basics solid first
84
+ doTransaction('networkGot: ' + key, this, async (innerBlockstore) => {
85
+ await innerBlockstore.put(CID.parse(key), value)
86
+ })
87
+ return value
88
+ }
89
+ } else {
90
+ throw new Error('No remoteBlockFunction')
91
+ }
61
92
  }
62
93
 
63
94
  /**
@@ -91,11 +122,11 @@ export default class TransactionBlockstore {
91
122
  }
92
123
 
93
124
  /**
94
- * Begin a transaction. Ensures the uncommited blocks are empty at the begining.
95
- * Returns the blocks to read and write during the transaction.
96
- * @returns {InnerBlockstore}
97
- * @memberof TransactionBlockstore
98
- */
125
+ * Begin a transaction. Ensures the uncommited blocks are empty at the begining.
126
+ * Returns the blocks to read and write during the transaction.
127
+ * @returns {InnerBlockstore}
128
+ * @memberof TransactionBlockstore
129
+ */
99
130
  begin (label = '') {
100
131
  const innerTransactionBlockstore = new InnerBlockstore(label, this)
101
132
  this.#inflightTransactions.add(innerTransactionBlockstore)
@@ -103,10 +134,10 @@ export default class TransactionBlockstore {
103
134
  }
104
135
 
105
136
  /**
106
- * Commit the transaction. Writes the blocks to the store.
107
- * @returns {Promise<void>}
108
- * @memberof TransactionBlockstore
109
- */
137
+ * Commit the transaction. Writes the blocks to the store.
138
+ * @returns {Promise<void>}
139
+ * @memberof TransactionBlockstore
140
+ */
110
141
  async commit (innerBlockstore) {
111
142
  await this.#doCommit(innerBlockstore)
112
143
  }
@@ -128,7 +159,7 @@ export default class TransactionBlockstore {
128
159
  }
129
160
  }
130
161
  if (cids.size > 0) {
131
- console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
162
+ // console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
132
163
  await this.#valetWriteTransaction(innerBlockstore, cids)
133
164
  }
134
165
  }
@@ -144,15 +175,15 @@ export default class TransactionBlockstore {
144
175
  #valetWriteTransaction = async (innerBlockstore, cids) => {
145
176
  if (innerBlockstore.lastCid) {
146
177
  const newCar = await blocksToCarBlock(innerBlockstore.lastCid, innerBlockstore)
147
- await this.#valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
178
+ await this.valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
148
179
  }
149
180
  }
150
181
 
151
182
  /**
152
- * Retire the transaction. Clears the uncommited blocks.
153
- * @returns {void}
154
- * @memberof TransactionBlockstore
155
- */
183
+ * Retire the transaction. Clears the uncommited blocks.
184
+ * @returns {void}
185
+ * @memberof TransactionBlockstore
186
+ */
156
187
  retire (innerBlockstore) {
157
188
  this.#inflightTransactions.delete(innerBlockstore)
158
189
  }
package/src/db-index.js CHANGED
@@ -16,16 +16,14 @@ const makeGetBlock = (blocks) => async (address) => {
16
16
  }
17
17
  const makeDoc = ({ key, value }) => ({ _id: key, ...value })
18
18
 
19
- console.x = function () {}
20
-
21
19
  /**
22
- * Transforms a set of changes to index entries using a map function.
20
+ * Transforms a set of changes to DbIndex entries using a map function.
23
21
  *
24
22
  * @param {Array<{ key: string, value: import('./link').AnyLink, del?: boolean }>} changes
25
23
  * @param {Function} mapFun
26
- * @returns {Array<{ key: [string, string], value: any }>} The index entries generated by the map function.
24
+ * @returns {Array<{ key: [string, string], value: any }>} The DbIndex entries generated by the map function.
25
+ * @private
27
26
  */
28
-
29
27
  const indexEntriesForChanges = (changes, mapFun) => {
30
28
  const indexEntries = []
31
29
  changes.forEach(({ key, value, del }) => {
@@ -49,24 +47,19 @@ const indexEntriesForOldChanges = async (blocks, byIDindexRoot, ids, mapFun) =>
49
47
  }
50
48
 
51
49
  /**
52
- * Represents an index for a Fireproof database.
50
+ * Represents an DbIndex for a Fireproof database.
53
51
  *
54
- * @class
55
- * @classdesc An index can be used to order and filter the documents in a Fireproof database.
52
+ * @class DbIndex
53
+ * @classdesc An DbIndex can be used to order and filter the documents in a Fireproof database.
56
54
  *
57
- * @param {import('./fireproof').Fireproof} database - The Fireproof database instance to index.
55
+ * @param {import('./fireproof').Fireproof} database - The Fireproof database instance to DbIndex.
58
56
  * @param {Function} mapFun - The map function to apply to each entry in the database.
59
57
  *
60
58
  */
61
- export default class Index {
62
- /**
63
- * Creates a new index with the given map function and database.
64
- * @param {import('./fireproof').Fireproof} database - The Fireproof database instance to index.
65
- * @param {Function} mapFun - The map function to apply to each entry in the database.
66
- */
59
+ export default class DbIndex {
67
60
  constructor (database, mapFun) {
68
61
  /**
69
- * The database instance to index.
62
+ * The database instance to DbIndex.
70
63
  * @type {import('./fireproof').Fireproof}
71
64
  */
72
65
  this.database = database
@@ -82,24 +75,29 @@ export default class Index {
82
75
 
83
76
  /**
84
77
  * Query object can have {range}
85
- *
78
+ * @param {Object<{range:[startKey, endKey]}>} query - the query range to use
79
+ * @param {CID} [root] - an optional root to query a snapshot
80
+ * @returns {Promise<{rows: Array<{id: string, key: string, value: any}>}>}
81
+ * @memberof DbIndex
82
+ * @instance
86
83
  */
87
84
  async query (query, root = null) {
88
- if (!root) { // pass a root to query a snapshot
85
+ if (!root) {
86
+ // pass a root to query a snapshot
89
87
  await doTransaction('#updateIndex', this.database.blocks, async (blocks) => {
90
88
  await this.#updateIndex(blocks)
91
89
  })
92
90
  }
93
91
  const response = await doIndexQuery(this.database.blocks, root || this.indexRoot, query)
94
92
  return {
95
- // TODO fix this naming upstream in prolly/db-index
93
+ // TODO fix this naming upstream in prolly/db-DbIndex
96
94
  // todo maybe this is a hint about why deletes arent working?
97
95
  rows: response.result.map(({ id, key, row }) => ({ id: key, key: charwise.decode(id), value: row }))
98
96
  }
99
97
  }
100
98
 
101
99
  /**
102
- * Update the index with the latest changes
100
+ * Update the DbIndex with the latest changes
103
101
  * @private
104
102
  * @returns {Promise<void>}
105
103
  */
@@ -111,16 +109,23 @@ export default class Index {
111
109
  }
112
110
  const result = await this.database.changesSince(this.dbHead) // {key, value, del}
113
111
  if (this.dbHead) {
114
- const oldIndexEntries = (await indexEntriesForOldChanges(blocks, this.byIDindexRoot, result.rows.map(({ key }) => key), this.mapFun))
112
+ const oldIndexEntries = (
113
+ await indexEntriesForOldChanges(
114
+ blocks,
115
+ this.byIDindexRoot,
116
+ result.rows.map(({ key }) => key),
117
+ this.mapFun
118
+ )
119
+ )
115
120
  // .map((key) => ({ key, value: null })) // tombstone just adds more rows...
116
121
  .map((key) => ({ key, del: true })) // should be this
117
- // .map((key) => ({ key: undefined, del: true })) // todo why does this work?
122
+ // .map((key) => ({ key: undefined, del: true })) // todo why does this work?
118
123
 
119
124
  this.indexRoot = await bulkIndex(blocks, this.indexRoot, oldIndexEntries, opts)
120
125
  // console.x('oldIndexEntries', oldIndexEntries)
121
126
  // [ { key: ['b', 1], del: true } ]
122
127
  // [ { key: [ 5, 'x' ], del: true } ]
123
- // for now we just let the by id index grow and then don't use the results...
128
+ // for now we just let the by id DbIndex grow and then don't use the results...
124
129
  // const removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
125
130
  // this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, removeByIdIndexEntries, opts)
126
131
  }
@@ -130,67 +135,69 @@ export default class Index {
130
135
  this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, byIdIndexEntries, opts)
131
136
  // console.log('indexEntries', indexEntries)
132
137
  this.indexRoot = await bulkIndex(blocks, this.indexRoot, indexEntries, opts)
133
- // console.log('did index', this.indexRoot)
138
+ // console.log('did DbIndex', this.indexRoot)
134
139
  this.dbHead = result.clock
135
140
  }
136
141
 
137
- // todo use the index from other peers?
142
+ // todo use the DbIndex from other peers?
138
143
  // we might need to add CRDT logic to it for that
139
144
  // it would only be a performance improvement, but might add a lot of complexity
140
145
  // advanceIndex ()) {}
141
146
  }
142
147
 
143
148
  /**
144
- * Update the index with the given entries
149
+ * Update the DbIndex with the given entries
145
150
  * @param {Blockstore} blocks
146
151
  * @param {import('multiformats/block').Block} inRoot
147
- * @param {import('prolly-trees/db-index').IndexEntry[]} indexEntries
152
+ * @param {import('prolly-trees/db-DbIndex').IndexEntry[]} indexEntries
153
+ * @private
148
154
  */
149
155
  async function bulkIndex (blocks, inRoot, indexEntries) {
150
156
  if (!indexEntries.length) return inRoot
151
157
  const putBlock = blocks.put.bind(blocks)
152
158
  const getBlock = makeGetBlock(blocks)
153
159
  if (!inRoot) {
154
- // make a new index
160
+ // make a new DbIndex
155
161
 
156
162
  for await (const node of await create({ get: getBlock, list: indexEntries, ...opts })) {
157
163
  const block = await node.block
158
164
  await putBlock(block.cid, block.bytes)
159
165
  inRoot = block
160
166
  }
161
- // console.x('created index', inRoot.cid)
167
+ // console.x('created DbIndex', inRoot.cid)
162
168
  return inRoot
163
169
  } else {
164
- // load existing index
165
- // console.x('loading index', inRoot.cid)
166
- const index = await load({ cid: inRoot.cid, get: getBlock, ...opts })
170
+ // load existing DbIndex
171
+ // console.x('loading DbIndex', inRoot.cid)
172
+ const DbIndex = await load({ cid: inRoot.cid, get: getBlock, ...opts })
167
173
  // console.log('new indexEntries', indexEntries)
168
- const { root, blocks } = await index.bulk(indexEntries)
174
+ const { root, blocks } = await DbIndex.bulk(indexEntries)
169
175
  for await (const block of blocks) {
170
176
  await putBlock(block.cid, block.bytes)
171
177
  }
172
- // console.x('updated index', root.block.cid)
178
+ // console.x('updated DbIndex', root.block.cid)
173
179
  return await root.block // if we hold the root we won't have to load every time
174
180
  }
175
181
  }
176
182
 
177
183
  /**
178
- * Query the index for the given range
184
+ * Query the DbIndex for the given range
179
185
  * @param {Blockstore} blocks
180
186
  * @param {import('multiformats/block').Block} inRoot
181
- * @param {import('prolly-trees/db-index').Query} query
182
- * @returns {Promise<import('prolly-trees/db-index').QueryResult>}
187
+ * @param {import('prolly-trees/db-DbIndex').Query} query
188
+ * @returns {Promise<import('prolly-trees/db-DbIndex').QueryResult>}
189
+ * @private
183
190
  **/
184
191
  async function doIndexQuery (blocks, root, query) {
185
192
  const cid = root && root.cid
186
193
  if (!cid) return { result: [] }
187
194
  const getBlock = makeGetBlock(blocks)
188
- const index = await load({ cid, get: getBlock, ...opts })
195
+ const DbIndex = await load({ cid, get: getBlock, ...opts })
189
196
  if (query.range) {
190
197
  const encodedRange = query.range.map((key) => charwise.encode(key))
191
- return index.range(...encodedRange)
198
+ return DbIndex.range(...encodedRange)
192
199
  } else if (query.key) {
193
200
  const encodedKey = charwise.encode(query.key)
194
- return index.get(encodedKey)
201
+ return DbIndex.get(encodedKey)
195
202
  }
196
203
  }
package/src/fireproof.js CHANGED
@@ -1,44 +1,51 @@
1
- // import { vis } from './clock.js'
2
1
  import { put, get, getAll, eventsSince } from './prolly.js'
3
2
  import Blockstore, { doTransaction } from './blockstore.js'
4
3
 
5
4
  // const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
6
5
 
7
6
  /**
8
- * Represents a Fireproof instance that wraps a ProllyDB instance and Merkle clock head.
7
+ * @class Fireproof
8
+ * @classdesc Fireproof stores data in IndexedDB and provides a Merkle clock.
9
+ * This is the main class for saving and loading JSON and other documents with the database. You can find additional examples and
10
+ * usage guides in the repository README.
9
11
  *
10
- * @class
11
- * @classdesc A Fireproof instance can be used to store and retrieve values from a ProllyDB instance.
12
- *
13
- * @param {Blockstore} blocks - The block storage instance to use for the underlying ProllyDB instance.
14
- * @param {import('../clock').EventLink<import('../crdt').EventData>[]} clock - The Merkle clock head to use for the Fireproof instance.
12
+ * @param {Blockstore} blocks - The block storage instance to use documents and indexes
13
+ * @param {CID[]} clock - The Merkle clock head to use for the Fireproof instance.
15
14
  * @param {object} [config] - Optional configuration options for the Fireproof instance.
16
15
  * @param {object} [authCtx] - Optional authorization context object to use for any authentication checks.
17
16
  *
18
17
  */
19
-
20
18
  export default class Fireproof {
19
+ #listeners = new Set()
20
+
21
21
  /**
22
- * @param {Blockstore} blocks
23
- * @param {import('../clock').EventLink<import('../crdt').EventData>[]} clock
22
+ * @function storage
23
+ * @memberof Fireproof
24
+ * Creates a new Fireproof instance with default storage settings
25
+ * Most apps should use this and not worry about the details.
26
+ * @static
27
+ * @returns {Fireproof} - a new Fireproof instance
24
28
  */
25
- #listeners = new Set()
29
+ static storage = () => {
30
+ return new Fireproof(new Blockstore(), [])
31
+ }
26
32
 
27
33
  constructor (blocks, clock, config = {}, authCtx = {}) {
28
34
  this.blocks = blocks
29
35
  this.clock = clock
30
36
  this.config = config
31
37
  this.authCtx = authCtx
32
- this.instanceId = 'db.' + Math.random().toString(36).substring(2, 7)
38
+ this.instanceId = 'fp.' + Math.random().toString(36).substring(2, 7)
33
39
  }
34
40
 
35
41
  /**
36
- * Returns a snapshot of the current Fireproof instance.
37
- *
38
- * @param {import('../clock').EventLink<import('../crdt').EventData>[]} clock
39
- * Clock to use for the snapshot.
42
+ * Returns a snapshot of the current Fireproof instance as a new instance.
43
+ * @function snapshot
44
+ * @param {CID[]} clock - The Merkle clock head to use for the snapshot.
40
45
  * @returns {Fireproof}
41
46
  * A new Fireproof instance representing the snapshot.
47
+ * @memberof Fireproof
48
+ * @instance
42
49
  */
43
50
  snapshot (clock) {
44
51
  // how to handle listeners, views, and config?
@@ -47,14 +54,26 @@ export default class Fireproof {
47
54
  }
48
55
 
49
56
  /**
50
- * This triggers a notification to all listeners of the Fireproof instance.
57
+ * Move the current instance to a new point in time. This triggers a notification to all listeners
58
+ * of the Fireproof instance so they can repaint UI, etc.
59
+ * @param {CID[] } clock
60
+ * Clock to use for the snapshot.
61
+ * @returns {Promise<void>}
62
+ * @memberof Fireproof
63
+ * @instance
51
64
  */
52
65
  async setClock (clock) {
53
66
  // console.log('setClock', this.instanceId, clock)
54
- this.clock = clock.map((item) => item['/'] ? item['/'] : item)
67
+ this.clock = clock.map((item) => (item['/'] ? item['/'] : item))
55
68
  await this.#notifyListeners({ reset: true, clock })
56
69
  }
57
70
 
71
+ /**
72
+ * Renders the Fireproof instance as a JSON object.
73
+ * @returns {Object} - The JSON representation of the Fireproof instance. Includes clock heads for the database and its indexes.
74
+ * @memberof Fireproof
75
+ * @instance
76
+ */
58
77
  toJSON () {
59
78
  // todo this also needs to return the index roots...
60
79
  return { clock: this.clock }
@@ -62,13 +81,11 @@ export default class Fireproof {
62
81
 
63
82
  /**
64
83
  * Returns the changes made to the Fireproof instance since the specified event.
65
- *
66
- * @param {import('../clock').EventLink<import('../crdt').EventData>?} event -
67
- * The event to retrieve changes since. If null or undefined, retrieves all changes.
68
- * @returns {Promise<{
69
- * rows: { key: string, value?: any, del?: boolean }[],
70
- * head: import('../clock').EventLink<import('../crdt').EventData>[]
71
- * }>} - An object `{rows : [...{key, value, del}], head}` containing the rows and the head of the instance's clock.
84
+ * @function changesSince
85
+ * @param {CID[]} [event] - The clock head to retrieve changes since. If null or undefined, retrieves all changes.
86
+ * @returns {Object<{rows : Object[], clock: CID[]}>} An object containing the rows and the head of the instance's clock.
87
+ * @memberof Fireproof
88
+ * @instance
72
89
  */
73
90
  async changesSince (event) {
74
91
  // console.log('changesSince', this.instanceId, event, this.clock)
@@ -97,6 +114,7 @@ export default class Fireproof {
97
114
  * Recieves live changes from the database after they are committed.
98
115
  * @param {Function} listener - The listener to be called when the clock is updated.
99
116
  * @returns {Function} - A function that can be called to unregister the listener.
117
+ * @memberof Fireproof
100
118
  */
101
119
  registerListener (listener) {
102
120
  this.#listeners.add(listener)
@@ -107,15 +125,21 @@ export default class Fireproof {
107
125
 
108
126
  async #notifyListeners (changes) {
109
127
  // await sleep(0)
110
- this.#listeners.forEach((listener) => listener(changes))
128
+ for (const listener of this.#listeners) {
129
+ await listener(changes)
130
+ }
111
131
  }
112
132
 
113
133
  /**
114
- * Runs validation on the specified document using the Fireproof instance's configuration.
134
+ * Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
115
135
  *
116
136
  * @param {Object} doc - The document to validate.
137
+ * @returns {Promise<void>}
138
+ * @throws {Error} - Throws an error if the document is invalid.
139
+ * @memberof Fireproof
140
+ * @instance
117
141
  */
118
- async runValidation (doc) {
142
+ async #runValidation (doc) {
119
143
  if (this.config && this.config.validateChange) {
120
144
  const oldDoc = await this.get(doc._id)
121
145
  .then((doc) => doc)
@@ -125,35 +149,50 @@ export default class Fireproof {
125
149
  }
126
150
 
127
151
  /**
128
- * Adds a new document to the database, or updates an existing document.
152
+ * Adds a new document to the database, or updates an existing document. Returns the ID of the document and the new clock head.
129
153
  *
130
154
  * @param {Object} doc - the document to be added
131
155
  * @param {string} doc._id - the document ID. If not provided, a random ID will be generated.
132
156
  * @param {Object} doc.* - the document data to be added
133
- * @returns {Promise<import('./prolly').PutResult>} - the result of adding the document
157
+ * @returns {Object<{ id: string, clock: CID[] }>} - The result of adding the document to the database
158
+ * @memberof Fireproof
159
+ * @instance
134
160
  */
135
161
  async put ({ _id, ...doc }) {
136
162
  const id = _id || 'f' + Math.random().toString(36).slice(2)
137
- await this.runValidation({ _id: id, ...doc })
138
- return await this.putToProllyTree({ key: id, value: doc })
163
+ await this.#runValidation({ _id: id, ...doc })
164
+ return await this.#putToProllyTree({ key: id, value: doc })
139
165
  }
140
166
 
141
167
  /**
142
168
  * Deletes a document from the database
143
169
  * @param {string} id - the document ID
144
- * @returns {Promise<import('./prolly').PutResult>} - the result of deleting the document
170
+ * @returns {Object<{ id: string, clock: CID[] }>} - The result of deleting the document from the database
171
+ * @memberof Fireproof
172
+ * @instance
145
173
  */
146
174
  async del (id) {
147
- await this.runValidation({ _id: id, _deleted: true })
148
- // return await this.putToProllyTree({ key: id, del: true }) // not working at prolly tree layer?
175
+ await this.#runValidation({ _id: id, _deleted: true })
176
+ // return await this.#putToProllyTree({ key: id, del: true }) // not working at prolly tree layer?
149
177
  // this tombstone is temporary until we can get the prolly tree to delete
150
- return await this.putToProllyTree({ key: id, value: null })
178
+ return await this.#putToProllyTree({ key: id, value: null })
151
179
  }
152
180
 
153
- async putToProllyTree (event) {
154
- const result = await doTransaction('putToProllyTree', this.blocks, async (blocks) => await put(blocks, this.clock, event))
181
+ /**
182
+ * Updates the underlying storage with the specified event.
183
+ * @private
184
+ * @param {import('../clock').EventLink<import('../crdt').EventData>} event - the event to add
185
+ * @returns {Object<{ id: string, clock: import('../clock').EventLink<import('../crdt').EventData }>} - The result of adding the event to storage
186
+ */
187
+ async #putToProllyTree (event) {
188
+ const result = await doTransaction(
189
+ '#putToProllyTree',
190
+ this.blocks,
191
+ async (blocks) => await put(blocks, this.clock, event)
192
+ )
155
193
  if (!result) {
156
- console.log('failed', event)
194
+ console.error('failed', event)
195
+ throw new Error('failed to put at storage layer')
157
196
  }
158
197
  this.clock = result.head // do we want to do this as a finally block
159
198
  result.id = event.key
@@ -191,7 +230,9 @@ export default class Fireproof {
191
230
  * Retrieves the document with the specified ID from the database
192
231
  *
193
232
  * @param {string} key - the ID of the document to retrieve
194
- * @returns {Promise<import('./prolly').GetResult>} - the document with the specified ID
233
+ * @returns {Object<{_id: string, ...doc: Object}>} - the document with the specified ID
234
+ * @memberof Fireproof
235
+ * @instance
195
236
  */
196
237
  async get (key) {
197
238
  const got = await get(this.blocks, this.clock, key)
@@ -202,8 +243,21 @@ export default class Fireproof {
202
243
  got._id = key
203
244
  return got
204
245
  }
205
- }
206
246
 
207
- Fireproof.storage = (_email) => {
208
- return new Fireproof(new Blockstore(), [])
247
+ setCarUploader (carUploaderFn) {
248
+ console.log('registering car uploader')
249
+ // https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
250
+ this.blocks.valet.uploadFunction = carUploaderFn
251
+ }
252
+
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
+ setRemoteBlockReader (remoteBlockReaderFn) {
260
+ // console.log('registering remote block reader')
261
+ this.blocks.valet.remoteBlockFunction = remoteBlockReaderFn
262
+ }
209
263
  }
package/src/listener.js CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * A Fireproof database Listener allows you to react to events in the database.
3
3
  *
4
- * @class
5
- * @classdesc An listener can be notified of events as they happen or on reconection
4
+ * @class Listener
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
7
  * @param {import('./fireproof').Fireproof} database - The Fireproof database instance to index.
8
- * @param {Function} eventFun - The map function to apply to each entry in the database.
9
- *
8
+ * @param {Function} routingFn - The routing function to apply to each entry in the database.
10
9
  */
11
10
  export default class Listener {
12
11
  #subcribers = new Map()
@@ -18,13 +17,8 @@ export default class Listener {
18
17
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef
19
18
  #doStopListening = null
20
19
 
21
- /**
22
- * Creates a new index with the given map function and database.
23
- * @param {import('./fireproof').Fireproof} database - The Fireproof database instance to index.
24
- * @param {Function} eventFun - The event function to apply to each current change to the database.
25
- */
26
- constructor (database, eventFun) {
27
- /** eventFun
20
+ constructor (database, routingFn) {
21
+ /** routingFn
28
22
  * The database instance to index.
29
23
  * @type {import('./fireproof').Fireproof}
30
24
  */
@@ -34,7 +28,11 @@ export default class Listener {
34
28
  * The map function to apply to each entry in the database.
35
29
  * @type {Function}
36
30
  */
37
- this.eventFun = eventFun || function (_, emit) { emit('*') }
31
+ this.routingFn =
32
+ routingFn ||
33
+ function (_, emit) {
34
+ emit('*')
35
+ }
38
36
  this.dbHead = null
39
37
  }
40
38
 
@@ -43,13 +41,15 @@ export default class Listener {
43
41
  * @param {string} topic - The topic to subscribe to.
44
42
  * @param {Function} subscriber - The function to call when the topic is emitted.
45
43
  * @returns {Function} A function to unsubscribe from the topic.
44
+ * @memberof Listener
45
+ * @instance
46
46
  */
47
47
  on (topic, subscriber, since) {
48
48
  const listOfTopicSubscribers = getTopicList(this.#subcribers, topic)
49
49
  listOfTopicSubscribers.push(subscriber)
50
50
  if (typeof since !== 'undefined') {
51
51
  this.database.changesSince(since).then(({ rows: changes }) => {
52
- const keys = topicsForChanges(changes, this.eventFun).get(topic)
52
+ const keys = topicsForChanges(changes, this.routingFn).get(topic)
53
53
  if (keys) keys.forEach((key) => subscriber(key))
54
54
  })
55
55
  }
@@ -61,7 +61,7 @@ export default class Listener {
61
61
 
62
62
  #onChanges (changes) {
63
63
  if (Array.isArray(changes)) {
64
- const seenTopics = topicsForChanges(changes, this.eventFun)
64
+ const seenTopics = topicsForChanges(changes, this.routingFn)
65
65
  for (const [topic, keys] of seenTopics) {
66
66
  const listOfTopicSubscribers = getTopicList(this.#subcribers, topic)
67
67
  listOfTopicSubscribers.forEach((subscriber) => keys.forEach((key) => subscriber(key)))
@@ -95,14 +95,15 @@ const makeDoc = ({ key, value }) => ({ _id: key, ...value })
95
95
  * Transforms a set of changes to events using an emitter function.
96
96
  *
97
97
  * @param {Array<{ key: string, value: import('./link').AnyLink, del?: boolean }>} changes
98
- * @param {Function} eventFun
98
+ * @param {Function} routingFn
99
99
  * @returns {Array<string>} The topics emmitted by the event function.
100
+ * @private
100
101
  */
101
- const topicsForChanges = (changes, eventFun) => {
102
+ const topicsForChanges = (changes, routingFn) => {
102
103
  const seenTopics = new Map()
103
104
  changes.forEach(({ key, value, del }) => {
104
105
  if (del || !value) value = { _deleted: true }
105
- eventFun(makeDoc({ key, value }), (t) => {
106
+ routingFn(makeDoc({ key, value }), (t) => {
106
107
  const topicList = getTopicList(seenTopics, t)
107
108
  topicList.push(key)
108
109
  })