@fireproof/core 0.0.4 → 0.0.5

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.
@@ -2,7 +2,7 @@
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 '@fireproof/core'
5
+ import { Fireproof, Listener } from '../index'
6
6
 
7
7
  export interface FireproofCtxValue {
8
8
  addSubscriber: (label: String, fn: Function) => void
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireproof/core",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Realtime database for IPFS",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/db-index.js CHANGED
@@ -1,14 +1,30 @@
1
1
  import { create, load } from 'prolly-trees/db-index'
2
2
  import { sha256 as hasher } from 'multiformats/hashes/sha2'
3
3
  import { nocache as cache } from 'prolly-trees/cache'
4
- import { bf, simpleCompare as compare } from 'prolly-trees/utils'
4
+ import { bf, simpleCompare } from 'prolly-trees/utils'
5
5
  import * as codec from '@ipld/dag-cbor'
6
6
  import { create as createBlock } from 'multiformats/block'
7
7
  import { doTransaction } from './blockstore.js'
8
8
  import charwise from 'charwise'
9
- const opts = { cache, chunker: bf(3), codec, hasher, compare }
10
9
 
11
- const ALWAYS_REBUILD = true // todo: remove this
10
+ const arrayCompare = (a, b) => {
11
+ if (Array.isArray(a) && Array.isArray(b)) {
12
+ const len = Math.min(a.length, b.length)
13
+ for (let i = 0; i < len; i++) {
14
+ const comp = simpleCompare(a[i], b[i])
15
+ if (comp !== 0) {
16
+ return comp
17
+ }
18
+ }
19
+ return simpleCompare(a.length, b.length)
20
+ } else {
21
+ return simpleCompare(a, b)
22
+ }
23
+ }
24
+
25
+ const opts = { cache, chunker: bf(3), codec, hasher, compare: arrayCompare }
26
+
27
+ const ALWAYS_REBUILD = false // todo: remove this
12
28
 
13
29
  const makeGetBlock = (blocks) => async (address) => {
14
30
  const { cid, bytes } = await blocks.get(address)
@@ -60,7 +76,7 @@ const indexEntriesForChanges = (changes, mapFun) => {
60
76
  const indexEntriesForOldChanges = async (blocks, byIDindexRoot, ids, mapFun) => {
61
77
  const getBlock = makeGetBlock(blocks)
62
78
  const byIDindex = await load({ cid: byIDindexRoot.cid, get: getBlock, ...opts })
63
- // console.trace('ids', ids)
79
+
64
80
  const result = await byIDindex.getMany(ids)
65
81
  return result.result
66
82
  }
@@ -142,26 +158,16 @@ export default class DbIndex {
142
158
  result.rows.map(({ key }) => key),
143
159
  this.mapFun
144
160
  )
145
- )
146
- // .map((key) => ({ key, value: null })) // tombstone just adds more rows...
147
- .map((key) => ({ key, del: true })) // should be this
148
- // .map((key) => ({ key: undefined, del: true })) // todo why does this work?
149
-
161
+ ).map((key) => ({ key, del: true })) // should be this
150
162
  this.indexRoot = await bulkIndex(blocks, this.indexRoot, oldIndexEntries, opts)
151
- // console.x('oldIndexEntries', oldIndexEntries)
152
- // [ { key: ['b', 1], del: true } ]
153
- // [ { key: [ 5, 'x' ], del: true } ]
154
- // for now we just let the by id DbIndex grow and then don't use the results...
155
- // const removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
156
- // this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, removeByIdIndexEntries, opts)
163
+ const removeByIdIndexEntries = oldIndexEntries.map(({ key }) => ({ key: key[1], del: true }))
164
+ this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, removeByIdIndexEntries, opts)
157
165
  }
158
166
  const indexEntries = indexEntriesForChanges(result.rows, this.mapFun)
159
167
  const byIdIndexEntries = indexEntries.map(({ key }) => ({ key: key[1], value: key }))
