An extensible collection of operation-based CRDTs that are meant to work over a p2p network.
- API
- Composing
- Dynamic composition
- Built-in types
- Extending types
- Read-only nodes
- Zero-knowledge replication
- signAndEncrypt/decryptAndVerify contract
- Interfaces
- Internals
- License
Returns a CRDT collection that has these defaults for the crdt.create()
options.
type
: string representing the CRDT typeid
: string representing the identifier of this CRDT. This ID will be passed down into theLog
constructor, identifying this CRDT to all peers.options
: optional options. See options.
Returns an new instance of the CRDT type.
Emitted when the CRDT value has changed.
Returns the latest computed CRDT value.
Resolves to a peer id. May be useful to identify current peer.
Different CRDT types can define different methods for manipulating the CRDT.
For instance, a G-Counter CRDT can define an increment
method.
Allows the user to define high-level schemas, composing these low and high-level CRDTs into their own (observable) high-level structure classes.
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)
schema
: an object defining a schema. Example:
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())
})
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()
})
Here are the options for the CRDT.create
and composed CRDT constructor are:
network
: a network plugin constructor. Should be a function with the following signature:function (id, log, onRemoteHead)
and return an instance ofNetwork
store
: a constructor function with thw following signature:function (id)
, which returns an implementation of theStore
interfacesign
: a function that's used to generate the authentication data for a certain log entry. It will be called with the log entry (Object) and an array of parent entry ids (string), like this:
async function sign (entry, parents) {
return await signSomehow(entry, parents)
}
authenticate
: a function that's used to valudate the authentication data for a certain log entry. Should resolve to a boolean, indicating if this entry is authentic or not. It will be called with the log entry (Object), an array of parent entry ids (string) and a signature like this:
async function authenticate (entry, parents, signature) {
return await authenticateSomehow(entry, parents, signature)
}
signAndEncrypt
: an optional function that accepts a value object and resolves to a buffer, someting like this:
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).
decryptAndVerify
: a function that accepts an encrypted message buffer and resolves to a value object, something like this:
async function decryptAndVerify(buffer) {
const serialized = await decryptAndVerifySomehow(buffer)
return JSON.parse(Buffer.from(serialized).toString())
}
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)
If options.decryptAndVerify(buffer)
cannot verify a message, it should resolve to an error.
All the types in this package are operation-based CRDTs.
The following types are built-in:
Name | Identifier | Mutators | Value Type |
---|---|---|---|
Increment-only Counter | g-counter |
.increment() |
int |
PN-Counter | pn-counter |
.increment() ,.decrement() |
int |
Name | Identifier | Mutators | Value Type |
---|---|---|---|
Grow-Only Set | g-set |
.add(element) |
Set |
Two-Phase Set | 2p-set |
.add(element) , .remove(element) |
Set |
Last-Write-Wins Set | lww-set |
.add(element) , .remove(element) |
Set |
Observerd-Remove Set | or-set |
.add(element) , .remove(element) |
Set |
Name | Identifier | Mutators | Value Type |
---|---|---|---|
Replicable Growable Array | rga |
.push(element) , .insertAt(pos, element) , .removeAt(pos) , .set(pos, element) |
Array |
TreeDoc | treedoc |
.push(element) , .insertAt(pos, element) , .removeAt(pos, length) , .set(pos, element) |
Array |
Name | Identifier | Mutators | Value Type |
---|---|---|---|
Last-Write-Wins Register | lww-register |
.set(key, value) |
Map |
Multi-Value Register | mv-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.)
Name | Identifier | Mutators | Value Type |
---|---|---|---|
Text based on Treedoc | treedoc-text |
.push(string) , .insertAt(pos, string) , .removeAt(pos, length) |
String |
This package allows you to define new CRDT types.
Defines a new CRDT type with a given name and definition.
The definition is an object with the following attributes:
first
: a function that returns the initial valuereduce
: a function that accepts a message and the previous value and returns the new valuemutators
: an object containing named mutator functions, which should return the generated message for each mutation
Example of a G-Counter:
{
first: () => 0,
reduce: (message, previous) => message + previous,
mutators: {
increment: () => 1
}
}
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()
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()
A store instance should expose the following methods:
async empty ()
: resovles to a boolean indicating if this store has no entriesasync put (entry)
: puts an arbitrary JS object and resolves to a unique identifier for that object. The same object should generate the exact same id.async get (id)
: gets an object from the store. Resolves toundefined
if entry couldn't be found.async setHead(id)
: stores the current head (string).async getHead()
: retrieves the current head.
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:
async start()
: starts the networkasync stop()
: stops the networkasync get(id)
: tries retrieveing a specific entry from the networksetHead(headId)
: sets the current log head
MIT