@fireproof/core 0.1.1 → 0.3.0

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.
@@ -4,44 +4,53 @@
4
4
  import { useEffect, useState, createContext } from 'react'
5
5
  import { Fireproof, Listener, Hydrator } from '../index'
6
6
 
7
-
8
7
  export interface FireproofCtxValue {
9
8
  addSubscriber: (label: String, fn: Function) => void
10
9
  database: Fireproof
11
10
  ready: boolean
11
+ persist: () => void
12
12
  }
13
13
  export const FireproofCtx = createContext<FireproofCtxValue>({
14
14
  addSubscriber: () => {},
15
15
  database: null,
16
- ready: false,
16
+ ready: false
17
17
  })
18
18
 
19
19
  const inboundSubscriberQueue = new Map()
20
- const database = Fireproof.storage()
21
- const listener = new Listener(database)
22
- let startedSetup = false;
20
+
21
+ let startedSetup = false
22
+ let database
23
+ let listener
24
+ const initializeDatabase = name => {
25
+ if (database) return
26
+ database = Fireproof.storage(name)
27
+ listener = new Listener(database)
28
+ }
23
29
 
24
30
  /**
25
31
  * @function useFireproof
26
32
  * React hook to initialize a Fireproof database, automatically saving and loading the clock.
33
+ * You might need to `import { nodePolyfills } from 'vite-plugin-node-polyfills'` in your vite.config.ts
27
34
  * @param [defineDatabaseFn] Synchronous function that defines the database, run this before any async calls
28
35
  * @param [setupDatabaseFn] Asynchronous function that sets up the database, run this to load fixture data etc
29
36
  * @returns {FireproofCtxValue} { addSubscriber, database, ready }
30
37
  */
31
- export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Function): FireproofCtxValue {
38
+ export function useFireproof(
39
+ defineDatabaseFn = (database: Fireproof) => {},
40
+ setupDatabaseFn = async (database: Fireproof) => {},
41
+ name: string
42
+ ): FireproofCtxValue {
32
43
  const [ready, setReady] = useState(false)
33
- defineDatabaseFn = defineDatabaseFn || (() => {})
34
- setupDatabaseFn = setupDatabaseFn || (() => {})
35
- // console.log('useFireproof', database, ready)
36
-
44
+ initializeDatabase(name || 'useFireproof')
45
+ const localStorageKey = 'fp.' + database.name
37
46
 
38
47
  const addSubscriber = (label: String, fn: Function) => {
39
48
  inboundSubscriberQueue.set(label, fn)
40
49
  }
41
50
 
42
- const listenerCallback = async () => {
43
- // console.log ('listenerCallback', JSON.stringify(database))
44
- localSet('fireproof', JSON.stringify(database))
51
+ const listenerCallback = async event => {
52
+ localSet(localStorageKey, JSON.stringify(database))
53
+ if (event._external) return
45
54
  for (const [, fn] of inboundSubscriberQueue) fn()
46
55
  }
47
56
 
@@ -51,14 +60,14 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
51
60
  if (startedSetup) return
52
61
  startedSetup = true
53
62
  defineDatabaseFn(database) // define indexes before querying them
54
- const fp = localGet('fireproof')
63
+ console.log('Initializing database', database.name)
64
+ const fp = localGet(localStorageKey) // todo use db.name
55
65
  if (fp) {
56
- const serialized = JSON.parse(fp)
57
- // console.log('serialized', JSON.stringify(serialized.indexes.map(c => c.clock)))
58
- console.log("Loading previous database clock. (localStorage.removeItem('fireproof') to reset)")
59
- Hydrator.fromJSON(serialized, database)
60
- // await database.setClock(clock)
61
66
  try {
67
+ const serialized = JSON.parse(fp)
68
+ // console.log('serialized', JSON.stringify(serialized.indexes.map(c => c.clock)))
69
+ console.log(`Loading previous database clock. (localStorage.removeItem('${localStorageKey}') to reset)`)
70
+ await Hydrator.fromJSON(serialized, database)
62
71
  const changes = await database.changesSince()
63
72
  if (changes.rows.length < 2) {
64
73
  // console.log('Resetting database')
@@ -66,16 +75,16 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
66
75
  }
67
76
  } catch (e) {
68
77
  console.error(`Error loading previous database clock. ${fp} Resetting.`, e)
69
- await database.setClock([]) // todo this should be resetClock and also reset the indexes
78
+ await Hydrator.zoom(database, [])
70
79
  await setupDatabaseFn(database)
71
- localSet('fireproof', JSON.stringify(database))
80
+ localSet(localStorageKey, JSON.stringify(database))
72
81
  }
73
82
  } else {
74
83
  await setupDatabaseFn(database)
75
- localSet('fireproof', JSON.stringify(database))
84
+ localSet(localStorageKey, JSON.stringify(database))
76
85
  }
77
86
  setReady(true)
78
- listener.on('*', hushed('*', listenerCallback, 250))
87
+ listener.on('*', listenerCallback)//hushed('*', listenerCallback, 250))
79
88
  }
80
89
  doSetup()
81
90
  }, [ready])
