Git Product home page Git Product logo

orbit-db-store's Introduction

orbit-db-store

npm version Gitter Matrix

Base class for orbit-db data stores. You generally don't need to use this module if you want to use orbit-db. This module contains shared methods between all data stores in orbit-db and can be used as a base class for a new data model.

Used in

Requirements

  • Node.js >= 8.0.0

Table of Contents

API

constructor(ipfs, identity, address, options)

ipfs can be an IPFS instance or an IPFS-API instance. identity is an instance of Identity. address is the OrbitDB address to be used for the store.

options is an object with the following required properties:

  • cache: A Cache instance to use for storing heads and snapshots.
  • Index : By default it uses an instance of Index.

the following properties are optional:

  • maxHistory (Integer): The number of entries to load (Default: -1).
  • syncLocal (Boolean): Load local database before performing any append operations. (Default: false).
  • fetchEntryTimeout (Integer): The number in ms specifying a timeout when fetching entries from IPFS. (Default: null).
  • referenceCount (Integer): The number of previous ipfs-log entries a new entry should reference (Default: 64).
  • replicationConcurrency (Integer): The number of concurrent replication processes (Default: 128).
  • accessController (Object): An instance of AccessController with the following interface. See orbit-db-access-controllers for more information on how to create custom access controllers. By default only the owner will have write access.
  • sortFn (Function): A function used to sort ipfs-log entries (Default: undefined).
  • onClose (Function): A function to be called with a string of the OrbitDB address of the database that is closing.
  • onDrop (Function): A function to be called with the orbit-db-store instance when the database is being removed.
  • onLoad (Function): A function to be called with the orbit-db-store instance when the database is being loaded.

Public methods

load([amount], [opts])

Load the database using locally persisted state.

Returns a Promise that resolves once complete. Provide an optional amount argument to specify how many entries to load. By default the maxHistory option is used. Provide an optional options object with a fetchEntryTimeout property to be used when loading entries from IPFS.

loadMoreFrom(amount, entries)

TODO

//TODO
db.loadMoreFrom()

setIdentity (identity)

Set the identity for the database

saveSnapshot()

Save the current state of the database locally.

Returns a Promise that resolves to an array containing an object with the following properties:

  • path of the snapshot file
  • hash representing the IPFS Multihash (as a Base58 encoded string) of the snapshot file
  • size of the snapshot file

loadFromSnapshot()

Load the state of the database from a snapshot.

Returns a Promise that resolves to a store instance once it has been loaded.

close()

Uninitialize the store.

Returns a promise that resolves once complete. Emits close after the store has been uninitialized.

drop()

Remove the database locally.

Returns a promise that resolves once complete. This doesn't remove or delete the database from peers who have replicated the database.

sync(heads)

Sync this database with entries from heads where heads is an array of ipfs-log Entries.

Usually, you don't need to call this method manually as OrbitDB takes care of this for you.

Properties

address

Get the address of this database.

Returns an object { root: <manifestHash>, path: <path> }. Convert to a string with db.address.toString().

console.log(db.address.toString())
// /orbitdb/zdpuB383kQWjyCd5nv4FKqZwe2FH4nqxBBE7kzoDrmdtZ6GPu/databaseName

identity

Each store has an identity property containing the public key used with this store to sign and access entries. This publicKey property of identity is the peer/node/user key.

console.log(db.identity.publicKey)
// 042c07044e7ea51a489c02854db5e09f0191690dc59db0afd95328c9db614a2976e088cab7c86d7e48183191258fc59dc699653508ce25bf0369d67f33d5d77839

all

Get all of the entries in the store index

Returns an array of all store entries within the index.

db.all

type

Get the store type

Returns a string of the type of datastore model of the current instance.

console.log(db.type) // "eventlog"

replicationStatus

Get database replication status information such as total number of entries and loading progress.

Returns an instance of ReplicationInfo.

console.log(db.replicationStatus)
// { buffered: 0, queued: 0, progress: 2, max: 5 }

Events

