@fireproof/core 0.5.6 → 0.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireproof/core",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Cloudless database for apps, the browser, and IPFS",
5
5
  "main": "dist/src/fireproof.js",
6
6
  "module": "dist/src/fireproof.mjs",
@@ -36,6 +36,7 @@
36
36
  "Mikeal Rogers"
37
37
  ],
38
38
  "license": "Apache-2.0 OR MIT",
39
+ "gptdoc": "Fireproof/API/Usage: import { Fireproof, Listener, Index } from '@fireproof/core'; const db = fireproof.storage('app-db-name'); const ok = await db.put({ any: 'json' }); const doc = await db.get(ok.id); await db.del(doc._id); const all = await db.allDocuments(); all.rows.map(({key, value}) => value); const listener = new Listener(db); listener.on('*', updateReactStateFn); const index = new Index(db, (doc, map) => map(doc.any, {custom: Object.keys(doc)})); const result = await index.query({range : ['a', 'z']}); result.rows.map(({ key }) => key);",
39
40
  "dependencies": {
40
41
  "@ipld/car": "^5.1.0",
41
42
  "@ipld/dag-cbor": "^9.0.0",
@@ -53,7 +54,8 @@
53
54
  "prolly-trees": "1.0.4",
54
55
  "randombytes": "^2.1.0",
55
56
  "rollup-plugin-commonjs": "^10.1.0",
56
- "sade": "^1.8.1"
57
+ "sade": "^1.8.1",
58
+ "simple-peer": "^9.11.1"
57
59
  },
