@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/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/block.js.html +280 -0
- package/coverage/blockstore.js.html +916 -0
- package/coverage/clock.js.html +1141 -0
- package/coverage/db-index.js.html +694 -0
- package/coverage/favicon.png +0 -0
- package/coverage/fireproof.js.html +856 -0
- package/coverage/index.html +221 -0
- package/coverage/listener.js.html +421 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/prolly.js.html +883 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/tmp/coverage-42191-1678146904346-0.json +1 -0
- package/coverage/tmp/coverage-42193-1678146903521-0.json +1 -0
- package/coverage/tmp/coverage-42196-1678146904322-0.json +1 -0
- package/coverage/tmp/coverage-42197-1678146904292-0.json +1 -0
- package/coverage/valet.js.html +589 -0
- package/package.json +7 -4
- package/src/block.js +0 -10
- package/src/blockstore.js +53 -22
- package/src/db-index.js +47 -40
- package/src/fireproof.js +97 -43
- package/src/listener.js +18 -17
- package/src/prolly.js +52 -21
- package/src/valet.js +119 -93
- package/test/clock.test.js +60 -12
- package/test/db-index.test.js +3 -0
- package/test/listener.test.js +2 -3
- package/test/valet.test.js +59 -0
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
178
|
+
await this.valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
|
148
179
|
}
|
149
180
|
}
|
150
181
|
|
151
182
|
/**
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
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
|
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
|
50
|
+
* Represents an DbIndex for a Fireproof database.
|
53
51
|
*
|
54
|
-
* @class
|
55
|
-
* @classdesc An
|
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
|
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
|
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
|
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) {
|
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-
|
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
|
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 = (
|
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
|
-
|
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
|
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
|
138
|
+
// console.log('did DbIndex', this.indexRoot)
|
134
139
|
this.dbHead = result.clock
|
135
140
|
}
|
136
141
|
|
137
|
-
// todo use the
|
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
|
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-
|
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
|
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
|
167
|
+
// console.x('created DbIndex', inRoot.cid)
|
162
168
|
return inRoot
|
163
169
|
} else {
|
164
|
-
// load existing
|
165
|
-
// console.x('loading
|
166
|
-
const
|
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
|
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
|
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
|
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-
|
182
|
-
* @returns {Promise<import('prolly-trees/db-
|
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
|
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
|
198
|
+
return DbIndex.range(...encodedRange)
|
192
199
|
} else if (query.key) {
|
193
200
|
const encodedKey = charwise.encode(query.key)
|
194
|
-
return
|
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
|
-
*
|
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
|
-
* @
|
11
|
-
* @
|
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
|
-
* @
|
23
|
-
* @
|
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
|
-
|
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 = '
|
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 {
|
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
|
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 {
|
67
|
-
*
|
68
|
-
* @
|
69
|
-
*
|
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
|
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 {
|
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
|
138
|
-
return await this
|
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 {
|
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
|
148
|
-
// return await this
|
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
|
178
|
+
return await this.#putToProllyTree({ key: id, value: null })
|
151
179
|
}
|
152
180
|
|
153
|
-
|
154
|
-
|
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.
|
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 {
|
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
|
-
|
208
|
-
|
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
|
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}
|
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
|
-
|
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.
|
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.
|
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.
|
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}
|
98
|
+
* @param {Function} routingFn
|
99
99
|
* @returns {Array<string>} The topics emmitted by the event function.
|
100
|
+
* @private
|
100
101
|
*/
|
101
|
-
const topicsForChanges = (changes,
|
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
|
-
|
106
|
+
routingFn(makeDoc({ key, value }), (t) => {
|
106
107
|
const topicList = getTopicList(seenTopics, t)
|
107
108
|
topicList.push(key)
|
108
109
|
})
|