@fireproof/core 0.2.0 → 0.3.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.
@@ -1,16 +1,15 @@
1
1
  /* global localStorage */
2
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
2
  // @ts-ignore
4
3
  import { useEffect, useState, createContext } from 'react'
5
4
  import { Fireproof, Listener, Hydrator } from '../index'
6
5
 
7
- export interface FireproofCtxValue {
8
- addSubscriber: (label: String, fn: Function) => void
9
- database: Fireproof
10
- ready: boolean
11
- persist: () => void
12
- }
13
- export const FireproofCtx = createContext<FireproofCtxValue>({
6
+ // export interface FireproofCtxValue {
7
+ // addSubscriber: (label: String, fn: Function) => void
8
+ // database: Fireproof
9
+ // ready: boolean
10
+ // persist: () => void
11
+ // }
12
+ export const FireproofCtx = createContext({
14
13
  addSubscriber: () => {},
15
14
  database: null,
16
15
  ready: false
@@ -35,21 +34,21 @@ const initializeDatabase = name => {
35
34
  * @param [setupDatabaseFn] Asynchronous function that sets up the database, run this to load fixture data etc
36
35
  * @returns {FireproofCtxValue} { addSubscriber, database, ready }
37
36
  */
38
- export function useFireproof(
39
- defineDatabaseFn = (database: Fireproof) => {},
40
- setupDatabaseFn = async (database: Fireproof) => {},
41
- name: string
42
- ): FireproofCtxValue {
37
+ export function useFireproof (
38
+ defineDatabaseFn = () => {},
39
+ setupDatabaseFn = async () => {},
40
+ name
41
+ ) {
43
42
  const [ready, setReady] = useState(false)
44
43
  initializeDatabase(name || 'useFireproof')
45
44
  const localStorageKey = 'fp.' + database.name
46
45
 
47
- const addSubscriber = (label: String, fn: Function) => {
46
+ const addSubscriber = (label, fn) => {
48
47
  inboundSubscriberQueue.set(label, fn)
49
48
  }
50
49
 
51
50
  const listenerCallback = async event => {
52
- localSet(localStorageKey, JSON.stringify(database))
51
+ localSet(localStorageKey, JSON.stringify(database))
53
52
  if (event._external) return
54
53
  for (const [, fn] of inboundSubscriberQueue) fn()
55
54
  }
@@ -84,7 +83,7 @@ export function useFireproof(
84
83
  localSet(localStorageKey, JSON.stringify(database))
85
84
  }
86
85
  setReady(true)
87
- listener.on('*', listenerCallback)//hushed('*', listenerCallback, 250))
86
+ listener.on('*', listenerCallback)// hushed('*', listenerCallback, 250))
88
87
  }
89
88
  doSetup()
90
89
  }, [ready])
@@ -99,32 +98,32 @@ export function useFireproof(
99
98
  }
100
99
  }
101
100
 
102
- const husherMap = new Map()
103
- const husher = (id: string, workFn: { (): Promise<any> }, ms: number) => {
104
- if (!husherMap.has(id)) {
105
- const start: number = Date.now()
106
- husherMap.set(
107
- id,
108
- workFn().finally(() => setTimeout(() => husherMap.delete(id), ms - (Date.now() - start)))
109
- )
110
- }
111
- return husherMap.get(id)
112
- }
113
- const hushed =
114
- (id: string, workFn: { (...args): Promise<any> }, ms: number) =>
115
- (...args) =>
116
- husher(id, () => workFn(...args), ms)
101
+ // const husherMap = new Map()
102
+ // const husher = (id, workFn, ms) => {
103
+ // if (!husherMap.has(id)) {
104
+ // const start = Date.now()
105
+ // husherMap.set(
106
+ // id,
107
+ // workFn().finally(() => setTimeout(() => husherMap.delete(id), ms - (Date.now() - start)))
108
+ // )
109
+ // }
110
+ // return husherMap.get(id)
111
+ // }
112
+ // const hushed =
113
+ // (id, workFn, ms) =>
114
+ // (...args) =>
115
+ // husher(id, () => workFn(...args), ms)
117
116
 
118
117
  let storageSupported = false
119
118
  try {
120
119
  storageSupported = window.localStorage && true
121
120
  } catch (e) {}
122
- export function localGet(key: string) {
121
+ export function localGet (key) {
123
122
  if (storageSupported) {
124
123
  return localStorage && localStorage.getItem(key)
125
124
  }
126
125
  }
127
- function localSet(key: string, value: string) {
126
+ function localSet (key, value) {
128
127
  if (storageSupported) {
129
128
  return localStorage && localStorage.setItem(key, value)
130
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireproof/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Realtime database for IPFS",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  ],
33
33
  "license": "Apache-2.0 OR MIT",
34
34
  "dependencies": {
35
+ "prolly-trees": "1.0.4",
35
36
  "@ipld/car": "^5.1.0",
36
37
  "@ipld/dag-cbor": "^9.0.0",
37
38
  "archy": "^1.0.0",
@@ -42,7 +43,6 @@
42
43
  "encrypted-block": "^0.0.3",
43
44
  "idb": "^7.1.1",
44
45
  "multiformats": "^11.0.1",
45
- "prolly-trees": "1.0.3",
46
46
  "sade": "^1.8.1"
47
47
  },
48
48
  "devDependencies": {
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
@@ -124,6 +109,7 @@ export default class DbIndex {
124
109
  this.mapFn = mapFn
125
110
  this.mapFnString = mapFn.toString()
126
111
  }
112
+ this.name = opts.name || this.makeName()
127
113
  this.indexById = { root: null, cid: null }
128
114
  this.indexByKey = { root: null, cid: null }
129
115
  this.dbHead = null
@@ -137,42 +123,43 @@ export default class DbIndex {
137
123
  if (!opts.temporary) { DbIndex.registerWithDatabase(this, this.database) }
138
124
  }
139
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]
130
+ }
131
+
140
132
  static registerWithDatabase (inIndex, database) {
141
- // console.log('.reg > in Index', inIndex.instanceId, { live: !!inIndex.mapFn }, inIndex.indexByKey, inIndex.mapFnString)
142
- if (database.indexes.has(inIndex.mapFnString)) {
133
+ if (!database.indexes.has(inIndex.mapFnString)) {
134
+ database.indexes.set(inIndex.mapFnString, inIndex)
135
+ } else {
143
136
  // merge our inIndex code with the inIndex clock or vice versa
144
- // keep the code instance, discard the clock instance
145
137
  const existingIndex = database.indexes.get(inIndex.mapFnString)
146
- // console.log('.reg - existingIndex', existingIndex.instanceId, { live: !!inIndex.mapFn }, existingIndex.indexByKey)
138
+ // keep the code instance, discard the clock instance
147
139
  if (existingIndex.mapFn) { // this one also has other config
148
140
  existingIndex.dbHead = inIndex.dbHead
149
141
  existingIndex.indexById.cid = inIndex.indexById.cid
150
142
  existingIndex.indexByKey.cid = inIndex.indexByKey.cid
151
143
  } else {
152
- // console.log('.reg use inIndex with existingIndex clock')
153
144
  inIndex.dbHead = existingIndex.dbHead
154
145
  inIndex.indexById.cid = existingIndex.indexById.cid
155
146
  inIndex.indexByKey.cid = existingIndex.indexByKey.cid
156
147
  database.indexes.set(inIndex.mapFnString, inIndex)
157
148
  }
158
- } else {
159
- // console.log('.reg - fresh')
160
- database.indexes.set(inIndex.mapFnString, inIndex)
161
149
  }
162
- // console.log('.reg after', JSON.stringify([...database.indexes.values()].map(i => [i.instanceId, typeof i.mapFn, i.indexByKey, i.indexById])))
163
150
  }
164
151
 
165
152
  toJSON () {
166
- const indexJson = { code: this.mapFnString, clock: { db: null, byId: null, byKey: null } }
153
+ const indexJson = { name: this.name, code: this.mapFnString, clock: { db: null, byId: null, byKey: null } }
167
154
  indexJson.clock.db = this.dbHead?.map(cid => cid.toString())
168
155
  indexJson.clock.byId = this.indexById.cid?.toString()
169
156
  indexJson.clock.byKey = this.indexByKey.cid?.toString()
170
157
  return indexJson
171
158
  }
172
159
 
173
- static fromJSON (database, { code, clock }) {
160
+ static fromJSON (database, { code, clock, name }) {
174
161
  // console.log('DbIndex.fromJSON', database.constructor.name, code, clock)
175
- return new DbIndex(database, code, clock)
162
+ return new DbIndex(database, code, clock, { name })
176
163
  }
177
164
 
178
165
  /**
@@ -191,23 +178,17 @@ export default class DbIndex {
191
178
  */
192
179
  async query (query, update = true) {
193
180
  // const callId = Math.random().toString(36).substring(2, 7)
194
- // if (!root) {
195
- // pass a root to query a snapshot
181
+ // todo pass a root to query a snapshot
196
182
  // console.time(callId + '.#updateIndex')
197
183
  update && await this.#updateIndex(this.database.indexBlocks)
198
184
  // console.timeEnd(callId + '.#updateIndex')
199
-
200
- // }
201
185
  // console.time(callId + '.doIndexQuery')
202
186
  // console.log('query', query)
203
187
  const response = await doIndexQuery(this.database.indexBlocks, this.indexByKey, query)
204
188
  // console.timeEnd(callId + '.doIndexQuery')
205
-
206
189
  return {
207
190
  proof: { index: await cidsToProof(response.cids) },
208
- // TODO fix this naming upstream in prolly/db-DbIndex?
209
191
  rows: response.result.map(({ id, key, row }) => {
210
- // console.log('query', id, key, row)
211
192
  return ({ id, key: charwise.decode(key), value: row })
212
193
  })
213
194
  }
@@ -322,20 +303,22 @@ async function loadIndex (blocks, index, indexOpts) {
322
303
  return index.root
323
304
  }
324
305
 
306
+ async function applyLimit (results, limit) {
307
+ results.result = results.result.slice(0, limit)
308
+ return results
309
+ }
310
+
325
311
  async function doIndexQuery (blocks, indexByKey, query = {}) {
326
312
  await loadIndex(blocks, indexByKey, dbIndexOpts)
313
+ if (!indexByKey.root) return { result: [] }
327
314
  if (query.range) {
328
315
  const encodedRange = query.range.map((key) => charwise.encode(key))
329
- return indexByKey.root.range(...encodedRange)
316
+ return applyLimit(await indexByKey.root.range(...encodedRange), query.limit)
330
317
  } else if (query.key) {
331
318
  const encodedKey = charwise.encode(query.key)
332
319
  return indexByKey.root.get(encodedKey)
333
320
  } else {
334
- if (indexByKey.root) {
335
- const { result, ...all } = await indexByKey.root.getAllEntries()
336
- return { result: result.map(({ key: [k, id], value }) => ({ key: k, id, row: value })), ...all }
337
- } else {
338
- return { result: [] }
339
- }
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)
340
323
  }
341
324
  }
package/src/fireproof.js CHANGED
@@ -138,27 +138,6 @@ export default class Fireproof {
138
138
  }
139
139
  }
140
140
 
141
- /**
142
- * Registers a Listener to be called when the Fireproof instance's clock is updated.
143
- * Recieves live changes from the database after they are committed.
144
- * @param {Function} listener - The listener to be called when the clock is updated.
145
- * @returns {Function} - A function that can be called to unregister the listener.
146
- * @memberof Fireproof
147
- */
148
- registerListener (listener) {
149
- this.#listeners.add(listener)
150
- return () => {
151
- this.#listeners.delete(listener)
152
- }
153
- }
154
-
155
- async #notifyListeners (changes) {
156
- // await sleep(10)
157
- for (const listener of this.#listeners) {
158
- await listener(changes)
159
- }
160
- }
161
-
162
141
  /**
163
142
  * Runs validation on the specified document using the Fireproof instance's configuration. Throws an error if the document is invalid.
164
143
  *
@@ -177,6 +156,35 @@ export default class Fireproof {
177
156
  }
178
157
  }
179
158
 
159
+ /**
160
+ * Retrieves the document with the specified ID from the database
161
+ *
162
+ * @param {string} key - the ID of the document to retrieve
163
+ * @param {Object} [opts] - options
164
+ * @returns {Object<{_id: string, ...doc: Object}>} - the document with the specified ID
165
+ * @memberof Fireproof
166
+ * @instance
167
+ */
168
+ async get (key, opts = {}) {
169
+ const clock = opts.clock || this.clock
170
+ const resp = await get(this.blocks, clock, charwise.encode(key))
171
+
172
+ // this tombstone is temporary until we can get the prolly tree to delete
173
+ if (!resp || resp.result === null) {
174
+ throw new Error('Not found')
175
+ }
176
+ const doc = resp.result
177
+ if (opts.mvcc === true) {
178
+ doc._clock = this.clockToJSON()
179
+ }
180
+ doc._proof = {
181
+ data: await cidsToProof(resp.cids),
182
+ clock: this.clockToJSON()
183
+ }
184
+ doc._id = key
185
+ return doc
186
+ }
187
+
180
188
  /**
181
189
  * Adds a new document to the database, or updates an existing document. Returns the ID of the document and the new clock head.
182
190
  *
@@ -210,9 +218,9 @@ export default class Fireproof {
210
218
  id = docOrId
211
219
  }
212
220
  await this.#runValidation({ _id: id, _deleted: true })
213
- // return await this.#putToProllyTree({ key: id, del: true }) // not working at prolly tree layer?
221
+ return await this.#putToProllyTree({ key: id, del: true }, clock) // not working at prolly tree layer?
214
222
  // this tombstone is temporary until we can get the prolly tree to delete
215
- return await this.#putToProllyTree({ key: id, value: null }, clock)
223
+ // return await this.#putToProllyTree({ key: id, value: null }, clock)
216
224
  }
217
225
 
218
226
  /**
@@ -262,49 +270,6 @@ export default class Fireproof {
262
270
  // return this.clock
263
271
  // }
264
272
 
265
- /**
266
- * Displays a visualization of the current clock in the console
267
- */
268
- // async visClock () {
269
- // const shortLink = (l) => `${String(l).slice(0, 4)}..${String(l).slice(-4)}`
270
- // const renderNodeLabel = (event) => {
271
- // return event.value.data.type === 'put'
272
- // ? `${shortLink(event.cid)}\\nput(${shortLink(event.value.data.key)},
273
- // {${Object.values(event.value.data.value)}})`
274
- // : `${shortLink(event.cid)}\\ndel(${event.value.data.key})`
275
- // }
276
- // for await (const line of vis(this.blocks, this.clock, { renderNodeLabel })) console.log(line)
277
- // }
278
-
279
- /**
280
- * Retrieves the document with the specified ID from the database
281
- *
282
- * @param {string} key - the ID of the document to retrieve
283
- * @param {Object} [opts] - options
284
- * @returns {Object<{_id: string, ...doc: Object}>} - the document with the specified ID
285
- * @memberof Fireproof
286
- * @instance
287
- */
288
- async get (key, opts = {}) {
289
- const clock = opts.clock || this.clock
290
- const resp = await get(this.blocks, clock, charwise.encode(key))
291
-
292
- // this tombstone is temporary until we can get the prolly tree to delete
293
- if (!resp || resp.result === null) {
294
- throw new Error('Not found')
295
- }
296
- const doc = resp.result
297
- if (opts.mvcc === true) {
298
- doc._clock = this.clockToJSON()
299
- }
300
- doc._proof = {
301
- data: await cidsToProof(resp.cids),
302
- clock: this.clockToJSON()
303
- }
304
- doc._id = key
305
- return doc
306
- }
307
-
308
273
  async * vis () {
309
274
  return yield * vis(this.blocks, this.clock)
310
275
  }
@@ -317,6 +282,27 @@ export default class Fireproof {
317
282
  return await visMerkleClock(this.blocks, this.clock)
318
283
  }
319
284
 
285
+ /**
286
+ * Registers a Listener to be called when the Fireproof instance's clock is updated.
287
+ * Recieves live changes from the database after they are committed.
288
+ * @param {Function} listener - The listener to be called when the clock is updated.
289
+ * @returns {Function} - A function that can be called to unregister the listener.
290
+ * @memberof Fireproof
291
+ */
292
+ registerListener (listener) {
293
+ this.#listeners.add(listener)
294
+ return () => {
295
+ this.#listeners.delete(listener)
296
+ }
297
+ }
298
+
299
+ async #notifyListeners (changes) {
300
+ // await sleep(10)
301
+ for (const listener of this.#listeners) {
302
+ await listener(changes)
303
+ }
304
+ }
305
+
320
306
  setCarUploader (carUploaderFn) {
321
307
  // console.log('registering car uploader')
322
308
  // https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
package/src/hydrator.js CHANGED
@@ -8,14 +8,15 @@ export default class Hydrator {
8
8
  static fromJSON (json, database) {
9
9
  database.hydrate({ clock: json.clock.map(c => parseCID(c)), name: json.name, key: json.key })
10
10
  if (json.indexes) {
11
- for (const { code, clock: { byId, byKey, db } } of json.indexes) {
11
+ for (const { name, code, clock: { byId, byKey, db } } of json.indexes) {
12
12
  DbIndex.fromJSON(database, {
13
13
  clock: {
14
14
  byId: byId ? parseCID(byId) : null,
15
15
  byKey: byKey ? parseCID(byKey) : null,
16
16
  db: db ? db.map(c => parseCID(c)) : null
17
17
  },
18
- code
18
+ code,
19
+ name
19
20
  })
20
21
  }
21
22
  }
package/src/listener.js CHANGED
@@ -80,9 +80,6 @@ function getTopicList (subscribersMap, name) {
80
80
  return topicList
81
81
  }
82
82
 
83
- // copied from src/db-index.js
84
- const makeDoc = ({ key, value }) => ({ _id: key, ...value })
85
-
86
83
  /**
87
84
  * Transforms a set of changes to events using an emitter function.
88
85
  *
@@ -95,7 +92,7 @@ const topicsForChanges = (changes, routingFn) => {
95
92
  const seenTopics = new Map()
96
93
  changes.forEach(({ key, value, del }) => {
97
94
  if (del || !value) value = { _deleted: true }
98
- routingFn(makeDoc({ key, value }), t => {
95
+ routingFn(({ _id: key, ...value }), t => {
99
96
  const topicList = getTopicList(seenTopics, t)
100
97
  topicList.push(key)
101
98
  })
package/src/prolly.js CHANGED
@@ -67,12 +67,13 @@ async function createAndSaveNewEvent ({
67
67
  let cids
68
68
  const { key, value, del } = inEvent
69
69
  const data = {
70
- type: 'put',
71
- root: {
72
- cid: root.cid,
73
- bytes: root.bytes,
74
- value: root.value
75
- },
70
+ root: (root
71
+ ? {
72
+ cid: root.cid,
73
+ bytes: root.bytes, // can we remove this?
74
+ value: root.value // can we remove this?
75
+ }
76
+ : null),
76
77
  key
77
78
  }
78
79
 
@@ -81,6 +82,7 @@ async function createAndSaveNewEvent ({
81
82
  data.type = 'del'
82
83
  } else {
83
84
  data.value = value
85
+ data.type = 'put'
84
86
  }
85
87
  /** @type {EventData} */
86
88
 
@@ -115,13 +117,27 @@ const makeGetAndPutBlock = (inBlocks) => {
115
117
  return { getBlock, bigPut, blocks: inBlocks, cids }
116
118
  }
117
119
 
118
- const bulkFromEvents = (sorted) =>
119
- sorted.map(({ value: event }) => {
120
+ const bulkFromEvents = (sorted, event) => {
121
+ if (event) {
122
+ const update = { value: { data: { key: event.key } } }
123
+ if (event.del) {
124
+ update.value.data.type = 'del'
125
+ } else {
126
+ update.value.data.type = 'put'
127
+ update.value.data.value = event.value
128
+ }
129
+ sorted.push(update)
130
+ }
131
+ const bulk = new Map()
132
+ for (const { value: event } of sorted) {
120
133
  const {
121
134
  data: { type, value, key }
122
135
  } = event
123
- return type === 'put' ? { key, value } : { key, del: true }
124
- })
136
+ const bulkEvent = type === 'put' ? { key, value } : { key, del: true }
137
+ bulk.set(bulkEvent.key, bulkEvent) // last wins
138
+ }
139
+ return Array.from(bulk.values())
140
+ }
125
141
 
126
142
  // Get the value of the root from the ancestor event
127
143
  /**
@@ -136,7 +152,47 @@ const prollyRootFromAncestor = async (events, ancestor, getBlock) => {
136
152
  const event = await events.get(ancestor)
137
153
  const { root } = event.value.data
138
154
  // console.log('prollyRootFromAncestor', root.cid, JSON.stringify(root.value))
139
- return load({ cid: root.cid, get: getBlock, ...blockOpts })
155
+ if (root) {
156
+ return load({ cid: root.cid, get: getBlock, ...blockOpts })
157
+ } else {
158
+ return null
159
+ }
160
+ }
161
+
162
+ const doProllyBulk = async (inBlocks, head, event) => {
163
+ const { getBlock, blocks } = makeGetAndPutBlock(inBlocks)
164
+ let bulkSorted = []
165
+ let prollyRootNode = null
166
+ if (head.length) {
167
+ // Otherwise, we find the common ancestor and update the root and other blocks
168
+ const events = new EventFetcher(blocks)
169
+ // todo this is returning more events than necessary, lets define the desired semantics from the top down
170
+ // good semantics mean we can cache the results of this call
171
+ const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
172
+ bulkSorted = sorted
173
+ // console.log('sorted', JSON.stringify(sorted.map(({ value: { data: { key, value } } }) => ({ key, value }))))
174
+ prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
175
+ // console.log('event', event)
176
+ }
177
+
178
+ const bulkOperations = bulkFromEvents(bulkSorted, event)
179
+
180
+ // if prolly root node is null, we need to create a new one
181
+ if (!prollyRootNode) {
182
+ let root
183
+ const newBlocks = []
184
+ // if all operations are deletes, we can just return an empty root
185
+ if (bulkOperations.every((op) => op.del)) {
186
+ return { root: null, blocks: [] }
187
+ }
188
+ for await (const node of create({ get: getBlock, list: bulkOperations, ...blockOpts })) {
189
+ root = await node.block
190
+ newBlocks.push(root)
191
+ }
192
+ return { root, blocks: newBlocks }
193
+ } else {
194
+ return await prollyRootNode.bulk(bulkOperations) // { root: newProllyRootNode, blocks: newBlocks }
195
+ }
140
196
  }
141
197
 
142
198
  /**
@@ -150,44 +206,45 @@ const prollyRootFromAncestor = async (events, ancestor, getBlock) => {
150
206
  * @returns {Promise<Result>}
151
207
  */
152
208
  export async function put (inBlocks, head, event, options) {
153
- const { getBlock, bigPut, blocks } = makeGetAndPutBlock(inBlocks)
209
+ const { bigPut } = makeGetAndPutBlock(inBlocks)
154
210
 
155
211
  // If the head is empty, we create a new event and return the root and addition blocks
156
212
  if (!head.length) {
157
213
  const additions = new Map()
158
- let root
159
- for await (const node of create({ get: getBlock, list: [event], ...blockOpts })) {
160
- root = await node.block
161
- bigPut(root, additions)
214
+ const { root, blocks } = await doProllyBulk(inBlocks, head, event)
215
+ for (const b of blocks) {
216
+ bigPut(b, additions)
162
217
  }
163
218
  return createAndSaveNewEvent({ inBlocks, bigPut, root, event, head, additions: Array.from(additions.values()) })
164
219
  }
220
+ const { root: newProllyRootNode, blocks: newBlocks } = await doProllyBulk(inBlocks, head, event)
165
221
 
166
- // Otherwise, we find the common ancestor and update the root and other blocks
167
- const events = new EventFetcher(blocks)
168
- // todo this is returning more events than necessary, lets define the desired semantics from the top down
169
- // good semantics mean we can cache the results of this call
170
- const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
171
- // console.log('sorted', JSON.stringify(sorted.map(({ value: { data: { key, value } } }) => ({ key, value }))))
172
- const prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
173
-
174
- const bulkOperations = bulkFromEvents(sorted)
175
- const { root: newProllyRootNode, blocks: newBlocks } = await prollyRootNode.bulk([...bulkOperations, event]) // ading delete support here
176
- const prollyRootBlock = await newProllyRootNode.block
177
- const additions = new Map() // ; const removals = new Map()
178
- bigPut(prollyRootBlock, additions)
179
- for (const nb of newBlocks) {
180
- bigPut(nb, additions)
222
+ if (!newProllyRootNode) {
223
+ return createAndSaveNewEvent({
224
+ inBlocks,
225
+ bigPut,
226
+ root: null,
227
+ event,
228
+ head,
229
+ additions: []
230
+ })
231
+ } else {
232
+ const prollyRootBlock = await newProllyRootNode.block
233
+ const additions = new Map() // ; const removals = new Map()
234
+ bigPut(prollyRootBlock, additions)
235
+ for (const nb of newBlocks) {
236
+ bigPut(nb, additions)
237
+ }
238
+ // additions are new blocks
239
+ return createAndSaveNewEvent({
240
+ inBlocks,
241
+ bigPut,
242
+ root: prollyRootBlock,
243
+ event,
244
+ head,
245
+ additions: Array.from(additions.values()) /*, todo? Array.from(removals.values()) */
246
+ })
181
247
  }
182
- // additions are new blocks
183
- return createAndSaveNewEvent({
184
- inBlocks,
185
- bigPut,
186
- root: prollyRootBlock,
187
- event,
188
- head,
189
- additions: Array.from(additions.values()) /*, todo? Array.from(removals.values()) */
190
- })
191
248
  }
192
249
 
193
250
  /**
@@ -200,25 +257,15 @@ export async function root (inBlocks, head) {
200
257
  if (!head.length) {
201
258
  throw new Error('no head')
202
259
  }
203
- const { getBlock, blocks } = makeGetAndPutBlock(inBlocks)
204
- const events = new EventFetcher(blocks)
205
- const { ancestor, sorted } = await findCommonAncestorWithSortedEvents(events, head)
206
- const prollyRootNode = await prollyRootFromAncestor(events, ancestor, getBlock)
207
-
208
- // Perform bulk operations (put or delete) for each event in the sorted array
209
- const bulkOperations = bulkFromEvents(sorted)
210
- const { root: newProllyRootNode, blocks: newBlocks } = await prollyRootNode.bulk(bulkOperations)
211
- // const prollyRootBlock = await newProllyRootNode.block
212
- // console.log('newBlocks', newBlocks.map((nb) => nb.cid.toString()))
260
+ const { root: newProllyRootNode, blocks: newBlocks, cids } = await doProllyBulk(inBlocks, head)
213
261
  // todo maybe these should go to a temp blockstore?
214
262
  await doTransaction('root', inBlocks, async (transactionBlockstore) => {
215
263
  const { bigPut } = makeGetAndPutBlock(transactionBlockstore)
216
264
  for (const nb of newBlocks) {
217
265
  bigPut(nb)
218
266
  }
219
- // bigPut(prollyRootBlock)
220
267
  })
221
- return { cids: events.cids, node: newProllyRootNode }
268
+ return { cids, node: newProllyRootNode }
222
269
  }
223
270
 
224
271
  /**
@@ -252,6 +299,9 @@ export async function getAll (blocks, head) {
252
299
  return { clockCIDs: new CIDCounter(), cids: new CIDCounter(), result: [] }
253
300
  }
254
301
  const { node: prollyRootNode, cids: clockCIDs } = await root(blocks, head)
302
+ if (!prollyRootNode) {
303
+ return { clockCIDs, cids: new CIDCounter(), result: [] }
304
+ }
255
305
  const { result, cids } = await prollyRootNode.getAllEntries() // todo params
256
306
  return { clockCIDs, cids, result: result.map(({ key, value }) => ({ key, value })) }
257
307
  }
@@ -267,6 +317,9 @@ export async function get (blocks, head, key) {
267
317
  return { cids: new CIDCounter(), result: null }
268
318
  }
269
319
  const { node: prollyRootNode, cids: clockCIDs } = await root(blocks, head)
320
+ if (!prollyRootNode) {
321
+ return { clockCIDs, cids: new CIDCounter(), result: null }
322
+ }
270
323
  const { result, cids } = await prollyRootNode.get(key)
271
324
  return { result, cids, clockCIDs }
272
325
  }
@@ -27,7 +27,10 @@ describe('DbIndex query', () => {
27
27
  }
28
28
  index = new DbIndex(database, function (doc, map) {
29
29
  map(doc.age, doc.name)
30
- })
30
+ }, null, { name: 'namesByAge' })
31
+ })
32
+ it('has a name', () => {
33
+ assert.equal(index.name, 'namesByAge')
31
34
  })
32
35
  it('query index range', async () => {
33
36
  const result = await index.query({ range: [41, 49] })
@@ -56,6 +59,14 @@ describe('DbIndex query', () => {
56
59
  assert.equal(result.rows[0].value, 'emily')
57
60
  assert.equal(result.rows[result.rows.length - 1].value, 'dave')
58
61
  })
62
+ it('query index limit', async () => {
63
+ const result = await index.query({ limit: 3 })
64
+ assert(result, 'did return result')
65
+ assert(result.rows)
66
+ assert.equal(result.rows.length, 3, 'six row matched')
67
+ assert.equal(result.rows[0].key, 4)
68
+ assert.equal(result.rows[0].value, 'emily')
69
+ })
59
70
  it('query index NaN', async () => {
60
71
  const result = await index.query({ range: [NaN, 44] })
61
72
  assert(result, 'did return result')
@@ -236,6 +247,9 @@ describe('DbIndex query with bad index definition', () => {
236
247
  map(doc.oops.missingField, doc.name)
237
248
  })
238
249
  })
250
+ it('has a default name', () => {
251
+ assert.equal(index.name, 'doc.oops.missingField, doc.name')
252
+ })
239
253
  it('query index range', async () => {
240
254
  const oldErrFn = console.error
241
255
  console.error = () => {}
@@ -124,6 +124,14 @@ describe('Fireproof', () => {
124
124
  const changes = await db.changesSince()
125
125
  assert.equal(changes.rows.length, 0)
126
126
  })
127
+ it('delete on an empty database', async () => {
128
+ const db = Fireproof.storage()
129
+ assert(db instanceof Fireproof)
130
+ const e = await db.del('8c5c0c5c0c5c').catch((err) => err)
131
+ assert.equal(e.id, '8c5c0c5c0c5c')
132
+ const changes = await db.changesSince()
133
+ assert.equal(changes.rows.length, 0)
134
+ })
127
135
  it('update existing document', async () => {
128
136
  // const alice = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
129
137
  // assert.equal(alice.name, 'alice')
@@ -194,7 +202,7 @@ describe('Fireproof', () => {
194
202
  const e = await database.get('missing').catch((e) => e)
195
203
  assert.equal(e.message, 'Not found')
196
204
  })
197
- it('delete a document', async () => {
205
+ it('delete the only document', async () => {
198
206
  const id = '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c'
199
207
  const found = await database.get(id)
200
208
  assert.equal(found._id, id)
@@ -212,6 +220,31 @@ describe('Fireproof', () => {
212
220
  assert.equal(e.message, 'Not found')
213
221
  })
214
222
 
223
+ it('delete not last document', async () => {
224
+ const resp1 = await database.put({
225
+ _id: 'second',
226
+ name: 'bob',
227
+ age: 39
228
+ })
229
+
230
+ // const id = '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c'
231
+ const id = resp1.id
232
+ const found = await database.get(id)
233
+ assert.equal(found._id, id)
234
+ const deleted = await database.del(id)
235
+ assert.equal(deleted.id, id)
236
+ const e = await database
237
+ .get(id)
238
+ .then((doc) => assert.equal('should be deleted', JSON.stringify(doc)))
239
+ .catch((e) => {
240
+ if (e.message !== 'Not found') {
241
+ throw e
242
+ }
243
+ return e
244
+ })
245
+ assert.equal(e.message, 'Not found')
246
+ })
247
+
215
248
  it("delete a document with validation function that doesn't allow it", async () => {
216
249
  const validationDatabase = new Fireproof(new Blockstore(), [], {
217
250
  validateChange: (newDoc, oldDoc, authCtx) => {
@@ -25,7 +25,7 @@ describe('DbIndex query', () => {
25
25
  }
26
26
  index = new DbIndex(database, function (doc, map) {
27
27
  map(doc.age, doc.name)
28
- })
28
+ }, null, { name: 'names_by_age' })
29
29
  })
30
30
  it('serialize database with index', async () => {
31
31
  await database.put({ _id: 'rehy', name: 'drate', age: 1 })
@@ -44,6 +44,8 @@ describe('DbIndex query', () => {
44
44
  assert.equal(serialized.indexes[0].code, `function (doc, map) {
45
45
  map(doc.age, doc.name)
46
46
  }`)
47
+ assert.equal(serialized.indexes[0].name, 'names_by_age')
48
+
47
49
  assert.equal(serialized.indexes[0].clock.byId.constructor.name, 'String')
48
50
  assert.equal(serialized.indexes[0].clock.byKey.constructor.name, 'String')
49
51
  assert.equal(serialized.indexes[0].clock.db[0].constructor.name, 'String')
@@ -71,6 +73,8 @@ describe('DbIndex query', () => {
71
73
  assert.equal(newIndex.indexByKey.cid, 'bafyreicr5rpvsxnqchcwk5rxlmdvd3fah2vexmbsp2dvr4cfdxd2q2ycgu')
72
74
  // assert.equal(newIndex.indexByKey.root, null)
73
75
 
76
+ assert.equal(newIndex.name, 'names_by_age')
77
+
74
78
  const newResult = await newIndex.query({ range: [0, 54] })
75
79
  assert.equal(newResult.rows[0].value, 'drate')
76
80
  })