@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.
@@ -0,0 +1,209 @@
1
+ // import { vis } from './clock.js'
2
+ import { put, get, getAll, eventsSince } from './prolly.js'
3
+ import Blockstore, { doTransaction } from './blockstore.js'
4
+
5
+ // const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
6
+
7
+ /**
8
+ * Represents a Fireproof instance that wraps a ProllyDB instance and Merkle clock head.
9
+ *
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.
15
+ * @param {object} [config] - Optional configuration options for the Fireproof instance.
16
+ * @param {object} [authCtx] - Optional authorization context object to use for any authentication checks.
17
+ *
18
+ */
19
+
20
+ export default class Fireproof {
21
+ /**
22
+ * @param {Blockstore} blocks
23
+ * @param {import('../clock').EventLink<import('../crdt').EventData>[]} clock
24
+ */
25
+ #listeners = new Set()
26
+
27
+ constructor (blocks, clock, config = {}, authCtx = {}) {
28
+ this.blocks = blocks
29
+ this.clock = clock
30
+ this.config = config
31
+ this.authCtx = authCtx
32
+ this.instanceId = 'db.' + Math.random().toString(36).substring(2, 7)
33
+ }
34
+
35
+ /**
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.
40
+ * @returns {Fireproof}
41
+ * A new Fireproof instance representing the snapshot.
42
+ */
43
+ snapshot (clock) {
44
+ // how to handle listeners, views, and config?
45
+ // todo needs a test for that
46
+ return new Fireproof(this.blocks, clock || this.clock)
47
+ }
48
+
49
+ /**
50
+ * This triggers a notification to all listeners of the Fireproof instance.
51
+ */
52
+ async setClock (clock) {
53
+ // console.log('setClock', this.instanceId, clock)
54
+ this.clock = clock.map((item) => item['/'] ? item['/'] : item)
55
+ await this.#notifyListeners({ reset: true, clock })
56
+ }
57
+
58
+ toJSON () {
59
+ // todo this also needs to return the index roots...
60
+ return { clock: this.clock }
61
+ }
62
+
63
+ /**
64
+ * 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.
72
+ */
73
+ async changesSince (event) {
74
+ // console.log('changesSince', this.instanceId, event, this.clock)
75
+ let rows
76
+ if (event) {
77
+ const resp = await eventsSince(this.blocks, this.clock, event)
78
+ const docsMap = new Map()
79
+ for (const { key, type, value } of resp) {
80
+ if (type === 'del') {
81
+ docsMap.set(key, { key, del: true })
82
+ } else {
83
+ docsMap.set(key, { key, value })
84
+ }
85
+ }
86
+ rows = Array.from(docsMap.values())
87
+ // console.log('change rows', this.instanceId, rows)
88
+ } else {
89
+ rows = (await getAll(this.blocks, this.clock)).map(({ key, value }) => ({ key, value }))
90
+ // console.log('dbdoc rows', this.instanceId, rows)
91
+ }
92
+ return { rows, clock: this.clock }
93
+ }
94
+
95
+ /**
96
+ * Registers a Listener to be called when the Fireproof instance's clock is updated.
97
+ * Recieves live changes from the database after they are committed.
98
+ * @param {Function} listener - The listener to be called when the clock is updated.
99
+ * @returns {Function} - A function that can be called to unregister the listener.
100
+ */
101
+ registerListener (listener) {
102
+ this.#listeners.add(listener)
103
+ return () => {
104
+ this.#listeners.delete(listener)
105
+ }
106
+ }
107
+
108
+ async #notifyListeners (changes) {
109
+ // await sleep(0)
110
+ this.#listeners.forEach((listener) => listener(changes))
111
+ }
112
+
113
+ /**
114
+ * Runs validation on the specified document using the Fireproof instance's configuration.
115
+ *
116
+ * @param {Object} doc - The document to validate.
117
+ */
118
+ async runValidation (doc) {
119
+ if (this.config && this.config.validateChange) {
120
+ const oldDoc = await this.get(doc._id)
121
+ .then((doc) => doc)
122
+ .catch(() => ({}))
123
+ this.config.validateChange(doc, oldDoc, this.authCtx)
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Adds a new document to the database, or updates an existing document.
129
+ *
130
+ * @param {Object} doc - the document to be added
131
+ * @param {string} doc._id - the document ID. If not provided, a random ID will be generated.
132
+ * @param {Object} doc.* - the document data to be added
133
+ * @returns {Promise<import('./prolly').PutResult>} - the result of adding the document
134
+ */
135
+ async put ({ _id, ...doc }) {
136
+ 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 })
139
+ }
140
+
141
+ /**
142
+ * Deletes a document from the database
143
+ * @param {string} id - the document ID
144
+ * @returns {Promise<import('./prolly').PutResult>} - the result of deleting the document
145
+ */
146
+ 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?
149
+ // this tombstone is temporary until we can get the prolly tree to delete
150
+ return await this.putToProllyTree({ key: id, value: null })
151
+ }
152
+
153
+ async putToProllyTree (event) {
154
+ const result = await doTransaction('putToProllyTree', this.blocks, async (blocks) => await put(blocks, this.clock, event))
155
+ if (!result) {
156
+ console.log('failed', event)
157
+ }
158
+ this.clock = result.head // do we want to do this as a finally block
159
+ result.id = event.key
160
+ await this.#notifyListeners([event])
161
+ return { id: result.id, clock: this.clock }
162
+ }
163
+
164
+ // /**
165
+ // * Advances the clock to the specified event and updates the root CID
166
+ // * Will be used by replication
167
+ // * @param {import('../clock').EventLink<import('../crdt').EventData>} event - the event to advance to
168
+ // * @returns {import('../clock').EventLink<import('../crdt').EventData>[]} - the new clock after advancing
169
+ // */
170
+ // async advance (event) {
171
+ // this.clock = await advance(this.blocks, this.clock, event)
172
+ // this.rootCid = await root(this.blocks, this.clock)
173
+ // return this.clock
174
+ // }
175
+
176
+ /**
177
+ * Displays a visualization of the current clock in the console
178
+ */
179
+ // async visClock () {
180
+ // const shortLink = (l) => `${String(l).slice(0, 4)}..${String(l).slice(-4)}`
181
+ // const renderNodeLabel = (event) => {
182
+ // return event.value.data.type === 'put'
183
+ // ? `${shortLink(event.cid)}\\nput(${shortLink(event.value.data.key)},
184
+ // {${Object.values(event.value.data.value)}})`
185
+ // : `${shortLink(event.cid)}\\ndel(${event.value.data.key})`
186
+ // }
187
+ // for await (const line of vis(this.blocks, this.clock, { renderNodeLabel })) console.log(line)
188
+ // }
189
+
190
+ /**
191
+ * Retrieves the document with the specified ID from the database
192
+ *
193
+ * @param {string} key - the ID of the document to retrieve
194
+ * @returns {Promise<import('./prolly').GetResult>} - the document with the specified ID
195
+ */
196
+ async get (key) {
197
+ const got = await get(this.blocks, this.clock, key)
198
+ // this tombstone is temporary until we can get the prolly tree to delete
199
+ if (got === null) {
200
+ throw new Error('Not found')
201
+ }
202
+ got._id = key
203
+ return got
204
+ }
205
+ }
206
+
207
+ Fireproof.storage = (_email) => {
208
+ return new Fireproof(new Blockstore(), [])
209
+ }
package/src/link.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Link } from 'multiformats'
2
+
3
+ export type AnyLink = Link<unknown, number, number, 1|0>
@@ -0,0 +1,111 @@
1
+ /**
2
+ * A Fireproof database Listener allows you to react to events in the database.
3
+ *
4
+ * @class
5
+ * @classdesc An listener can be notified of events as they happen or on reconection
6
+ *
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
+ *
10
+ */
11
+ export default class Listener {
12
+ #subcribers = new Map()
13
+
14
+ // todo code review if there is a better way that doesn't create a circular reference
15
+ // because otherwise we need to document that the user must call stopListening
16
+ // or else the listener will never be garbage collected
17
+ // maybe we can use WeakRef on the db side
18
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef
19
+ #doStopListening = null
20
+
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
28
+ * The database instance to index.
29
+ * @type {import('./fireproof').Fireproof}
30
+ */
31
+ this.database = database
32
+ this.#doStopListening = database.registerListener((changes) => this.#onChanges(changes))
33
+ /**
34
+ * The map function to apply to each entry in the database.
35
+ * @type {Function}
36
+ */
37
+ this.eventFun = eventFun || function (_, emit) { emit('*') }
38
+ this.dbHead = null
39
+ }
40
+
41
+ /**
42
+ * Subscribe to a topic emitted by the event function.
43
+ * @param {string} topic - The topic to subscribe to.
44
+ * @param {Function} subscriber - The function to call when the topic is emitted.
45
+ * @returns {Function} A function to unsubscribe from the topic.
46
+ */
47
+ on (topic, subscriber, since) {
48
+ const listOfTopicSubscribers = getTopicList(this.#subcribers, topic)
49
+ listOfTopicSubscribers.push(subscriber)
50
+ if (typeof since !== 'undefined') {
51
+ this.database.changesSince(since).then(({ rows: changes }) => {
52
+ const keys = topicsForChanges(changes, this.eventFun).get(topic)
53
+ if (keys) keys.forEach((key) => subscriber(key))
54
+ })
55
+ }
56
+ return () => {
57
+ const index = listOfTopicSubscribers.indexOf(subscriber)
58
+ if (index > -1) listOfTopicSubscribers.splice(index, 1)
59
+ }
60
+ }
61
+
62
+ #onChanges (changes) {
63
+ if (Array.isArray(changes)) {
64
+ const seenTopics = topicsForChanges(changes, this.eventFun)
65
+ for (const [topic, keys] of seenTopics) {
66
+ const listOfTopicSubscribers = getTopicList(this.#subcribers, topic)
67
+ listOfTopicSubscribers.forEach((subscriber) => keys.forEach((key) => subscriber(key)))
68
+ }
69
+ } else {
70
+ // reset event
71
+ if (changes.reset) {
72
+ for (const [, listOfTopicSubscribers] of this.#subcribers) {
73
+ listOfTopicSubscribers.forEach((subscriber) => subscriber(changes))
74
+ }
75
+ }
76
+ }
77
+ // if changes is special, notify all listeners?
78
+ // first make the example app use listeners
79
+ }
80
+ }
81
+
82
+ function getTopicList (subscribersMap, name) {
83
+ let topicList = subscribersMap.get(name)
84
+ if (!topicList) {
85
+ topicList = []
86
+ subscribersMap.set(name, topicList)
87
+ }
88
+ return topicList
89
+ }
90
+
91
+ // copied from src/db-index.js
92
+ const makeDoc = ({ key, value }) => ({ _id: key, ...value })
93
+
94
+ /**
95
+ * Transforms a set of changes to events using an emitter function.
96
+ *
97
+ * @param {Array<{ key: string, value: import('./link').AnyLink, del?: boolean }>} changes
98
+ * @param {Function} eventFun
99
+ * @returns {Array<string>} The topics emmitted by the event function.
100
+ */
101
+ const topicsForChanges = (changes, eventFun) => {
102
+ const seenTopics = new Map()
103
+ changes.forEach(({ key, value, del }) => {
104
+ if (del || !value) value = { _deleted: true }
105
+ eventFun(makeDoc({ key, value }), (t) => {
106
+ const topicList = getTopicList(seenTopics, t)
107
+ topicList.push(key)
108
+ })
109
+ })
110
+ return seenTopics
111
+ }
package/src/prolly.js ADDED
@@ -0,0 +1,235 @@
1
+ import { advance, EventFetcher, EventBlock, findCommonAncestorWithSortedEvents, findUnknownSortedEvents } from './clock.js'
2
+ import { create, load } from 'prolly-trees/map'
3
+ import * as codec from '@ipld/dag-cbor'
4
+ import { sha256 as hasher } from 'multiformats/hashes/sha2'
5
+ import { MemoryBlockstore, MultiBlockFetcher } from './block.js'
6
+ import { doTransaction } from './blockstore.js'
7
+
8
+ import { nocache as cache } from 'prolly-trees/cache'
9
+ import { bf, simpleCompare as compare } from 'prolly-trees/utils'
10
+ import { create as createBlock } from 'multiformats/block'
11
+ const opts = { cache, chunker: bf(3), codec, hasher, compare }
12
+
13
+ const withLog = async (label, fn) => {
14
+ const resp = await fn()
15
+ // console.log('withLog', label, !!resp)
16
+ return resp
17
+ }
18
+
19
+ const makeGetBlock = (blocks) => async (address) => {
20
+ const { cid, bytes } = await withLog(address, () => blocks.get(address))
21
+ return createBlock({ cid, bytes, hasher, codec })
22
+ }
23
+
24
+ async function createAndSaveNewEvent (inBlocks, mblocks, getBlock, bigPut, root, { key, value, del }, head, additions, removals = []) {
25
+ const data = {
26
+ type: 'put',
27
+ root: {
28
+ cid: root.cid,
29
+ bytes: root.bytes,
30
+ value: root.value
31
+ },
32
+ key
33
+ }
34
+
35
+ if (del) {
36
+ data.value = null
37
+ data.type = 'del'
38
+ } else {
39
+ data.value = value
40
+ }
41
+ /** @type {EventData} */
42
+
43
+ const event = await EventBlock.create(data, head)
44
+ bigPut(event)
45
+ head = await advance(inBlocks, head, event.cid)
46
+
47
+ return {
48
+ root,
49
+ additions,
50
+ removals,
51
+ head,
52
+ event
53
+ }
54
+ }
55
+
56
+ const makeGetAndPutBlock = (inBlocks) => {
57
+ const mblocks = new MemoryBlockstore()
58
+ const blocks = new MultiBlockFetcher(mblocks, inBlocks)
59
+ const getBlock = makeGetBlock(blocks)
60
+ const put = inBlocks.put.bind(inBlocks)
61
+ const bigPut = async (block, additions) => {
62
+ // console.log('bigPut', block.cid.toString())
63
+ const { cid, bytes } = block
64
+ put(cid, bytes)
65
+ mblocks.putSync(cid, bytes)
66
+ if (additions) {
67
+ additions.set(cid.toString(), block)
68
+ }
69
+ }
70
+ return { getBlock, bigPut, mblocks, blocks }
71
+ }
72
+
73
+ const bulkFromEvents = (sorted) =>
74
+ sorted.map(({ value: event }) => {
75
+ const {
76
+ data: { type, value, key }
77
+ } = event
78
+ return type === 'put' ? { key, value } : { key, del: true }
79
+ })
80
+
81
+ // Get the value of the root from the ancestor event
82
+ /**
83
+ *
84
+ * @param {EventFetcher} events
85
+ * @param {Link} ancestor
86
+ * @param {*} getBlock
87
+ * @returns
88
+ */
89
+ const prollyRootFromAncestor = async (events, ancestor, getBlock) => {
90
+ // console.log('prollyRootFromAncestor', ancestor)
91
+ const event = await events.get(ancestor)
92
+ const { root } = event.value.data
93
+ // console.log('prollyRootFromAncestor', root.cid, JSON.stringify(root.value))
94
+ return load({ cid: root.cid, get: getBlock, ...opts })
95
+ }
96
+
97
+ /**
98
+ * Put a value (a CID) for the given key. If the key exists it's value is overwritten.
99
+ *
100
+ * @param {import('./block').BlockFetcher} blocks Bucket block storage.
101
+ * @param {import('./clock').EventLink<EventData>[]} head Merkle clock head.
102
+ * @param {string} key The key of the value to put.
103
+ * @param {import('./link').AnyLink} value The value to put.
104
+ * @param {object} [options]
105
+ * @returns {Promise<Result>}
106
+ */
107
+ export async function put (inBlocks, head, event, options) {
108
+ const { getBlock, bigPut, mblocks, blocks } = makeGetAndPutBlock(inBlocks)
109
+
110
+ // If the head is empty, we create a new event and return the root and addition blocks
111
+ if (!head.length) {
112
+ const additions = new Map()
113
+ let root
114
+ for await (const node of create({ get: getBlock, list: [event], ...opts })) {
115
+ root = await node.block
116
+ bigPut(root, additions)
117
+ }
118
+ return createAndSaveNewEvent(inBlocks, mblocks, getBlock, bigPut, root, event, head, Array.from(additions.values()))
119
+ }
120
+
121
+ // Otherwise, we find the common ancestor and update the root and other blocks
122
+ const events = new EventFetcher(blocks)
123
+ const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
124
+ const prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
125
+
126
+ const bulkOperations = bulkFromEvents(sorted)
127
+ const { root: newProllyRootNode, blocks: newBlocks } = await prollyRootNode.bulk([...bulkOperations, event]) // ading delete support here
128
+ const prollyRootBlock = await newProllyRootNode.block
129
+ const additions = new Map() // ; const removals = new Map()
130
+ bigPut(prollyRootBlock, additions)
131
+ for (const nb of newBlocks) {
132
+ bigPut(nb, additions)
133
+ }
134
+
135
+ return createAndSaveNewEvent(inBlocks, mblocks, getBlock, bigPut,
136
+ prollyRootBlock, event, head, Array.from(additions.values()) /*, Array.from(removals.values()) */)
137
+ }
138
+
139
+ /**
140
+ * Determine the effective prolly root given the current merkle clock head.
141
+ *
142
+ * @param {import('./block').BlockFetcher} blocks Bucket block storage.
143
+ * @param {import('./clock').EventLink<EventData>[]} head Merkle clock head.
144
+ */
145
+ export async function root (inBlocks, head) {
146
+ if (!head.length) {
147
+ throw new Error('no head')
148
+ }
149
+ const { getBlock, blocks } = makeGetAndPutBlock(inBlocks)
150
+ const events = new EventFetcher(blocks)
151
+ const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
152
+ const prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
153
+
154
+ // Perform bulk operations (put or delete) for each event in the sorted array
155
+ const bulkOperations = bulkFromEvents(sorted)
156
+ const { root: newProllyRootNode, blocks: newBlocks } = await prollyRootNode.bulk(bulkOperations)
157
+ const prollyRootBlock = await newProllyRootNode.block
158
+ // console.log('emphemeral blocks', newBlocks.map((nb) => nb.cid.toString()))
159
+ // todo maybe these should go to a temp blockstore?
160
+ await doTransaction('root', inBlocks, async (transactionBlockstore) => {
161
+ const { bigPut } = makeGetAndPutBlock(transactionBlockstore)
162
+ for (const nb of newBlocks) {
163
+ bigPut(nb)
164
+ }
165
+ bigPut(prollyRootBlock)
166
+ })
167
+
168
+ return newProllyRootNode // .block).cid // todo return live object not cid
169
+ }
170
+
171
+ /**
172
+ * Get the list of events not known by the `since` event
173
+ * @param {import('./block').BlockFetcher} blocks Bucket block storage.
174
+ * @param {import('./clock').EventLink<EventData>[]} head Merkle clock head.
175
+ * @param {import('./clock').EventLink<EventData>} since Event to compare against.
176
+ * @returns {Promise<import('./clock').EventLink<EventData>[]>}
177
+ */
178
+ export async function eventsSince (blocks, head, since) {
179
+ if (!head.length) {
180
+ throw new Error('no head')
181
+ }
182
+ const sinceHead = [...since, ...head]
183
+ const unknownSorted3 = await findUnknownSortedEvents(blocks, sinceHead,
184
+ await findCommonAncestorWithSortedEvents(blocks, sinceHead))
185
+ return unknownSorted3.map(({ value: { data } }) => data)
186
+ }
187
+
188
+ /**
189
+ *
190
+ * @param {import('./block').BlockFetcher} blocks Bucket block storage.
191
+ * @param {import('./clock').EventLink<EventData>[]} head Merkle clock head.
192
+ *
193
+ * @returns {Promise<import('./prolly').Entry[]>}
194
+ *
195
+ */
196
+ export async function getAll (blocks, head) {
197
+ // todo use the root node left around from put, etc
198
+ // move load to a central place
199
+ if (!head.length) {
200
+ return []
201
+ }
202
+ const prollyRootNode = await root(blocks, head)
203
+ const { result } = await prollyRootNode.getAllEntries()
204
+ return result.map(({ key, value }) => ({ key, value }))
205
+ }
206
+
207
+ /**
208
+ * @param {import('./block').BlockFetcher} blocks Bucket block storage.
209
+ * @param {import('./clock').EventLink<EventData>[]} head Merkle clock head.
210
+ * @param {string} key The key of the value to retrieve.
211
+ */
212
+ export async function get (blocks, head, key) {
213
+ // instead pass root from db? and always update on change
214
+ if (!head.length) {
215
+ return null
216
+ }
217
+ const prollyRootNode = await root(blocks, head)
218
+ const { result } = await prollyRootNode.get(key)
219
+ return result
220
+ }
221
+
222
+ /**
223
+ * @typedef {{
224
+ * type: 'put'|'del'
225
+ * key: string
226
+ * value: import('./link').AnyLink
227
+ * root: import('./shard').ShardLink
228
+ * }} EventData
229
+ *
230
+ * @typedef {{
231
+ * root: import('./shard').ShardLink
232
+ * head: import('./clock').EventLink<EventData>[]
233
+ * event: import('./clock').EventBlockView<EventData>
234
+ * } & import('./db-index').ShardDiff} Result
235
+ */
package/src/valet.js ADDED
@@ -0,0 +1,142 @@
1
+ import { CarReader } from '@ipld/car'
2
+ import { CID } from 'multiformats/cid'
3
+ import { openDB } from 'idb'
4
+
5
+ // const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
6
+ let storageSupported = false
7
+ try {
8
+ storageSupported = (window.localStorage && true)
9
+ } catch (e) { }
10
+
11
+ export default class Valet {
12
+ #cars = new Map() // cars by cid
13
+ #cidToCar = new Map() // cid to car
14
+
15
+ #db = null
16
+
17
+ withDB = async (dbWorkFun) => {
18
+ if (!storageSupported) return
19
+ if (!this.#db) {
20
+ this.#db = await openDB('valet', 1, {
21
+ upgrade (db) {
22
+ db.createObjectStore('cars') // todo use database name
23
+ const cidToCar = db.createObjectStore('cidToCar', { keyPath: 'car' })
24
+ cidToCar.createIndex('cids', 'cids', { multiEntry: true })
25
+ }
26
+ })
27
+ }
28
+ return await dbWorkFun(this.#db)
29
+ }
30
+
31
+ /**
32
+ *
33
+ * @param {string} carCid
34
+ * @param {*} value
35
+ */
36
+ async parkCar (carCid, value, cids) {
37
+ this.#cars.set(carCid, value)
38
+ for (const cid of cids) {
39
+ this.#cidToCar.set(cid, carCid)
40
+ }
41
+
42
+ await this.withDB(async (db) => {
43
+ const tx = db.transaction(['cars', 'cidToCar'], 'readwrite')
44
+ await tx.objectStore('cars').put(value, carCid)
45
+ await tx.objectStore('cidToCar').put({ car: carCid, cids: Array.from(cids) })
46
+ return await tx.done
47
+ })
48
+ }
49
+
50
+ async getBlock (dataCID) {
51
+ // return await this.#valetGet(dataCID)
52
+ // const MEMcarCid = this.#cidToCar.get(dataCID)
53
+
54
+ // return await this.withDB(async (db) => {
55
+ // const tx = db.transaction(['cars', 'cidToCar'], 'readonly')
56
+ // const carBytes = await tx.objectStore('cars').get(MEMcarCid)
57
+ // const reader = await CarReader.fromBytes(carBytes)
58
+ // const gotBlock = await reader.get(CID.parse(dataCID))
59
+ // if (gotBlock) {
60
+ // return gotBlock.bytes
61
+ // }
62
+ // })
63
+
64
+ return await this.withDB(async (db) => {
65
+ const tx = db.transaction(['cars', 'cidToCar'], 'readonly')
66
+ const indexResp = await tx.objectStore('cidToCar').index('cids').get(dataCID)
67
+ const carCid = indexResp?.car
68
+ if (!carCid) {
69
+ return
70
+ }
71
+ const carBytes = await tx.objectStore('cars').get(carCid)
72
+ const reader = await CarReader.fromBytes(carBytes)
73
+ const gotBlock = await reader.get(CID.parse(dataCID))
74
+ if (gotBlock) {
75
+ return gotBlock.bytes
76
+ }
77
+ })
78
+ }
79
+
80
+ /**
81
+ * Internal function to load blocks from persistent storage.
82
+ * Currently it just searches all the cars for the block, but in the future
83
+ * we need to index the block CIDs to the cars, and reference that to find the block.
84
+ * This index will also allow us to use accelerator links for the gateway when needed.
85
+ * It can itself be a prolly tree...
86
+ * @param {string} cid
87
+ * @returns {Promise<Uint8Array|undefined>}
88
+ */
89
+ #valetGet = async (cid) => {
90
+ const carCid = this.#cidToCar.get(cid)
91
+ if (carCid) {
92
+ const carBytes = this.#cars.get(carCid)
93
+ const reader = await CarReader.fromBytes(carBytes)
94
+ const gotBlock = await reader.get(CID.parse(cid))
95
+ if (gotBlock) {
96
+ return gotBlock.bytes
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ export class MemoryValet {
103
+ #cars = new Map() // cars by cid
104
+ #cidToCar = new Map() // cid to car
105
+
106
+ /**
107
+ *
108
+ * @param {string} carCid
109
+ * @param {*} value
110
+ */
111
+ async parkCar (carCid, value, cids) {
112
+ this.#cars.set(carCid, value)
113
+ for (const cid of cids) {
114
+ this.#cidToCar.set(cid, carCid)
115
+ }
116
+ }
117
+
118
+ async getBlock (dataCID) {
119
+ return await this.#valetGet(dataCID)
120
+ }
121
+
122
+ /**
123
+ * Internal function to load blocks from persistent storage.
124
+ * Currently it just searches all the cars for the block, but in the future
125
+ * we need to index the block CIDs to the cars, and reference that to find the block.
126
+ * This index will also allow us to use accelerator links for the gateway when needed.
127
+ * It can itself be a prolly tree...
128
+ * @param {string} cid
129
+ * @returns {Promise<Uint8Array|undefined>}
130
+ */
131
+ #valetGet = async (cid) => {
132
+ const carCid = this.#cidToCar.get(cid)
133
+ if (carCid) {
134
+ const carBytes = this.#cars.get(carCid)
135
+ const reader = await CarReader.fromBytes(carBytes)
136
+ const gotBlock = await reader.get(CID.parse(cid))
137
+ if (gotBlock) {
138
+ return gotBlock.bytes
139
+ }
140
+ }
141
+ }
142
+ }