npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lazy-ipfs/versidag

v0.2.3

Published

Concurrent version history based on a Merkle-DAG

Downloads

4

Readme

versidag

NPM version Downloads Build Status Coverage Status Dependency status Dev Dependency status

Concurrent version history based on a Merkle-DAG.

version + dag = versidag

Motivation

In distributed systems, the replicas' clocks aren't reliable to get the total order of versions. This is specially true in p2p networks where the difference in clocks may be exacerbated. There are ways to get a partial order of versions by preserving causality, such as by using Lamport timestamps, Vector clocks and DAGs to name a few.

This module leverages Merkle-DAGs to preserve causality (happen-before) and a tie-breaker to determine the order of concurrent versions. The DAG will converge amongst replicas because:

  • The nodes on Merkle-DAGs are labeled based on their contents, meaning that whenever a merge occurs, they converge to the same label (cid).
  • The tie-breaker is deterministic among replicas, meaning that the total order of versions will be the same among replicas.
  • The merge operation guarantees that non-concurrent heads are removed.

Installation

$ npm install versidag

Usage

import createVersidag from 'versidag';

const myVersidag = createVersidag({
    tieBreaker: (node1, node2) => /* */,
    readNode: (cid) => /* */,
    writeNode: (node) => /* */,
});

const myVersidagA = await myVersidag.add('Hi', 1);
const myVersidagB = await myVersidagA.add('Hello', 2);
const myVersidagC = await myVersidagA.add('Hi World', 3);
const myVersidagD = await myVersidagB.merge(myVersidagC.headCids, 'Hello World');

const versions = await myVersidagD.resolve();
// [
//   { version: 'Hello World' },
//   { version: 'Hi World', meta: 3 }
//   { version: 'Hello', meta: 2 }
//   { version: 'Hi', meta: 1 }
// ]

API

createVersidag([headCids], config)

Creates a versidag instance with the specified heads.

If no headCids are supplied it means that it will be headless. The config is an object that has the following shape:

{
  // Writes a node, returning its content id
  // This function may return a promise
  writeNode: (node, config) => <nodeCid>,
  // The maximum concurrent writeNode calls, defaults to Infinity
  writeConcurrency: 10,
  // Maximum amount of time to wait for a writeNode to complete, defaults to null
  writeTimeout: 10000,
  // Reads a node by its content id
  // This function may return a promise
  readNode: (cid, config) => <node>,
  // The maximum concurrent writeNode calls, defaults to Infinity
  readConcurrency: 10,
  // Maximum amount of time to wait for a writeNode to complete, defaults to null
  readTimeout: 10000,
  // A tie-breaker that compares concurrent nodes, where the node's shape is { version, meta }
  // This is a comparator function that must return -1, 1 or 0
  tieBreaker: (node1, node2) => <number>,
}

The writeNode, readNode and tieBreaker are mandatory.

Important considerations:

  • The return value of writeNode must be based on the node contents, meaning that it should produce that same result for the same node, across replicas. This is often called a content id or cid.
  • The return value of tieBreaker must be consistent, that is, for the same arguments it should return exactly the same result, across replicas. In essence, it should be the same pure function in all replicas.
  • The readNode and writeNode must take into consideration their respective timeout configurations in case they perform async I/O. In simpler cases, you might use p-timeout which plays nicely with promises created with p-cancelable.

Example:

import hashObj from 'hash-obj';

const nodesMap = new Map();

// Example of a in-memory versidag where the tie-breaker is a simple meta comparison
const versidag = createVersidag({
  writeNode: (node) => {
    // The hash-obj module returns an hash of the object
    const cid = hashObj(node)

    nodesMap.set(cid, node);

    return cid;
  },
  readNode: (cid) => nodesMap.get(cid),
  tieBreaker: (node1, node2) => node1.meta - node2.meta
});

.headCids

A getter for the underlying DAG heads content ids.

.config

A getter for the underlying config.

.add(version, meta)

Adds a new version with the supplied meta, creating a DAG node pointing to the current heads.

Returns a promise that resolves to a new versidag pointing to the new head.

Example:

import createVersidag from 'versidag';

const myVersidag = createVersidag({ /* config */ });
const myVersidagA = await myVersidag.add('Hi', 1);

.union(headCids)

Concatenates the current heads with the supplied headCids.

Any duplicate heads are removed and the final heads will be sorted lexically.
Returns a new versidag pointing to the concatenated heads.

Note that no new DAG node will be written.

Example:

import createVersidag from 'versidag';

const myVersidag = createVersidag({ /* config */ });
const myVersidagA = await myVersidag.add('Hi', 1);
const myVersidagB = await myVersidagA.add('Hello', 2);
const myVersidagC = await myVersidagA.add('Hi World', 3);

const myVersidagD = await myVersidagB.union(myVersidagC.headCids);
// myVersidagD heads are ['B', 'C']

.merge(headCids, [version])

Creates a DAG node pointing to the union of the current heads with the supplied headCids, optionally pointing to a version.

Any duplicate heads are removed and the final heads will be sorted lexically.
Moreover, any non-concurrent heads will be removed so that the result is deterministic among replicas.

Returns a new versidag pointing to the concatenated heads.

In case you specify a version, it's important that it is consistent across all replicas. This means that the replicas must converge to the same version. The way the convergence happens is out of the scope of this module, but one possible solution is to use CRDTs.

Example:

import createVersidag from 'versidag';

const myVersidag = createVersidag({ /* config */ });
const myVersidagA = await myVersidag.add('Hi', 1);
const myVersidagB = await myVersidagA.add('Hello', 2);
const myVersidagC = await myVersidagA.add('Hi World', 3);

const myVersidagD = await myVersidagB.merge(myVersidagC.headCids);

// myVersidagD has a single head that is merge node pointing B and C

.resolve([options])

Resolves the versions by traversing the DAG.

Available options:

  • limit: The maximum number of versions to retrieve, defaults to Infinity.
  • fromCids: Start the traversal from these heads instead of the current ones, used for pagination.

Returns an object containing the versions. If limit was specified, the object also contains the nextCids that may be used for the next iteration.

If you just need a few versions, you should specify a limit. This ensures that the traversal will stop as soon as we get that amount of versions.

Example:

import createVersidag from 'versidag';

const myVersidag = createVersidag({ /* config */ });
const myVersidagA = await myVersidag.add('Hi', 1);
const myVersidagB = await myVersidagA.add('Hello', 2);
const myVersidagC = await myVersidagA.add('Hi World', 3);
const myVersidagD = await myVersidagB.merge(myVersidagC.headCids, 'Hello World');

const result1 = await versidag.resolve();
// {
//   versions:[
//     { version: 'Hello world' },
//     { version: 'Hi World', meta: 3 }
//     { version: 'Hello', meta: 2 }
//     { version: 'Hi', meta: 1 }
//   ],
//   nextCids: [],
// }

// With pagination
const result1 = await versidag.resolve({ limit: 2 });
// {
//   versions:[
//     { version: 'Hello world' },
//     { version: 'Hi World', meta: 3 }
//   ],
//   nextCids: ['B', 'A'],
// }

const result2 = versidag.resolve({ limit: 2, fromCids: result1.nextCids });
// {
//   versions:[
//     { version: 'Hello', meta: 2 },
//     { version: 'Hi', meta: 1 }
//   ],
//   nextCids: [],
// }

Tests

$ npm test
$ npm test -- --watch  # during development

License

Released under the MIT License.