Home

Awesome

peer-crdt

NOT BEING ACTIVELY MAINTAINED

(Superseded by delta-crdts).

An extensible collection of operation-based CRDTs that are meant to work over a p2p network.

Index

API

CRDT.defaults(options)

Returns a CRDT collection that has these defaults for the crdt.create() options.

CRDT.create(type, id[, options])

Returns an new instance of the CRDT type.

crdt.on('change', () => {})

Emitted when the CRDT value has changed.

crdt.value()

Returns the latest computed CRDT value.

async crdt.peerId()

Resolves to a peer id. May be useful to identify current peer.

Other crdt methods

Different CRDT types can define different methods for manipulating the CRDT.

For instance, a G-Counter CRDT can define an increment method.

Composing

Allows the user to define high-level schemas, composing these low and high-level CRDTs into their own (observable) high-level structure classes.

CRDT.compose(schema)

Composes a new CRDT based on a schema.

Returns a constructor function for this composed CRDT.

const MyCRDT = CRDT.compose(schema)
const myCrdtInstance = MyCRDT(id)
const schema = {
  a: 'g-set',
  b: 'lww-set'
}

(Internally, the IDs of the sub-CRDTs will be composed by appending the key to the CRDT ID. Ex: 'id-of-my-crdt/a')

Instead of a key-value map, you can create a schema based on an array. The keys for these values will be the array indexes. Example:

const schema = [
  'g-set',
  'lww-set'
]

Any change in a nested object will trigger a change event in the container CRDT.

You can then get the current value by doing:

const value = myCrdtInstance.value()

Full example:

const schema = {
  a: 'g-set',
  b: 'lww-set'
}
const MyCRDT = CRDT.compose(schema)
const myCrdtInstance = MyCRDT(id)

myCrdtInstance.on('deep change', () => {
  console.log('new value:', myCrdtInstance.value())
})

Dynamic composition

You can use a CRDT as a value of another CRDT. For that, you should use crdt.createForEmbed(type) like this:

const array = myCRDT.create('rga', 'embedding-test', options)

const counter = array.createForEmbed('g-counter')
array.push(counter)

array.once('change', (event) => {
  console.log(array.value()) // [0]

  array.on('deep change', () => {
    console.log(array.value()) // [1]
  })

  event.value.increment()
})

Options

Here are the options for the CRDT.create and composed CRDT constructor are:

async function sign (entry, parents) {
  return await signSomehow(entry, parents)
}
async function authenticate (entry, parents, signature) {
  return await authenticateSomehow(entry, parents, signature)
}
async function signAndEncrypt(value) {
  const serialized = Buffer.from(JSON.stringify(value))
  const buffer = signAndEncryptSomehow(serialized)
  return buffer
}

(if no options.signAndEncrypt is provided, the node is on read-only mode and cannot create entries).

async function decryptAndVerify(buffer) {
  const serialized = await decryptAndVerifySomehow(buffer)
  return JSON.parse(Buffer.from(serialized).toString())
}

signAndEncrypt/decryptAndVerify contract

The options.decryptAndVerify function should be the inverse of options.signAndEncrypt.

const value = 'some value'
const signedAndEncrypted = await options.signAndEncrypt(value)
const decryptedValue = await options.decryptAndVerify(signedAndEncrypted)

assert(value === decryptedValue)

Errors

If options.decryptAndVerify(buffer) cannot verify a message, it should resolve to an error.

Built-in types

All the types in this package are operation-based CRDTs.

The following types are built-in:

Counters

NameIdentifierMutatorsValue Type
Increment-only Counterg-counter.increment()int
PN-Counterpn-counter.increment(),.decrement()int

Sets

NameIdentifierMutatorsValue Type
Grow-Only Setg-set.add(element)Set
Two-Phase Set2p-set.add(element), .remove(element)Set
Last-Write-Wins Setlww-set.add(element), .remove(element)Set
Observerd-Remove Setor-set.add(element), .remove(element)Set

Arrays

NameIdentifierMutatorsValue Type
Replicable Growable Arrayrga.push(element), .insertAt(pos, element), .removeAt(pos), .set(pos, element)Array
TreeDoctreedoc.push(element), .insertAt(pos, element), .removeAt(pos, length), .set(pos, element)Array

Registers

NameIdentifierMutatorsValue Type
Last-Write-Wins Registerlww-register.set(key, value)Map
Multi-Value Registermv-register.set(key, value)Map (maps a key to an array of concurrent values)

(TreeDoc is explained in this document)

(For the other types, a detailed explanation is in this document.)

Text

NameIdentifierMutatorsValue Type
Text based on Treedoctreedoc-text.push(string), .insertAt(pos, string), .removeAt(pos, length)String

Extending types

This package allows you to define new CRDT types.

CRDT.define(name, definition)

Defines a new CRDT type with a given name and definition.

The definition is an object with the following attributes:

Example of a G-Counter:

{
  first: () => 0,
  reduce: (message, previous) => message + previous,
  mutators: {
    increment: () => 1
  }
}

Read-only nodes

You can create a read-only node if you don't pass it an options.encrypt function.

const readOnlyNode = crdt.create('g-counter', 'some-id', {
  network, store, decrypt
})

await readOnlyNode.network.start()

Opaque replication

A node can be setup as a replicating node, while not being able to decrypt any of the CRDT operation data, thus not being able to track state.

Example:

const replicatingNode = crdt.replicate('some-id')

await replicatingNode.network.start()

Interfaces

Store

A store instance should expose the following methods:

Network

A network constructor should return a network instance and have the following signature:

function createNetwork(id, log, onRemoteHead) {
  return new SomeKindOfNetwork()
}

onRemoteHead is a function that should be called once a remote head is detected. It should be called with one argument: the remote head id.

A network instance should expose the following interface:

Internals

docs/INTERNAL.md

License

MIT