Store has an events (EventEmitter) object that emits events that describe what's happening in the database.

  • load - (address, heads)

    Emitted before loading the database history. address is a string of the OrbitDB address being loaded. heads is an array of ipfs-log Entries from which the history is loaded from. heads is omitted when this event is emitted as a result of loadFromSnapshot.

    db.events.on('load', (address, heads) => ... )
    db.load()
  • ready - (address, heads)

    Emitted after fully loading the database history. address is a string of the OrbitDB address that emitted the event. heads is an array of ipfs-log Entries.

    db.events.on('ready', (address, heads) => ... )
    db.load()
  • load.progress - (address, hash, entry, progress, total)

    Emitted for each entry during load. address is a string of the OrbitDB address that emitted the event. hash is the multihash of the entry that was just loaded. entry is the ipfs-log Entry that was loaded. Progress is the current load count. Total is the maximum load count (ie. length of the full database). These are useful eg. for displaying a load progress percentage.

    db.events.on('load.progress', (address, hash, entry, progress, total) => ... )
    db.load()
  • replicate - (address, entry)

    Emitted before replicating a part of the database. address is a string of the OrbitDB address that emitted the event. entry is the ipfs-log Entry that is being processed.

    db.events.on('replicate', (address, entry) => ... )
  • replicate.progress - (address, hash, entry, progress, total)

    Emitted while replicating a database. address is a string of the OrbitDB address of the database that emitted the event. hash is the multihash of the entry that was just replicated. entry is the ipfs-log Entry that was replicated. progress is an integer representing the current progress. total is an integer representing the remaining operations.

    db.events.on('replicate.progress', (address, hash, entry, progress, total) => ... )
  • log.op.${operation} - (entry)

    Emitted after an entry was added to the database regardless of whether the entry is added remotely, or locally. ${operation} is replaced with a specified oplog operation. none is specified to listen for a oplog entry without an operation specified. The supported operations are diagrammed in the entry payload.

    db.events.on('log.op.ADD', (id, hash, payload) => ... )
  • replicated - (address, count)

    Emitted after the database was synced with an update from a peer database. address is a string of the OrbitDB address that emitted the event. count number of items replicated. count is omitted when this event is emitted as a result of loadFromSnapshot.

    db.events.on('replicated', (address, count) => ... )
  • write - (address, entry, heads)

    Emitted after an entry was added locally to the database. address is a string of the OrbitDB address that emitted the event. entry is the Entry that was added. heads is an array of ipfs-log Entries.

    db.events.on('write', (address, entry, heads) => ... )
  • closed - (address)

    Emitted once the database has finished closing. address is a string of the OrbitDB address that emitted the event.

    db.events.on('closed', (address) => ... )
    db.close()

Private methods

_addOperation(data, [options])

Add an entry to the store.

Returns a Promise that resolves to the IPFS Multihash of the added entry. Takes data as a parameter which can be of any type. Provide an optional options arguement, which is an object with the following properties:

  • onProgressCallback (Function): To be called once the data is appended.
  • pin (Boolean): To specify whether or not to pin the entry in IPFS. (Default: false).
this._addOperation({
  op: 'PUT',
  key: 'greeting',
  value: 'hello world!'
});

Creating Custom Data Stores

You can create a custom data stores that stores data in a way you need it to. To do this, you need to import orbit-db-store to your custom store and extend your store class from orbit-db-store's Store. Below is the orbit-db-kvstore which is a custom data store for orbit-db.

import Store from 'orbit-db-store';
import KeyValueIndex from './KeyValueIndex.js';

export default class KeyValueStore extends Store {
  constructor(ipfs, identity, address, options) {
    Object.assign(options || {}, { Index: KeyValueIndex });
    super(ipfs, identity, address, options)
  }

  get(key) {
    return this._index.get(key);
  }

  set(key, data) {
    this.put(key, data);
  }

  put(key, data) {
    return this._addOperation({
      op: 'PUT',
      key: key,
      value: data,
      meta: {
        ts: new Date().getTime()
      }
    });
  }

  del(key) {
    return this._addOperation({
      op: 'DEL',
      key: key,
      value: null,
      meta: {
        ts: new Date().getTime()
      }
    });
  }
}

Indices

The Store class instances do not store the current state of the Store.

Index contains the state of a datastore, ie. what data we currently have. Index receives a call from a Store when the operations log for the Store was updated, ie. new operations were added. In updateIndex, the Index implements its CRDT logic: add, remove or update items in the data structure.

Implementing each CRDT as an Index, we can implement both operation-based and state-based CRDTs with the same higher level abstractions.

Usage:

const Index = new Index(userId)

How to implement your own Index

The KeyValueIndex is implemented as follows and then used by KeyValueStore.

export default class KeyValueIndex {
  constructor() {
    this._index = {}
  }

  get(key) {
    return this._index[key]
  }