160
- // [{key: 'xxxx-3c3a-4b5e-9c1c-8c5c0c5c0c5c', value : [ 53, 'xxxx-3c3a-4b5e-9c1c-8c5c0c5c0c5c' ]}]
161
168
  this.byIDindexRoot = await bulkIndex(blocks, this.byIDindexRoot, byIdIndexEntries, opts)
162
169
  // console.log('indexEntries', indexEntries)
163
170
  this.indexRoot = await bulkIndex(blocks, this.indexRoot, indexEntries, opts)
164
- // console.log('did DbIndex', this.indexRoot)
165
171
  this.dbHead = result.clock
166
172
  }
167
173
 
@@ -183,25 +189,18 @@ async function bulkIndex (blocks, inRoot, indexEntries) {
183
189
  const putBlock = blocks.put.bind(blocks)
184
190
  const getBlock = makeGetBlock(blocks)
185
191
  if (!inRoot) {
186
- // make a new DbIndex
187
-
188
192
  for await (const node of await create({ get: getBlock, list: indexEntries, ...opts })) {
189
193
  const block = await node.block
190
194
  await putBlock(block.cid, block.bytes)
191
195
  inRoot = block
192
196
  }
193
- // console.x('created DbIndex', inRoot.cid)
194
197
  return inRoot
195
198
  } else {
196
- // load existing DbIndex
197
- // console.x('loading DbIndex', inRoot.cid)
198
199
  const DbIndex = await load({ cid: inRoot.cid, get: getBlock, ...opts })
199
- // console.log('new indexEntries', indexEntries)
200
200
  const { root, blocks } = await DbIndex.bulk(indexEntries)
201
201
  for await (const block of blocks) {
202
202
  await putBlock(block.cid, block.bytes)
203
203
  }
204
- // console.x('updated DbIndex', root.block.cid)
205
204
  return await root.block // if we hold the root we won't have to load every time
206
205
  }
207
206
  }
package/src/fireproof.js CHANGED
@@ -35,7 +35,7 @@ export default class Fireproof {
35
35
  this.clock = clock
36
36
  this.config = config
37
37
  this.authCtx = authCtx
38
- this.instanceId = 'db.' + Math.random().toString(36).substring(2, 7)
38
+ this.instanceId = 'fp.' + Math.random().toString(36).substring(2, 7)
39
39
  }
40
40
 
41
41
  /**
@@ -161,7 +161,7 @@ export default class Fireproof {
161
161
  async put ({ _id, ...doc }) {
162
162
  const id = _id || 'f' + Math.random().toString(36).slice(2)
163
163
  await this.#runValidation({ _id: id, ...doc })
164
- return await this.#putToProllyTree({ key: id, value: doc })
164
+ return await this.#putToProllyTree({ key: id, value: doc }, doc._clock)
165
165
  }
166
166
 
167
167
  /**
@@ -171,20 +171,37 @@ export default class Fireproof {
171
171
  * @memberof Fireproof
172
172
  * @instance
173
173
  */