58
60
  "devDependencies": {
59
61
  "@rollup/plugin-alias": "^5.0.0",
package/src/blockstore.js CHANGED
@@ -34,10 +34,12 @@ export class TransactionBlockstore {
34
34
  /** @type {Map<string, Uint8Array>} */
35
35
  committedBlocks = new Map()
36
36
 
37
+ /** @type {Valet} */
37
38
  valet = null
38
39
 
39
40
  instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4)
40
41
  inflightTransactions = new Set()
42
+ syncs = new Set()
41
43
 
42
44
  constructor (name, encryptionKey) {
43
45
  if (name) {
@@ -75,10 +77,10 @@ export class TransactionBlockstore {
75
77
 
76
78
  async committedGet (key) {
77
79
  const old = this.committedBlocks.get(key)
80
+ // console.log('committedGet: ' + key + ' ' + this.instanceId, old.length)
78
81
  if (old) return old
79
82
  if (!this.valet) throw new Error('Missing block: ' + key)
80
83
  const got = await this.valet.getBlock(key)
81
- // console.log('committedGet: ' + key)
82
84
  this.committedBlocks.set(key, got)
83
85
  return got
84
86
  }
@@ -120,18 +122,24 @@ export class TransactionBlockstore {
120
122
  /**
121
123
  * Iterate over all blocks in the store.
122
124
  *
123
- * @yields {AnyBlock}
124
- * @returns {AsyncGenerator<AnyBlock>}
125
+ * @yields {{cid: string, bytes: Uint8Array}}
126
+ * @returns {AsyncGenerator<any, any, any>}
125
127
  */
126
- // * entries () {
127
- // // needs transaction blocks?
128
- // // for (const [str, bytes] of this.blocks) {
129
- // // yield { cid: parse(str), bytes }
130
- // // }
131
- // for (const [str, bytes] of this.committedBlocks) {
132
- // yield { cid: parse(str), bytes }
133
- // }
134
- // }
128
+ async * entries () {
129
+ for (const transaction of this.inflightTransactions) {
130
+ for (const [str, bytes] of transaction) {
131
+ yield { cid: str, bytes }
132
+ }
133
+ }
134
+ for (const [str, bytes] of this.committedBlocks) {
135
+ yield { cid: str, bytes }
136
+ }
137
+ if (this.valet) {
138
+ for await (const { cid } of this.valet.cids()) {
139
+ yield { cid }
140
+ }
141
+ }
142
+ }
135
143
 
136
144
  /**
137
145
  * Begin a transaction. Ensures the uncommited blocks are empty at the begining.
@@ -150,8 +158,16 @@ export class TransactionBlockstore {
150
158
  * @returns {Promise<void>}
151
159
  * @memberof TransactionBlockstore
152
160
  */
153
- async commit (innerBlockstore) {
161
+ async commit (innerBlockstore, doSync = true) {
162
+ // console.log('commit', doSync, innerBlockstore.label)
154
163
  await this.doCommit(innerBlockstore)
164
+ if (doSync) {
165
+ // const all =
166
+ await Promise.all([...this.syncs].map(async sync => sync.sendUpdate(innerBlockstore).catch(e => {
167
+ console.error('sync error', e)
168
+ this.syncs.delete(sync)
169
+ })))
170
+ }
155
171
  }
156
172
 
157
173
  // first get the transaction blockstore from the map of transaction blockstores
@@ -170,8 +186,8 @@ export class TransactionBlockstore {
170
186
  cids.add(stringCid)
171
187
  }
172
188
  }
189
+ // console.log(innerBlockstore.label, 'committing', cids.size, 'blocks', [...cids].map(cid => cid.toString()), this.valet)
173
190
  if (cids.size > 0 && this.valet) {
174
- // console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
175
191
  await this.valet.writeTransaction(innerBlockstore, cids)
176
192
  }
177
193
  }
@@ -195,7 +211,7 @@ export class TransactionBlockstore {
195
211
  * @returns {Promise<any>}
196
212
  * @memberof TransactionBlockstore
197
213
  */
198
- export const doTransaction = async (label, blockstore, doFun) => {
214
+ export const doTransaction = async (label, blockstore, doFun, doSync = true) => {
199
215
  // @ts-ignore
200
216
  if (!blockstore.commit) return await doFun(blockstore)
201
217
  // @ts-ignore
@@ -203,7 +219,7 @@ export const doTransaction = async (label, blockstore, doFun) => {
203
219
  try {
204
220
  const result = await doFun(innerBlockstore)
205
221
  // @ts-ignore
206
- await blockstore.commit(innerBlockstore)
222
+ await blockstore.commit(innerBlockstore, doSync)
207
223
  return result
208
224
  } catch (e) {
209
225
  console.error(`Transaction ${label} failed`, e, e.stack)
package/src/clock.js CHANGED
@@ -190,7 +190,11 @@ async function contains (events, a, b) {
190
190
  */
191
191
  export async function * vis (blocks, head, options = {}) {
192
192
  // @ts-ignore
193
- const renderNodeLabel = options.renderNodeLabel ?? ((b) => b.value.data.value)
193
+ const renderNodeLabel = options.renderNodeLabel ?? ((b) => {
194
+ // @ts-ignore
195
+ const { key, root, type } = b.value.data
196
+ return b.cid.toString() + '\n' + JSON.stringify({ key, root: root.cid.toString(), type }, null, 2).replace(/"/g, '\'')
197
+ })
194
198
  const events = new EventFetcher(blocks)
195
199
  yield 'digraph clock {'
196
200
  yield ' node [shape=point fontname="Courier"]; head;'
@@ -231,24 +235,29 @@ export async function findEventsToSync (blocks, head) {
231
235
  // console.time(callTag + '.contains')
232
236
  const toSync = await asyncFilter(sorted, async (uks) => !(await contains(events, ancestor, uks.cid)))
233
237
  // console.timeEnd(callTag + '.contains')
238
+ // console.log('toSync.contains', toSync.length)
234
239
 
235
- return { cids: events.all(), events: toSync }
240
+ return { cids: events, events: toSync }
236
241
  }
237
242
 
238
243
  const asyncFilter = async (arr, predicate) =>
239
244
  Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index]))
240
245
 
241
- export async function findCommonAncestorWithSortedEvents (events, children) {
246
+ export async function findCommonAncestorWithSortedEvents (events, children, doFull = false) {
247
+ // console.trace('findCommonAncestorWithSortedEvents')
242
248
  // const callTag = Math.random().toString(36).substring(7)
249
+ // console.log(callTag + '.children', children.map((c) => c.toString()))
243
250
  // console.time(callTag + '.findCommonAncestor')
244
251
  const ancestor = await findCommonAncestor(events, children)
245
252
  // console.timeEnd(callTag + '.findCommonAncestor')
253
+ // console.log('ancestor', ancestor.toString())
246
254
  if (!ancestor) {
247
255
  throw new Error('failed to find common ancestor event')
248
256
  }
249
257
  // console.time(callTag + '.findSortedEvents')
250
- const sorted = await findSortedEvents(events, children, ancestor)
258
+ const sorted = await findSortedEvents(events, children, ancestor, doFull)
251
259
  // console.timeEnd(callTag + '.findSortedEvents')
260
+ // console.log('sorted', sorted.length)
252
261
  return { ancestor, sorted }
253
262
  }
254
263
 
@@ -261,6 +270,7 @@ export async function findCommonAncestorWithSortedEvents (events, children) {
261
270
  */
262
271
  async function findCommonAncestor (events, children) {
263
272
  if (!children.length) return
273
+ if (children.length === 1) return children[0]
264
274
  const candidates = children.map((c) => [c])
265
275
  while (true) {
266
276
  let changed = false
@@ -281,7 +291,7 @@ async function findCommonAncestor (events, children) {
281
291
  * @param {import('./clock').EventLink<EventData>} root
282
292
  */
283
293
  async function findAncestorCandidate (events, root) {
284
- const { value: event } = await events.get(root)
294
+ const { value: event } = await events.get(root)// .catch(() => ({ value: { parents: [] } }))
285
295
  if (!event.parents.length) return root
286
296
  return event.parents.length === 1 ? event.parents[0] : findCommonAncestor(events, event.parents)
287
297
  }
@@ -291,6 +301,7 @@ async function findAncestorCandidate (events, root) {
291
301
  * @param {Array<T[]>} arrays
292
302
  */
293
303
  function findCommonString (arrays) {
304
+ // console.log('findCommonString', arrays.map((a) => a.map((i) => String(i))))
294
305
  arrays = arrays.map((a) => [...a])
295
306
  for (const arr of arrays) {
296
307
  for (const item of arr) {
@@ -308,15 +319,33 @@ function findCommonString (arrays) {
308
319
  /**
309
320
  * Find and sort events between the head(s) and the tail.
310
321
  * @param {import('./clock').EventFetcher} events
311
- * @param {import('./clock').EventLink<EventData>[]} head
322
+ * @param {any[]} head
312
323
  * @param {import('./clock').EventLink<EventData>} tail
313
324
  */
314
- async function findSortedEvents (events, head, tail) {
325
+ async function findSortedEvents (events, head, tail, doFull) {
315
326
  // const callTag = Math.random().toString(36).substring(7)
316
327
  // get weighted events - heavier events happened first
328
+ // const callTag = Math.random().toString(36).substring(7)
329
+
317
330
  /** @type {Map<string, { event: import('./clock').EventBlockView<EventData>, weight: number }>} */
318
331
  const weights = new Map()
332
+ head = [...new Set([...head.map((h) => h.toString())])]
333
+ // console.log(callTag + '.head', head.length)
334
+
335
+ const allEvents = new Set([tail.toString(), ...head])
336
+ if (!doFull && allEvents.size === 1) {
337
+ // console.log('head contains tail', tail.toString())
338
+ return []
339
+ // const event = await events.get(tail)
340
+ // return [event]
341
+ }
342
+
343
+ // console.log('finding events')
344
+ // console.log(callTag + '.head', head.length, [...head.map((h) => h.toString())], tail.toString())
345
+
346
+ // console.time(callTag + '.findEvents')
319
347
  const all = await Promise.all(head.map((h) => findEvents(events, h, tail)))
348
+ // console.timeEnd(callTag + '.findEvents')
320
349
  for (const arr of all) {
321
350
  for (const { event, depth } of arr) {
322
351
  // console.log('event value', event.value.data.value)
@@ -345,7 +374,7 @@ async function findSortedEvents (events, head, tail) {
345
374
  const sorted = Array.from(buckets)
346
375
  .sort((a, b) => b[0] - a[0])
347
376
  .flatMap(([, es]) => es.sort((a, b) => (String(a.cid) < String(b.cid) ? -1 : 1)))
348
- // console.log('sorted', sorted.map(s => s.value.data.value))
377
+ // console.log('sorted', sorted.map(s => s.cid))
349
378
 
350
379
  return sorted
351
380
  }
@@ -357,11 +386,14 @@ async function findSortedEvents (events, head, tail) {
357
386
  * @returns {Promise<Array<{ event: EventBlockView<EventData>, depth: number }>>}
358
387
  */
359
388
  async function findEvents (events, start, end, depth = 0) {
360
- // console.log('findEvents', start)
389
+ // console.log('findEvents', start.toString(), end.toString(), depth)
361
390
  const event = await events.get(start)
391
+ const send = String(end)
362
392
  const acc = [{ event, depth }]
363
393
  const { parents } = event.value
364
- if (parents.length === 1 && String(parents[0]) === String(end)) return acc
394
+ // if (parents.length === 1 && String(parents[0]) === send) return acc
395
+ if (parents.findIndex((p) => String(p) === send) !== -1) return acc
396
+ // if (parents.length === 1) return acc
365
397
  const rest = await Promise.all(parents.map((p) => findEvents(events, p, end, depth + 1)))
366
398
  return acc.concat(...rest)
367
399
  }
package/src/database.js CHANGED
@@ -26,15 +26,16 @@ export const parseCID = cid => (typeof cid === 'string' ? CID.parse(cid) : cid)
26
26
  */
27
27
  export class Database {
28
28
  listeners = new Set()
29
+ indexes = new Map()
30
+ rootCache = null
31
+ eventsCache = new Map()
29
32
 
30
- // todo refactor this for the next version
31
33
  constructor (blocks, clock, config = {}) {
32
34
  this.name = config.name
33
35
  this.instanceId = `fp.${this.name}.${Math.random().toString(36).substring(2, 7)}`
34
36
  this.blocks = blocks
35
37
  this.clock = clock
36
38
  this.config = config
37
- this.indexes = new Map()
38
39
  }
39
40
 
40
41
  /**
@@ -101,11 +102,22 @@ export class Database {
101
102
  * @instance
102
103
  */
103
104
  async changesSince (event) {
105
+ // console.log('events for', this.instanceId, event.constructor.name)
104
106
  // console.log('changesSince', this.instanceId, event, this.clock)
105
107
  let rows, dataCIDs, clockCIDs
106
108
  // if (!event) event = []
107
109
  if (event) {
108
- const resp = await eventsSince(this.blocks, this.clock, event)
110
+ event = event.map((cid) => cid.toString())
111
+ const eventKey = JSON.stringify([...event, ...this.clockToJSON()])
112
+
113
+ let resp
114
+ if (this.eventsCache.has(eventKey)) {
115
+ console.log('events from cache')
116
+ resp = this.eventsCache.get(eventKey)
117
+ } else {
118
+ resp = await eventsSince(this.blocks, this.clock, event)
119
+ this.eventsCache.set(eventKey, resp)
120
+ }
109
121
  const docsMap = new Map()
110
122
  for (const { key, type, value } of resp.result.map(decodeEvent)) {
111
123
  if (type === 'del') {
@@ -118,7 +130,9 @@ export class Database {
118
130
  clockCIDs = resp.clockCIDs
119
131
  // console.log('change rows', this.instanceId, rows)
120
132
  } else {
121
- const allResp = await getAll(this.blocks, this.clock)
133
+ const allResp = await getAll(this.blocks, this.clock, this.rootCache)
134
+ this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs }
135
+
122
136
  rows = allResp.result.map(({ key, value }) => decodeEvent({ key, value }))
123
137
  dataCIDs = allResp.cids
124
138
  // console.log('dbdoc rows', this.instanceId, rows)
@@ -131,7 +145,9 @@ export class Database {
131
145
  }
132
146
 
133
147
  async allDocuments () {
134
- const allResp = await getAll(this.blocks, this.clock)
148
+ const allResp = await getAll(this.blocks, this.clock, this.rootCache)
149
+ this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs }
150
+
135
151
  const rows = allResp.result
136
152
  .map(({ key, value }) => decodeEvent({ key, value }))
137
153
  .map(({ key, value }) => ({ key, value: { _id: key, ...value } }))
@@ -143,7 +159,9 @@ export class Database {
143
159
  }
144
160
 
145
161
  async allCIDs () {
146
- const allResp = await getAll(this.blocks, this.clock)
162
+ const allResp = await getAll(this.blocks, this.clock, this.rootCache, true)
163
+ this.rootCache = { root: allResp.root, clockCIDs: allResp.clockCIDs }
164
+ // console.log('allcids', allResp.cids, allResp.clockCIDs)
147
165
  const cids = await cidsToProof(allResp.cids)
148
166
  const clockCids = await cidsToProof(allResp.clockCIDs)
149
167
  // console.log('allcids', cids, clockCids)
@@ -151,6 +169,14 @@ export class Database {
151
169
  return [...cids, ...clockCids] // need a single block version of clock head, maybe an encoded block for it
152
170
  }
153
171
 
172
+ async allStoredCIDs () {
173
+ const allCIDs = []
174
+ for await (const { cid } of this.blocks.entries()) {
175
+ allCIDs.push(cid)
176
+ }
177
+ return allCIDs
178
+ }
179
+
154
180
  /**
155
181
  * Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
156
182
  *
@@ -180,13 +206,13 @@ export class Database {
180
206
  */
181
207
  async get (key, opts = {}) {
182
208
  const clock = opts.clock || this.clock
183
- const resp = await get(this.blocks, clock, charwise.encode(key))
184
-
209
+ const resp = await get(this.blocks, clock, charwise.encode(key), this.rootCache)
210
+ this.rootCache = { root: resp.root, clockCIDs: resp.clockCIDs }
185
211
  // this tombstone is temporary until we can get the prolly tree to delete
186
212
  if (!resp || resp.result === null) {
187
213
  throw new Error('Not found')
188
214
  }
189
- const doc = resp.result
215
+ const doc = { ...resp.result }
190
216
  if (opts.mvcc === true) {
191
217
  doc._clock = this.clockToJSON()
192
218
  }
@@ -280,9 +306,20 @@ export class Database {
280
306
  }
281
307
 
282
308
  applyClock (prevClock, newClock) {
283
- // console.log('applyClock', prevClock, newClock, this.clock)
284
- const removedprevCIDs = this.clock.filter(cid => prevClock.indexOf(cid) === -1)
285
- this.clock = removedprevCIDs.concat(newClock)
309
+ // console.log('prevClock', prevClock.length, prevClock.map((cid) => cid.toString()))
310
+ // console.log('newClock', newClock.length, newClock.map((cid) => cid.toString()))
311
+ // console.log('this.clock', this.clock.length, this.clockToJSON())
312
+ const stPrev = prevClock.map(cid => cid.toString())
313
+ const keptPrevClock = this.clock.filter(cid => stPrev.indexOf(cid.toString()) === -1)
314
+ const merged = keptPrevClock.concat(newClock)
315
+ const uniquebyCid = new Map()
316
+ for (const cid of merged) {
317
+ uniquebyCid.set(cid.toString(), cid)
318
+ }
319
+ this.clock = Array.from(uniquebyCid.values()).sort((a, b) => a.toString().localeCompare(b.toString()))
320
+ this.rootCache = null
321
+ this.eventsCache.clear()
322
+ // console.log('afterClock', this.clock.length, this.clockToJSON())
286
323
  }
287
324
 
288
325
  // /**
@@ -341,7 +378,9 @@ export class Database {
341
378
  }
342
379
 
343
380
  export async function cidsToProof (cids) {
344
- if (!cids || !cids.all) return []
381
+ if (!cids) return []
382
+ if (!cids.all) { return [...cids] }
383
+
345
384
  const all = await cids.all()
346
385
  return [...all].map(cid => cid.toString())
347
386
  }
package/src/db-index.js CHANGED
@@ -7,7 +7,7 @@ import { sha256 as hasher } from 'multiformats/hashes/sha2'
7
7
  import { nocache as cache } from 'prolly-trees/cache'
8
8
  // @ts-ignore
9
9
  import { bf, simpleCompare } from 'prolly-trees/utils'
10
- import { makeGetBlock } from './prolly.js'
10
+ import { makeGetBlock, visMerkleTree } from './prolly.js'
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  import { Database, cidsToProof } from './database.js'
13
13
 
@@ -35,8 +35,8 @@ const refCompare = (aRef, bRef) => {
35
35
  return simpleCompare(aRef, bRef)
36
36
  }
37
37
 
38
- const dbIndexOpts = { cache, chunker: bf(3), codec, hasher, compare }
39
- const idIndexOpts = { cache, chunker: bf(3), codec, hasher, compare: simpleCompare }
38
+ const dbIndexOpts = { cache, chunker: bf(30), codec, hasher, compare }
39
+ const idIndexOpts = { cache, chunker: bf(30), codec, hasher, compare: simpleCompare }
40
40
 
41
41
  const makeDoc = ({ key, value }) => ({ _id: key, ...value })
42
42
 
@@ -93,6 +93,9 @@ const indexEntriesForChanges = (changes, mapFn) => {
93
93
  *
94
94
  */
95
95
  export class DbIndex {
96
+ /**
97
+ * @param {Database} database
98
+ */
96
99
  constructor (database, name, mapFn, clock = null, opts = {}) {
97
100
  this.database = database
98
101
  if (!database.indexBlocks) {
@@ -164,6 +167,14 @@ export class DbIndex {
164
167
  return new DbIndex(database, name, code, clock)
165
168
  }
166
169
 
170
+ async visKeyTree () {
171
+ return await visMerkleTree(this.database.indexBlocks, this.indexById.cid)
172
+ }
173
+
174
+ async visIdTree () {
175
+ return await visMerkleTree(this.database.indexBlocks, this.indexByKey.cid)
176
+ }
177
+
167
178
  /**
168
179
  * JSDoc for Query type.
169
180
  * @typedef {Object} DbQuery
package/src/fireproof.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import randomBytes from 'randombytes'
2
-
3
2
  import { Database, parseCID } from './database.js'
4
3
  import { Listener } from './listener.js'
5
4
  import { DbIndex as Index } from './db-index.js'
6
5
  import { TransactionBlockstore } from './blockstore.js'
7
6
  import { localGet } from './utils.js'
8
- import { blocksToCarBlock, blocksToEncryptedCarBlock } from './valet.js'
7
+ import { Sync } from './sync.js'
9
8
 
10
- export { Index, Listener, Database }
9
+ export { Index, Listener, Database, Sync }
11
10
 
12
11
  export class Fireproof {
13
12
  /**
@@ -85,41 +84,4 @@ export class Fireproof {
85
84
  await database.notifyReset() // hmm... indexes should listen to this? might be more complex than worth it. so far this is the only caller
86
85
  return database
87
86
  }
88
-
89
- // get all the cids
90
- // tell valet to make a file
91
- static async makeCar (database, key) {
92
- const allCIDs = await database.allCIDs()
93
- const blocks = database.blocks
94
-
95
- const rootCid = parseCID(allCIDs[allCIDs.length - 1])
96
- if (typeof key === 'undefined') {
97
- key = blocks.valet?.getKeyMaterial()
98
- }
99
- if (key) {
100
- return blocksToEncryptedCarBlock(
101
- rootCid,
102
- {
103
- entries: () => allCIDs.map(cid => ({ cid })),
104
- get: async cid => await blocks.get(cid)
105
- },
106
- key
107
- )
108
- } else {
109
- const carBlocks = await Promise.all(
110
- allCIDs.map(async c => {
111
- const b = await blocks.get(c)
112
- // console.log('block', b)
113
- if (typeof b.cid === 'string') {
114
- b.cid = parseCID(b.cid)
115
- }
116
- // if (b.bytes.constructor.name === 'Buffer') console.log('conver vbuff')
117
- return b
118
- })
119
- )
120
- return blocksToCarBlock(rootCid, {
121
- entries: () => carBlocks
122
- })
123
- }
124
- }
125
87
  }
package/src/listener.js CHANGED
@@ -41,7 +41,7 @@ export class Listener {
41
41
  * @returns {Function} A function to unsubscribe from the topic.
42
42
  * @memberof Listener
43
43
  * @instance
44
- * @param {any} [since] - clock to flush from on launch
44
+ * @param {any} [since] - clock to flush from on launch, pass null for all
45
45
  */
46
46
  on (topic, subscriber, since = undefined) {
47
47
  const listOfTopicSubscribers = getTopicList(this.subcribers, topic)