@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/LICENSE.md +232 -0
- package/index.d.ts +2 -0
- package/index.js +3 -5
- package/index.js.map +1 -0
- package/index.ts +2 -0
- package/package.json +26 -49
- package/tsconfig.json +18 -0
- package/scripts/propernames/gen.sh +0 -3
- package/scripts/randomcid.js +0 -12
- package/scripts/words/gen.js +0 -55
- package/src/block.js +0 -75
- package/src/blockstore.js +0 -246
- package/src/clock.js +0 -352
- package/src/db-index.js +0 -196
- package/src/fireproof.js +0 -209
- package/src/link.d.ts +0 -3
- package/src/listener.js +0 -111
- package/src/prolly.js +0 -235
- package/src/valet.js +0 -142
- package/test/clock.test.js +0 -725
- package/test/db-index.test.js +0 -214
- package/test/fireproof.test.js +0 -287
- package/test/helpers.js +0 -45
- package/test/listener.test.js +0 -102
- package/test/prolly.test.js +0 -203
package/src/db-index.js
DELETED
@@ -1,196 +0,0 @@
|
|
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
|
-
}
|
package/src/fireproof.js
DELETED
@@ -1,209 +0,0 @@
|
|
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
DELETED
package/src/listener.js
DELETED
@@ -1,111 +0,0 @@
|
|
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
|
-
}
|