@@ -84,6 +93,9 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
84
93
  addSubscriber,
85
94
  database,
86
95
  ready,
96
+ persist: () => {
97
+ localSet(localStorageKey, JSON.stringify(database))
98
+ }
87
99
  }
88
100
  }
89
101
 
@@ -91,12 +103,17 @@ const husherMap = new Map()
91
103
  const husher = (id: string, workFn: { (): Promise<any> }, ms: number) => {
92
104
  if (!husherMap.has(id)) {
93
105
  const start: number = Date.now()
94
- husherMap.set(id, workFn().finally(() =>
95
- setTimeout(() => husherMap.delete(id), ms - (Date.now() - start))))
106
+ husherMap.set(
107
+ id,
108
+ workFn().finally(() => setTimeout(() => husherMap.delete(id), ms - (Date.now() - start)))
109
+ )
96
110
  }
97
111
  return husherMap.get(id)
98
112
  }
99
- const hushed = (id: string, workFn: { (): Promise<any> }, ms: number) => () => husher(id, workFn, ms)
113
+ const hushed =
114
+ (id: string, workFn: { (...args): Promise<any> }, ms: number) =>
115
+ (...args) =>
116
+ husher(id, () => workFn(...args), ms)
100
117
 
101
118
  let storageSupported = false
