@fireproof/core 0.0.1 → 0.0.2-dev-test

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 DELETED
@@ -1,246 +0,0 @@
1
- import { parse } from 'multiformats/link'
2
- import * as raw from 'multiformats/codecs/raw'
3
- import { sha256 } from 'multiformats/hashes/sha2'
4
- import * as Block from 'multiformats/block'
5
- import * as CBW from '@ipld/car/buffer-writer'
6
-
7
- import Valet from './valet.js'
8
-
9
- // const sleep = ms => new Promise(r => setTimeout(r, ms))
10
-
11
- /**
12
- * @typedef {Object} AnyBlock
13
- * @property {import('./link').AnyLink} cid - The CID of the block
14
- * @property {Uint8Array} bytes - The block's data
15
- *
16
- * @typedef {Object} Blockstore
17
- * @property {function(import('./link').AnyLink): Promise<AnyBlock|undefined>} get - A function to retrieve a block by CID
18
- * @property {function(import('./link').AnyLink, Uint8Array): Promise<void>} put - A function to store a block's data and CID
19
- *
20
- * A blockstore that caches writes to a transaction and only persists them when committed.
21
- * @implements {Blockstore}
22
- */
23
- export default class TransactionBlockstore {
24
- /** @type {Map<string, Uint8Array>} */
25
- #oldBlocks = new Map()
26
-
27
- #valet = new Valet() // cars by cid
28
-
29
- #instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4)
30
- #inflightTransactions = new Set()
31
-
32
- /**
33
- * Get a block from the store.
34
- *
35
- * @param {import('./link').AnyLink} cid
36
- * @returns {Promise<AnyBlock | undefined>}
37
- */
38
- async get (cid) {
39
- const key = cid.toString()
40
- // 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)
45
- if (!bytes) throw new Error('Missing block: ' + key)
46
- return { cid, bytes }
47
- }
48
-
49
- // this iterates over the in-flight transactions
50
- // and returns the first matching block it finds
51
- async #transactionsGet (key) {
52
- for (const transaction of this.#inflightTransactions) {
53
- const got = await transaction.get(key)
54
- if (got && got.bytes) return got.bytes
55
- }
56
- }
57
-
58
- 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
61
- }
62
-
63
- /**
64
- * Add a block to the store. Usually bound to a transaction by a closure.
65
- * It sets the lastCid property to the CID of the block that was put.
66
- * This is used by the transaction as the head of the car when written to the valet.
67
- * We don't have to worry about which transaction we are when we are here because
68
- * we are the transactionBlockstore.
69
- *
70
- * @param {import('./link').AnyLink} cid
71
- * @param {Uint8Array} bytes
72
- */
73
- put (cid, bytes) {
74
- throw new Error('use a transaction to put')
75
- }
76
-
77
- /**
78
- * Iterate over all blocks in the store.
79
- *
80
- * @yields {AnyBlock}
81
- * @returns {AsyncGenerator<AnyBlock>}
82
- */
83
- * entries () {
84
- // todo needs transaction blocks?
85
- // for (const [str, bytes] of this.#blocks) {
86
- // yield { cid: parse(str), bytes }
87
- // }
88
- for (const [str, bytes] of this.#oldBlocks) {
89
- yield { cid: parse(str), bytes }
90
- }
91
- }
92
-
93
- /**
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
- */
99
- begin (label = '') {
100
- const innerTransactionBlockstore = new InnerBlockstore(label, this)
101
- this.#inflightTransactions.add(innerTransactionBlockstore)
102
- return innerTransactionBlockstore
103
- }
104
-
105
- /**
106
- * Commit the transaction. Writes the blocks to the store.
107
- * @returns {Promise<void>}
108
- * @memberof TransactionBlockstore
109
- */
110
- async commit (innerBlockstore) {
111
- await this.#doCommit(innerBlockstore)
112
- }
113
-
114
- // first get the transaction blockstore from the map of transaction blockstores
115
- // then copy it to oldBlocks
116
- // then write the transaction blockstore to a car
117
- // then write the car to the valet
118
- // then remove the transaction blockstore from the map of transaction blockstores
119
- #doCommit = async (innerBlockstore) => {
120
- const cids = new Set()
121
- for (const { cid, bytes } of innerBlockstore.entries()) {
122
- const stringCid = cid.toString() // unnecessary string conversion, can we fix upstream?
123
- if (this.#oldBlocks.has(stringCid)) {
124
- // console.log('Duplicate block: ' + stringCid)
125
- } else {
126
- this.#oldBlocks.set(stringCid, bytes)
127
- cids.add(stringCid)
128
- }
129
- }
130
- if (cids.size > 0) {
131
- console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
132
- await this.#valetWriteTransaction(innerBlockstore, cids)
133
- }
134
- }
135
-
136
- /**
137
- * Group the blocks into a car and write it to the valet.
138
- * @param {InnerBlockstore} innerBlockstore
139
- * @param {Set<string>} cids
140
- * @returns {Promise<void>}
141
- * @memberof TransactionBlockstore
142
- * @private
143
- */
144
- #valetWriteTransaction = async (innerBlockstore, cids) => {
145
- if (innerBlockstore.lastCid) {
146
- const newCar = await blocksToCarBlock(innerBlockstore.lastCid, innerBlockstore)
147
- await this.#valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
148
- }
149
- }
150
-
151
- /**
152
- * Retire the transaction. Clears the uncommited blocks.
153
- * @returns {void}
154
- * @memberof TransactionBlockstore
155
- */
156
- retire (innerBlockstore) {
157
- this.#inflightTransactions.delete(innerBlockstore)
158
- }
159
- }
160
-
161
- /**
162
- * Runs a function on an inner blockstore, then persists the change to a car writer
163
- * or other outer blockstore.
164
- * @param {string} label
165
- * @param {TransactionBlockstore} blockstore
166
- * @param {(innerBlockstore: Blockstore) => Promise<any>} doFun
167
- * @returns {Promise<any>}
168
- * @memberof TransactionBlockstore
169
- */
170
- export const doTransaction = async (label, blockstore, doFun) => {
171
- if (!blockstore.commit) return await doFun(blockstore)
172
- const innerBlockstore = blockstore.begin(label)
173
- try {
174
- const result = await doFun(innerBlockstore)
175
- await blockstore.commit(innerBlockstore)
176
- return result
177
- } catch (e) {
178
- console.error(`Transaction ${label} failed`, e, e.stack)
179
- throw e
180
- } finally {
181
- blockstore.retire(innerBlockstore)
182
- }
183
- }
184
-
185
- const blocksToCarBlock = async (lastCid, blocks) => {
186
- let size = 0
187
- const headerSize = CBW.headerLength({ roots: [lastCid] })
188
- size += headerSize
189
- for (const { cid, bytes } of blocks.entries()) {
190
- size += CBW.blockLength({ cid, bytes })
191
- }
192
- const buffer = new Uint8Array(size)
193
- const writer = await CBW.createWriter(buffer, { headerSize })
194
-
195
- writer.addRoot(lastCid)
196
-
197
- for (const { cid, bytes } of blocks.entries()) {
198
- writer.write({ cid, bytes })
199
- }
200
- await writer.close()
201
- return await Block.encode({ value: writer.bytes, hasher: sha256, codec: raw })
202
- }
203
-
204
- /** @implements {BlockFetcher} */
205
- export class InnerBlockstore {
206
- /** @type {Map<string, Uint8Array>} */
207
- #blocks = new Map()
208
- lastCid = null
209
- label = ''
210
- parentBlockstore = null
211
-
212
- constructor (label, parentBlockstore) {
213
- this.label = label
214
- this.parentBlockstore = parentBlockstore
215
- }
216
-
217
- /**
218
- * @param {import('./link').AnyLink} cid
219
- * @returns {Promise<AnyBlock | undefined>}
220
- */
221
- async get (cid) {
222
- const key = cid.toString()
223
- let bytes = this.#blocks.get(key)
224
- if (bytes) { return { cid, bytes } }
225
- bytes = await this.parentBlockstore.commitedGet(key)
226
- if (bytes) {
227
- return { cid, bytes }
228
- }
229
- }
230
-
231
- /**
232
- * @param {import('./link').AnyLink} cid
233
- * @param {Uint8Array} bytes
234
- */
235
- put (cid, bytes) {
236
- // console.log('put', cid)
237
- this.#blocks.set(cid.toString(), bytes)
238
- this.lastCid = cid
239
- }
240
-
241
- * entries () {
242
- for (const [str, bytes] of this.#blocks) {
243
- yield { cid: parse(str), bytes }
244
- }
245
- }
246
- }
package/src/clock.js DELETED
@@ -1,352 +0,0 @@
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
- }