• Stars
    star
    281
  • Rank 146,111 (Top 3 %)
  • Language
    JavaScript
  • Created over 11 years ago
  • Updated over 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Changeset library with operational transformation -- for node and the browser!

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

More Repositories

1

toposort

Topologically sort directed acyclic graphs (such as dependency lists) in javascript
JavaScript
296
star
2

smokesignal

Build your own small (or larger) peer to peer network with node.js
JavaScript
149
star
3

vdom-virtualize

Virtualize a DOM node
JavaScript
131
star
4

tivoka

JSON-RPC client/server library for PHP (supports v1.0 and v2.0 specs)
PHP
74
star
5

dom-ot

Transform DOM tree patches against each other (operational transformation)
JavaScript
51
star
6

buzzmap

draw and edit mindmaps interactively, using force-directed layouts (jQuery plugin)
JavaScript
22
star
7

prism.io

Share and edit documents in real-time. Superseded by http://github.com/marcelklehr/gulf
JavaScript
18
star
8

chordreader

Search for, display, transpose and save chords on your phone, that you get from the interwebs. 🎢
Java
13
star
9

beardless

Beardless templating for node.js. Start shaving!
JavaScript
12
star
10

warp

Proof of concept dom-ot editor -- superseded by https://github.com/gulf and https://github.com/hivejs
JavaScript
9
star
11

r-string

Commutative conflic-free replicated String (CRDT/Scuttlebutt)
JavaScript
7
star
12

magnet

An experiment: A duck-typed, object-oriented, syntax-driven (and potentially awesome) programming language.
JavaScript
6
star
13

zoocache

Output cache that neatly integrates into your PHP application.
PHP
6
star
14

p2p-rpc-stream

rpc streams for node. done right (TM)
JavaScript
5
star
15

vdom-serialize

JavaScript
5
star
16

y-connector-dat

A dat transport for y.js
JavaScript
4
star
17

PeerPad

peer-to-peer collaborative editing
JavaScript
3
star
18

ep_push2delete

delete your etherpad with one click.
JavaScript
3
star
19

minetest-conveyor

Conveyor mod for minetest, the awesome voxel mining game.
Lua
3
star
20

ep_infopanel

An "about" section for Etherpad-lite. Donuts do usually help.
JavaScript
3
star
21

waterline-to-jsonapi

Convert your waterline query results to jsonapi compliant responses.
JavaScript
3
star
22

intervarl

A variable interval. The timeout period can be changed dynamically.
JavaScript
2
star
23

ot-socialcalc

Operational transformation for socialcalc commands (shareJS compatible)
JavaScript
2
star
24

observ-emitter

an observable atomic emitter
JavaScript
2
star
25

browser-stream

Node.js streams in your browser
JavaScript
2
star
26

mutation-summary

Makes observing the DOM fast and easy
JavaScript
2
star
27

deep-reinforcement-learning-2022

Course work for the Deep Reinforcement Learning Course in Spring 2022 at University OsnabrΓΌck
Python
2
star
28

umbilical

Bidirectional rpc over tcp for node.js
JavaScript
1
star
29

magpie-typicality-mt

HTML
1
star
30

domnode-at-path

Supply a path and a root node and receive the domnode at that path.
JavaScript
1
star
31

ep_timeslider

The timeslider of etherpad lite, extracted and squashed into a plugin...
JavaScript
1
star
32

lseq-between

Sort sequences without re-indexing on insert ✨
JavaScript
1
star
33

ep_ether-o-meter

Display metrics. For etherpad. In real-time. With swag.
HTML
1
star
34

obj-to-argv

Takes an object and spits out the argv/args array as libraries like optimist would expect it.
JavaScript
1
star
35

theatre

my own, chocolate-flavoured lisp dialect
JavaScript
1
star
36

path-to-domnode

JavaScript
1
star
37

wutsdis

πŸ§™ Auto-tag your photo collection using an ImageNet object detection model.
Python
1
star
38

nextcloud-apps-ranking

Nextcloud apps listed by popularity
JavaScript
1
star
39

shoe2

Binary-safe sockJS streams with all necessary events
JavaScript
1
star
40

atomic-emitter

Emitters as values that you can pass around and assign, optionally with private event writing access.
JavaScript
1
star