@fireproof/core 0.0.8 → 0.0.10

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/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # 🔥 Fireproof
2
+
3
+ Fireproof is a realtime database for today's interactive applications. It uses immutable data and distributed protocols
4
+ to offer a new kind of database that:
5
+ - can be embedded in any page or app, with a flexible data ownership model
6
+ - scales without incurring developer costs, thanks to Filecoin
7
+ - uses cryptographically verifiable protocols (what plants crave)
8
+
9
+ Learn more about the [concepts and architecture behind Fireproof](https://fireproof.storage/documentation/how-the-database-engine-works/), or jump to the [quick start](#quick-start) for React and server-side examples.
10
+
11
+ ### Status
12
+
13
+ Fireproof is alpha software, you should only use it if you are planning to contribute. For now, [check out our React TodoMVC implementation running in browser-local mode.](https://main--lucky-naiad-5aa507.netlify.app/) It demonstrates document persistence, index queries, and event subscriptions, and uses the [`useFireproof()` React hook.](https://github.com/fireproof-storage/fireproof/blob/main/packages/fireproof/hooks/use-fireproof.tsx)
14
+
15
+ [![Test](https://github.com/jchris/fireproof/actions/workflows/test.yml/badge.svg)](https://github.com/jchris/fireproof/actions/workflows/test.yml)
16
+ [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ import Fireproof from 'fireproof';
22
+
23
+ async function main() {
24
+ const database = Fireproof.storage('my-db');
25
+ const ok = await database.put({
26
+ name: 'alice',
27
+ age: 42
28
+ });
29
+
30
+ const doc = await database.get(ok.id);
31
+ console.log(doc.name); // 'alice'
32
+ }
33
+
34
+ main();
35
+ ```
36
+
37
+ ## Features
38
+
39
+ ### Document Store
40
+
41
+ A simple put, get, and delete interface for keeping track of all your JSON documents. Once your data is in Fireproof you can access it from any app or website. Fireproof document store uses MVCC versioning and Merkle clocks so you can always recover the version you are looking for.
42
+
43
+ ```js
44
+ const { id, ref } = await database.put({
45
+ _id: 'three-thousand'
46
+ name: 'André',
47
+ age: 47
48
+ });
49
+ const doc = await database.get('three-thousand', {mvcc : true}) // mvcc is optional
50
+ // {
51
+ // _id : 'three-thousand'
52
+ // _clock : CID(bafy84...agfw7)
53
+ // name : 'André',
54
+ // age : 47
55
+ // }
56
+ ```
57
+
58
+ The `_clock` allows you to query a stable snapshot of that version of the database. Fireproof uses immutable data structures under the hood, so you can always rollback to old data. Files can be embedded anywhere in your document using IPFS links like `{"/":"bafybeih3e3zdiehbqfpxzpppxrb6kaaw4xkbqzyr2f5pwr5refq2te2ape"}`, with API sugar coming soon.
59
+
60
+ ### Flexible Indexes
61
+
62
+ Fireproof indexes are defined by custom JavaScript functions that you write, allowing you to easily index and search your data in the way that works best for your application. Easily handle data variety and schema drift by normalizing any data to the desired index.
63
+
64
+ ```js
65
+ const index = new Index(database, function (doc, map) {
66
+ map(doc.age, doc.name)
67
+ })
68
+ const { rows, ref } = await index.query({ range: [40, 52] })
69
+ // [ { key: 42, value: 'alice', id: 'a1s3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c' },
70
+ // { key: 47, value: 'André', id: 'three-thousand' } ]
71
+ ```
72
+
73
+ ### Realtime Updates
74
+
75
+ Subscribe to query changes in your application, so your UI updates automatically. Use the supplied React hooks, our Redux connector, or simple function calls to be notified of relevant changes.
76
+
77
+ ```js
78
+ const listener = new Listener(database, function(doc, emit) {
79
+ if (doc.type == 'member') {
80
+ emit('member')
81
+ }
82
+ })
83
+ listener.on('member', (id) => {
84
+ const doc = await db.get(id)
85
+ alert(`Member update ${doc.name}`)
86
+ })
87
+ ```
88
+
89
+ ### Self-sovereign Identity
90
+
91
+ Fireproof is so easy to integrate with any site or app because you can get started right away, and set up an account later. By default users write to their own database copy, so you can get pretty far before you even have to think about API keys. [Authorization is via non-extractable keypair](https://ucan.xyz), like TouchID / FaceID.
92
+
93
+ ### Automatic Replication
94
+
95
+ Documents changes are persisted to [Filecoin](https://filecoin.io) via [web3.storage](https://web3.storage), and made available over [IPFS] and on a global content delivery network. All you need to do to sync state is send a link to the latest database head, and Fireproof will take care of the rest. [Learn how to enable replication.](#status)
96
+
97
+ ### Cryptographic Proofs
98
+
99
+ The [UCAN protocol](https://ucan.xyz) verifably links Fireproof updates to authorized agents via cryptographic proof chains. These proofs are portable like bearer tokens, but because invocations are signed by end-user device keys, UCAN proofs don't need to be hidden to be secure, allowing for delegation of service capabilities across devices and parties. Additionally, Fireproof's Merkle clocks and hash trees are immutable and self-validating, making merging changes safe and efficient. Fireproof makes cryptographic proofs available for all of it's operations, making it an ideal verfiable document database for smart contracts and other applications running in trustless environments. [Proof chains provide performance benefits as well](https://purrfect-tracker-45c.notion.site/Data-Routing-23c37b269b4c4c3dacb60d0077113bcb), by allowing recipients to skip costly I/O operations and instead cryptographically verify that changes contain all of the required context.
100
+
101
+ ## Limitations 💣
102
+
103
+ ### Security
104
+
105
+ Until encryption support is enabled, all data written to Fireproof is public. There are no big hurdles for this feature but it's not ready yet.
106
+
107
+ ### Replication
108
+
109
+ Currently Fireproof writes transactions and proofs to [CAR files](https://ipld.io/specs/transport/car/carv2/) which are well suited for peer and cloud replication. They are stored in IndexedDB locally, with cloud replication coming very soon.
110
+
111
+ ### Pre-beta Software
112
+
113
+ While the underlying data structures and libraries Fireproof uses are trusted with billions of dollars worth of data, Fireproof started in February of 2023. Results may vary.
114
+
115
+ ## Thanks 🙏
116
+
117
+ Fireproof is a synthesis of work done by people in the web community over the years. I couldn't even begin to name all the folks who made pivotal contributions. Without npm, React, and VS Code all this would have taken so much longer. Thanks to everyone who supported me getting into database development via Apache CouchDB, one of the original document databases. The distinguishing work on immutable datastructures comes from the years of consideration [IPFS](https://ipfs.tech), [IPLD](https://ipld.io), and the [Filecoin APIs](https://docs.filecoin.io) have enjoyed.
118
+
119
+ Thanks to Alan Shaw and Mikeal Rogers without whom this project would have never got started. The core Merkle hash-tree clock is based on [Alan's Pail](https://github.com/alanshaw/pail), and you can see the repository history goes all the way back to work begun as a branch of that repo. Mikeal wrote [the prolly trees implementation](https://github.com/mikeal/prolly-trees).
120
+
121
+ ## Quick Start
122
+
123
+ Look in the `examples/` directory for projects using the database. It's not picky how you use it, but we want to provide convenient jumping off places. Think of the examples as great to fork when starting your next project.
124
+
125
+ If are adding Fireproof to an existing page, just install it and try some operations.
126
+
127
+ ```sh
128
+ npm install @fireproof/core
129
+ ```
130
+
131
+ In your `app.js` or `app.tsx` file:
132
+
133
+ ```js
134
+ import { Fireproof } from '@fireproof/core'
135
+ const fireproof = Fireproof.storage()
136
+ const ok = await fireproof.put({ hello: 'world' })
137
+ const doc = await fireproof.get(ok.id)
138
+ ```
139
+
140
+ 🤫 I like to drop a `window.fireproof = fireproof` in there as a development aid.
141
+
142
+ # Contributing
143
+
144
+ Feel free to join in. All welcome. [Open an issue](https://github.com/jchris/fireproof/issues)!
145
+
146
+ # License
147
+
148
+ Dual-licensed under [MIT or Apache 2.0](https://github.com/jchris/fireproof/blob/main/LICENSE.md)
@@ -2,7 +2,8 @@
2
2
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
3
  // @ts-ignore
4
4
  import { useEffect, useState, createContext } from 'react'
5
- import { Fireproof, Listener } from '../index'
5
+ import { Fireproof, Listener, Hydrator } from '../index'
6
+
6
7
 
7
8
  export interface FireproofCtxValue {
8
9
  addSubscriber: (label: String, fn: Function) => void
@@ -18,6 +19,7 @@ export const FireproofCtx = createContext<FireproofCtxValue>({
18
19
  const inboundSubscriberQueue = new Map()
19
20
  const database = Fireproof.storage()
20
21
  const listener = new Listener(database)
22
+ let startedSetup = false;
21
23
 
22
24
  /**
23
25
  * @function useFireproof
@@ -30,16 +32,15 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
30
32
  const [ready, setReady] = useState(false)
31
33
  defineDatabaseFn = defineDatabaseFn || (() => {})
32
34
  setupDatabaseFn = setupDatabaseFn || (() => {})
35
+ // console.log('useFireproof', database, ready)
33
36
 
34
- if (!ready) {
35
- defineDatabaseFn(database)
36
- }
37
37
 
38
38
  const addSubscriber = (label: String, fn: Function) => {
39
39
  inboundSubscriberQueue.set(label, fn)
40
40
  }
41
41
 
42
42
  const listenerCallback = async () => {
43
+ // console.log ('listenerCallback', JSON.stringify(database))
43
44
  localSet('fireproof', JSON.stringify(database))
44
45
  for (const [, fn] of inboundSubscriberQueue) fn()
45
46
  }
@@ -47,20 +48,25 @@ export function useFireproof(defineDatabaseFn: Function, setupDatabaseFn: Functi
47
48
  useEffect(() => {
48
49
  const doSetup = async () => {
49
50
  if (ready) return
51
+ if (startedSetup) return
52
+ startedSetup = true
53
+ defineDatabaseFn(database) // define indexes before querying them
50
54
  const fp = localGet('fireproof')
51
55
  if (fp) {
52
- const { clock } = JSON.parse(fp)
56
+ const serialized = JSON.parse(fp)
57
+ // console.log('serialized', JSON.stringify(serialized.indexes.map(c => c.clock)))
53
58
  console.log("Loading previous database clock. (localStorage.removeItem('fireproof') to reset)")
54
- await database.setClock(clock)
59
+ Hydrator.fromJSON(serialized, database)
60
+ // await database.setClock(clock)
55
61
  try {
56
62
  const changes = await database.changesSince()
57
63
  if (changes.rows.length < 2) {
58
- console.log('Resetting database')
64
+ // console.log('Resetting database')
59
65
  throw new Error('Resetting database')
60
66
  }
61
67
  } catch (e) {
62
68
  console.error(`Error loading previous database clock. ${fp} Resetting.`, e)
63
- await database.setClock([])
69
+ await database.setClock([]) // todo this should be resetClock and also reset the indexes
64
70
  await setupDatabaseFn(database)
65
71
  localSet('fireproof', JSON.stringify(database))
66
72
  }
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Fireproof from './src/fireproof'
2
2
  import Index from './src/db-index'
3
3
  import Listener from './src/listener'
4
+ import Hydrator from './src/hydrator'
4
5
 
5
- export { Fireproof, Index, Listener }
6
+ export { Fireproof, Index, Listener, Hydrator }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireproof/core",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Realtime database for IPFS",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  "test:mocha": "mocha test/*.test.js",
10
10
  "test:watch": "npm run test:mocha -- -w --parallel test/*.test.js",
11
11
  "coverage": "c8 -r html -r text npm test",
12
+ "prepublishOnly" : "cp ../../README.md .",
12
13
  "lint": "standard",
13
14
  "lint:fix": "standard --fix"
14
15
  },
package/src/db-index.js CHANGED
@@ -1,12 +1,11 @@
1
- // import { create, load } from 'prolly-trees/db-index'
2
- import { create, load } from '../../../../prolly-trees/src/db-index.js'
1
+ import { create, load } from 'prolly-trees/db-index'
2
+ // import { create, load } from '../../../../prolly-trees/src/db-index.js'
3
3
 
4
4
  import { sha256 as hasher } from 'multiformats/hashes/sha2'
5
5
  import { nocache as cache } from 'prolly-trees/cache'
6
6
  import { bf, simpleCompare } from 'prolly-trees/utils'
7
7
  import { makeGetBlock } from './prolly.js'
8
8
  import { cidsToProof } from './fireproof.js'
9
- import { CID } from 'multiformats'
10
9
 
11
10
  import * as codec from '@ipld/dag-cbor'
12
11
  // import { create as createBlock } from 'multiformats/block'
@@ -73,16 +72,16 @@ const makeDoc = ({ key, value }) => ({ _id: key, ...value })
73
72
  * Transforms a set of changes to DbIndex entries using a map function.
74
73
  *
75
74
  * @param {ChangeEvent[]} changes
76
- * @param {Function} mapFun
75
+ * @param {Function} mapFn
77
76
  * @returns {DbIndexEntry[]} The DbIndex entries generated by the map function.
78
77
  * @private
79
78
  * @memberof DbIndex
80
79
  */
81
- const indexEntriesForChanges = (changes, mapFun) => {
80
+ const indexEntriesForChanges = (changes, mapFn) => {
82
81
  const indexEntries = []
83
82
  changes.forEach(({ key, value, del }) => {
84
83
  if (del || !value) return
85
- mapFun(makeDoc({ key, value }), (k, v) => {
84
+ mapFn(makeDoc({ key, value }), (k, v) => {
86
85
  indexEntries.push({
87
86
  key: [charwise.encode(k), key],
88
87
  value: v
@@ -99,11 +98,12 @@ const indexEntriesForChanges = (changes, mapFun) => {
99
98
  * @classdesc An DbIndex can be used to order and filter the documents in a Fireproof database.
100
99
  *
101
100
  * @param {Fireproof} database - The Fireproof database instance to DbIndex.
102
- * @param {Function} mapFun - The map function to apply to each entry in the database.
101
+ * @param {Function} mapFn - The map function to apply to each entry in the database.
103
102
  *
104
103
  */
105
104
  export default class DbIndex {
106
- constructor (database, mapFun) {
105
+ constructor (database, mapFn, clock) {
106
+ // console.log('DbIndex constructor', database.constructor.name, typeof mapFn, clock)
107
107
  /**
108
108
  * The database instance to DbIndex.
109
109
  * @type {Fireproof}
@@ -113,33 +113,62 @@ export default class DbIndex {
113
113
  * The map function to apply to each entry in the database.
114
114
  * @type {Function}
115
115
  */
116
- this.mapFun = mapFun
117
-
118
- this.database.indexes.set(mapFun.toString(), this)
119
116
 
117
+ if (typeof mapFn === 'string') {
118
+ this.mapFnString = mapFn
119
+ } else {
120
+ this.mapFn = mapFn
121
+ this.mapFnString = mapFn.toString()
122
+ }
120
123
  this.indexById = { root: null, cid: null }
121
124
  this.indexByKey = { root: null, cid: null }
122
-
123
125
  this.dbHead = null
124
-
126
+ if (clock) {
127
+ this.indexById.cid = clock.byId
128
+ this.indexByKey.cid = clock.byKey
129
+ this.dbHead = clock.db
130
+ }
125
131
  this.instanceId = this.database.instanceId + `.DbIndex.${Math.random().toString(36).substring(2, 7)}`
126
-
127
132
  this.updateIndexPromise = null
133
+ DbIndex.registerWithDatabase(this, this.database)
134
+ }
135
+
136
+ static registerWithDatabase (inIndex, database) {
137
+ // console.log('.reg > in Index', inIndex.instanceId, { live: !!inIndex.mapFn }, inIndex.indexByKey, inIndex.mapFnString)
138
+ if (database.indexes.has(inIndex.mapFnString)) {
139
+ // merge our inIndex code with the inIndex clock or vice versa
140
+ // keep the code instance, discard the clock instance
141
+ const existingIndex = database.indexes.get(inIndex.mapFnString)
142
+ // console.log('.reg - existingIndex', existingIndex.instanceId, { live: !!inIndex.mapFn }, existingIndex.indexByKey)
143
+ if (existingIndex.mapFn) { // this one also has other config
144
+ existingIndex.dbHead = inIndex.dbHead
145
+ existingIndex.indexById.cid = inIndex.indexById.cid
146
+ existingIndex.indexByKey.cid = inIndex.indexByKey.cid
147
+ } else {
148
+ // console.log('.reg use inIndex with existingIndex clock')
149
+ inIndex.dbHead = existingIndex.dbHead
150
+ inIndex.indexById.cid = existingIndex.indexById.cid
151
+ inIndex.indexByKey.cid = existingIndex.indexByKey.cid
152
+ database.indexes.set(inIndex.mapFnString, inIndex)
153
+ }
154
+ } else {
155
+ // console.log('.reg - fresh')
156
+ database.indexes.set(inIndex.mapFnString, inIndex)
157
+ }
158
+ // console.log('.reg after', JSON.stringify([...database.indexes.values()].map(i => [i.instanceId, typeof i.mapFn, i.indexByKey, i.indexById])))
128
159
  }
129
160
 
130
161
  toJSON () {
131
- return { code: this.mapFun?.toString(), clock: { db: this.dbHead?.map(cid => cid.toString()), byId: this.indexById.cid?.toString(), byKey: this.indexByKey.cid?.toString() } }
162
+ const indexJson = { code: this.mapFn?.toString(), clock: { db: null, byId: null, byKey: null } }
163
+ indexJson.clock.db = this.dbHead?.map(cid => cid.toString())
164
+ indexJson.clock.byId = this.indexById.cid?.toString()
165
+ indexJson.clock.byKey = this.indexByKey.cid?.toString()
166
+ return indexJson
132
167
  }
133
168
 
134
- static fromJSON (database, { code, clock: { byId, byKey, db } }) {
135
- let mapFun
136
- // eslint-disable-next-line
137
- eval("mapFun = "+ code)
138
- const index = new DbIndex(database, mapFun)
139
- index.indexById.cid = CID.parse(byId)
140
- index.indexByKey.cid = CID.parse(byKey)
141
- index.dbHead = db.map(cid => CID.parse(cid))
142
- return index
169
+ static fromJSON (database, { code, clock }) {
170
+ // console.log('DbIndex.fromJSON', database.constructor.name, code, clock)
171
+ return new DbIndex(database, code, clock)
143
172
  }
144
173
 
145
174
  /**
@@ -166,6 +195,7 @@ export default class DbIndex {
166
195
 
167
196
  // }
168
197
  // console.time(callId + '.doIndexQuery')
198
+ // console.log('query', query)
169
199
  const response = await doIndexQuery(this.database.blocks, this.indexByKey, query)
170
200
  // console.timeEnd(callId + '.doIndexQuery')
171
201
 
@@ -196,12 +226,12 @@ export default class DbIndex {
196
226
 
197
227
  async #innerUpdateIndex (inBlocks) {
198
228
  // const callTag = Math.random().toString(36).substring(4)
199
- // console.log(`#updateIndex ${callTag} >`, this.instanceId, this.dbHead?.toString(), this.dbIndexRoot?.cid.toString(), this.indexByIdRoot?.cid.toString())
229
+ // console.log(`#updateIndex ${callTag} >`, this.instanceId, this.dbHead?.toString(), this.indexByKey.cid?.toString(), this.indexById.cid?.toString())
200
230
  // todo remove this hack
201
231
  if (ALWAYS_REBUILD) {
202
- this.dbHead = null // hack
203
- this.indexByKey = null // hack
204
- this.dbIndexRoot = null
232
+ this.indexById = { root: null, cid: null }
233
+ this.indexByKey = { root: null, cid: null }
234
+ this.dbHead = null
205
235
  }
206
236
  // console.log('dbHead', this.dbHead)
207
237
  // console.time(callTag + '.changesSince')
@@ -210,28 +240,34 @@ export default class DbIndex {
210
240
  // console.log('result.rows.length', result.rows.length)
211
241
 
212
242
  // console.time(callTag + '.doTransaction#updateIndex')
243
+ // console.log('#updateIndex changes length', result.rows.length)
213
244
 
214
245
  if (result.rows.length === 0) {
215
- // console.log('#updateIndex < no changes')
246
+ // console.log('#updateIndex < no changes', result.clock)
216
247
  this.dbHead = result.clock
217
248
  return
218
249
  }
219
250
  await doTransaction('#updateIndex', inBlocks, async (blocks) => {
220
251
  let oldIndexEntries = []
221
252
  let removeByIdIndexEntries = []
222
- if (this.dbHead) { // need a maybe load
253
+ await loadIndex(blocks, this.indexById, idIndexOpts)
254
+ await loadIndex(blocks, this.indexByKey, dbIndexOpts)
255
+ if (this.dbHead) {
223
256
  const oldChangeEntries = await this.indexById.root.getMany(result.rows.map(({ key }) => key))
224
257
  oldIndexEntries = oldChangeEntries.result.map((key) => ({ key, del: true }))
225
258
  removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
226
259
  }
227
- const indexEntries = indexEntriesForChanges(result.rows, this.mapFun)
260
+ if (!this.mapFn) {
261
+ throw new Error('No live map function installed for index, cannot update. Make sure your index definition runs before any queries.' + (this.mapFnString ? ' Your code should match the stored map function source:\n' + this.mapFnString : ''))
262
+ }
263
+ const indexEntries = indexEntriesForChanges(result.rows, this.mapFn)
228
264
  const byIdIndexEntries = indexEntries.map(({ key }) => ({ key: key[1], value: key }))
229
265
  this.indexById = await bulkIndex(blocks, this.indexById, removeByIdIndexEntries.concat(byIdIndexEntries), idIndexOpts)
230
266
  this.indexByKey = await bulkIndex(blocks, this.indexByKey, oldIndexEntries.concat(indexEntries), dbIndexOpts)
231
267
  this.dbHead = result.clock
232
268
  })
233
269
  // console.timeEnd(callTag + '.doTransaction#updateIndex')
234
- // console.log(`#updateIndex ${callTag} <`, this.instanceId, this.dbHead?.toString(), this.dbIndexRoot?.cid.toString(), this.indexByIdRoot?.cid.toString())
270
+ // console.log(`#updateIndex ${callTag} <`, this.instanceId, this.dbHead?.toString(), this.indexByKey.cid?.toString(), this.indexById.cid?.toString())
235
271
  }
236
272
  }
237
273
 
@@ -271,13 +307,18 @@ async function bulkIndex (blocks, inIndex, indexEntries, opts) {
271
307
  return { root: returnNode, cid: returnRootBlock.cid }
272
308
  }
273
309
 
274
- async function doIndexQuery (blocks, indexByKey, query) {
275
- if (!indexByKey.root) {
276
- const cid = indexByKey.cid
277
- if (!cid) return { result: [] }
310
+ async function loadIndex (blocks, index, indexOpts) {
311
+ if (!index.root) {
312
+ const cid = index.cid
313
+ if (!cid) return
278
314
  const { getBlock } = makeGetBlock(blocks)
279
- indexByKey.root = await load({ cid, get: getBlock, ...dbIndexOpts })
315
+ index.root = await load({ cid, get: getBlock, ...indexOpts })
280
316
  }
317
+ return index.root
318
+ }
319
+
320
+ async function doIndexQuery (blocks, indexByKey, query) {
321
+ await loadIndex(blocks, indexByKey, dbIndexOpts)
281
322
  if (query.range) {
282
323
  const encodedRange = query.range.map((key) => charwise.encode(key))
283
324
  return indexByKey.root.range(...encodedRange)
package/src/fireproof.js CHANGED
@@ -42,22 +42,27 @@ export default class Fireproof {
42
42
  }
43
43
 
44
44
  /**
45
- * Returns a snapshot of the current Fireproof instance as a new instance.
46
- * @function snapshot
47
- * @param {CID[]} clock - The Merkle clock head to use for the snapshot.
48
- * @returns {Fireproof}
49
- * A new Fireproof instance representing the snapshot.
45
+ * Renders the Fireproof instance as a JSON object.
46
+ * @returns {Object} - The JSON representation of the Fireproof instance. Includes clock heads for the database and its indexes.
50
47
  * @memberof Fireproof
51
48
  * @instance
52
49
  */
53
- snapshot (clock) {
54
- // how to handle listeners, views, and config?
55
- // todo needs a test for listeners, views, and config
56
- return new Fireproof(this.blocks, clock || this.clock)
50
+ toJSON () {
51
+ // todo this also needs to return the index roots...
52
+ return {
53
+ clock: this.clock.map(cid => cid.toString()),
54
+ name: this.name,
55
+ indexes: [...this.indexes.values()].map(index => index.toJSON())
56
+ }
57
+ }
58
+
59
+ hydrate ({ clock, name }) {
60
+ this.name = name
61
+ this.clock = clock
57
62
  }
58
63
 
59
64
  /**
60
- * Move the current instance to a new point in time. This triggers a notification to all listeners
65
+ * Triggers a notification to all listeners
61
66
  * of the Fireproof instance so they can repaint UI, etc.
62
67
  * @param {CID[] } clock
63
68
  * Clock to use for the snapshot.
@@ -65,25 +70,8 @@ export default class Fireproof {
65
70
  * @memberof Fireproof
66
71
  * @instance
67
72
  */
68
- async setClock (clock) {
69
- // console.log('setClock', this.instanceId, clock)
70
- this.clock = clock.map((item) => (item['/'] ? item['/'] : item))
71
- await this.#notifyListeners({ reset: true, clock })
72
- }
73
-
74
- /**
75
- * Renders the Fireproof instance as a JSON object.
76
- * @returns {Object} - The JSON representation of the Fireproof instance. Includes clock heads for the database and its indexes.
77
- * @memberof Fireproof
78
- * @instance
79
- */
80
- toJSON () {
81
- // todo this also needs to return the index roots...
82
- return {
83
- clock: this.clock.map(cid => cid.toString()),
84
- name: this.name,
85
- indexes: [...this.indexes.values()].map((index) => index.toJSON())
86
- }
73
+ async notifyReset () {
74
+ await this.#notifyListeners({ reset: true, clock: this.clock })
87
75
  }
88
76
 
89
77
  /**
@@ -295,7 +283,7 @@ export default class Fireproof {
295
283
  }
296
284
 
297
285
  setCarUploader (carUploaderFn) {
298
- console.log('registering car uploader')
286
+ // console.log('registering car uploader')
299
287
  // https://en.wikipedia.org/wiki/Law_of_Demeter - this is a violation of the law of demeter
300
288
  this.blocks.valet.uploadFunction = carUploaderFn
301
289
  }
package/src/hydrator.js CHANGED
@@ -1,10 +1,51 @@
1
- import Fireproof from './fireproof.js'
2
1
  import DbIndex from './db-index.js'
2
+ import Fireproof from './fireproof.js'
3
+ import { CID } from 'multiformats'
4
+
5
+ const parseCID = cid => typeof cid === 'string' ? CID.parse(cid) : cid
6
+
7
+ export default class Hydrator {
8
+ static fromJSON (json, database) {
9
+ database.hydrate({ clock: json.clock.map(c => parseCID(c)), name: json.name })
10
+ for (const { code, clock: { byId, byKey, db } } of json.indexes) {
11
+ DbIndex.fromJSON(database, {
12
+ clock: {
13
+ byId: byId ? parseCID(byId) : null,
14
+ byKey: byKey ? parseCID(byKey) : null,
15
+ db: db ? db.map(c => parseCID(c)) : null
16
+ },
17
+ code
18
+ })
19
+ }
20
+ return database
21
+ }
22
+
23
+ static snapshot (database, clock) {
24
+ const definition = database.toJSON()
25
+ const withBlocks = new Fireproof(database.blocks)
26
+ if (clock) {
27
+ definition.clock = clock.map(c => parseCID(c))
28
+ definition.indexes.forEach(index => {
29
+ index.clock.byId = null
30
+ index.clock.byKey = null
31
+ index.clock.db = null
32
+ })
33
+ }
34
+ const snappedDb = this.fromJSON(definition, withBlocks)
35
+ ;([...database.indexes.values()]).forEach(index => {
36
+ snappedDb.indexes.get(index.mapFnString).mapFn = index.mapFn
37
+ })
38
+ return snappedDb
39
+ }
3
40
 
4
- export function fromJSON (json, blocks) {
5
- const fp = new Fireproof(blocks, json.clock, { name: json.name })
6
- for (const index of json.indexes) {
7
- DbIndex.fromJSON(fp, index)
41
+ static async zoom (database, clock) {
42
+ ;([...database.indexes.values()]).forEach(index => {
43
+ index.indexById = { root: null, cid: null }
44
+ index.indexByKey = { root: null, cid: null }
45
+ index.dbHead = null
46
+ })
47
+ database.clock = clock.map(c => parseCID(c))
48
+ await database.notifyReset()
49
+ return database
8
50
  }
9
- return fp
10
51
  }
@@ -3,6 +3,7 @@ import assert from 'node:assert'
3
3
  import Blockstore from '../src/blockstore.js'
4
4
  import Fireproof from '../src/fireproof.js'
5
5
  import DbIndex from '../src/db-index.js'
6
+ import Hydrator from '../src/hydrator.js'
6
7
  console.x = function () {}
7
8
 
8
9
  describe('DbIndex query', () => {
@@ -77,7 +78,7 @@ describe('DbIndex query', () => {
77
78
  // console.x('bresult.rows', bresult.rows)
78
79
  assert.equal(bresult.rows.length, 6, 'all row matched')
79
80
 
80
- const oldHead = database.clock
81
+ const snapClock = database.clock
81
82
 
82
83
  const notYet = await database.get('xxxx-3c3a-4b5e-9c1c-8c5c0c5c0c5c').catch((e) => e)
83
84
  assert.equal(notYet.message, 'Not found', 'not yet there')
@@ -91,7 +92,7 @@ describe('DbIndex query', () => {
91
92
  assert(gotX.name === 'Xander', 'got Xander')
92
93
  console.x('got X')
93
94
 
94
- const snap = database.snapshot(oldHead)
95
+ const snap = Hydrator.snapshot(database, snapClock)
95
96
 
96
97
  const aliceOld = await snap.get('a1s3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')// .catch((e) => e)
97
98
  console.x('aliceOld', aliceOld)
@@ -124,7 +125,7 @@ describe('DbIndex query', () => {
124
125
  assert.equal(result.rows.length, 1, '1 row matched')
125
126
  assert(result.rows[0].key === 53, 'correct key')
126
127
 
127
- const snap = database.snapshot(database.clock)
128
+ const snap = Hydrator.snapshot(database)
128
129
 
129
130
  console.x('--- make Xander 63')
130
131
  const response = await database.put({ _id: DOCID, name: 'Xander', age: 63 })
@@ -172,7 +173,7 @@ describe('DbIndex query', () => {
172
173
  assert.equal(result.rows.length, 1, '1 row matched')
173
174
  assert(result.rows[0].key === 53, 'correct key')
174
175
 
175
- const snap = database.snapshot(database.clock)
176
+ const snap = Hydrator.snapshot(database)
176
177
 
177
178
  console.x('--- delete Xander 53')
178
179
  const response = await database.del(DOCID)
@@ -2,6 +2,7 @@ import { describe, it, beforeEach } from 'mocha'
2
2
  import assert from 'node:assert'
3
3
  import Blockstore from '../src/blockstore.js'
4
4
  import Fireproof from '../src/fireproof.js'
5
+ import Hydrator from '../src/hydrator.js'
5
6
  // import * as codec from '@ipld/dag-cbor'
6
7
 
7
8
  let database, resp0
@@ -130,7 +131,7 @@ describe('Fireproof', () => {
130
131
  assert(response.id, 'should have id')
131
132
  assert.equal(response.id, dogKey)
132
133
  assert.equal(value._id, dogKey)
133
- const oldClock = database.clock
134
+ const snapshot = Hydrator.snapshot(database)
134
135
 
135
136
  const avalue = await database.get(dogKey)
136
137
  assert.equal(avalue.name, value.name)
@@ -147,7 +148,6 @@ describe('Fireproof', () => {
147
148
  assert.equal(bvalue.age, 3)
148
149
  assert.equal(bvalue._id, dogKey)
149
150
 
150
- const snapshot = database.snapshot(oldClock)
151
151
  const snapdoc = await snapshot.get(dogKey)
152
152
  // console.log('snapdoc', snapdoc)
153
153
  // assert(snapdoc.id, 'should have id')