@fireproof/core 0.0.1

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/clock.js ADDED
@@ -0,0 +1,352 @@
1
+ import { Block, encode, decode } from 'multiformats/block'
2
+ import { sha256 } from 'multiformats/hashes/sha2'
3
+ import * as cbor from '@ipld/dag-cbor'
4
+
5
+ /**
6
+ * @template T
7
+ * @typedef {{ parents: EventLink<T>[], data: T }} EventView
8
+ */
9
+
10
+ /**
11
+ * @template T
12
+ * @typedef {import('multiformats').BlockView<EventView<T>>} EventBlockView
13
+ */
14
+
15
+ /**
16
+ * @template T
17
+ * @typedef {import('multiformats').Link<EventView<T>>} EventLink
18
+ */
19
+
20
+ /**
21
+ * Advance the clock by adding an event.
22
+ *
23
+ * @template T
24
+ * @param {import('./block').BlockFetcher} blocks Block storage.
25
+ * @param {EventLink<T>[]} head The head of the clock.
26
+ * @param {EventLink<T>} event The event to add.
27
+ * @returns {Promise<EventLink<T>[]>} The new head of the clock.
28
+ */
29
+ export async function advance (blocks, head, event) {
30
+ /** @type {EventFetcher<T>} */
31
+ const events = new EventFetcher(blocks)
32
+ const headmap = new Map(head.map(cid => [cid.toString(), cid]))
33
+
34
+ // Check if the headmap already includes the event, return head if it does
35
+ if (headmap.has(event.toString())) return head
36
+
37
+ // Does event contain the clock?
38
+ let changed = false
39
+ for (const cid of head) {
40
+ if (await contains(events, event, cid)) {
41
+ headmap.delete(cid.toString())
42
+ headmap.set(event.toString(), event)
43
+ changed = true
44
+ }
45
+ }
46
+
47
+ // If the headmap has been changed, return the new headmap values
48
+ if (changed) {
49
+ return [...headmap.values()]
50
+ }
51
+
52
+ // Does clock contain the event?
53
+ for (const p of head) {
54
+ if (await contains(events, p, event)) {
55
+ return head
56
+ }
57
+ }
58
+
59
+ // Return the head concatenated with the new event if it passes both checks
60
+ return head.concat(event)
61
+ }
62
+
63
+ /**
64
+ * @template T
65
+ * @implements {EventBlockView<T>}
66
+ */
67
+ export class EventBlock extends Block {
68
+ /**
69
+ * @param {object} config
70
+ * @param {EventLink<T>} config.cid
71
+ * @param {Event} config.value
72
+ * @param {Uint8Array} config.bytes
73
+ */
74
+ constructor ({ cid, value, bytes }) {
75
+ // @ts-expect-error
76
+ super({ cid, value, bytes })
77
+ }
78
+
79
+ /**
80
+ * @template T
81
+ * @param {T} data
82
+ * @param {EventLink<T>[]} [parents]
83
+ */
84
+ static create (data, parents) {
85
+ return encodeEventBlock({ data, parents: parents ?? [] })
86
+ }
87
+ }
88
+
89
+ /** @template T */
90
+ export class EventFetcher {
91
+ /** @param {import('./block').BlockFetcher} blocks */
92
+ constructor (blocks) {
93
+ /** @private */
94
+ this._blocks = blocks
95
+ }
96
+
97
+ /**
98
+ * @param {EventLink<T>} link
99
+ * @returns {Promise<EventBlockView<T>>}
100
+ */
101
+ async get (link) {
102
+ const block = await this._blocks.get(link)
103
+ if (!block) throw new Error(`missing block: ${link}`)
104
+ return decodeEventBlock(block.bytes)
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @template T
110
+ * @param {EventView<T>} value
111
+ * @returns {Promise<EventBlockView<T>>}
112
+ */
113
+ export async function encodeEventBlock (value) {
114
+ // TODO: sort parents
115
+ const { cid, bytes } = await encode({ value, codec: cbor, hasher: sha256 })
116
+ // @ts-expect-error
117
+ return new Block({ cid, value, bytes })
118
+ }
119
+
120
+ /**
121
+ * @template T
122
+ * @param {Uint8Array} bytes
123
+ * @returns {Promise<EventBlockView<T>>}
124
+ */
125
+ export async function decodeEventBlock (bytes) {
126
+ const { cid, value } = await decode({ bytes, codec: cbor, hasher: sha256 })
127
+ // @ts-expect-error
128
+ return new Block({ cid, value, bytes })
129
+ }
130
+
131
+ /**
132
+ * Returns true if event "a" contains event "b". Breadth first search.
133
+ * @template T
134
+ * @param {EventFetcher} events
135
+ * @param {EventLink<T>} a
136
+ * @param {EventLink<T>} b
137
+ */
138
+ async function contains (events, a, b) {
139
+ if (a.toString() === b.toString()) return true
140
+ const [{ value: aevent }, { value: bevent }] = await Promise.all([events.get(a), events.get(b)])
141
+ const links = [...aevent.parents]
142
+ while (links.length) {
143
+ const link = links.shift()
144
+ if (!link) break
145
+ if (link.toString() === b.toString()) return true
146
+ // if any of b's parents are this link, then b cannot exist in any of the
147
+ // tree below, since that would create a cycle.
148
+ if (bevent.parents.some(p => link.toString() === p.toString())) continue
149
+ const { value: event } = await events.get(link)
150
+ links.push(...event.parents)
151
+ }
152
+ return false
153
+ }
154
+
155
+ /**
156
+ * @template T
157
+ * @param {import('./block').BlockFetcher} blocks Block storage.
158
+ * @param {EventLink<T>[]} head
159
+ * @param {object} [options]
160
+ * @param {(b: EventBlockView<T>) => string} [options.renderNodeLabel]
161
+ */
162
+ export async function * vis (blocks, head, options = {}) {
163
+ const renderNodeLabel = options.renderNodeLabel ?? (b => (b.value.data.value))
164
+ const events = new EventFetcher(blocks)
165
+ yield 'digraph clock {'
166
+ yield ' node [shape=point fontname="Courier"]; head;'
167
+ const hevents = await Promise.all(head.map(link => events.get(link)))
168
+ const links = []
169
+ const nodes = new Set()
170
+ for (const e of hevents) {
171
+ nodes.add(e.cid.toString())
172
+ yield ` node [shape=oval fontname="Courier"]; ${e.cid} [label="${renderNodeLabel(e)}"];`
173
+ yield ` head -> ${e.cid};`
174
+ for (const p of e.value.parents) {
175
+ yield ` ${e.cid} -> ${p};`
176
+ }
177
+ links.push(...e.value.parents)
178
+ }
179
+ while (links.length) {
180
+ const link = links.shift()
181
+ if (!link) break
182
+ if (nodes.has(link.toString())) continue
183
+ nodes.add(link.toString())
184
+ const block = await events.get(link)
185
+ yield ` node [shape=oval]; ${link} [label="${renderNodeLabel(block)}" fontname="Courier"];`
186
+ for (const p of block.value.parents) {
187
+ yield ` ${link} -> ${p};`
188
+ }
189
+ links.push(...block.value.parents)
190
+ }
191
+ yield '}'
192
+ }
193
+
194
+ export async function findEventsToSync (blocks, head) {
195
+ const toSync = await findUnknownSortedEvents(blocks, head, await findCommonAncestorWithSortedEvents(blocks, head))
196
+ return toSync
197
+ }
198
+
199
+ export async function findUnknownSortedEvents (blocks, children, { ancestor, sorted }) {
200
+ const events = new EventFetcher(blocks)
201
+ // const childrenCids = children.map(c => c.toString())
202
+ // const lowerEvent = sorted.find(({ cid }) => childrenCids.includes(cid.toString()))
203
+ // const knownAncestor = await findCommonAncestor(events, [lowerEvent.cid]) // should this be [lowerEvent.cid] ?
204
+ // const knownAncestor = await findCommonAncestor(events, [...children]) // should this be [lowerEvent.cid] ?
205
+ // console.x('already knownAncestor', knownAncestor.toString() === ancestor.toString(),
206
+ // (await (await decodeEventBlock((await blocks.get(knownAncestor)).bytes)).value.data?.value), knownAncestor
207
+ // )
208
+
209
+ const matchHead = [ancestor]
210
+ const unknownSorted = await asyncFilter(sorted, async (uks) => {
211
+ for (const ev of matchHead) {
212
+ const isIn = await contains(events, ev, uks.cid)
213
+ if (isIn) return false
214
+ }
215
+ return true
216
+ })
217
+ // console.x('unknownSorted contains', unknownSorted.length, sorted.length)
218
+ return unknownSorted
219
+ }
220
+
221
+ const asyncFilter = async (arr, predicate) => Promise.all(arr.map(predicate))
222
+ .then((results) => arr.filter((_v, index) => results[index]))
223
+
224
+ export async function findCommonAncestorWithSortedEvents (blocks, children) {
225
+ const events = new EventFetcher(blocks)
226
+
227
+ const ancestor = await findCommonAncestor(events, children)
228
+ if (!ancestor) {
229
+ throw new Error('failed to find common ancestor event')
230
+ }
231
+ // Sort the events by their sequence number
232
+ const sorted = await findSortedEvents(events, children, ancestor)
233
+ // console.x('ancstor', ancestor, (await decodeEventBlock((await blocks.get(ancestor)).bytes)).value.data?.value)
234
+ // sorted.forEach(({ cid, value }) => console.x('xsorted', cid, value.data.value))
235
+ return { ancestor, sorted }
236
+ }
237
+
238
+ /**
239
+ * Find the common ancestor event of the passed children. A common ancestor is
240
+ * the first single event in the DAG that _all_ paths from children lead to.
241
+ *
242
+ * @param {import('./clock').EventFetcher} events
243
+ * @param {import('./clock').EventLink<EventData>[]} children
244
+ */
245
+ async function findCommonAncestor (events, children) {
246
+ if (!children.length) return
247
+ const candidates = children.map((c) => [c])
248
+ while (true) {
249
+ let changed = false
250
+ for (const c of candidates) {
251
+ const candidate = await findAncestorCandidate(events, c[c.length - 1])
252
+ if (!candidate) continue
253
+ changed = true
254
+ c.push(candidate)
255
+ const ancestor = findCommonString(candidates)
256
+ if (ancestor) return ancestor
257
+ }
258
+ if (!changed) return
259
+ }
260
+ }
261
+
262
+ /**
263
+ * @param {import('./clock').EventFetcher} events
264
+ * @param {import('./clock').EventLink<EventData>} root
265
+ */
266
+ async function findAncestorCandidate (events, root) {
267
+ const { value: event } = await events.get(root)
268
+ if (!event.parents.length) return root
269
+ return event.parents.length === 1
270
+ ? event.parents[0]
271
+ : findCommonAncestor(events, event.parents)
272
+ }
273
+
274
+ /**
275
+ * @template {{ toString: () => string }} T
276
+ * @param {Array<T[]>} arrays
277
+ */
278
+ function findCommonString (arrays) {
279
+ arrays = arrays.map((a) => [...a])
280
+ for (const arr of arrays) {
281
+ for (const item of arr) {
282
+ let matched = true
283
+ for (const other of arrays) {
284
+ if (arr === other) continue
285
+ matched = other.some((i) => String(i) === String(item))
286
+ if (!matched) break
287
+ }
288
+ if (matched) return item
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Find and sort events between the head(s) and the tail.
295
+ * @param {import('./clock').EventFetcher} events
296
+ * @param {import('./clock').EventLink<EventData>[]} head
297
+ * @param {import('./clock').EventLink<EventData>} tail
298
+ */
299
+ async function findSortedEvents (events, head, tail) {
300
+ // get weighted events - heavier events happened first
301
+ /** @type {Map<string, { event: import('./clock').EventBlockView<EventData>, weight: number }>} */
302
+ const weights = new Map()
303
+ const all = await Promise.all(head.map((h) => findEvents(events, h, tail)))
304
+ for (const arr of all) {
305
+ for (const { event, depth } of arr) {
306
+ const info = weights.get(event.cid.toString())
307
+ if (info) {
308
+ info.weight += depth
309
+ } else {
310
+ weights.set(event.cid.toString(), { event, weight: depth })
311
+ }
312
+ }
313
+ }
314
+
315
+ // group events into buckets by weight
316
+ /** @type {Map<number, import('./clock').EventBlockView<EventData>[]>} */
317
+ const buckets = new Map()
318
+ for (const { event, weight } of weights.values()) {
319
+ const bucket = buckets.get(weight)
320
+ if (bucket) {
321
+ bucket.push(event)
322
+ } else {
323
+ buckets.set(weight, [event])
324
+ }
325
+ }
326
+
327
+ // sort by weight, and by CID within weight
328
+ const sorted = Array.from(buckets)
329
+ .sort((a, b) => b[0] - a[0])
330
+ .flatMap(([, es]) =>
331
+ es.sort((a, b) => (String(a.cid) < String(b.cid) ? -1 : 1))
332
+ )
333
+ // console.log('sorted', sorted.map(s => s.value.data.value))
334
+ return sorted
335
+ }
336
+
337
+ /**
338
+ * @param {import('./clock').EventFetcher} events
339
+ * @param {import('./clock').EventLink<EventData>} start
340
+ * @param {import('./clock').EventLink<EventData>} end
341
+ * @returns {Promise<Array<{ event: import('./clock').EventBlockView<EventData>, depth: number }>>}
342
+ */
343
+ async function findEvents (events, start, end, depth = 0) {
344
+ const event = await events.get(start)
345
+ const acc = [{ event, depth }]
346
+ const { parents } = event.value
347
+ if (parents.length === 1 && String(parents[0]) === String(end)) return acc
348
+ const rest = await Promise.all(
349
+ parents.map((p) => findEvents(events, p, end, depth + 1))
350
+ )
351
+ return acc.concat(...rest)
352
+ }
@@ -0,0 +1,196 @@
1
+ import { create, load } from 'prolly-trees/db-index'
2
+ import { sha256 as hasher } from 'multiformats/hashes/sha2'
3
+ import { nocache as cache } from 'prolly-trees/cache'
4
+ import { bf, simpleCompare as compare } from 'prolly-trees/utils'
5
+ import * as codec from '@ipld/dag-cbor'
6
+ import { create as createBlock } from 'multiformats/block'
7
+ import { doTransaction } from './blockstore.js'
8
+ import charwise from 'charwise'
9
+ const opts = { cache, chunker: bf(3), codec, hasher, compare }
10
+
11
+ const ALWAYS_REBUILD = true // todo: remove this
12
+
13
+ const makeGetBlock = (blocks) => async (address) => {
14
+ const { cid, bytes } = await blocks.get(address)
15
+ return createBlock({ cid, bytes, hasher, codec })
16
+ }
17
+ const makeDoc = ({ key, value }) => ({ _id: key, ...value })
18
+
19
+ console.x = function () {}
20
+
21
+ /**
22
+ * Transforms a set of changes to index entries using a map function.
23
+ *
24
+ * @param {Array<{ key: string, value: import('./link').AnyLink, del?: boolean }>} changes
25
+ * @param {Function} mapFun
26
+ * @returns {Array<{ key: [string, string], value: any }>} The index entries generated by the map function.
27
+ */
28
+
29
+ const indexEntriesForChanges = (changes, mapFun) => {
30
+ const indexEntries = []
31
+ changes.forEach(({ key, value, del }) => {
32
+ if (del || !value) return
33
+ mapFun(makeDoc({ key, value }), (k, v) => {
34
+ indexEntries.push({
35
+ key: [charwise.encode(k), key],
36
+ value: v
37
+ })
38
+ })
39
+ })
40
+ return indexEntries
41
+ }
42
+
43
+ const indexEntriesForOldChanges = async (blocks, byIDindexRoot, ids, mapFun) => {
44
+ const getBlock = makeGetBlock(blocks)
45
+ const byIDindex = await load({ cid: byIDindexRoot.cid, get: getBlock, ...opts })
46
+ // console.trace('ids', ids)
47
+ const result = await byIDindex.getMany(ids)
48
+ return result.result
49
+ }
50
+
51
+ /**
52
+ * Represents an index for a Fireproof database.
53
+ *
54
+ * @class
55
+ * @classdesc An index can be used to order and filter the documents in a Fireproof database.
56
+ *
57
+ * @param {import('./fireproof').Fireproof} database - The Fireproof database instance to index.
58
+ * @param {Function} mapFun - The map function to apply to each entry in the database.
59
+ *
60
+ */
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
+ */
67
+ constructor (database, mapFun) {
68
+ /**
69
+ * The database instance to index.
70
+ * @type {import('./fireproof').Fireproof}
71
+ */
72
+ this.database = database
73
+ /**
74
+ * The map function to apply to each entry in the database.
75
+ * @type {Function}
76
+ */
77
+ this.mapFun = mapFun
78
+ this.indexRoot = null
79
+ this.byIDindexRoot = null
80
+ this.dbHead = null
81
+ }
82
+
83
+ /**
84
+ * Query object can have {range}
85
+ *
86
+ */
87
+ async query (query, root = null) {
88
+ if (!root) { // pass a root to query a snapshot
89
+ await doTransaction('#updateIndex', this.database.blocks, async (blocks) => {
90
+ await this.#updateIndex(blocks)
91
+ })
92
+ }
93
+ const response = await doIndexQuery(this.database.blocks, root || this.indexRoot, query)
94
+ return {
95
+ // TODO fix this naming upstream in prolly/db-index
96
+ // todo maybe this is a hint about why deletes arent working?
97
+ rows: response.result.map(({ id, key, row }) => ({ id: key, key: charwise.decode(id), value: row }))
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Update the index with the latest changes
103
+ * @private
104
+ * @returns {Promise<void>}
105
+ */
106
+ async #updateIndex (blocks) {
107
+ // todo remove this hack
108
+ if (ALWAYS_REBUILD) {
109
+ this.dbHead = null // hack
110
+ this.indexRoot = null // hack
111
+ }
112
+ const result = await this.database.changesSince(this.dbHead) // {key, value, del}
113
+ if (this.dbHead) {
114
+ const oldIndexEntries = (await indexEntriesForOldChanges(blocks, this.byIDindexRoot, result.rows.map(({ key }) => key), this.mapFun))
115
+ // .map((key) => ({ key, value: null })) // tombstone just adds more rows...
116
+ .map((key) => ({ key, del: true })) // should be this
117
+ // .map((key) => ({ key: undefined, del: true })) // todo why does this work?
118
+
119
+ this.indexRoot = await bulkIndex(blocks, this.indexRoot, oldIndexEntries, opts)
120
+ // console.x('oldIndexEntries', oldIndexEntries)
121
+ // [ { key: ['b', 1], del: true } ]
122
+ // [ { key: [ 5, 'x' ], del: true } ]
123
+ // for now we just let the by id index grow and then don't use the results...
124
+ // const removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
125
+ // this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, removeByIdIndexEntries, opts)
126
+ }
127
+ const indexEntries = indexEntriesForChanges(result.rows, this.mapFun)
128
+ const byIdIndexEntries = indexEntries.map(({ key }) => ({ key: key[1], value: key }))
129
+ // [{key: 'xxxx-3c3a-4b5e-9c1c-8c5c0c5c0c5c', value : [ 53, 'xxxx-3c3a-4b5e-9c1c-8c5c0c5c0c5c' ]}]
130
+ this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, byIdIndexEntries, opts)
131
+ // console.log('indexEntries', indexEntries)
132
+ this.indexRoot = await bulkIndex(blocks, this.indexRoot, indexEntries, opts)
133
+ // console.log('did index', this.indexRoot)
134
+ this.dbHead = result.clock
135
+ }
136
+
137
+ // todo use the index from other peers?
138
+ // we might need to add CRDT logic to it for that
139
+ // it would only be a performance improvement, but might add a lot of complexity
140
+ // advanceIndex ()) {}
141
+ }
142
+
143
+ /**
144
+ * Update the index with the given entries
145
+ * @param {Blockstore} blocks
146
+ * @param {import('multiformats/block').Block} inRoot
147
+ * @param {import('prolly-trees/db-index').IndexEntry[]} indexEntries
148
+ */
149
+ async function bulkIndex (blocks, inRoot, indexEntries) {
150
+ if (!indexEntries.length) return inRoot
151
+ const putBlock = blocks.put.bind(blocks)
152
+ const getBlock = makeGetBlock(blocks)
153
+ if (!inRoot) {
154
+ // make a new index
155
+
156
+ for await (const node of await create({ get: getBlock, list: indexEntries, ...opts })) {
157
+ const block = await node.block
158
+ await putBlock(block.cid, block.bytes)
159
+ inRoot = block
160
+ }
161
+ // console.x('created index', inRoot.cid)
162
+ return inRoot
163
+ } else {
164
+ // load existing index
165
+ // console.x('loading index', inRoot.cid)
166
+ const index = await load({ cid: inRoot.cid, get: getBlock, ...opts })
167
+ // console.log('new indexEntries', indexEntries)
168
+ const { root, blocks } = await index.bulk(indexEntries)
169
+ for await (const block of blocks) {
170
+ await putBlock(block.cid, block.bytes)
171
+ }
172
+ // console.x('updated index', root.block.cid)
173
+ return await root.block // if we hold the root we won't have to load every time
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Query the index for the given range
179
+ * @param {Blockstore} blocks
180
+ * @param {import('multiformats/block').Block} inRoot
181
+ * @param {import('prolly-trees/db-index').Query} query
182
+ * @returns {Promise<import('prolly-trees/db-index').QueryResult>}
183
+ **/
184
+ async function doIndexQuery (blocks, root, query) {
185
+ const cid = root && root.cid
186
+ if (!cid) return { result: [] }
187
+ const getBlock = makeGetBlock(blocks)
188
+ const index = await load({ cid, get: getBlock, ...opts })
189
+ if (query.range) {
190
+ const encodedRange = query.range.map((key) => charwise.encode(key))
191
+ return index.range(...encodedRange)
192
+ } else if (query.key) {
193
+ const encodedKey = charwise.encode(query.key)
194
+ return index.get(encodedKey)
195
+ }
196
+ }