  updateIndex(oplog) {
    oplog.values
      .slice()
      .reverse()
      .reduce((handled, item) => {
        if(!handled.includes(item.payload.key)) {
          handled.push(item.payload.key)
          if(item.payload.op === 'PUT') {
            this._index[item.payload.key] = item.payload.value
          } else if(item.payload.op === 'DEL') {
            delete this._index[item.payload.key]
          }
        }
        return handled
      }, [])
  }
}

KeyValueIndex.js

updateIndex

Whenever you call Store._addOperation the data is stored and then passed as an argument in chronological order into updateIndex, which implements the CRDT logic.

get

An Index can implement whatever querying logic is most opportune or desired by the Developers of that store.

This querying logic is then implemented in this get.

Contributing

See orbit-db's contributing guideline.

License

MIT ยฉ๏ธ 2016-2018 Protocol Labs Inc., Haja Networks Oy

orbit-db-store's People

Contributors

aphelionz avatar crazybuster avatar csdummi avatar dependabot[bot] avatar greenkeeperio-bot avatar haadcode avatar haydenyoung avatar julienmalard avatar mistakia avatar msterle avatar oed avatar phillmac avatar richardlitt avatar shamb0t avatar tabcat avatar thiagodelgado111 avatar tyleryasaka avatar vaultec81 avatar wouldgo avatar xmader avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

orbit-db-store's Issues

Best Practices for Restoring a Store + Pinning Store Data

Are there best practices / docs for how we can restore store data from data?

For example, if a peer has no local copy and attempts to restore data from a centralized database, what happens during replication? Does this mean that we will have duplicate documents if another peer comes online and attempts to replicate data?

On a related topic, how would we "pin" a store to a pinning service and retrieve the pinned data?

Add setIdentity method

Feature request

Right now the identity of the store is set when it's opened. It would be great if it was possible to change the identity after the store has been initialized. A use case for this might be if you want to sync the data of the store before the user has been authenticated, if so a temporary identity could be used until that happens.

How it would work

Add a new method called setIdentity. This method would change this.identity of the store to the identity that is passed to the method. It would also update the identity in the ipfs-log so a change is needed there as well.

source.js:17 Uncaught Error: already resolved

source.js:17 Uncaught Error: already resolved
at Function.i.resolve (source.js:17)
at index.js:10
at reduce.js:10
at drain.js:20
at async-map.js:28
at map.js:19
at async-map.js:28
at f (index.js:84)
at c (index.js:68)
at s (index.js:29)
at i (async-map.js:27)
at map.js:11
at i (async-map.js:27)
at i (drain.js:16)
at drain.js:29
at async-map.js:39

when call saveSnapshot() ,got the above error

Allow updateIndex to be async

Hello @haadcode, I love orbit-db!

I am implementing a custom store for a specific application, but the index-building operation (updateIndex) involves some external I/O. I know this makes the index not deterministic, but it should not be an issue for my use case, at least for now.

At this line the updateIndex call is made, but the call does not use await (even though the caller is async) so if updateIndex returns a promise it would be executed concurrently and the replicated event would get emitted while the index isn't ready yet.

I'd be happy to submit a PR, just wanted to know if I'm missing something important

.npmignore

Things like .nyc_output and the test folder can be added to .npmignore to reduce the package size.

image

Remove TODOs

After my PR #103 is merged, there would be only one TODO left in the README.md:

loadMoreFrom(amount, entries)

TODO

//TODO
db.loadMoreFrom()

'replicate.progress' event is not always sent

orbit-db: 0.28.3
orbit-db-store: 4.3.3

Hi,
we are using OrbitDb for developing a p2p chat application.
Some time ago we noticed that once in a while users are lacking older messages ("message" - entry in a EventStore). It happens rarely but it already happened at least few times and we were sure that it wasn't a connection issue simply because new messages were arriving and could be sent with no problem.

Right now we are relying on replicate.progress event to send newly received messages to frontend. After some intensive testing I managed to get to the broken state (missing one message) and gather some logs.

missingMessage
(Notice missing "I received a message but Windows did not start replicating missing messages. Will it trigger now?" on the left side).

What happened was that replicate.progress event didn't fire for this particular message because none of the conditions in onReplicationProgress (https://github.com/orbitdb/orbit-db-store/blob/main/src/Store.js#L98) were met.

These are the logs from the application with a broken state. They are a bit messy because I was logging the db snapshot on every 'replicate.progress' to see how oplog is changing.
app1MissingMessage.log

This is the final snapshot that proves that the "missing" entry is in the local db, information about receiving it just wasn't propagated:
app1MissingMessagesFinalSnapshot.log