174
- async del (id) {
174
+ async del (docOrId) {
175
+ let id
176
+ let clock = null
177
+ if (docOrId._id) {
178
+ id = docOrId._id
179
+ clock = docOrId._clock
180
+ } else {
181
+ id = docOrId
182
+ }
175
183
  await this.#runValidation({ _id: id, _deleted: true })
176
184
  // return await this.#putToProllyTree({ key: id, del: true }) // not working at prolly tree layer?
177
185
  // this tombstone is temporary until we can get the prolly tree to delete
178
- return await this.#putToProllyTree({ key: id, value: null })
186
+ return await this.#putToProllyTree({ key: id, value: null }, clock)
179
187
  }
180
188
 
181
189
  /**
182
190
  * Updates the underlying storage with the specified event.
183
191
  * @private
184
- * @param {CID[]} event - the event to add
192
+ * @param {Object<{key : string, value: any}>} event - the event to add
185
193
  * @returns {Object<{ id: string, clock: CID[] }>} - The result of adding the event to storage
186
194
  */
187
- async #putToProllyTree (event) {
195
+ async #putToProllyTree (event, clock = null) {
196
+ if (clock && JSON.stringify(clock) !== JSON.stringify(this.clock)) {
197
+ // we need to check and see what version of the document exists at the clock specified
198
+ // if it is the same as the one we are trying to put, then we can proceed
199
+ const resp = await eventsSince(this.blocks, this.clock, event.value._clock)
200
+ const missedChange = resp.find(({ key }) => key === event.key)
201
+ if (missedChange) {
202
+ throw new Error('MVCC conflict, document is changed, please reload the document and try again.')
203
+ }
204
+ }
188
205
  const result = await doTransaction(
189
206
  '#putToProllyTree',
190
207
  this.blocks,
@@ -228,16 +245,25 @@ export default class Fireproof {
228
245
  * Retrieves the document with the specified ID from the database
229
246
  *
230
247
  * @param {string} key - the ID of the document to retrieve
248
+ * @param {Object} [opts] - options
231
249
  * @returns {Object<{_id: string, ...doc: Object}>} - the document with the specified ID
232
250
  * @memberof Fireproof
233
251
  * @instance
234
252
  */
235
- async get (key) {
236
- const got = await get(this.blocks, this.clock, key)
253
+ async get (key, opts = {}) {
254
+ let got
255
+ if (opts.clock) {
256
+ got = await get(this.blocks, opts.clock, key)
257
+ } else {
258
+ got = await get(this.blocks, this.clock, key)
259
+ }
237
260
  // this tombstone is temporary until we can get the prolly tree to delete
238
261
  if (got === null) {
239
262
  throw new Error('Not found')
240
263
  }
264
+ if (opts.mvcc === true) {
265
+ got._clock = this.clock
266
+ }
241
267
  got._id = key
242
268
  return got
243
269
  }
package/src/valet.js CHANGED
@@ -32,9 +32,15 @@ export default class Valet {
32
32
  )
33
33
  if (this.uploadFunction) {
34
34
  // todo we can coalesce these into a single car file
35
- for (const task of tasks) {
36
- await this.uploadFunction(task.carCid, task.value)
37
- }
35
+ return await this.withDB(async (db) => {
36
+ for (const task of tasks) {
37
+ await this.uploadFunction(task.carCid, task.value)
38
+ // update the indexedb to mark this car as no longer pending
39
+ const carMeta = await db.get('cidToCar', task.carCid)
40
+ delete carMeta.pending
41
+ await db.put('cidToCar', carMeta)
42
+ }
43
+ })
38
44
  }
39
45
  callback()
40
46
  })
@@ -20,12 +20,73 @@ describe('Fireproof', () => {
20
20
  it('put and get document', async () => {
21
21
  assert(resp0.id, 'should have id')
22
22
  assert.equal(resp0.id, '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
23
-
24
23
  const avalue = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
25
24
  assert.equal(avalue.name, 'alice')
26
25
  assert.equal(avalue.age, 42)
27
26
  assert.equal(avalue._id, '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
28
27
  })
28
+ it('mvcc put and get document with _clock that matches', async () => {
29
+ assert(resp0.clock, 'should have clock')
30
+ assert.equal(resp0.clock[0].toString(), 'bafyreieth2ckopwivda5mf6vu76xwqvox3q5wsaxgbmxy2dgrd4hfuzmma')
31
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
32
+ theDoc._clock = database.clock
33
+ const put2 = await database.put(theDoc)
34
+ assert.equal(put2.id, '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
35
+ assert.equal(put2.clock[0].toString(), 'bafyreida2c2ckhjfoz5ulmbbfe66ey4svvedrl4tzbvtoxags2qck7lj2i')
36
+ })
37
+ it('get should return an object instance that is not the same as the one in the db', async () => {
38
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
39
+ const theDoc2 = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
40
+ assert.notEqual(theDoc, theDoc2)
41
+ theDoc.name = 'really alice'
42
+ assert.equal(theDoc.name, 'really alice')
43
+ assert.equal(theDoc2.name, 'alice')
44
+ })
45
+ it('get with mvcc option', async () => {
46
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c', { mvcc: true })
47
+ assert(theDoc._clock, 'should have _clock')
48
+ assert.equal(theDoc._clock[0].toString(), 'bafyreieth2ckopwivda5mf6vu76xwqvox3q5wsaxgbmxy2dgrd4hfuzmma')
49
+ })
50
+ it('get from an old snapshot with mvcc option', async () => {
51
+ const ogClock = resp0.clock
52
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
53
+ theDoc.name = 'not alice'
54
+ const put2 = await database.put(theDoc)
55
+ assert.equal(put2.id, '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
56
+ assert.notEqual(put2.clock.toString(), ogClock.toString())
57
+ const theDoc2 = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c', { clock: ogClock })
58
+ assert.equal(theDoc2.name, 'alice')
59
+ })
60
+ it('put and get document with _clock that does not match b/c the doc changed', async () => {
61
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c', { mvcc: true })
62
+ theDoc.name = 'not alice'
63
+ const put2 = await database.put(theDoc)
64
+ assert.equal(put2.id, '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
65
+ assert.notEqual(put2.clock.toString(), theDoc._clock.toString())
66
+
67
+ const err = await database.put(theDoc).catch((err) => err)
68
+ assert.match(err.message, /MVCC conflict/)
69
+ })
70
+ it('put and get document with _clock that does not match b/c a different doc changed should succeed', async () => {
71
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c', { mvcc: true })
72
+ assert.equal(theDoc.name, 'alice')
73
+
74
+ const putAnotherDoc = await database.put({ nothing: 'to see here' })
75
+ assert.notEqual(putAnotherDoc.clock.toString(), theDoc._clock.toString())
76
+
77
+ const ok = await database.put({ name: "isn't alice", ...theDoc })
78
+ assert.equal(ok.id, '1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c')
79
+ })
80
+ it('put and get document with _clock that does not match b/c the doc was deleted', async () => {
81
+ const theDoc = await database.get('1ef3b32a-3c3a-4b5e-9c1c-8c5c0c5c0c5c', { mvcc: true })
82
+ assert.equal(theDoc.name, 'alice')
83
+ const del = await database.del(theDoc)
84
+ assert(del.id)
85
+ const err = await database.put(theDoc).catch((err) => err)
86
+ console.log('err', err)
87
+ assert.match(err.message, /MVCC conflict/)
88
+ })
89
+
29
90
  it('has a factory for making new instances with default settings', async () => {
30
91
  // TODO if you pass it an email it asks the local keyring, and if no key, does the email validation thing
31
92
  const db = await Fireproof.storage({ email: 'jchris@gmail.com' })
package/README.md DELETED
@@ -1,148 +0,0 @@
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 [in our plan,](https://hackmd.io/@j-chris/SyoE-Plpj) 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/jchris/fireproof/blob/main/examples/todomvc/src/hooks/useFireproof.js)
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 = new Fireproof();
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')
50
- // {
51
- // _id : 'three-thousand'
52
- // _ref : CID(bafy84...agfw7)
53
- // name : 'André',
54
- // age : 47
55
- // }
56
- ```
57
-
58
- The `_ref` 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-soverign 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
- ### Persistence
108
-
109
- Currently Fireproof writes transactions and proofs to in-memory [CAR files](https://ipld.io/specs/transport/car/carv2/) which are well suited for peer and cloud replication. Durability coming 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
- ```
128
- npm install @fireproof/core
129
- ```
130
-
131
- In your `app.js` or `app.tsx` file:
132
-
133
- ```
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)