├── .babelrc ├── lib ├── logoot.js └── logoot │ └── sequence.js ├── Makefile ├── bower.json ├── package.json ├── LICENSE.md ├── test └── logoot │ └── sequence-test.js ├── dist ├── logoot.min.js └── logoot.js ├── .eslintrc.yml └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /lib/logoot.js: -------------------------------------------------------------------------------- 1 | import * as _Sequence from './logoot/sequence'; 2 | export const Sequence = _Sequence; 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BROWSERIFY = ./node_modules/.bin/browserify 2 | UGLIFYJS = ./node_modules/.bin/uglifyjs 3 | SOURCE = $(shell ls ./lib/**/*.js) 4 | 5 | default: dist/logoot.min.js 6 | 7 | dist/logoot.min.js: dist/logoot.js 8 | cat dist/logoot.js | $(UGLIFYJS) > dist/logoot.min.js 9 | 10 | dist/logoot.js: $(SOURCE) 11 | $(BROWSERIFY) ./lib/logoot.js --standalone Logoot \ 12 | --transform [ babelify --presets [ es2015 ] ] \ 13 | --outfile dist/logoot.js 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logoot", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Jonathan Clem " 6 | ], 7 | "description": "A JavaScript implementation of the Logoot CRDT", 8 | "main": "dist/logoot.min.js", 9 | "keywords": [ 10 | "logoot", 11 | "crdt" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/usecanvas/logoot-js", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logoot", 3 | "version": "1.0.1", 4 | "description": "A JavaScript implementation of the Logoot CRDT", 5 | "keywords": ["logoot", "crdt"], 6 | "homepage": "https://github.com/usecanvas/logoot-js", 7 | "bugs": "https://github.com/usecanvas/logoot-js/issues", 8 | "repository": "github:usecanvas/logoot-js", 9 | "license": "MIT", 10 | "author": "Jonathan Clem ", 11 | "contributors": ["Jonathan Clem "], 12 | "main": "dist/logoot.js", 13 | "devDependencies": { 14 | "babel-preset-es2015": "^6.13.2", 15 | "babel-register": "^6.11.6", 16 | "babelify": "^7.3.0", 17 | "browserify": "^13.1.0", 18 | "mocha": "^3.0.2", 19 | "uglifyjs": "^2.4.10" 20 | }, 21 | "scripts": { 22 | "test": "mocha --compilers js:babel-register \"test/**/*-test.js\"" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jonathan Clem 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /test/logoot/sequence-test.js: -------------------------------------------------------------------------------- 1 | const Sequence = require('../../lib/logoot/sequence'); 2 | const assert = require('assert'); 3 | const { describe, it } = require('mocha'); 4 | 5 | describe('Sequence', function() { 6 | describe('.compareAtomIdents', function() { 7 | it('returns -1 when less-than', function() { 8 | assert.equal( 9 | Sequence.compareAtomIdents(Sequence.min, Sequence.max), 10 | -1 11 | ); 12 | }); 13 | 14 | it('returns 0 when equal', function() { 15 | assert.equal( 16 | Sequence.compareAtomIdents(Sequence.min, Sequence.min), 17 | 0 18 | ); 19 | }); 20 | 21 | it('returns 1 when greater-than', function() { 22 | assert.equal( 23 | Sequence.compareAtomIdents(Sequence.max, Sequence.min), 24 | 1 25 | ); 26 | }); 27 | }); 28 | 29 | describe('.emptySequence', function() { 30 | it('returns the empty sequence', function() { 31 | assert.deepEqual( 32 | Sequence.emptySequence(), 33 | [ 34 | [[[[0, 0]], 0], null], 35 | [[[[32767, 0]], 1], null] 36 | ] 37 | ) 38 | }); 39 | }); 40 | 41 | describe('.genAtomIdent', function() { 42 | it('returns a new atom ident between the two given', function() { 43 | const atomIdent = Sequence.genAtomIdent(1, 1, Sequence.min, Sequence.max); 44 | assertGreaterAtomIdent(atomIdent, Sequence.min); 45 | assertLesserAtomIdent(atomIdent, Sequence.max); 46 | }); 47 | 48 | it('falls back to the min atom when necessary', function() { 49 | const atomIdent = 50 | Sequence.genAtomIdent(1, 1, Sequence.min, [[[0, 0], [1, 0]], 1]); 51 | assertGreaterAtomIdent(atomIdent, Sequence.min); 52 | assertLesserAtomIdent(atomIdent, [[[0, 0], [1, 0]], 1]); 53 | }); 54 | }); 55 | 56 | describe('.insertAtom', function() { 57 | it('inserts the atom into the sequence with the function', function() { 58 | const sequence = Sequence.emptySequence(); 59 | const atomIdent = Sequence.genAtomIdent(1, 1, Sequence.min, Sequence.max); 60 | const atom = [atomIdent, "Hello, World"]; 61 | const newSequence = Sequence.insertAtom(sequence, atom, insertMut); 62 | const expectedSequence = 63 | [[Sequence.min, null], atom, [Sequence.max, null]]; 64 | 65 | assert.deepEqual(newSequence, expectedSequence); 66 | }); 67 | 68 | it('inserts atoms in longer sequences', function() { 69 | const sequence = Sequence.emptySequence(); 70 | const atomIdent = Sequence.genAtomIdent(1, 1, Sequence.min, Sequence.max); 71 | const atom = [atomIdent, "Hello, World"]; 72 | const newSequence = Sequence.insertAtom(sequence, atom, insertMut); 73 | 74 | const atomBIdent = Sequence.genAtomIdent(1, 2, atomIdent, Sequence.max); 75 | const atomB = [atomBIdent, "Foo Bar"]; 76 | const finalSequence = Sequence.insertAtom(newSequence, atomB, insertMut); 77 | const expectedSequence = 78 | [[Sequence.min, null], atom, atomB, [Sequence.max, null]]; 79 | 80 | assert.deepEqual(finalSequence, expectedSequence); 81 | }); 82 | }); 83 | }); 84 | 85 | function assertGreaterAtomIdent(identA, identB) { 86 | return assert.equal(Sequence.compareAtomIdents(identA, identB), 1); 87 | } 88 | 89 | function assertLesserAtomIdent(identA, identB) { 90 | return assert.equal(Sequence.compareAtomIdents(identA, identB), -1); 91 | } 92 | 93 | function insertMut(sequence, index, atom) { 94 | sequence.splice(index, 0, atom); 95 | return sequence; 96 | } 97 | -------------------------------------------------------------------------------- /dist/logoot.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Logoot=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oidentBInt)return 1;if(identAIntidentBSite)return 1;if(identASite0?prevPos:min;nextPos=nextPos.length>0?nextPos:max;var prevHead=prevPos[0];var nextHead=nextPos[0];var _prevHead=_slicedToArray(prevHead,2);var prevInt=_prevHead[0];var prevSiteID=_prevHead[1];var _nextHead=_slicedToArray(nextHead,2);var nextInt=_nextHead[0];var _nextSiteID=_nextHead[1];switch(compareIdents(prevHead,nextHead)){case-1:{var diff=nextInt-prevInt;if(diff>1){return[[randomIntBetween(prevInt,nextInt),siteID]]}else if(diff===1&&siteID>prevSiteID){return[[prevInt,siteID]]}else{return[prevHead,genPosition(siteID,prevPos.slice(1),nextPos.slice(1))]}}case 0:{return[prevHead,genPosition(siteID,prevPos.slice(1),nextPos.slice(1))]}case 1:{throw new Error('"Next" position was less than "previous" position.')}}}function randomIntBetween(min,max){return Math.floor(Math.random()*(max-(min+1)))+min+1}},{}]},{},[1])(1)}); -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | extends: 'eslint:recommended' 5 | parserOptions: 6 | sourceType: module 7 | rules: 8 | accessor-pairs: error 9 | array-bracket-spacing: 10 | - error 11 | - never 12 | array-callback-return: error 13 | arrow-body-style: error 14 | arrow-parens: error 15 | arrow-spacing: error 16 | block-scoped-var: error 17 | block-spacing: error 18 | brace-style: 19 | - error 20 | - 1tbs 21 | callback-return: error 22 | camelcase: error 23 | comma-dangle: error 24 | comma-spacing: 25 | - error 26 | - after: true 27 | before: false 28 | comma-style: 29 | - error 30 | - last 31 | complexity: error 32 | computed-property-spacing: 33 | - error 34 | - never 35 | consistent-return: 'off' 36 | consistent-this: error 37 | curly: 'off' 38 | default-case: 'off' 39 | dot-location: error 40 | dot-notation: error 41 | eol-last: error 42 | eqeqeq: error 43 | func-names: error 44 | func-style: 45 | - error 46 | - declaration 47 | generator-star-spacing: error 48 | global-require: error 49 | guard-for-in: error 50 | handle-callback-err: error 51 | id-blacklist: error 52 | id-length: 'off' 53 | id-match: error 54 | indent: 'off' 55 | init-declarations: error 56 | jsx-quotes: error 57 | key-spacing: error 58 | keyword-spacing: 59 | - error 60 | - after: true 61 | before: true 62 | linebreak-style: 63 | - error 64 | - unix 65 | lines-around-comment: error 66 | max-depth: error 67 | max-lines: error 68 | max-len: error 69 | max-nested-callbacks: error 70 | max-params: 'off' 71 | max-statements: 'off' 72 | max-statements-per-line: error 73 | multiline-ternary: 'off' 74 | new-cap: error 75 | new-parens: error 76 | newline-after-var: 77 | - error 78 | - always 79 | newline-before-return: 'off' 80 | newline-per-chained-call: error 81 | no-alert: error 82 | no-array-constructor: error 83 | no-bitwise: error 84 | no-caller: error 85 | no-catch-shadow: error 86 | no-confusing-arrow: error 87 | no-continue: 'off' 88 | no-div-regex: error 89 | no-duplicate-imports: error 90 | no-else-return: 'off' 91 | no-empty-function: error 92 | no-eq-null: error 93 | no-eval: error 94 | no-extend-native: error 95 | no-extra-bind: error 96 | no-extra-label: error 97 | no-extra-parens: error 98 | no-floating-decimal: error 99 | no-implicit-coercion: error 100 | no-implicit-globals: error 101 | no-implied-eval: error 102 | no-inline-comments: error 103 | no-invalid-this: error 104 | no-iterator: error 105 | no-label-var: error 106 | no-labels: error 107 | no-lone-blocks: error 108 | no-lonely-if: error 109 | no-loop-func: error 110 | no-magic-numbers: 'off' 111 | no-mixed-operators: 'off' 112 | no-mixed-requires: error 113 | no-multi-spaces: error 114 | no-multi-str: error 115 | no-multiple-empty-lines: error 116 | no-negated-condition: error 117 | no-nested-ternary: error 118 | no-new: error 119 | no-new-func: error 120 | no-new-object: error 121 | no-new-require: error 122 | no-new-wrappers: error 123 | no-octal-escape: error 124 | no-param-reassign: 'off' 125 | no-path-concat: error 126 | no-plusplus: 127 | - error 128 | - allowForLoopAfterthoughts: true 129 | no-process-env: error 130 | no-process-exit: error 131 | no-proto: error 132 | no-prototype-builtins: error 133 | no-restricted-globals: error 134 | no-restricted-imports: error 135 | no-restricted-modules: error 136 | no-restricted-syntax: error 137 | no-return-assign: error 138 | no-script-url: error 139 | no-self-compare: error 140 | no-sequences: error 141 | no-shadow: 'off' 142 | no-shadow-restricted-names: error 143 | no-spaced-func: error 144 | no-sync: error 145 | no-tabs: error 146 | no-ternary: 'off' 147 | no-throw-literal: error 148 | no-trailing-spaces: error 149 | no-undef-init: error 150 | no-undefined: error 151 | no-underscore-dangle: error 152 | no-unmodified-loop-condition: error 153 | no-unneeded-ternary: error 154 | no-unused-expressions: error 155 | no-unused-vars: 156 | - error 157 | - varsIgnorePattern: "^_" 158 | no-use-before-define: 'off' 159 | no-useless-call: error 160 | no-useless-computed-key: error 161 | no-useless-concat: error 162 | no-useless-constructor: error 163 | no-useless-escape: error 164 | no-useless-rename: error 165 | no-var: error 166 | no-void: error 167 | no-warning-comments: error 168 | no-whitespace-before-property: error 169 | no-with: error 170 | object-curly-newline: error 171 | object-curly-spacing: error 172 | object-property-newline: error 173 | object-shorthand: error 174 | one-var: 'off' 175 | one-var-declaration-per-line: error 176 | operator-assignment: error 177 | operator-linebreak: error 178 | padded-blocks: 'off' 179 | prefer-arrow-callback: error 180 | prefer-const: error 181 | prefer-reflect: error 182 | prefer-rest-params: error 183 | prefer-spread: error 184 | prefer-template: error 185 | quote-props: error 186 | quotes: 187 | - error 188 | - single 189 | radix: error 190 | require-jsdoc: error 191 | rest-spread-spacing: error 192 | semi: 'off' 193 | semi-spacing: 194 | - error 195 | - after: true 196 | before: false 197 | sort-imports: error 198 | sort-vars: error 199 | space-before-blocks: error 200 | space-before-function-paren: 'off' 201 | space-in-parens: 202 | - error 203 | - never 204 | space-infix-ops: error 205 | space-unary-ops: error 206 | spaced-comment: error 207 | strict: error 208 | template-curly-spacing: error 209 | unicode-bom: 210 | - error 211 | - never 212 | valid-jsdoc: 'off' 213 | vars-on-top: error 214 | wrap-iife: error 215 | wrap-regex: error 216 | yield-star-spacing: error 217 | yoda: 218 | - error 219 | - never 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logoot 2 | 3 | A JavaScript implementation of the 4 | [Logoot CRDT](https://hal.archives-ouvertes.fr/inria-00432368/document). There is 5 | an Elixir companion library to this one at 6 | [usecanvas/logoot_ex](https://github.com/usecanvas/logoot_ex). 7 | 8 | **This is a work-in-progress and is not battle-tested.** 9 | 10 | ## Installation 11 | 12 | Browser: 13 | 14 | ``` 15 | bower install --save logoot 16 | ``` 17 | 18 | Node: 19 | 20 | ``` 21 | npm install --save logoot 22 | ``` 23 | 24 | ## What is Logoot? 25 | 26 | Logoot is a 27 | [conflict-free replicated data type](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) 28 | that can be used to represent a sequence of atoms. Atoms in a Logoot sequence 29 | might be chunks of content or even characters in a string of text. 30 | 31 | The key to Logoot is the way in which it generates identifiers for atoms. To 32 | understand this, here are a few definitions to start with: 33 | 34 | - `identifier` A pair `` where `p` is an integer and `s` is a globally 35 | unique site identifier. A "site" represents any independent copy of a given 36 | Logoot CRDT. One web client with independent editable views of the same 37 | sequence would be two separate sites on that client. 38 | - `position` A list of identifiers. 39 | - `atom identifier` A pair `` where `pos` is a position, and `c` is the 40 | value of a vector clock at a given site. The site maintains a vector clock 41 | that increases incrementally with each no atom identifier generated. 42 | - `sequence atom` A pair `` where `ident` is an atom identifier, and 43 | `v` is any arbitrary value. This could be a single text character or a block 44 | in a text editor. 45 | - `sequence` A list of sequence atoms. This might represent a document or a 46 | list of blocks in an editor. Every sequence implicitly has a minimum sequence 47 | atom and a maximum sequence atom. All other atoms in the sequence are created 48 | somewhere between these two. 49 | 50 | Because of how these atom identifiers are structured, they are *totally 51 | ordered*, as opposed to *causally ordered*. No identifier cares about the 52 | identifier before it once it's been created, and so **tombstones are not 53 | necessary**. 54 | 55 | ### Generating a Sequence Atom 56 | 57 | Let's start with the empty sequence before any user has made edits to it: 58 | 59 | ```javascript 60 | [ 61 | [[[[0, 0]], 0], null], // Minimum sequence atom 62 | [[[[32767, 0]], 1], null] // Maximum sequence atom 63 | ] 64 | ``` 65 | 66 | *__Aside:__ Note that the integer in the maximum sequence atom's value is 67 | `32767`. This is chosen somewhat arbitrarily, but it is common for 68 | implementations to use the maximum unsigned 16-bit integer (and the original 69 | paper recommends it). One wouldn't want to choose an integer greater than any 70 | implementation's maximum safe integer value, and all implementations that 71 | communicate with one another must share this same maximum.* 72 | 73 | Now, a user at site `1` inserts the first line into their local copy of the 74 | document: 75 | 76 | ```javascript 77 | [ 78 | [[[[0, 0]], 0], null], // Minimum sequence atom 79 | [[[[6589, 1]], 0], "Hello, world!"], 80 | [[[[32767, 0]], 1], null] // Maximum sequence atom 81 | ] 82 | ``` 83 | 84 | Because there is free space between the integer of the minimum sequence atom and 85 | the integer of the maximum sequence atom, Logoot chooses a random integer 86 | between the two (how this is chosen is somewhat arbitrary—it just must be 87 | between them) and ends up with the sequence identifier: 88 | 89 | ```javascript 90 | [ 91 | [ 92 | [ 93 | 6589, // Number between min/max 94 | 1 // Site identifier 95 | ] 96 | ], 97 | 0 // Next value of site's vector clock 98 | ] 99 | ``` 100 | 101 | As a result, the document is properly sequenced. Ordering of sequence atoms is 102 | done by iterating over their position list and comparing first the integer, and 103 | then the site identifier if the integer is equal. 104 | 105 | *Note that vector clock values are not compared. Vector clock values are used to 106 | ensure unique atom identifiers, not for ordering.* 107 | 108 | Let's look at a more complex example. Start with a document that looks like 109 | this: 110 | 111 | ```javascript 112 | [ 113 | [[[[0, 0]]], null], // Minimum sequence atom 114 | [[[[1, 1], [3, 2]], 5], "Hello, world from site 2!"], 115 | [[[[1, 1], [5, 4]], 1], "I came from site 4!"], 116 | [[[[32767, 0]]], null] // Maximum sequence atom 117 | ] 118 | ``` 119 | 120 | Now, at site `3`, the user wants to insert a line between the two user-created 121 | lines in the above sequence. Logoot iterates over the pairs of identifiers in 122 | the "before" and "after" positions. Because the first identifier of each 123 | position is `[1, 1]`, Logoot can not insert an identifier directly between them, 124 | so it moves on to the next pair, `[3, 2]` and `[5, 4]`. Because site 3's 125 | site identifier is greater than site 2's, it can insert the identifier `[3, 3]` 126 | here and preserve ordering, since `[3, 2] < [3, 3] < [5, 4]`. 127 | 128 | The resulting sequence would be (assuming 3's vector clock is at `1`): 129 | 130 | ```javascript 131 | [ 132 | [[[[0, 0]]], null], // Minimum sequence atom 133 | [[[[1, 1], [3, 2]], 5], "Hello, world from site 2!"], 134 | [[[[1, 1], [3, 3]], 1], "Hello from site 3!"], 135 | [[[[1, 1], [5, 4]], 1], "I came from site 4!"], 136 | [[[[32767, 0]]], null] // Maximum sequence atom 137 | ] 138 | ``` 139 | 140 | Note that if this were actually site `1`, things would be different, because 141 | `[3, 2]` is not less than `[3, 1]`. Instead, Logoot generates a random integer 142 | between 3 and 5 (which is of course `4`), and our resulting identifier would be: 143 | 144 | ```javascript 145 | [[[1, 1], [4, 1]], 1] 146 | ``` 147 | 148 | Hopefully this provides a good enough explanation of what Logoot is and why it 149 | may be an excellent option for a sequence CRDT. The 150 | [paper](https://hal.archives-ouvertes.fr/inria-00432368/document) presenting it 151 | is a relatively easy read, and you may also want to look at this project's 152 | [Logoot.Sequence module](https://github.com/usecanvas/logoot-js/blob/master/lib/logoot/sequence.js) 153 | and its [tests](https://github.com/usecanvas/logoot-js/blob/master/test/logoot/sequence-test.js). 154 | 155 | ## TODO 156 | 157 | - [ ] Make min and max implicit, do not force user to provide them. 158 | - [ ] Prevent deleting min and max atoms. 159 | - [x] Make idempotent insert atom function. 160 | - [x] Make idempotent delete atom function. 161 | -------------------------------------------------------------------------------- /lib/logoot/sequence.js: -------------------------------------------------------------------------------- 1 | const MAX_POS = 32767; 2 | const ABS_MIN_ATOM_IDENT = [[[0, 0]], 0]; 3 | const ABS_MAX_ATOM_IDENT = [[[MAX_POS, 0]], 1]; 4 | 5 | /** 6 | * A sequence of atoms identified by atom identifiers. 7 | * 8 | * Across all replicas, a sequence is guaranteed to converge to the same value 9 | * given all operations have been received in causal order. 10 | * 11 | * @module Logoot.Sequence 12 | */ 13 | 14 | /** 15 | * The result of a comparison operation. 16 | * @typedef {(-1|0|1)} comparison 17 | */ 18 | 19 | /** 20 | * An array `[int, site]` where `int` is an integer and `site` is a site 21 | * identifier. 22 | * 23 | * The site identifier may be any comparable value. 24 | * 25 | * @typedef {Array} ident 26 | */ 27 | 28 | /** 29 | * A list of `ident`s. 30 | * @typedef {Array} position 31 | */ 32 | 33 | /** 34 | * An array `[pos, vector]` where `pos` is a position and `vector` is the value 35 | * of a vector clock at the site that created the associated atom. 36 | * 37 | * @typedef {Array} atomIdent 38 | */ 39 | 40 | /** 41 | * An array `[atomIdent, value]` where `atomIdent` is the globally unique 42 | * identifier for this atom and `value` is the atom's value. 43 | * 44 | * @typedef {Array} atom 45 | */ 46 | 47 | /** 48 | * An ordered sequence of `atom`s, whose first atom will always be 49 | * `[ABS_MIN_ATOM_IDENT, null]` and whose last atom will always be 50 | * `[ABS_MAX_ATOM_IDENT, null]`. 51 | * 52 | * @typedef {Array} sequence 53 | */ 54 | 55 | export const min = ABS_MIN_ATOM_IDENT; 56 | export const max = ABS_MAX_ATOM_IDENT; 57 | 58 | /** 59 | * Compare two atom identifiers, returning `1` if the first is greater than the 60 | * second, `-1` if it is less, and `0` if they are equal. 61 | * 62 | * @function 63 | * @param {atomIdent} atomIdentA The atom to compare another atom against 64 | * @param {atomIdent} atomIdentB The atom to compare against the first 65 | * @returns {comparison} 66 | */ 67 | export function compareAtomIdents(atomIdentA, atomIdentB) { 68 | return comparePositions(atomIdentA[0], atomIdentB[0]); 69 | } 70 | 71 | /** 72 | * Return the "empty" sequence, which is a sequence containing only the min and 73 | * max default atoms. 74 | * 75 | * @function 76 | * @returns {sequence} 77 | */ 78 | export function emptySequence() { 79 | return [[ABS_MIN_ATOM_IDENT, null], [ABS_MAX_ATOM_IDENT, null]]; 80 | } 81 | 82 | /** 83 | * Generate an atom ID between the two given atom IDs for the given site ID. 84 | * 85 | * @function 86 | * @param {*} siteID The ID of the site at which the atom originates 87 | * @param {number} clock The value of the site's vector clock 88 | * @param {atomIdent} prevAtomIdent The atom identify before the new one 89 | * @param {atomIdent} nextAtomIdent The atom identify after the new one 90 | * @return {atomIdent} 91 | */ 92 | export function genAtomIdent(siteID, clock, prevAtomIdent, nextAtomIdent) { 93 | return [genPosition(siteID, prevAtomIdent[0], nextAtomIdent[0]), clock]; 94 | } 95 | 96 | /** 97 | * Insert an atom into a sequence using the given function. 98 | * 99 | * The function will receive the sequence, an index to insert at, and the atom 100 | * as arguments. 101 | * 102 | * If the atom is already in the sequence, the **original sequence object** will 103 | * be returned. 104 | * 105 | * @function 106 | * @param {sequence} sequence The sequence to insert the atom into 107 | * @param {atom} atom The atom to insert into the sequence 108 | * @param {function(sequence, number, atom) : sequence} insertFunc The function 109 | * that will be called on to do the insert 110 | * @return {sequence} 111 | */ 112 | export function insertAtom(sequence, atom, insertFunc) { 113 | const sequenceLength = sequence.length; 114 | 115 | for (let i = 0; i < sequenceLength; i++) { 116 | const prev = sequence[i]; 117 | const next = sequence[i + 1]; 118 | 119 | const position = atom[0][0]; 120 | const prevPosition = prev[0][0]; 121 | const nextPosition = next[0][0]; 122 | 123 | const comparisons = 124 | [comparePositions(position, prevPosition), 125 | comparePositions(position, nextPosition)]; 126 | 127 | if (comparisons[0] === 1 && comparisons[1] === -1) { 128 | return insertFunc(sequence, i + 1, atom); 129 | } else if (comparisons[0] === 1 && comparisons[1] === 1) { 130 | continue; 131 | } else if (comparisons[0] === -1 && comparisons[1] === 1 || 132 | comparisons[0] === -1 && comparisons[1] === -1) { 133 | throw new Error('Sequence out of order!'); 134 | } else { 135 | return sequence; 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Compare two positions, returning `1` if the first is greater than the second, 142 | * `-1` if it is less, and `0` if they are equal. 143 | * 144 | * @function 145 | * @private 146 | * @param {position} posA The position to compare another position against 147 | * @param {position} posB The position to compare against the first 148 | * @returns {comparison} 149 | */ 150 | function comparePositions(posA, posB) { 151 | if (posA.length === 0 && posB.length === 0) return 0; 152 | if (posA.length === 0) return -1; 153 | if (posB.length === 0) return 1; 154 | 155 | switch (compareIdents(posA[0], posB[0])) { 156 | case 1: 157 | return 1; 158 | case -1: 159 | return -1; 160 | case 0: 161 | return comparePositions(posA.slice(1), posB.slice(1)); 162 | } 163 | } 164 | 165 | /** 166 | * Compare two idents, returning `1` if the first is greater than the second, 167 | * `-1` if it is less, and `0` if they are equal. 168 | * 169 | * @function 170 | * @private 171 | * @param {ident} identA The ident to compare another ident against 172 | * @param {ident} identB The ident to compare against the first 173 | * @returns {comparison} 174 | */ 175 | function compareIdents([identAInt, identASite], [identBInt, identBSite]) { 176 | if (identAInt > identBInt) return 1; 177 | if (identAInt < identBInt) return -1; 178 | if (identASite > identBSite) return 1; 179 | if (identASite < identBSite) return -1; 180 | return 0; 181 | } 182 | 183 | /** 184 | * Generate a position for an site ID between two other positions. 185 | * 186 | * @function 187 | * @private 188 | * @param {*} siteID The ID of the site at which the position originates 189 | * @param {position} prevPos The position before the new one 190 | * @param {position} nextPos The position after the new one 191 | */ 192 | function genPosition(siteID, prevPos, nextPos) { 193 | prevPos = prevPos.length > 0 ? prevPos : min[0]; 194 | nextPos = nextPos.length > 0 ? nextPos : max[0]; 195 | 196 | const prevHead = prevPos[0]; 197 | const nextHead = nextPos[0]; 198 | 199 | const [prevInt, prevSiteID] = prevHead; 200 | const [nextInt, _nextSiteID] = nextHead; 201 | 202 | switch (compareIdents(prevHead, nextHead)) { 203 | case -1: { 204 | const diff = nextInt - prevInt; 205 | 206 | if (diff > 1) { 207 | return [[randomIntBetween(prevInt, nextInt), siteID]]; 208 | } else if (diff === 1 && siteID > prevSiteID) { 209 | return [[prevInt, siteID]]; 210 | } else { 211 | return [prevHead].concat( 212 | genPosition(siteID, prevPos.slice(1), nextPos.slice(1))); 213 | } 214 | } case 0: { 215 | return [prevHead].concat( 216 | genPosition(siteID, prevPos.slice(1), nextPos.slice(1))); 217 | } case 1: { 218 | throw new Error('"Next" position was less than "previous" position.') 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Return a random number between two others. 225 | * 226 | * @function 227 | * @private 228 | * @param {number} min The floor (random will be greater-than) 229 | * @param {number} max The ceiling (ranodm will be less-than) 230 | * @returns {number} 231 | */ 232 | function randomIntBetween(min, max) { 233 | return Math.floor(Math.random() * (max - (min + 1))) + min + 1; 234 | } 235 | -------------------------------------------------------------------------------- /dist/logoot.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Logoot = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o} ident 55 | */ 56 | 57 | /** 58 | * A list of `ident`s. 59 | * @typedef {Array} position 60 | */ 61 | 62 | /** 63 | * An array `[pos, vector]` where `pos` is a position and `vector` is the value 64 | * of a vector clock at the site that created the associated atom. 65 | * 66 | * @typedef {Array} atomIdent 67 | */ 68 | 69 | /** 70 | * An array `[atomIdent, value]` where `atomIdent` is the globally unique 71 | * identifier for this atom and `value` is the atom's value. 72 | * 73 | * @typedef {Array} atom 74 | */ 75 | 76 | /** 77 | * An ordered sequence of `atom`s, whose first atom will always be 78 | * `[ABS_MIN_ATOM_IDENT, null]` and whose last atom will always be 79 | * `[ABS_MAX_ATOM_IDENT, null]`. 80 | * 81 | * @typedef {Array} sequence 82 | */ 83 | 84 | var min = exports.min = ABS_MIN_ATOM_IDENT; 85 | var max = exports.max = ABS_MAX_ATOM_IDENT; 86 | 87 | /** 88 | * Compare two atom identifiers, returning `1` if the first is greater than the 89 | * second, `-1` if it is less, and `0` if they are equal. 90 | * 91 | * @function 92 | * @param {atomIdent} atomIdentA The atom to compare another atom against 93 | * @param {atomIdent} atomIdentB The atom to compare against the first 94 | * @returns {comparison} 95 | */ 96 | function compareAtomIdents(atomIdentA, atomIdentB) { 97 | return comparePositions(atomIdentA[0], atomIdentB[0]); 98 | } 99 | 100 | /** 101 | * Return the "empty" sequence, which is a sequence containing only the min and 102 | * max default atoms. 103 | * 104 | * @function 105 | * @returns {sequence} 106 | */ 107 | function emptySequence() { 108 | return [[ABS_MIN_ATOM_IDENT, null], [ABS_MAX_ATOM_IDENT, null]]; 109 | } 110 | 111 | /** 112 | * Generate an atom ID between the two given atom IDs for the given site ID. 113 | * 114 | * @function 115 | * @param {*} siteID The ID of the site at which the atom originates 116 | * @param {number} clock The value of the site's vector clock 117 | * @param {atomIdent} prevAtomIdent The atom identify before the new one 118 | * @param {atomIdent} nextAtomIdent The atom identify after the new one 119 | * @return {atomIdent} 120 | */ 121 | function genAtomIdent(siteID, clock, prevAtomIdent, nextAtomIdent) { 122 | return [genPosition(siteID, prevAtomIdent[0], nextAtomIdent[0]), clock]; 123 | } 124 | 125 | /** 126 | * Insert an atom into a sequence using the given function. 127 | * 128 | * The function will receive the sequence, an index to insert at, and the atom 129 | * as arguments. 130 | * 131 | * If the atom is already in the sequence, the **original sequence object** will 132 | * be returned. 133 | * 134 | * @function 135 | * @function 136 | * @param {sequence} sequence The sequence to insert the atom into 137 | * @param {atom} atom The atom to insert into the sequence 138 | * @param {function(sequence, number, atom) : sequence} insertFunc The function 139 | * that will be called on to do the insert 140 | * @return {sequence} 141 | */ 142 | function insertAtom(sequence, atom, insertFunc) { 143 | var sequenceLength = sequence.length; 144 | 145 | for (var i = 0; i < sequenceLength; i++) { 146 | var prev = sequence[i]; 147 | var next = sequence[i + 1]; 148 | 149 | var position = atom[0][0]; 150 | var prevPosition = prev[0][0]; 151 | var nextPosition = next[0][0]; 152 | 153 | var comparisons = [comparePositions(position, prevPosition), comparePositions(position, nextPosition)]; 154 | 155 | if (comparisons[0] === 1 && comparisons[1] === -1) { 156 | return insertFunc(sequence, i + 1, atom); 157 | } else if (comparisons[0] === 1 && comparisons[1] === 1) { 158 | continue; 159 | } else if (comparisons[0] === -1 && comparisons[1] === 1 || comparisons[0] === -1 && comparisons[1] === -1) { 160 | throw new Error('Sequence out of order!'); 161 | } else { 162 | return sequence; 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Compare two positions, returning `1` if the first is greater than the second, 169 | * `-1` if it is less, and `0` if they are equal. 170 | * 171 | * @function 172 | * @private 173 | * @param {position} posA The position to compare another position against 174 | * @param {position} posB The position to compare against the first 175 | * @returns {comparison} 176 | */ 177 | function comparePositions(posA, posB) { 178 | if (posA.length === 0 && posB.length === 0) return 0; 179 | if (posA.length === 0) return -1; 180 | if (posB.length === 0) return 1; 181 | 182 | switch (compareIdents(posA[0], posB[0])) { 183 | case 1: 184 | return 1; 185 | case -1: 186 | return -1; 187 | case 0: 188 | return comparePositions(posA.slice(1), posB.slice(1)); 189 | } 190 | } 191 | 192 | /** 193 | * Compare two idents, returning `1` if the first is greater than the second, 194 | * `-1` if it is less, and `0` if they are equal. 195 | * 196 | * @function 197 | * @private 198 | * @param {ident} identA The ident to compare another ident against 199 | * @param {ident} identB The ident to compare against the first 200 | * @returns {comparison} 201 | */ 202 | function compareIdents(_ref, _ref2) { 203 | var _ref4 = _slicedToArray(_ref, 2); 204 | 205 | var identAInt = _ref4[0]; 206 | var identASite = _ref4[1]; 207 | 208 | var _ref3 = _slicedToArray(_ref2, 2); 209 | 210 | var identBInt = _ref3[0]; 211 | var identBSite = _ref3[1]; 212 | 213 | if (identAInt > identBInt) return 1; 214 | if (identAInt < identBInt) return -1; 215 | if (identASite > identBSite) return 1; 216 | if (identASite < identBSite) return -1; 217 | return 0; 218 | } 219 | 220 | /** 221 | * Generate a position for an site ID between two other positions. 222 | * 223 | * @function 224 | * @private 225 | * @param {*} siteID The ID of the site at which the position originates 226 | * @param {position} prevPos The position before the new one 227 | * @param {position} nextPos The position after the new one 228 | */ 229 | function genPosition(siteID, prevPos, nextPos) { 230 | prevPos = prevPos.length > 0 ? prevPos : min; 231 | nextPos = nextPos.length > 0 ? nextPos : max; 232 | 233 | var prevHead = prevPos[0]; 234 | var nextHead = nextPos[0]; 235 | 236 | var _prevHead = _slicedToArray(prevHead, 2); 237 | 238 | var prevInt = _prevHead[0]; 239 | var prevSiteID = _prevHead[1]; 240 | 241 | var _nextHead = _slicedToArray(nextHead, 2); 242 | 243 | var nextInt = _nextHead[0]; 244 | var _nextSiteID = _nextHead[1]; 245 | 246 | 247 | switch (compareIdents(prevHead, nextHead)) { 248 | case -1: 249 | { 250 | var diff = nextInt - prevInt; 251 | 252 | if (diff > 1) { 253 | return [[randomIntBetween(prevInt, nextInt), siteID]]; 254 | } else if (diff === 1 && siteID > prevSiteID) { 255 | return [[prevInt, siteID]]; 256 | } else { 257 | return [prevHead, genPosition(siteID, prevPos.slice(1), nextPos.slice(1))]; 258 | } 259 | }case 0: 260 | { 261 | return [prevHead, genPosition(siteID, prevPos.slice(1), nextPos.slice(1))]; 262 | }case 1: 263 | { 264 | throw new Error('"Next" position was less than "previous" position.'); 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * Return a random number between two others. 271 | * 272 | * @function 273 | * @private 274 | * @param {number} min The floor (random will be greater-than) 275 | * @param {number} max The ceiling (ranodm will be less-than) 276 | * @returns {number} 277 | */ 278 | function randomIntBetween(min, max) { 279 | return Math.floor(Math.random() * (max - (min + 1))) + min + 1; 280 | } 281 | 282 | },{}]},{},[1])(1) 283 | }); --------------------------------------------------------------------------------