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

changesets

v1.0.2

Published

Changeset library incorporating an operational transformation (OT) algorithm - for node and the browser, with shareJS support

Downloads

3,288

Readme

changesets Build Status

build text-based concurrent multi-user applications using operational transformation!

Easily create and apply changesets at all sites of a distributed system, leveraging Operational Transformation with:

  • convergence (everybody sees the same state, eventually)
  • intention preservation (put the 's' into 'sock' and it'll stay a sock)
  • reversibility (undo any edit without problems)

News: changesets now supports the ottypes API spec of shareJS. If you'd like a more unixy, transport agnostic tool, though, check out gulf.

News: Changesets v1.0.0 corrects the semantics of Changeset#merge, which now requires you to pass a consecutive changeset, instead of one that was created concurrently to the first one. This is inline with shareJS's API spec.

Install

npm install changesets or component install marcelklehr/changesets

In node and with component:

var Changeset = require('changesets').Changeset

In the bare browser:

<script type="text/javascript" src="node_modules/changesets/client-side.js"></script>
<script type="text/javascript">
// ...
var Changeset = changesets.Changeset
// ...
</script>

Support for adding more module systems is greatly appreaciated.

The return value of require('changesets') or the global changesets has a shareJS ottype interface.

Usage

A changeset is an ordered list of operations. There are 3 types of operations: Retain (retains a number of chars), Insert (inserts a number of chars), Skip (deletes them).

Now, Suppose we have two texts

var text1 = 'Rockets fly higher than rocks'
  , text2 = 'Rockets can fly higher than rocks, usually'

To construct a changeset by hand, just do

var cs = Changeset.create()
  .retain(8)
  .insert('can ')
  .retain(21)
  .insert(', usually')
  .end()

You can also directly pass a diff created with diff_match_patch, so to construct a changeset between two texts:

var dmp = require('diff_match_patch')
  , engine = new dmp.diff_match_patch

var diff = engine.diff_main(text1, text2)
var changeset = Changeset.fromDiff(diff)

Changesets can be applied to a text as follows:

var applied = changeset.apply(text1)

applied == text2 // true

In many cases you will find the need to serialize your changesets in order to efficiently transfer them through the network or store them on disk.

var serialized = changeset.pack() // '=5-1+2=2+5=6+b|habeen -ish thing.|i'

Changeset.unpack() takes the output of Changeset#pack() and returns a changeset object.

Changeset.unpack(serialized) // {"0":{"length":5,"symbol":"="},"1":{"length":1,"symbol":"-"},"2":{"length":2,"symbol":"+"},"3":{"length":2,"sym ...

If you'd like to display a changeset in a more humanly readable form, use Changeset#inspect (which is aliased to Changeset#toString):

changeset.inspect() // "=====-ha==been ======-ish thing."

Retained chars are displayed as = and removed chars as -. Insertions are displayed as the characters being inserted.

Transforming them

Inclusion Transformation

Say, for instance, you give a text to two different people. Each of them makes some changes and hands them back to you.

var rev0 = "Hello adventurer!"
  , revA = "Hello treasured adventurer!"
  , revB = "Good day adventurers, y'all!"

As a human you're certainly able to make out the changes and tell what's been changed to combine both revisions, for your computer it's harder. Firstly, you'll need to extract the changes in each version.

var csA = computeChanges(rev0, revA)
var csB = computeChanges(rev0, revB)

Now we can send the changes of revA from side A over the network to B and if we apply them on the original revision we get the full contents of revision A again.

csA.apply(rev0) == revA // true

But we don't want to apply them on the original revision, because we've already changed the text and created revB. We could of course try and apply it anyway:

csA.apply(revB) // apply csA on revision B -> "Good dtreasured ay adventurer!"

Ah, bad idea.

Since changeset A still assumes the original context, we need to adapt it, based on the changes of changeset B that have happened in the meantime, In order to be able to apply it on revB.

var transformedCsA = csA.transformAgainst(csB)

transformedCsA.apply(revB) // "Good day treasured adventurers, y'all!"

This transformation is called Inclusion Transformation, which adjusts a changeset in a way so that it assumes the changes of another changeset already happened.

Exclusion Transformation

Imagine a text editor, that allows users to undo any edit they've ever done to a document without undoing all edits that were done afterwards.

We decide to store all edits in a list of changesets, where each applied on top of the other results in the currently visible document.

Let's assume the following document with 4 revisions and 3 edits.

var versions =
[ ""
, "a"
, "ab"
, "abc"
]

// For posterity we create the edits like this

var edits = []
for (var i=1; i < versions.length; i++) {
  edits.push( computeChanges(text[i-1], text[i]) )
}

We can undo the last edit, by removing it from the stack of edits, inverting it and applying it on the current text.

var lastEdit = edits.pop()
newEditorContent = lastEdit.invert().apply(currentEditorContent)

Now, if we want to undo any edit, let's say the second instead of the last, we need to construct the inverse changeset of that second edit.

Then, we transform all following edits against this inverse changeset. But in order for this "undo changeset" to fit for the next changeset also, we in turn transform it against all previously iterated edits.

var undoIndex = 1

var undoCs = edits[undoIndex].invert()

var newEdits = [], edit
for (var i=undoIndex+1; i < edits.length; i++) {
  edit = edits[i]
  newEdits[i] = edit.transformAgainst(undoCs)
  undoCs = undoCs.transformAgainst(edit)
}

This way we can effectively exclude any given changes from all changes that follow it. This is called Exclusion Transformation.

Attributes

As you know, there are 3 types of operations (Retain, Skip and Insert) in a changeset, but actually, there are four. The forth is an operation type called Mark.

Mark can be used to apply attributes to a text. Currently attributes are like binary flags: Either a char has an attribute or it doesn't. Attributes are integer numbers (you'll need to implement some mapping between attribute names and these ids). You can pass attributes to the Mark operation as follows:

var mark = new Mark(/*length:*/5, {
  0: 1
, 7: 1
, 3: 1
, 15: 1
, -2: 1
, 11: 1
})

Did you notice the negative number? While positive numbers result in the application of some attribute, negative numbers enforce the removal of an attribute that has already been applied on some range of the text.

Now, how can you deal with those attributes? Currently, you'll have to keep changes to attributes in separate changesets. Storing attributes for a document can be done in a changeset with the length of the document into which you merge attribute changes. Applying them is as easy as iterating over the operations of that changeset (changeset.forEach(fn..)) and i.e. inserting HTML tags at respective positions in the corresponding document.

Warning: Attributes are still experimental. There are no tests, yet, and the API may change in the future.

Todo

  • Maybe support TP2? (lightwave solves the FT puzzle by retaining deleted chars)
  • vows is super ugly. Switch to mocha!

License

MIT

Changelog

1.0.0

  • Change semantics of Changeset#merge to adhere to logic as well as shareJS spec

0.4.0

  • Modularize operations
  • Attributes (Mark operation)
  • shareJS support as an ot type

0.3.1

  • fix Changeset#unpack() regex to allow for ops longer than 35 chars (thanks to @jonasp)

0.3.0

  • complete revamp of the algorithms and data structures
  • support for merging changesets