102
119
  try {
@@ -116,4 +133,4 @@ function localSet(key: string, value: string) {
116
133
  // if (storageSupported) {
117
134
  // return localStorage && localStorage.removeItem(key)
118
135
  // }
119
- // }
136
+ // }
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@fireproof/core",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Realtime database for IPFS",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "test": "standard && npm run test:mocha",
9
- "test:mocha": "mocha test/*.test.js",
10
- "test:watch": "npm run test:mocha -- -w --parallel test/*.test.js",
8
+ "keygen": "node scripts/keygen.js",
9
+ "test": "standard && npm run test:unencrypted && npm run test:mocha",
10
+ "test:unencrypted": "NO_ENCRYPT=true npm run test:mocha",
11
+ "test:mocha": "mocha --reporter list test/*.test.js",
12
+ "test:watch": "npm run test:mocha -- -w --parallel",
11
13
  "coverage": "c8 -r html -r text npm test",
12
- "prepublishOnly" : "cp ../../README.md .",
14
+ "prepublishOnly": "cp ../../README.md .",
13
15
  "postpublish": "rm README.md",
14
16
  "lint": "standard",
15
17
  "lint:fix": "standard --fix"
@@ -30,6 +32,7 @@
30
32
  ],
31
33
  "license": "Apache-2.0 OR MIT",
32
34
  "dependencies": {
35
+ "prolly-trees": "1.0.4",
33
36
  "@ipld/car": "^5.1.0",
34
37
  "@ipld/dag-cbor": "^9.0.0",
35
38
  "archy": "^1.0.0",
@@ -37,9 +40,9 @@
37
40
  "car-transaction": "^1.0.1",
38
41
  "charwise": "^3.0.1",
39
42
  "cli-color": "^2.0.3",
43
+ "encrypted-block": "^0.0.3",
40
44
  "idb": "^7.1.1",
41
45
  "multiformats": "^11.0.1",
42
- "prolly-trees": "1.0.3",
43
46
  "sade": "^1.8.1"
44
47
  },
45
48
  "devDependencies": {
@@ -0,0 +1,3 @@
1
+ import { randomBytes } from 'crypto'
2
+
3
+ console.log(randomBytes(32).toString('hex'))
package/src/blockstore.js CHANGED
@@ -1,10 +1,5 @@
1
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
2
  import { CID } from 'multiformats'
7
-
8
3
  import Valet from './valet.js'
9
4
 
10
5
  // const sleep = ms => new Promise(r => setTimeout(r, ms))
@@ -34,15 +29,15 @@ const husher = (id, workFn) => {
34
29
  */
35
30
  export default class TransactionBlockstore {
36
31
  /** @type {Map<string, Uint8Array>} */
37
- #oldBlocks = new Map()
32
+ #committedBlocks = new Map()
38
33
 
39
34
  valet = null
40
35
 
41
36
  #instanceId = 'blkz.' + Math.random().toString(36).substring(2, 4)
42
37
  #inflightTransactions = new Set()
43
38
 
44
- constructor (name) {
45
- this.valet = new Valet(name)
39
+ constructor (name, encryptionKey) {
40
+ this.valet = new Valet(name, encryptionKey)
46
41
  }
47
42
 
48
43
  /**
@@ -54,7 +49,7 @@ export default class TransactionBlockstore {
54
49
  async get (cid) {
55
50
  const key = cid.toString()
56
51
  // it is safe to read from the in-flight transactions becauase they are immutable
57
- const bytes = await Promise.any([this.#transactionsGet(key), this.commitedGet(key)]).catch((e) => {
52
+ const bytes = await Promise.any([this.#transactionsGet(key), this.committedGet(key)]).catch(e => {
58
53
  // console.log('networkGet', cid.toString(), e)
59
54
  return this.networkGet(key)
60
55
  })
@@ -72,18 +67,26 @@ export default class TransactionBlockstore {
72
67
  throw new Error('Missing block: ' + key)
73
68
  }
74
69
 
75
- async commitedGet (key) {
76
- const old = this.#oldBlocks.get(key)
70
+ async committedGet (key) {
71
+ const old = this.#committedBlocks.get(key)
77
72
  if (old) return old
78
- return await this.valet.getBlock(key)
73
+ const got = await this.valet.getBlock(key)
74
+ // console.log('committedGet: ' + key)
75
+ this.#committedBlocks.set(key, got)
76
+ return got
77
+ }
78
+
79
+ async clearCommittedCache () {
80
+ this.#committedBlocks.clear()
79
81
  }
80
82
 
81
83
  async networkGet (key) {
82
84
  if (this.valet.remoteBlockFunction) {
85
+ // todo why is this on valet?
83
86
  const value = await husher(key, async () => await this.valet.remoteBlockFunction(key))
84
87
  if (value) {
85
88
  // console.log('networkGot: ' + key, value.length)
86
- doTransaction('networkGot: ' + key, this, async (innerBlockstore) => {
89
+ doTransaction('networkGot: ' + key, this, async innerBlockstore => {
87
90
  await innerBlockstore.put(CID.parse(key), value)
88
91
  })
89
92
  return value
@@ -118,7 +121,7 @@ export default class TransactionBlockstore {
118
121
  // // for (const [str, bytes] of this.#blocks) {
119
122
  // // yield { cid: parse(str), bytes }
120
123
  // // }
121
- // for (const [str, bytes] of this.#oldBlocks) {
124
+ // for (const [str, bytes] of this.#committedBlocks) {
122
125
  // yield { cid: parse(str), bytes }
123
126
  // }
124
127
  // }
@@ -145,39 +148,24 @@ export default class TransactionBlockstore {
145
148
  }
146
149
 
147
150
  // first get the transaction blockstore from the map of transaction blockstores
148
- // then copy it to oldBlocks
151
+ // then copy it to committedBlocks
149
152
  // then write the transaction blockstore to a car
150
153
  // then write the car to the valet
151
154
  // then remove the transaction blockstore from the map of transaction blockstores
152
- #doCommit = async (innerBlockstore) => {
155
+ #doCommit = async innerBlockstore => {
153
156
  const cids = new Set()
154
157
  for (const { cid, bytes } of innerBlockstore.entries()) {
155
158
  const stringCid = cid.toString() // unnecessary string conversion, can we fix upstream?
156
- if (this.#oldBlocks.has(stringCid)) {
157
- // console.log('Duplicate block: ' + stringCid)
159
+ if (this.#committedBlocks.has(stringCid)) {
160
+ // console.log('Duplicate block: ' + stringCid) // todo some of this can be avoided, cost is extra size on car files
158
161
  } else {
159
- this.#oldBlocks.set(stringCid, bytes)
162
+ this.#committedBlocks.set(stringCid, bytes)
160
163
  cids.add(stringCid)
161
164
  }
162
165
  }
163
166
  if (cids.size > 0) {
164
167
  // console.log(innerBlockstore.label, 'committing', cids.size, 'blocks')
165
- await this.#valetWriteTransaction(innerBlockstore, cids)
166
- }
167
- }
168
-
169
- /**
170
- * Group the blocks into a car and write it to the valet.
171
- * @param {InnerBlockstore} innerBlockstore
172
- * @param {Set<string>} cids
173
- * @returns {Promise<void>}
174
- * @memberof TransactionBlockstore
175
- * @private
176
- */
177
- #valetWriteTransaction = async (innerBlockstore, cids) => {
178
- if (innerBlockstore.lastCid) {
179
- const newCar = await blocksToCarBlock(innerBlockstore.lastCid, innerBlockstore)
180
- await this.valet.parkCar(newCar.cid.toString(), newCar.bytes, cids)
168
+ await this.valet.writeTransaction(innerBlockstore, cids)
181
169
  }
182
170
  }
183
171
 
@@ -215,25 +203,6 @@ export const doTransaction = async (label, blockstore, doFun) => {
215
203
  }
216
204
  }
217
205
 
218
- const blocksToCarBlock = async (lastCid, blocks) => {
219
- let size = 0
220
- const headerSize = CBW.headerLength({ roots: [lastCid] })
221
- size += headerSize
222
- for (const { cid, bytes } of blocks.entries()) {
223
- size += CBW.blockLength({ cid, bytes })
224
- }
225
- const buffer = new Uint8Array(size)
226
- const writer = await CBW.createWriter(buffer, { headerSize })
227
-
228
- writer.addRoot(lastCid)
229
-
230
- for (const { cid, bytes } of blocks.entries()) {
231
- writer.write({ cid, bytes })
232
- }
233
- await writer.close()
234
- return await Block.encode({ value: writer.bytes, hasher: sha256, codec: raw })
235
- }
236
-
237
206
  /** @implements {BlockFetcher} */
238
207
  export class InnerBlockstore {
239
208
  /** @type {Map<string, Uint8Array>} */
@@ -254,8 +223,10 @@ export class InnerBlockstore {
254
223
  async get (cid) {
255
224
  const key = cid.toString()
256
225
  let bytes = this.#blocks.get(key)
257
- if (bytes) { return { cid, bytes } }
258
- bytes = await this.parentBlockstore.commitedGet(key)
226
+ if (bytes) {
227
+ return { cid, bytes }
228
+ }
229
+ bytes = await this.parentBlockstore.committedGet(key)
259
230
  if (bytes) {
260
231
  return { cid, bytes }
261
232
  }
package/src/clock.js CHANGED
@@ -220,7 +220,6 @@ export async function findEventsToSync (blocks, head) {
220
220
  }
221
221
 
222
222
  const asyncFilter = async (arr, predicate) =>
223
-
224
223
  Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index]))
225
224
 
226
225
  export async function findCommonAncestorWithSortedEvents (events, children) {
package/src/crypto.js ADDED
@@ -0,0 +1,65 @@
1
+ import * as codec from 'encrypted-block'
2
+ import {
3
+ create,
4
+ load
5
+ } from 'prolly-trees/cid-set'
6
+ import { CID } from 'multiformats'
7
+ import { encode, decode, create as mfCreate } from 'multiformats/block'
8
+ import * as dagcbor from '@ipld/dag-cbor'
9
+ import { sha256 as hasher } from 'multiformats/hashes/sha2'
10
+
11
+ const createBlock = (bytes, cid) => mfCreate({ cid, bytes, hasher, codec })
12
+
13
+ const encrypt = async function * ({ get, cids, hasher, key, cache, chunker, root }) {
14
+ const set = new Set()
15
+ let eroot
16
+ for (const string of cids) {
17
+ const cid = CID.parse(string)
18
+ const unencrypted = await get(cid)
19
+ const block = await encode({ ...await codec.encrypt({ ...unencrypted, key }), codec, hasher })
20
+ // console.log(`encrypting ${string} as ${block.cid}`)
21
+ yield block
22
+ set.add(block.cid.toString())
23
+ if (unencrypted.cid.equals(root)) eroot = block.cid
24
+ }
25
+ if (!eroot) throw new Error('cids does not include root')
26
+ const list = [...set].map(s => CID.parse(s))
27
+ let last
28
+ for await (const node of create({ list, get, cache, chunker, hasher, codec: dagcbor })) {
29
+ const block = await node.block
30
+ yield block
31
+ last = block
32
+ }
33
+ const head = [eroot, last.cid]
34
+ const block = await encode({ value: head, codec: dagcbor, hasher })
35
+ yield block
36
+ }
37
+
38
+ const decrypt = async function * ({ root, get, key, cache, chunker, hasher }) {
39
+ const o = { ...await get(root), codec: dagcbor, hasher }
40
+ const decodedRoot = await decode(o)
41
+ // console.log('decodedRoot', decodedRoot)
42
+ const { value: [eroot, tree] } = decodedRoot
43
+ const rootBlock = await get(eroot) // should I decrypt?
44
+ const cidset = await load({ cid: tree, get, cache, chunker, codec, hasher })
45
+ const { result: nodes } = await cidset.getAllEntries()
46
+ const unwrap = async (eblock) => {
47
+ const { bytes, cid } = await codec.decrypt({ ...eblock, key }).catch(e => {
48
+ console.log('ekey', e)
49
+ throw new Error('bad key: ' + key.toString('hex'))
50
+ })
51
+ const block = await createBlock(bytes, cid)
52
+ return block
53
+ }
54
+ const promises = []
55
+ for (const { cid } of nodes) {
56
+ if (!rootBlock.cid.equals(cid)) promises.push(get(cid).then(unwrap))
57
+ }
58
+ yield * promises
59
+ yield unwrap(rootBlock)
60
+ }
61
+
62
+ export {
63
+ encrypt,
64
+ decrypt
65
+ }
package/src/db-index.js CHANGED
@@ -14,21 +14,6 @@ import charwise from 'charwise'
14
14
 
15
15
  const ALWAYS_REBUILD = false // todo: make false
16
16
 
17
- // const arrayCompare = (a, b) => {
18
- // if (Array.isArray(a) && Array.isArray(b)) {
19
- // const len = Math.min(a.length, b.length)
20
- // for (let i = 0; i < len; i++) {
21
- // const comp = simpleCompare(a[i], b[i])
22
- // if (comp !== 0) {
23
- // return comp
24
- // }
25
- // }
26
- // return simpleCompare(a.length, b.length)
27
- // } else {
28
- // return simpleCompare(a, b)
29
- // }
30
- // }
31
-
32
17
  const compare = (a, b) => {
33
18
  const [aKey, aRef] = a
34
19
  const [bKey, bRef] = b
@@ -82,6 +67,7 @@ const indexEntriesForChanges = (changes, mapFn) => {
82
67
  changes.forEach(({ key, value, del }) => {
83
68
  if (del || !value) return
84
69
  mapFn(makeDoc({ key, value }), (k, v) => {
70
+ if (typeof v === 'undefined' || typeof k === 'undefined') return
85
71
  indexEntries.push({
86
72
  key: [charwise.encode(k), key],
87
73
  value: v
@@ -102,7 +88,7 @@ const indexEntriesForChanges = (changes, mapFn) => {
102
88
  *
103
89
  */
104
90
  export default class DbIndex {
105
- constructor (database, mapFn, clock) {
91
+ constructor (database, mapFn, clock, opts = {}) {
106
92
  // console.log('DbIndex constructor', database.constructor.name, typeof mapFn, clock)
107
93
  /**
108
94
  * The database instance to DbIndex.
@@ -110,7 +96,7 @@ export default class DbIndex {
110
96
  */
111
97
  this.database = database
112
98
  if (!database.indexBlocks) {
113
- database.indexBlocks = new TransactionBlockstore(database.name + '.indexes')
99
+ database.indexBlocks = new TransactionBlockstore(database.name + '.indexes', database.blocks.valet.getKeyMaterial())
114
100
  }
115
101
  /**
116
102
  * The map function to apply to each entry in the database.
@@ -123,6 +109,7 @@ export default class DbIndex {
123
109
  this.mapFn = mapFn
124
110
  this.mapFnString = mapFn.toString()
125
111
  }
112
+ this.name = opts.name || this.makeName()
126
113
  this.indexById = { root: null, cid: null }
127
114
  this.indexByKey = { root: null, cid: null }
128
115
  this.dbHead = null
@@ -133,45 +120,46 @@ export default class DbIndex {
133
120
  }
134
121
  this.instanceId = this.database.instanceId + `.DbIndex.${Math.random().toString(36).substring(2, 7)}`
135
122
  this.updateIndexPromise = null
136
- DbIndex.registerWithDatabase(this, this.database)
123
+ if (!opts.temporary) { DbIndex.registerWithDatabase(this, this.database) }
124
+ }
125
+
126
+ makeName () {
127
+ const regex = /\(([^,()]+,\s*[^,()]+|\[[^\]]+\],\s*[^,()]+)\)/g
128
+ const matches = Array.from(this.mapFnString.matchAll(regex), match => match[1].trim())
129
+ return matches[1]
137
130
  }
138
131
 
139
132
  static registerWithDatabase (inIndex, database) {
140
- // console.log('.reg > in Index', inIndex.instanceId, { live: !!inIndex.mapFn }, inIndex.indexByKey, inIndex.mapFnString)
141
- if (database.indexes.has(inIndex.mapFnString)) {
133
+ if (!database.indexes.has(inIndex.mapFnString)) {
134
+ database.indexes.set(inIndex.mapFnString, inIndex)
135
+ } else {
142
136
  // merge our inIndex code with the inIndex clock or vice versa
143
- // keep the code instance, discard the clock instance
144
137
  const existingIndex = database.indexes.get(inIndex.mapFnString)
145
- // console.log('.reg - existingIndex', existingIndex.instanceId, { live: !!inIndex.mapFn }, existingIndex.indexByKey)
138
+ // keep the code instance, discard the clock instance
146
139
  if (existingIndex.mapFn) { // this one also has other config
147
140
  existingIndex.dbHead = inIndex.dbHead
148
141
  existingIndex.indexById.cid = inIndex.indexById.cid
149
142
  existingIndex.indexByKey.cid = inIndex.indexByKey.cid
150
143
  } else {
151
- // console.log('.reg use inIndex with existingIndex clock')
152
144
  inIndex.dbHead = existingIndex.dbHead
153
145
  inIndex.indexById.cid = existingIndex.indexById.cid
154
146
  inIndex.indexByKey.cid = existingIndex.indexByKey.cid
155
147
  database.indexes.set(inIndex.mapFnString, inIndex)
156
148
  }
157
- } else {
158
- // console.log('.reg - fresh')
159
- database.indexes.set(inIndex.mapFnString, inIndex)
160
149
  }
161
- // console.log('.reg after', JSON.stringify([...database.indexes.values()].map(i => [i.instanceId, typeof i.mapFn, i.indexByKey, i.indexById])))
162
150
  }
163
151
 
164
152
  toJSON () {
165
- const indexJson = { code: this.mapFn?.toString(), clock: { db: null, byId: null, byKey: null } }
153
+ const indexJson = { name: this.name, code: this.mapFnString, clock: { db: null, byId: null, byKey: null } }
166
154
  indexJson.clock.db = this.dbHead?.map(cid => cid.toString())
167
155
  indexJson.clock.byId = this.indexById.cid?.toString()
168
156
  indexJson.clock.byKey = this.indexByKey.cid?.toString()
169
157
  return indexJson
170
158
  }
171
159
 
172
- static fromJSON (database, { code, clock }) {
160
+ static fromJSON (database, { code, clock, name }) {
173
161
  // console.log('DbIndex.fromJSON', database.constructor.name, code, clock)
174
- return new DbIndex(database, code, clock)
162
+ return new DbIndex(database, code, clock, { name })
175
163
  }
176
164
 
177
165
  /**
@@ -188,25 +176,19 @@ export default class DbIndex {
188
176
  * @memberof DbIndex
189
177
  * @instance
190
178
  */
191
- async query (query) {
179
+ async query (query, update = true) {
192
180
  // const callId = Math.random().toString(36).substring(2, 7)
193
- // if (!root) {
194
- // pass a root to query a snapshot
181
+ // todo pass a root to query a snapshot
195
182
  // console.time(callId + '.#updateIndex')
196
- await this.#updateIndex(this.database.indexBlocks)
183
+ update && await this.#updateIndex(this.database.indexBlocks)
197
184
  // console.timeEnd(callId + '.#updateIndex')
198
-
199
- // }
200
185
  // console.time(callId + '.doIndexQuery')
201
186
  // console.log('query', query)
202
187
  const response = await doIndexQuery(this.database.indexBlocks, this.indexByKey, query)
203
188
  // console.timeEnd(callId + '.doIndexQuery')
204
-
205
189
  return {
206
190
  proof: { index: await cidsToProof(response.cids) },
207
- // TODO fix this naming upstream in prolly/db-DbIndex?
208
191
  rows: response.result.map(({ id, key, row }) => {
209
- // console.log('query', id, key, row)
210
192
  return ({ id, key: charwise.decode(key), value: row })
211
193
  })
212
194
  }
@@ -269,6 +251,7 @@ export default class DbIndex {
269
251
  this.indexByKey = await bulkIndex(blocks, this.indexByKey, oldIndexEntries.concat(indexEntries), dbIndexOpts)
270
252
  this.dbHead = result.clock
271
253
  })
254
+ this.database.notifyExternal('dbIndex')
272
255
  // console.timeEnd(callTag + '.doTransaction#updateIndex')
273
256
  // console.log(`#updateIndex ${callTag} <`, this.instanceId, this.dbHead?.toString(), this.indexByKey.cid?.toString(), this.indexById.cid?.toString())
274
257
  }
@@ -320,16 +303,22 @@ async function loadIndex (blocks, index, indexOpts) {
320
303
  return index.root
321
304
  }
322
305
 
323
- async function doIndexQuery (blocks, indexByKey, query) {
306
+ async function applyLimit (results, limit) {
307
+ results.result = results.result.slice(0, limit)
308
+ return results
309
+ }
310
+
311
+ async function doIndexQuery (blocks, indexByKey, query = {}) {
324
312
  await loadIndex(blocks, indexByKey, dbIndexOpts)
325
- if (!query) {
326
- const { result, ...all } = await indexByKey.root.getAllEntries()
327
- return { result: result.map(({ key: [k, id], value }) => ({ key: k, id, row: value })), ...all }
328
- } else if (query.range) {
313
+ if (!indexByKey.root) return { result: [] }
314
+ if (query.range) {
329
315
  const encodedRange = query.range.map((key) => charwise.encode(key))
330
- return indexByKey.root.range(...encodedRange)
316
+ return applyLimit(await indexByKey.root.range(...encodedRange), query.limit)
331
317
  } else if (query.key) {
332
318
  const encodedKey = charwise.encode(query.key)
333
319
  return indexByKey.root.get(encodedKey)
320
+ } else {
321
+ const { result, ...all } = await indexByKey.root.getAllEntries()
322
+ return applyLimit({ result: result.map(({ key: [k, id], value }) => ({ key: k, id, row: value })), ...all }, query.limit)
334
323
  }
335
324
  }