Looking at the logs of the last 3 messages I noticed that the Replicator received "Yes it did trigger" message before "I received a message but Windows did not start replicating missing messages. Will it trigger now?". I am not sure if this matters but after "Yes it did trigger" the replicationStatus wasn't recalculated properly thus replicate.progress didn't happen:

entry.clock.time: 31
onReplicationProgress: I reconnected at 22:49. Will sending a message trigger replication?
onReplicationProgress -> (this._oplog.length + 1 | 43), (this.replicationStatus.progress | 43), {previousProgress | 42}, (this.replicationStatus.max | 44), (previousMax | 44)
entry.clock.time: 44
onReplicationProgress: Yes it did trigger
onReplicationProgress -> (this._oplog.length + 1 | 43), (this.replicationStatus.progress | 44), {previousProgress | 43}, (this.replicationStatus.max | 44), (previousMax | 44)
entry.clock.time: 43
onReplicationProgress: I received a message but Windows did not start replicating missing messages. Will it trigger now?
onReplicationProgress -> (this._oplog.length + 1 | 43), (this.replicationStatus.progress | 44), {previousProgress | 44}, (this.replicationStatus.max | 44), (previousMax | 44)

Unfortunatelly I don't have a working test yet because the path for reproducing the problem is a bit random. Opening and closing apps (aka peers) in some order seems to do the trick. I'll provide a test as soon as I create one.

Do you have any idea what could've happened here?

Refactoring

TODO

  • Move Cache to the store
  • Throw errors if a required argument isn't passed to the store
  • Remove .id property since it's the same as the address
  • Make the Store extend EventEmitter
  • Clean up replication event handling
  • Move snapshot related code to its own space
  • Remove Index from Store
  • Should we rename Index to IndexingStrategy?
  • Should Cache be named like that since it feels it does more than just caching data?

Handling of non-existent ipfs entries when loading a store

We ran into situations where an ipfs hash that wasn't pinned for some reason doesn't exist but would be referenced in ipfs-log. In these cases a store.load() would be virtually stuck forever as fetches in ipfs don't time out per default.

A solution would be to leverage the timeout that already exists in ipfs-log: https://github.com/orbitdb/ipfs-log/blob/master/src/entry-io.js#L21. We could expose that to the store.load function as the second argument and/or as a store default (https://github.com/orbitdb/orbit-db-store/blob/master/src/Store.js#L16):

store.load(amount, { fetchHeadTimeout: 5000 }).

Application throws error if it's closed during heavy replication

orbit-db: 0.28.4
orbit-db-store: 4.3.3

TypeError: Cannot read properties of null (reading 'length')
    at Replicator.onReplicationProgress (...\orbit-db-store\src\Store.js:98:25)
    at onProgressCallback (...\orbit-db-store\src\Replicator.js:147:14)

Here is the demo https://github.com/siepra/closeorbitdb

Also I tried enhancing the if statement in Store.js:98 with if (this._oplog && but going further results in different error (also reachable in the sample repo I posted above)

Can't resolve p-map

I use the orbit-db-store as a dependency of 3box-js. Compiling it with webpack --mode production I get the following error:

ERROR in ./node_modules/orbit-db-store/src/Replicator.js                                                                         
Module not found: Error: Can't resolve 'p-map' in '/dapp/node_modules/orbit-db-store/src'                                        
 @ ./node_modules/orbit-db-store/src/Replicator.js 2:13-29                                                                       
 @ ./node_modules/orbit-db-store/src/Store.js                                                                                    
 @ ./node_modules/orbit-db-docstore/src/DocumentStore.js                                                                         
 @ ./node_modules/orbit-db/src/OrbitDB.js                                                                                        
 @ ./node_modules/3box/lib/3box.js                                                                                               
 @ ./src/sagas/communityEntities.js                                                                                              
 @ ./src/sagas/index.js                                                                                                          
 @ ./src/index.js      

Looking into dependencies I see that p-map is missing a dependencies of this package. Adding it might fix that issue ๐Ÿ˜ƒ

Idiomatic Way to Get Log Entry from Hash

The Log entry contains the identity of the user that signed the data. This is very useful information.

How can I get this from a given hash? Do I just go to the IPFS CID and the block of data is there?

I was wondering because, to create a ledger chronicling ownership with OrbitDB is technically as simple as having a log with { to } (and the original owner is the identity). Otherwise, I'd have to store {to, from}, which would be redundant.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.