├── .flowconfig ├── .gitignore ├── .jshintignore ├── do ├── .travis.yml ├── .jshintrc ├── test.html ├── bower.json ├── package.json ├── client ├── sha256 │ ├── bench.js │ ├── exports.js │ ├── sha256.js │ ├── hash.js │ └── utils.js ├── AllTests.js ├── Diff.js ├── sha256.js ├── Common.js ├── transform │ ├── NaiveJSONTransformer.js │ ├── TextTransformer.js │ ├── SmartJSONTransformer_test.js │ ├── SmartJSONTransformer.js │ └── GenericJSONTransformer_test.js ├── Message.js ├── SHA256.js ├── Diff_test.js ├── Operation_test.js ├── Patch_test.js ├── Operation.js ├── Patch.js └── ChainPad_test.js ├── make.js ├── readme.md └── LICENSE /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /chainpad.js 2 | /alltests.js 3 | /node_modules 4 | .*.swp 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /chainpad.dist.js 3 | /chainpad.js 4 | /alltests.js 5 | /randhtml.js 6 | /otaml.js 7 | /client/SHA256.js 8 | /client/sha256/ 9 | -------------------------------------------------------------------------------- /do: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function die { 3 | echo $1; 4 | exit 100; 5 | } 6 | NODE=`which node` || `which nodejs` || die 'please install nodejs' 7 | $NODE make.js $@ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | - "5" 6 | - "6" 7 | - "7" 8 | script: 9 | - npm run-script lint 10 | - npm run-script flow 11 | - ./do 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "laxcomma": true, 3 | "laxbreak": true, 4 | "node": true, 5 | "sub": true, 6 | "unused": true, 7 | "undef": true, 8 | "globals": { 9 | "self": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainpad", 3 | "description": "Realtime Collaborative Editor Algorithm based on the Nakamoto BlockChain", 4 | "main": "chainpad.js", 5 | "authors": [ 6 | "cjd@cjdns.fr", 7 | "ansuz@transitiontech.ca" 8 | ], 9 | "license": "LGPL-2.1", 10 | "keywords": [ 11 | "realtime", 12 | "chain", 13 | "synchronization" 14 | ], 15 | "homepage": "https://github.com/xwiki-contrib/chainpad", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "*test*", 21 | "tests", 22 | "client", 23 | "server", 24 | "do", 25 | "make.js" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainpad", 3 | "description": "Realtime Collaborative Editor Algorithm based on the Nakamoto BlockChain", 4 | "version": "5.3.0", 5 | "license": "LGPL-2.1", 6 | "main": "chainpad.dist.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/xwiki-contrib/chainpad" 10 | }, 11 | "dependencies": { 12 | "fast-diff": "^1.1.2", 13 | "gluejs": "~2.4.0", 14 | "json.sortify": "^2.2.2", 15 | "nthen": "^0.1.5", 16 | "xmhell": "~0.1.2" 17 | }, 18 | "devDependencies": { 19 | "flow-bin": "^0.42.0", 20 | "jshint": "~2.13.4" 21 | }, 22 | "overrides": { 23 | "minimist": "~1.2.3", 24 | "minimatch": "~3.0.8" 25 | }, 26 | "scripts": { 27 | "lint": "jshint --config .jshintrc --exclude-path .jshintignore .", 28 | "flow": "./node_modules/.bin/flow", 29 | "test": "node ./test.js" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/sha256/bench.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | var DATA = new Array(300).fill( 20 | "The researchers demonstrated that their new battery cells have at least three times as " + 21 | "much energy density as today’s lithium-ion batteries" 22 | ).join(''); 23 | 24 | var old = require('../SHA256.js'); 25 | var asm = require('./exports.js'); 26 | 27 | var res; 28 | var t0 = (+new Date()); 29 | for (var i = 0; i < 1000; i++) { res = old.hex_sha256(DATA); } 30 | console.log('old ' + res + ' ' + ((+new Date()) - t0)); 31 | 32 | var t0 = (+new Date()); 33 | for (var i = 0; i < 1000; i++) { asm.hex(DATA); } 34 | console.log('new ' + res + ' ' + ((+new Date()) - t0)); 35 | -------------------------------------------------------------------------------- /client/sha256/exports.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | var Sha256 = require('./sha256.js'); 20 | var Utils = require('./utils.js'); 21 | 22 | /** 23 | * SHA256 exports 24 | */ 25 | 26 | function sha256_bytes ( data ) { 27 | if ( data === undefined ) throw new SyntaxError("data required"); 28 | return Sha256.get_sha256_instance().reset().process(data).finish().result; 29 | } 30 | 31 | function sha256_hex ( data ) { 32 | var result = sha256_bytes(data); 33 | return Utils.bytes_to_hex(result); 34 | } 35 | 36 | function sha256_base64 ( data ) { 37 | var result = sha256_bytes(data); 38 | return Utils.bytes_to_base64(result); 39 | } 40 | 41 | Sha256.sha256_constructor.bytes = sha256_bytes; 42 | Sha256.sha256_constructor.hex = sha256_hex; 43 | Sha256.sha256_constructor.base64 = sha256_base64; 44 | 45 | //exports.SHA256 = sha256_constructor; 46 | module.exports = Sha256.sha256_constructor; 47 | -------------------------------------------------------------------------------- /client/AllTests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | /* globals document */ 20 | "use strict"; 21 | var testNames = require('testNames'); 22 | var nThen = require('nthen'); 23 | 24 | var cycles = 1; 25 | 26 | if (typeof(document) !== 'undefined') { 27 | var textArea = document.getElementById('log-textarea'); 28 | console.log = function (x) { 29 | textArea.value = textArea.value + x + '\n'; 30 | textArea.scrollTop = textArea.scrollHeight; 31 | }; 32 | } 33 | 34 | var timeOne = new Date().getTime(); 35 | 36 | var nt = nThen; 37 | testNames.forEach(function (file) { 38 | nt = nt(function (waitFor) { 39 | var test = require(file); 40 | console.log("\n\nRunning Test " + file + "\n\n"); 41 | nThen(function (waitFor) { 42 | test.main(cycles, waitFor()); 43 | }).nThen(function () { 44 | console.log("\n\nCompleted Test " + file + "\n\n"); 45 | }).nThen(waitFor()); 46 | }).nThen; 47 | }); 48 | 49 | nt(function () { 50 | console.log('Done'); 51 | console.log('in ' + (new Date().getTime() - timeOne)); 52 | }); 53 | -------------------------------------------------------------------------------- /client/sha256/sha256.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | var Utils = require('./utils.js'); 20 | var Hash = require('./hash.js'); 21 | var Asm = require('./sha256.asm.js'); 22 | 23 | var _sha256_block_size = 64, 24 | _sha256_hash_size = 32; 25 | 26 | function sha256_constructor ( options ) { 27 | options = options || {}; 28 | 29 | this.heap = Utils._heap_init( Uint8Array, options ); 30 | this.asm = options.asm || Asm.sha256_asm( { Uint8Array: Uint8Array }, null, this.heap.buffer ); 31 | 32 | this.BLOCK_SIZE = _sha256_block_size; 33 | this.HASH_SIZE = _sha256_hash_size; 34 | 35 | this.reset(); 36 | } 37 | 38 | sha256_constructor.BLOCK_SIZE = _sha256_block_size; 39 | sha256_constructor.HASH_SIZE = _sha256_hash_size; 40 | var sha256_prototype = sha256_constructor.prototype; 41 | sha256_prototype.reset = Hash.hash_reset; 42 | sha256_prototype.process = Hash.hash_process; 43 | sha256_prototype.finish = Hash.hash_finish; 44 | 45 | var sha256_instance = null; 46 | 47 | function get_sha256_instance () { 48 | if ( sha256_instance === null ) sha256_instance = new sha256_constructor( { heapSize: 0x100000 } ); 49 | return sha256_instance; 50 | } 51 | 52 | module.exports.get_sha256_instance = get_sha256_instance; 53 | module.exports.sha256_constructor = sha256_constructor; 54 | -------------------------------------------------------------------------------- /client/Diff.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | var FastDiff = require('fast-diff'); 23 | 24 | var transform = function (matches) { 25 | var out = []; 26 | var offset = 0; 27 | var first = true; 28 | var current = { 29 | offset: 0, 30 | toInsert: "", 31 | toRemove: 0, 32 | type: "Operation" 33 | }; 34 | matches.forEach(function (el, i) { 35 | if (el[0] === 0) { 36 | if (!first) { 37 | out.push(current); 38 | offset = current.offset + current.toRemove; 39 | current = { 40 | offset: offset, 41 | toInsert: "", 42 | toRemove: 0, 43 | type: "Operation" 44 | }; 45 | } 46 | offset += el[1].length; 47 | current.offset = offset; 48 | } else if (el[0] === 1) { 49 | current.toInsert = el[1]; 50 | } else { 51 | current.toRemove = el[1].length; 52 | } 53 | if (i === matches.length - 1 && el[0] !== 0) { 54 | out.push(current); 55 | } 56 | if (first) { first = false; } 57 | }); 58 | 59 | return out; 60 | }; 61 | module.exports.diff = function (oldS /*:string*/, newS /*:string*/) { 62 | return transform(FastDiff(oldS, newS)); 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /client/sha256.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | var asm_sha256 = require('./sha256/exports.js'); 21 | var old = require('./SHA256.js'); 22 | var Common = require('./Common'); 23 | 24 | /*:: 25 | export type Sha256_t = string; 26 | */ 27 | 28 | var brokenTextEncode = function (str) { 29 | var out = new Uint8Array(str.length); 30 | for (var i = 0; i < str.length; i++) { 31 | out[i] = str.charCodeAt(i) & 0xff; 32 | } 33 | return out; 34 | }; 35 | 36 | module.exports.check = function (hex /*:any*/) /*:Sha256_t*/ { 37 | if (typeof(hex) !== 'string') { throw new Error(); } 38 | if (!/[a-f0-9]{64}/.test(hex)) { throw new Error(); } 39 | return hex; 40 | }; 41 | 42 | module.exports.hex_sha256 = function (d /*:string*/) /*:Sha256_t*/ { 43 | d = d+''; 44 | var ret = asm_sha256.hex(brokenTextEncode(d)); 45 | if (Common.PARANOIA) { 46 | var oldHash = old.hex_sha256(d); 47 | if (oldHash !== ret) { 48 | try { 49 | throw new Error(); 50 | } catch (e) { 51 | console.log({ 52 | hashErr: e, 53 | badHash: d, 54 | asmHasher: asm_sha256.hex, 55 | oldHasher: old.hex_sha256 56 | }); 57 | } 58 | return oldHash; 59 | } 60 | } 61 | return ret; 62 | }; 63 | 64 | Object.freeze(module.exports); 65 | -------------------------------------------------------------------------------- /client/sha256/hash.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | var Utils = require('./utils.js'); 20 | 21 | function hash_reset () { 22 | this.result = null; 23 | this.pos = 0; 24 | this.len = 0; 25 | 26 | this.asm.reset(); 27 | 28 | return this; 29 | } 30 | 31 | function hash_process ( data ) { 32 | if ( this.result !== null ) 33 | throw new IllegalStateError("state must be reset before processing new data"); 34 | 35 | if ( Utils.is_string(data) ) 36 | data = Utils.string_to_bytes(data); 37 | 38 | if ( Utils.is_buffer(data) ) 39 | data = new Uint8Array(data); 40 | 41 | if ( !Utils.is_bytes(data) ) 42 | throw new TypeError("data isn't of expected type"); 43 | 44 | var asm = this.asm, 45 | heap = this.heap, 46 | hpos = this.pos, 47 | hlen = this.len, 48 | dpos = 0, 49 | dlen = data.length, 50 | wlen = 0; 51 | 52 | while ( dlen > 0 ) { 53 | wlen = Utils._heap_write( heap, hpos+hlen, data, dpos, dlen ); 54 | hlen += wlen; 55 | dpos += wlen; 56 | dlen -= wlen; 57 | 58 | wlen = asm.process( hpos, hlen ); 59 | 60 | hpos += wlen; 61 | hlen -= wlen; 62 | 63 | if ( !hlen ) hpos = 0; 64 | } 65 | 66 | this.pos = hpos; 67 | this.len = hlen; 68 | 69 | return this; 70 | } 71 | 72 | function hash_finish () { 73 | if ( this.result !== null ) 74 | throw new IllegalStateError("state must be reset before processing new data"); 75 | 76 | this.asm.finish( this.pos, this.len, 0 ); 77 | 78 | this.result = new Uint8Array(this.HASH_SIZE); 79 | this.result.set( this.heap.subarray( 0, this.HASH_SIZE ) ); 80 | 81 | this.pos = 0; 82 | this.len = 0; 83 | 84 | return this; 85 | } 86 | 87 | module.exports.hash_reset = hash_reset; 88 | module.exports.hash_process = hash_process; 89 | module.exports.hash_finish = hash_finish; 90 | -------------------------------------------------------------------------------- /client/Common.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* globals localStorage, window */ 3 | /* 4 | * Copyright 2024 XWiki SAS 5 | * 6 | * This is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU Lesser General Public License as 8 | * published by the Free Software Foundation; either version 2.1 of 9 | * the License, or (at your option) any later version. 10 | * 11 | * This software is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this software; if not, write to the Free 18 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 19 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 20 | */ 21 | "use strict"; 22 | 23 | module.exports.global = (function () { 24 | if (typeof(self) !== 'undefined') { return self; } 25 | if (typeof(global) !== 'undefined') { return global; } 26 | if (typeof(window) !== 'undefined') { return window; } 27 | throw new Error("no self, nor global, nor window"); 28 | }()); 29 | 30 | var cfg = function (name) { 31 | if (typeof(localStorage) !== 'undefined' && localStorage[name]) { 32 | return localStorage[name]; 33 | } 34 | // flow thinks global may be undefined 35 | return module.exports.global[name]; 36 | }; 37 | 38 | var PARANOIA = module.exports.PARANOIA = cfg("ChainPad_PARANOIA"); 39 | 40 | /* Good testing but slooooooooooow */ 41 | module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = cfg("ChainPad_VALIDATE_ENTIRE_CHAIN_EACH_MSG"); 42 | 43 | /* throw errors over non-compliant messages which would otherwise be treated as invalid */ 44 | module.exports.TESTING = cfg("ChainPad_TESTING"); 45 | 46 | module.exports.assert = function (expr /*:any*/) { 47 | if (!expr) { throw new Error("Failed assertion"); } 48 | }; 49 | 50 | module.exports.isUint = function (integer /*:number*/) { 51 | return (typeof(integer) === 'number') && 52 | (Math.floor(integer) === integer) && 53 | (integer >= 0); 54 | }; 55 | 56 | module.exports.randomASCII = function (length /*:number*/) { 57 | var content = []; 58 | for (var i = 0; i < length; i++) { 59 | content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 ); 60 | } 61 | return content.join(''); 62 | }; 63 | 64 | module.exports.strcmp = function (a /*:string*/, b /*:string*/) { 65 | if (PARANOIA && typeof(a) !== 'string') { throw new Error(); } 66 | if (PARANOIA && typeof(b) !== 'string') { throw new Error(); } 67 | return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) ); 68 | }; 69 | 70 | Object.freeze(module.exports); 71 | -------------------------------------------------------------------------------- /client/transform/NaiveJSONTransformer.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | var TextTransformer = require('./TextTransformer'); 23 | //var ChainPad = require('../ChainPad'); 24 | var Operation = require('../Operation'); 25 | var Common = require('../Common'); 26 | 27 | /*:: 28 | import type { Operation_t } from '../Operation'; 29 | */ 30 | 31 | module.exports = function ( 32 | opsToTransform /*:Array*/, 33 | opsTransformBy /*:Array*/, 34 | text /*:string*/ ) /*:Array*/ 35 | { 36 | var DEBUG = Common.global.REALTIME_DEBUG = Common.global.REALTIME_DEBUG || {}; 37 | 38 | var resultOps, text2, text3; 39 | try { 40 | // text = O (mutual common ancestor) 41 | // toTransform = A (your own operation) 42 | // transformBy = B (the incoming operation) 43 | // threeway merge (0, A, B) 44 | 45 | resultOps = TextTransformer(opsToTransform, opsTransformBy, text); 46 | 47 | text2 = Operation.applyMulti(opsTransformBy, text); 48 | 49 | text3 = Operation.applyMulti(resultOps, text2); 50 | try { 51 | JSON.parse(text3); 52 | return resultOps; 53 | } catch (e) { 54 | console.error(e); 55 | DEBUG.ot_parseError = { 56 | type: 'resultParseError', 57 | resultOps: resultOps, 58 | 59 | toTransform: opsToTransform, 60 | transformBy: opsTransformBy, 61 | 62 | text1: text, 63 | text2: text2, 64 | text3: text3, 65 | error: e 66 | }; 67 | console.log('Debugging info available at `window.REALTIME_DEBUG.ot_parseError`'); 68 | } 69 | } catch (x) { 70 | console.error(x); 71 | DEBUG.ot_applyError = { 72 | type: 'resultParseError', 73 | resultOps: resultOps, 74 | 75 | toTransform: opsToTransform, 76 | transformBy: opsTransformBy, 77 | 78 | text1: text, 79 | text2: text2, 80 | text3: text3, 81 | error: x 82 | }; 83 | console.log('Debugging info available at `window.REALTIME_DEBUG.ot_applyError`'); 84 | } 85 | 86 | // return an empty patch in case we can't do anything else 87 | return []; 88 | }; 89 | -------------------------------------------------------------------------------- /client/transform/TextTransformer.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | /*:: 23 | import type { Operation_t } from '../Operation' 24 | */ 25 | var Operation = require('../Operation'); 26 | var Common = require('../Common'); 27 | 28 | var transformOp0 = function ( 29 | toTransform /*:Operation_t*/, 30 | transformBy /*:Operation_t*/) 31 | { 32 | if (toTransform.offset > transformBy.offset) { 33 | if (toTransform.offset > transformBy.offset + transformBy.toRemove) { 34 | // simple rebase 35 | return Operation.create( 36 | toTransform.offset - transformBy.toRemove + transformBy.toInsert.length, 37 | toTransform.toRemove, 38 | toTransform.toInsert 39 | ); 40 | } 41 | var newToRemove = 42 | toTransform.toRemove - (transformBy.offset + transformBy.toRemove - toTransform.offset); 43 | if (newToRemove < 0) { newToRemove = 0; } 44 | if (newToRemove === 0 && toTransform.toInsert.length === 0) { return null; } 45 | return Operation.create( 46 | transformBy.offset + transformBy.toInsert.length, 47 | newToRemove, 48 | toTransform.toInsert 49 | ); 50 | } 51 | // they don't touch, yay 52 | if (toTransform.offset + toTransform.toRemove < transformBy.offset) { return toTransform; } 53 | // Truncate what will be deleted... 54 | var _newToRemove = transformBy.offset - toTransform.offset; 55 | if (_newToRemove === 0 && toTransform.toInsert.length === 0) { return null; } 56 | return Operation.create(toTransform.offset, _newToRemove, toTransform.toInsert); 57 | }; 58 | 59 | var transformOp = function ( 60 | toTransform /*:Operation_t*/, 61 | transformBy /*:Operation_t*/) 62 | { 63 | if (Common.PARANOIA) { 64 | Operation.check(toTransform); 65 | Operation.check(transformBy); 66 | } 67 | var result = transformOp0(toTransform, transformBy); 68 | if (Common.PARANOIA && result) { Operation.check(result); } 69 | return result; 70 | }; 71 | 72 | module.exports = function ( 73 | opsToTransform /*:Array*/, 74 | opsTransformBy /*:Array*/, 75 | doc /*:string*/ ) /*:Array*/ 76 | { 77 | var resultOfTransformBy = doc; 78 | var i; 79 | for (i = opsTransformBy.length - 1; i >= 0; i--) { 80 | resultOfTransformBy = Operation.apply(opsTransformBy[i], resultOfTransformBy); 81 | } 82 | var out = []; 83 | for (i = opsToTransform.length - 1; i >= 0; i--) { 84 | var tti = opsToTransform[i]; 85 | for (var j = opsTransformBy.length - 1; j >= 0; j--) { 86 | try { 87 | tti = transformOp(tti, opsTransformBy[j]); 88 | } catch (e) { 89 | console.error("The pluggable transform function threw an error, " + 90 | "failing operational transformation"); 91 | console.error(e.stack); 92 | return []; 93 | } 94 | if (!tti) { 95 | break; 96 | } 97 | } 98 | if (tti) { 99 | if (Common.PARANOIA) { Operation.check(tti, resultOfTransformBy.length); } 100 | out.unshift(tti); 101 | } 102 | } 103 | return out; 104 | }; 105 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | var Glue = require('gluejs'); 20 | var Fs = require('fs'); 21 | var nThen = require('nthen'); 22 | //var Os = require('os'); 23 | 24 | var cycles = 1; 25 | var tests = []; 26 | var timeOne; 27 | 28 | nThen(function (waitFor) { 29 | var g = new Glue(); 30 | g.basepath('./client'); 31 | g.main('ChainPad.js'); 32 | g.include('./ChainPad.js'); 33 | g.include('./Message.js'); 34 | g.include('./SHA256.js'); 35 | g.include('./Common.js'); 36 | g.include('./Patch.js'); 37 | g.include('./Operation.js'); 38 | g.include('./transform/TextTransformer.js'); 39 | g.include('./transform/NaiveJSONTransformer.js'); 40 | g.include('./transform/SmartJSONTransformer.js'); 41 | g.include('./Diff.js'); 42 | g.include('./sha256.js'); 43 | g.include('./sha256/exports.js'); 44 | g.include('./sha256/hash.js'); 45 | g.include('./sha256/sha256.asm.js'); 46 | g.include('./sha256/sha256.js'); 47 | g.include('./sha256/utils.js'); 48 | g.include('../node_modules/json.sortify/dist/JSON.sortify.js'); 49 | g.include('../node_modules/fast-diff/diff.js'); 50 | 51 | g.set('reset-exclude', true); 52 | g.set('verbose', true); 53 | g.set('umd', true); 54 | g.export('ChainPad'); 55 | 56 | //g.set('command', 'uglifyjs --no-copyright --m "toplevel"'); 57 | 58 | g.render(waitFor(function (err, txt) { 59 | if (err) { throw err; } 60 | // make an anonymous define, don't insist on your name! 61 | txt = txt.replace( 62 | '"function"==typeof define&&define.amd&&define(f,function', 63 | '"function"==typeof define&&define.amd&&define(function' 64 | ); 65 | Fs.writeFile('./chainpad.js', txt, waitFor()); 66 | })); 67 | }).nThen(function (waitFor) { 68 | timeOne = new Date().getTime(); 69 | if (process.argv.indexOf('--cycles') !== -1) { 70 | cycles = process.argv[process.argv.indexOf('--cycles')+1]; 71 | console.log("Running [" + cycles + "] test cycles"); 72 | } 73 | var nt = nThen; 74 | ['./client/', './client/transform/'].forEach(function (path) { 75 | Fs.readdir(path, waitFor(function (err, ret) { 76 | if (err) { throw err; } 77 | ret.forEach(function (file) { 78 | if (/_test\.js$/.test(file)) { 79 | nt = nt(function (waitFor) { 80 | tests.push(file); 81 | var test = require(path + file); 82 | console.log("Running Test " + file); 83 | test.main(cycles, waitFor()); 84 | }).nThen; 85 | } 86 | }); 87 | nt(waitFor()); 88 | })); 89 | }); 90 | }).nThen(function () { 91 | console.log("Tests passed."); 92 | console.log('in ' + (new Date().getTime() - timeOne)); 93 | }).nThen(function () { 94 | 95 | var g = new Glue(); 96 | g.basepath('./client'); 97 | g.main('AllTests.js'); 98 | g.include('./'); 99 | g.include('../node_modules/nthen/index.js'); 100 | g.include('../node_modules/fast-diff/diff.js'); 101 | g.remap('testNames', JSON.stringify(tests)); 102 | g.export('AllTests'); 103 | //g.set('command', 'uglifyjs --no-copyright --m "toplevel"'); 104 | g.render(Fs.createWriteStream('./alltests.js')); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /client/Message.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | var Common = require('./Common'); 22 | //var Operation = require('./Operation'); 23 | var Patch = require('./Patch'); 24 | var Sha = require('./sha256'); 25 | 26 | var Message = module.exports; 27 | 28 | var PATCH = Message.PATCH = 2; 29 | var CHECKPOINT = Message.CHECKPOINT = 4; 30 | 31 | /*:: 32 | import type { Sha256_t } from './sha256' 33 | import type { Patch_t } from './Patch' 34 | export type Message_Type_t = 2 | 4; 35 | export type Message_t = { 36 | type: 'Message', 37 | messageType: Message_Type_t, 38 | content: Patch_t, 39 | lastMsgHash: Sha256_t, 40 | hashOf: Sha256_t, 41 | mut: { 42 | parentCount: ?number, 43 | isInitialMessage: boolean, 44 | parent: ?Message_t, 45 | 46 | isFromMe: ?boolean, 47 | time: ?number, 48 | author: ?string, 49 | serverHash: ?string, 50 | } 51 | } 52 | */ 53 | 54 | var check = Message.check = function(msg /*:any*/) /*:Message_t*/ { 55 | Common.assert(msg.type === 'Message'); 56 | Common.assert(msg.messageType === PATCH || msg.messageType === CHECKPOINT); 57 | Patch.check(msg.content); 58 | Common.assert(typeof(msg.lastMsgHash) === 'string'); 59 | return msg; 60 | }; 61 | 62 | var DUMMY_HASH /*:Sha256_t*/ = ""; 63 | 64 | var create = Message.create = function ( 65 | type /*:Message_Type_t*/, 66 | content /*:Patch_t*/, 67 | lastMsgHash /*:Sha256_t*/) /*:Message_t*/ 68 | { 69 | var msg = { 70 | type: 'Message', 71 | messageType: type, 72 | content: content, 73 | lastMsgHash: lastMsgHash, 74 | hashOf: DUMMY_HASH, 75 | mut: { 76 | parentCount: undefined, 77 | isInitialMessage: false, 78 | isFromMe: false, 79 | parent: undefined, 80 | time: undefined, 81 | author: undefined, 82 | serverHash: undefined, 83 | } 84 | }; 85 | msg.hashOf = hashOf(msg); 86 | if (Common.PARANOIA) { check(msg); } 87 | return Object.freeze(msg); 88 | }; 89 | 90 | // $FlowFixMe doesn't like the toString() 91 | var toString = Message.toStr = Message.toString = function (msg /*:Message_t*/) { 92 | if (Common.PARANOIA) { check(msg); } 93 | if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { 94 | if (!msg.content) { throw new Error(); } 95 | return JSON.stringify([msg.messageType, Patch.toObj(msg.content), msg.lastMsgHash]); 96 | } else { 97 | throw new Error(); 98 | } 99 | }; 100 | 101 | Message.fromString = function (str /*:string*/) /*:Message_t*/ { 102 | var obj = {}; 103 | if (typeof(str) === "object") { 104 | obj = str; 105 | str = str.msg; 106 | } 107 | var m = JSON.parse(str); 108 | if (m[0] !== CHECKPOINT && m[0] !== PATCH) { throw new Error("invalid message type " + m[0]); } 109 | var msg = create(m[0], Patch.fromObj(m[1], (m[0] === CHECKPOINT)), m[2]); 110 | msg.mut.author = obj.author; 111 | msg.mut.time = obj.time && new Date(obj.time); 112 | msg.mut.serverHash = obj.serverHash; 113 | return Object.freeze(msg); 114 | }; 115 | 116 | var hashOf = Message.hashOf = function (msg /*:Message_t*/) { 117 | if (Common.PARANOIA) { check(msg); } 118 | var hash = Sha.hex_sha256(toString(msg)); 119 | return hash; 120 | }; 121 | 122 | Object.freeze(module.exports); 123 | -------------------------------------------------------------------------------- /client/SHA256.js: -------------------------------------------------------------------------------- 1 | /* A JavaScript implementation of the Secure Hash Algorithm, SHA-256 2 | * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ 3 | * Distributed under the BSD License 4 | * Some bits taken from Paul Johnston's SHA-1 implementation 5 | */ 6 | (function () { 7 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 8 | function safe_add (x, y) { 9 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 10 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 11 | return (msw << 16) | (lsw & 0xFFFF); 12 | } 13 | function S (X, n) {return ( X >>> n ) | (X << (32 - n));} 14 | function R (X, n) {return ( X >>> n );} 15 | function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} 16 | function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} 17 | function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} 18 | function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} 19 | function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} 20 | function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} 21 | function newArray (n) { 22 | var a = []; 23 | for (;n>0;n--) { 24 | a.push(undefined); 25 | } 26 | return a; 27 | } 28 | function core_sha256 (m, l) { 29 | var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2]; 30 | var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; 31 | var W = newArray(64); 32 | var a, b, c, d, e, f, g, h, i, j; 33 | var T1, T2; 34 | /* append padding */ 35 | m[l >> 5] |= 0x80 << (24 - l % 32); 36 | m[((l + 64 >> 9) << 4) + 15] = l; 37 | for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); 65 | return bin; 66 | } 67 | function binb2hex (binarray) { 68 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 69 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 70 | var str = ""; 71 | for (var i = 0; i < binarray.length * 4; i++) { 72 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 73 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 74 | } 75 | return str; 76 | } 77 | function hex_sha256(s){ 78 | return binb2hex(core_sha256(str2binb(s),s.length * chrsz)); 79 | } 80 | module.exports.hex_sha256 = hex_sha256; 81 | }()); 82 | -------------------------------------------------------------------------------- /client/Diff_test.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | var Common = require('./Common'); 22 | var Operation = require('./Operation'); 23 | var Diff = require('./Diff'); 24 | var Patch = require('./Patch'); 25 | var Sha = require('./sha256'); 26 | var nThen = require('nthen'); 27 | 28 | var operationEquals = function (opA, opB, doc) { 29 | if (opA.toRemove !== opB.toRemove) { return false; } 30 | if (opA.offset === opB.offset) { 31 | if (opA.toInsert !== opB.toInsert) { return false; } 32 | } 33 | var docA = Operation.apply(opA, doc); 34 | var docB = Operation.apply(opB, doc); 35 | return docA === docB; 36 | }; 37 | 38 | var die = function (n) { return Math.floor(Math.random() * n); }; 39 | var choose = function (A) { return A[die(A.length)]; }; 40 | 41 | var words = [ 42 | 'pewpewpew', 43 | 'bangpew', 44 | 'boomboom', 45 | 'foobang', 46 | 'pewbangpew', 47 | 'bangbang', 48 | 'boombang', 49 | ]; 50 | 51 | var chooseThreeWords = function () { 52 | var i = 3; 53 | var s = ''; 54 | while (i--) { s += choose(words); } 55 | return s; 56 | }; 57 | 58 | var lowEntropyRandomOp = function (docLength) { 59 | Common.assert(Common.isUint(docLength)); 60 | var offset = die(docLength); 61 | var toRemove = die(docLength - offset); 62 | var toInsert = ''; 63 | do { 64 | toInsert = chooseThreeWords(); 65 | } while (toRemove === 0 && toInsert === ''); 66 | return Operation.create(offset, toRemove, toInsert); 67 | }; 68 | 69 | var fuzzCycle = function (doc, hash) { 70 | if (!doc) { throw new Error('!doc'); } 71 | var ops = []; 72 | var lastOp; 73 | for (;;) { 74 | if (!doc || doc.length === 0) { throw new Error("NO GOOD"); } 75 | var op = lowEntropyRandomOp(10); //doc.length); // Operation.random(10); 76 | if (lastOp) { 77 | op = Operation.create( 78 | op.offset + lastOp.offset + lastOp.toRemove + 10, 79 | op.toRemove, 80 | op.toInsert 81 | ); 82 | } 83 | if (op.offset + op.toRemove > doc.length) { break; } 84 | op = Operation.simplify(op, doc); 85 | if (!op) { continue; } 86 | ops.push(lastOp = op); 87 | } 88 | var p = Patch.create(hash); 89 | Array.prototype.push.apply(p.operations, ops); 90 | var doc2 = Patch.apply(p, doc); 91 | //console.log(doc2); 92 | 93 | var ops2 = Diff.diff(doc, doc2); 94 | 95 | var ok = true; 96 | var i; 97 | if (ops.length === ops2.length) { 98 | for (i = 0; i < ops.length; i++) { 99 | if (operationEquals(ops[i], ops2[i], doc)) { continue; } 100 | ok = false; 101 | } 102 | } 103 | /* 104 | if (ok) { return; } 105 | 106 | for (i = 0; i < Math.max(ops.length, ops2.length); i++) { 107 | if (ops[i] && ops2[i] && operationEquals(ops[i], ops2[i], doc)) { continue; } 108 | if (ops[i]) { 109 | console.log(1); 110 | console.log(JSON.stringify(ops[i])); 111 | console.log(JSON.stringify(Operation.invert(ops[i], doc))); 112 | } 113 | if (ops2[i]) { 114 | console.log(2); 115 | console.log(JSON.stringify(ops2[i])); 116 | console.log(JSON.stringify(Operation.invert(ops2[i], doc))); 117 | } 118 | console.log(); 119 | } 120 | throw new Error();*/ 121 | }; 122 | 123 | var fuzz = function (cycles, callback) { 124 | for (var i = 0; i < 10; i++) { 125 | var doc = chooseThreeWords(); 126 | //console.log('DOC'); 127 | //console.log(doc); 128 | //Math.random() * Common.randomASCII(Math.random() * 9000 + 1000); 129 | var hash = Sha.hex_sha256(doc); 130 | for (var j = 0; j < cycles; j++) { 131 | fuzzCycle(doc, hash); 132 | } 133 | } 134 | callback(); 135 | }; 136 | 137 | module.exports.main = function (cycles /*:number*/, callback /*:()=>void*/) { 138 | console.log('diff test'); 139 | nThen(function (waitFor) { 140 | fuzz(100 || cycles, waitFor()); 141 | }).nThen(callback); 142 | }; 143 | -------------------------------------------------------------------------------- /client/Operation_test.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | var Common = require('./Common'); 22 | var Operation = require('./Operation'); 23 | var nThen = require('nthen'); 24 | 25 | var applyReversibility = function () { 26 | var doc = Common.randomASCII(Math.floor(Math.random() * 2000)); 27 | var operations = []; 28 | var rOperations = []; 29 | var docx = doc; 30 | for (var i = 0; i < 1000; i++) { 31 | operations[i] = Operation.random(docx.length); 32 | rOperations[i] = Operation.invert(operations[i], docx); 33 | docx = Operation.apply(operations[i], docx); 34 | } 35 | (function () { 36 | for (var i = 1000-1; i >= 0; i--) { 37 | if (rOperations[i]) { 38 | //var inverse = Operation.invert(rOperations[i], docx); 39 | docx = Operation.apply(rOperations[i], docx); 40 | } 41 | /*if (JSON.stringify(operations[i]) !== JSON.stringify(inverse)) { 42 | throw new Error("the inverse of the inverse is not the forward:\n" + 43 | JSON.stringify(operations[i], null, ' ') + "\n" + 44 | JSON.stringify(inverse, null, ' ')); 45 | }*/ 46 | } 47 | }()); 48 | Common.assert(doc === docx); 49 | }; 50 | 51 | var applyReversibilityMany = function (cycles, callback) { 52 | for (var i = 0; i < 100 * cycles; i++) { 53 | applyReversibility(); 54 | } 55 | callback(); 56 | }; 57 | 58 | var toObjectFromObject = function (cycles, callback) { 59 | for (var i = 0; i < 100 * cycles; i++) { 60 | var op = Operation.random(Math.floor(Math.random() * 2000)+1); 61 | Common.assert(JSON.stringify(op) === JSON.stringify(Operation.fromObj(Operation.toObj(op)))); 62 | } 63 | callback(); 64 | }; 65 | 66 | var mergeOne = function () { 67 | var docA = Common.randomASCII(Math.floor(Math.random() * 100)+1); 68 | var opAB = Operation.random(docA.length); 69 | var docB = Operation.apply(opAB, docA); 70 | var opBC = Operation.random(docB.length); 71 | var docC = Operation.apply(opBC, docB); 72 | 73 | if (Operation.shouldMerge(opAB, opBC)) { 74 | var opAC = Operation.merge(opAB, opBC); 75 | var docC2 = docA; 76 | try { 77 | if (opAC !== null) { 78 | docC2 = Operation.apply(opAC, docA); 79 | } 80 | Common.assert(docC2 === docC); 81 | } catch (e) { 82 | console.log("merging:\n" + 83 | JSON.stringify(opAB, null, ' ') + "\n" + 84 | JSON.stringify(opBC, null, ' ')); 85 | console.log("result:\n" + JSON.stringify(opAC, null, ' ')); 86 | throw e; 87 | } 88 | } 89 | }; 90 | var merge = function (cycles, callback) { 91 | for (var i = 0; i < 1000 * cycles; i++) { 92 | mergeOne(); 93 | } 94 | callback(); 95 | }; 96 | 97 | var simplify = function (cycles, callback) { 98 | for (var i = 0; i < 1000 * cycles; i++) { 99 | // use a very short document to cause lots of common patches. 100 | var docA = Common.randomASCII(Math.floor(Math.random() * 8)+1); 101 | var opAB = Operation.random(docA.length); 102 | var sopAB = Operation.simplify(opAB, docA); 103 | var docB = Operation.apply(opAB, docA); 104 | var sdocB = docA; 105 | if (sopAB) { 106 | sdocB = Operation.apply(sopAB, docA); 107 | } 108 | if (sdocB !== docB) { 109 | console.log(docA); 110 | console.log(JSON.stringify(opAB, null, ' ')); 111 | console.log(JSON.stringify(sopAB, null, ' ')); 112 | } 113 | Common.assert(sdocB === docB); 114 | } 115 | callback(); 116 | }; 117 | 118 | var emoji = function(callback) { 119 | var oldEmoji = "abc\uD83D\uDE00def"; 120 | var newEmoji = "abc\uD83D\uDE11def"; 121 | 122 | var op = Operation.create(3, 2, newEmoji); 123 | var sop = Operation.simplify(op, oldEmoji); 124 | 125 | Common.assert(sop !== null); 126 | if (sop !== null) 127 | { 128 | Common.assert(op.toRemove === sop.toRemove); 129 | } 130 | callback(); 131 | }; 132 | 133 | module.exports.main = function (cycles /*:number*/, callback /*:()=>void*/) { 134 | nThen(function (waitFor) { 135 | simplify(cycles, waitFor()); 136 | }).nThen(function (waitFor) { 137 | applyReversibilityMany(cycles, waitFor()); 138 | }).nThen(function (waitFor) { 139 | toObjectFromObject(cycles, waitFor()); 140 | }).nThen(function (waitFor) { 141 | merge(cycles, waitFor()); 142 | }).nThen(function (waitFor) { 143 | emoji(waitFor()); 144 | }).nThen(callback); 145 | }; 146 | -------------------------------------------------------------------------------- /client/sha256/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 XWiki SAS 3 | * 4 | * This is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU Lesser General Public License as 6 | * published by the Free Software Foundation; either version 2.1 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This software is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this software; if not, write to the Free 16 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 17 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 18 | */ 19 | //var FloatArray = global.Float64Array || global.Float32Array; // make PhantomJS happy 20 | 21 | var string_to_bytes = module.exports.string_to_bytes = function( str, utf8 ) { 22 | utf8 = !!utf8; 23 | 24 | var len = str.length, 25 | bytes = new Uint8Array( utf8 ? 4*len : len ); 26 | 27 | for ( var i = 0, j = 0; i < len; i++ ) { 28 | var c = str.charCodeAt(i); 29 | 30 | if ( utf8 && 0xd800 <= c && c <= 0xdbff ) { 31 | if ( ++i >= len ) throw new Error( "Malformed string, low surrogate expected at position " + i ); 32 | c = ( (c ^ 0xd800) << 10 ) | 0x10000 | ( str.charCodeAt(i) ^ 0xdc00 ); 33 | } 34 | else if ( !utf8 && c >>> 8 ) { 35 | throw new Error("Wide characters are not allowed."); 36 | } 37 | 38 | if ( !utf8 || c <= 0x7f ) { 39 | bytes[j++] = c; 40 | } 41 | else if ( c <= 0x7ff ) { 42 | bytes[j++] = 0xc0 | (c >> 6); 43 | bytes[j++] = 0x80 | (c & 0x3f); 44 | } 45 | else if ( c <= 0xffff ) { 46 | bytes[j++] = 0xe0 | (c >> 12); 47 | bytes[j++] = 0x80 | (c >> 6 & 0x3f); 48 | bytes[j++] = 0x80 | (c & 0x3f); 49 | } 50 | else { 51 | bytes[j++] = 0xf0 | (c >> 18); 52 | bytes[j++] = 0x80 | (c >> 12 & 0x3f); 53 | bytes[j++] = 0x80 | (c >> 6 & 0x3f); 54 | bytes[j++] = 0x80 | (c & 0x3f); 55 | } 56 | } 57 | 58 | return bytes.subarray(0, j); 59 | }; 60 | 61 | var hex_to_bytes = module.exports.hex_to_bytes = function( str ) { 62 | var len = str.length; 63 | if ( len & 1 ) { 64 | str = '0'+str; 65 | len++; 66 | } 67 | var bytes = new Uint8Array(len>>1); 68 | for ( var i = 0; i < len; i += 2 ) { 69 | bytes[i>>1] = parseInt( str.substr( i, 2), 16 ); 70 | } 71 | return bytes; 72 | }; 73 | 74 | var base64_to_bytes = module.exports.base64_to_bytes = function( str ) { 75 | return string_to_bytes( atob( str ) ); 76 | }; 77 | 78 | var bytes_to_string = module.exports.bytes_to_string = function( bytes, utf8 ) { 79 | utf8 = !!utf8; 80 | 81 | var len = bytes.length, 82 | chars = new Array(len); 83 | 84 | for ( var i = 0, j = 0; i < len; i++ ) { 85 | var b = bytes[i]; 86 | if ( !utf8 || b < 128 ) { 87 | chars[j++] = b; 88 | } 89 | else if ( b >= 192 && b < 224 && i+1 < len ) { 90 | chars[j++] = ( (b & 0x1f) << 6 ) | (bytes[++i] & 0x3f); 91 | } 92 | else if ( b >= 224 && b < 240 && i+2 < len ) { 93 | chars[j++] = ( (b & 0xf) << 12 ) | ( (bytes[++i] & 0x3f) << 6 ) | (bytes[++i] & 0x3f); 94 | } 95 | else if ( b >= 240 && b < 248 && i+3 < len ) { 96 | var c = ( (b & 7) << 18 ) | ( (bytes[++i] & 0x3f) << 12 ) | ( (bytes[++i] & 0x3f) << 6 ) | (bytes[++i] & 0x3f); 97 | if ( c <= 0xffff ) { 98 | chars[j++] = c; 99 | } 100 | else { 101 | c ^= 0x10000; 102 | chars[j++] = 0xd800 | (c >> 10); 103 | chars[j++] = 0xdc00 | (c & 0x3ff); 104 | } 105 | } 106 | else { 107 | throw new Error("Malformed UTF8 character at byte offset " + i); 108 | } 109 | } 110 | 111 | var str = '', 112 | bs = 16384; 113 | for ( var _i = 0; _i < j; _i += bs ) { 114 | str += String.fromCharCode.apply( String, chars.slice( _i, _i+bs <= j ? _i+bs : j ) ); 115 | } 116 | 117 | return str; 118 | }; 119 | 120 | var bytes_to_hex = module.exports.bytes_to_hex = function( arr ) { 121 | var str = ''; 122 | for ( var i = 0; i < arr.length; i++ ) { 123 | var h = ( arr[i] & 0xff ).toString(16); 124 | if ( h.length < 2 ) str += '0'; 125 | str += h; 126 | } 127 | return str; 128 | }; 129 | 130 | var bytes_to_base64 = module.exports.bytes_to_base64 = function( arr ) { 131 | return btoa( bytes_to_string(arr) ); 132 | }; 133 | 134 | var pow2_ceil = module.exports.pow2_ceil = function( a ) { 135 | a -= 1; 136 | a |= a >>> 1; 137 | a |= a >>> 2; 138 | a |= a >>> 4; 139 | a |= a >>> 8; 140 | a |= a >>> 16; 141 | a += 1; 142 | return a; 143 | }; 144 | 145 | var is_number = module.exports.is_number = function( a ) { 146 | return ( typeof a === 'number' ); 147 | }; 148 | 149 | var is_string = module.exports.is_string = function( a ) { 150 | return ( typeof a === 'string' ); 151 | }; 152 | 153 | var is_buffer = module.exports.is_buffer = function( a ) { 154 | return ( a instanceof ArrayBuffer ); 155 | }; 156 | 157 | var is_bytes = module.exports.is_bytes = function( a ) { 158 | return ( a instanceof Uint8Array ); 159 | }; 160 | 161 | var is_typed_array = module.exports.is_typed_array = function( a ) { 162 | return ( a instanceof Int8Array ) || ( a instanceof Uint8Array ) 163 | || ( a instanceof Int16Array ) || ( a instanceof Uint16Array ) 164 | || ( a instanceof Int32Array ) || ( a instanceof Uint32Array ) 165 | || ( a instanceof Float32Array ) 166 | || ( a instanceof Float64Array ); 167 | }; 168 | 169 | var _heap_init = module.exports._heap_init = function( constructor, options ) { 170 | var heap = options.heap, 171 | size = heap ? heap.byteLength : options.heapSize || 65536; 172 | 173 | if ( size & 0xfff || size <= 0 ) 174 | throw new Error("heap size must be a positive integer and a multiple of 4096"); 175 | 176 | heap = heap || new constructor( new ArrayBuffer(size) ); 177 | 178 | return heap; 179 | }; 180 | 181 | var _heap_write = module.exports._heap_write = function( heap, hpos, data, dpos, dlen ) { 182 | var hlen = heap.length - hpos, 183 | wlen = ( hlen < dlen ) ? hlen : dlen; 184 | 185 | heap.set( data.subarray( dpos, dpos+wlen ), hpos ); 186 | 187 | return wlen; 188 | }; 189 | -------------------------------------------------------------------------------- /client/Patch_test.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | var Common = require('./Common'); 22 | var Operation = require('./Operation'); 23 | var Patch = require('./Patch'); 24 | var Sha = require('./sha256'); 25 | var nThen = require('nthen'); 26 | //var ChainPad = require('./ChainPad'); 27 | var TextTransformer = require('./transform/TextTransformer'); 28 | 29 | // These are fuzz tests so increasing this number might catch more errors. 30 | var OPERATIONS = 1000; 31 | 32 | var addOperationConst = function (origDoc, expectedDoc, operations) { 33 | //var docx = origDoc; 34 | var doc = origDoc; 35 | var patch = Patch.create(Sha.hex_sha256(origDoc)); 36 | 37 | //var rebasedOps = []; 38 | for (var i = 0; i < operations.length; i++) { 39 | Patch.addOperation(patch, operations[i]); 40 | // sanity check 41 | doc = Operation.apply(operations[i], doc); 42 | } 43 | Common.assert(doc === expectedDoc); 44 | 45 | doc = Patch.apply(patch, origDoc); 46 | 47 | Common.assert(doc === expectedDoc); 48 | 49 | return patch; 50 | }; 51 | 52 | var addOperationCycle = function () { 53 | var origDoc = Common.randomASCII(Math.floor(Math.random() * 5000)+1); 54 | var operations = []; 55 | var doc = origDoc; 56 | for (var i = 0; i < Math.floor(Math.random() * OPERATIONS) + 1; i++) { 57 | var op = operations[i] = Operation.random(doc.length); 58 | doc = Operation.apply(op, doc); 59 | } 60 | 61 | var patch = addOperationConst(origDoc, doc, operations); 62 | 63 | return { 64 | operations: operations, 65 | patchOps: patch.operations 66 | }; 67 | }; 68 | 69 | var addOperation = function (cycles, callback) { 70 | 71 | var opsLen = 0; 72 | var patchOpsLen = 0; 73 | for (var i = 0; i < 100 * cycles; i++) { 74 | var out = addOperationCycle(); 75 | opsLen += out.operations.length; 76 | patchOpsLen += out.patchOps.length; 77 | } 78 | var mcr = Math.floor((opsLen / patchOpsLen) * 1000) / 1000; 79 | console.log("Merge compression ratio: " + mcr + ":1"); 80 | callback(); 81 | }; 82 | 83 | var toObjectFromObject = function (cycles, callback) { 84 | for (var i = 0; i < cycles * 100; i++) { 85 | var docA = Common.randomASCII(Math.floor(Math.random() * 100)+1); 86 | var patch = Patch.random(docA); 87 | var patchObj = Patch.toObj(patch); 88 | var patchB = Patch.fromObj(patchObj); 89 | Common.assert(JSON.stringify(patch) === JSON.stringify(patchB)); 90 | } 91 | callback(); 92 | }; 93 | 94 | var applyReversibility = function (cycles, callback) { 95 | for (var i = 0; i < cycles * 100; i++) { 96 | var docA = Common.randomASCII(Math.floor(Math.random() * 2000)); 97 | var patch = Patch.random(docA); 98 | var docB = Patch.apply(patch, docA); 99 | var docAA = Patch.apply(Patch.invert(patch, docA), docB); 100 | Common.assert(docAA === docA); 101 | } 102 | callback(); 103 | }; 104 | 105 | var merge = function (cycles, callback) { 106 | for (var i = 0; i < cycles * 100; i++) { 107 | var docA = Common.randomASCII(Math.floor(Math.random() * 5000)+1); 108 | var patchAB = Patch.random(docA); 109 | var docB = Patch.apply(patchAB, docA); 110 | var patchBC = Patch.random(docB); 111 | var docC = Patch.apply(patchBC, docB); 112 | var patchAC = Patch.merge(patchAB, patchBC); 113 | var docC2 = Patch.apply(patchAC, docA); 114 | Common.assert(docC === docC2); 115 | } 116 | callback(); 117 | }; 118 | 119 | var convert = function (p) { 120 | var out = Patch.create(p[0]); 121 | p[1].forEach(function (o) { out.operations.push(Operation.create.apply(null, o)); }); 122 | return out; 123 | }; 124 | 125 | var transformStatic = function () { 126 | var p0 = [ 127 | "0349d89ef3eeca9b7e2b7b8136d8ffe43206938d7c5df37cb3600fc2cd1df235", 128 | [ [4, 63, "VAPN]Z[bwdn\\OvP"], [ 88, 2, "" ] ] 129 | ]; 130 | var p1 = [ 131 | "0349d89ef3eeca9b7e2b7b8136d8ffe43206938d7c5df37cb3600fc2cd1df235", 132 | [ [ 0, 92, "[[fWjLRmIVZV[BiG^IHqDGmCuooPE" ] ] 133 | ]; 134 | 135 | Patch.transform( 136 | convert(p0), 137 | convert(p1), 138 | "_VMsPV\\PNXjQiEoTdoUHYxZALnDjB]onfiN[dBP[vqeGJJZ\\vNaQ`\\Y_jHNnrHOoFN^UWrWjCKoKe" + 139 | "D[`nosFrM`EpY\\Ib", 140 | TextTransformer 141 | ); 142 | 143 | var p2 = [ 144 | "74065c145b0455b4a48249fdf9a04cf0e3fbcb6d175435851723c976fc6db2b4", 145 | [ [ 10, 5, "" ] ] 146 | ]; 147 | 148 | Patch.transform(convert(p2), convert(p2), "SofyheYQWsva[NLAGkB", TextTransformer); 149 | }; 150 | 151 | var transform = function (cycles, callback) { 152 | transformStatic(); 153 | for (var i = 0; i < 100 * cycles; i++) { 154 | var docA = Common.randomASCII(Math.floor(Math.random() * 100)+1); 155 | var patchAB = Patch.random(docA); 156 | var patchAC = Patch.random(docA); 157 | var patchBC = Patch.transform(patchAC, patchAB, docA, TextTransformer); 158 | var docB = Patch.apply(patchAB, docA); 159 | Patch.apply(patchBC, docB); 160 | } 161 | callback(); 162 | }; 163 | 164 | var simplify = function (cycles, callback) { 165 | for (var i = 0; i < 100 * cycles; i++) { 166 | // use a very short document to cause lots of common patches. 167 | var docA = Common.randomASCII(Math.floor(Math.random() * 50)+1); 168 | var patchAB = Patch.random(docA); 169 | var spatchAB = Patch.simplify(patchAB, docA, Operation.simplify); 170 | var docB = Patch.apply(patchAB, docA); 171 | var sdocB = Patch.apply(spatchAB, docA); 172 | Common.assert(sdocB === docB); 173 | } 174 | callback(); 175 | }; 176 | 177 | module.exports.main = function (cycles /*:number*/, callback /*:()=>void*/) { 178 | nThen(function (waitFor) { 179 | simplify(cycles, waitFor()); 180 | }).nThen(function (waitFor) { 181 | transform(cycles, waitFor()); 182 | }).nThen(function (waitFor) { 183 | addOperation(cycles, waitFor()); 184 | }).nThen(function (waitFor) { 185 | toObjectFromObject(cycles, waitFor()); 186 | }).nThen(function (waitFor) { 187 | applyReversibility(cycles, waitFor()); 188 | }).nThen(function (waitFor) { 189 | merge(cycles, waitFor()); 190 | }).nThen(callback); 191 | }; 192 | -------------------------------------------------------------------------------- /client/Operation.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | var Common = require('./Common'); 22 | 23 | var Operation = module.exports; 24 | 25 | /*:: 26 | export type Operation_t = { 27 | type: 'Operation', 28 | offset: number, 29 | toRemove: number, 30 | toInsert: string 31 | }; 32 | export type Operation_Packed_t = [number, number, string]; 33 | export type Operation_Simplify_t = (Operation_t, string, typeof(Operation.simplify))=>?Operation_t; 34 | export type Operation_Transform_t = (string, Operation_t, Operation_t)=>?Operation_t; 35 | */ 36 | 37 | var check = Operation.check = function (op /*:any*/, docLength_opt /*:?number*/) /*:Operation_t*/ { 38 | Common.assert(op.type === 'Operation'); 39 | if (!Common.isUint(op.offset)) { throw new Error(); } 40 | if (!Common.isUint(op.toRemove)) { throw new Error(); } 41 | if (typeof(op.toInsert) !== 'string') { throw new Error(); } 42 | if (op.toRemove < 1 && op.toInsert.length < 1) { throw new Error(); } 43 | Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt); 44 | return op; 45 | }; 46 | 47 | var create = Operation.create = function ( 48 | offset /*:?number*/, 49 | toRemove /*:?number*/, 50 | toInsert /*:?string*/) 51 | { 52 | var out = { 53 | type: 'Operation', 54 | offset: offset || 0, 55 | toRemove: toRemove || 0, 56 | toInsert: toInsert || '', 57 | }; 58 | if (Common.PARANOIA) { check(out); } 59 | return Object.freeze(out); 60 | }; 61 | 62 | Operation.toObj = function (op /*:Operation_t*/) { 63 | if (Common.PARANOIA) { check(op); } 64 | return [op.offset,op.toRemove,op.toInsert]; 65 | }; 66 | 67 | // Allow any as input because we assert its type internally.. 68 | Operation.fromObj = function (obj /*:any*/) { 69 | Common.assert(Array.isArray(obj) && obj.length === 3); 70 | return create(obj[0], obj[1], obj[2]); 71 | }; 72 | 73 | /** 74 | * @param op the operation to apply. 75 | * @param doc the content to apply the operation on 76 | */ 77 | var apply = Operation.apply = function (op /*:Operation_t*/, doc /*:string*/) 78 | { 79 | if (Common.PARANOIA) { 80 | Common.assert(typeof(doc) === 'string'); 81 | check(op, doc.length); 82 | } 83 | return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove); 84 | }; 85 | 86 | Operation.applyMulti = function (ops /*:Array*/, doc /*:string*/) 87 | { 88 | for (var i = ops.length - 1; i >= 0; i--) { doc = apply(ops[i], doc); } 89 | return doc; 90 | }; 91 | 92 | var invert = Operation.invert = function (op /*:Operation_t*/, doc /*:string*/) { 93 | if (Common.PARANOIA) { 94 | check(op); 95 | Common.assert(typeof(doc) === 'string'); 96 | Common.assert(op.offset + op.toRemove <= doc.length); 97 | } 98 | return create( 99 | op.offset, 100 | op.toInsert.length, 101 | // https://stackoverflow.com/a/31733628 102 | (' ' + doc.substring(op.offset, op.offset + op.toRemove)).slice(1) 103 | ); 104 | }; 105 | 106 | // see http://unicode.org/faq/utf_bom.html#utf16-7 107 | var surrogatePattern = /[\uD800-\uDBFF]|[\uDC00-\uDFFF]/; 108 | var hasSurrogate = Operation.hasSurrogate = function(str /*:string*/) { 109 | return surrogatePattern.test(str); 110 | }; 111 | 112 | /** 113 | * ATTENTION: This function is not just a neat way to make patches smaller, it's 114 | * actually part of the ChainPad consensus rules, so if you have a clever 115 | * idea to make it a bit faster, it is going to cause ChainPad to reject 116 | * old patches, which means when you go to load the history of a pad, you're 117 | * sunk. 118 | * tl;dr can't touch this 119 | */ 120 | Operation.simplify = function (op /*:Operation_t*/, doc /*:string*/) { 121 | if (Common.PARANOIA) { 122 | check(op); 123 | Common.assert(typeof(doc) === 'string'); 124 | Common.assert(op.offset + op.toRemove <= doc.length); 125 | } 126 | var rop = invert(op, doc); 127 | 128 | var minLen = Math.min(op.toInsert.length, rop.toInsert.length); 129 | var i = 0; 130 | while (i < minLen && rop.toInsert[i] === op.toInsert[i]) { 131 | if (hasSurrogate(rop.toInsert[i]) || hasSurrogate(op.toInsert[i])) { 132 | if (op.toInsert[i + 1] === rop.toInsert[i + 1]) { 133 | i++; 134 | } else { 135 | break; 136 | } 137 | } 138 | i++; 139 | } 140 | var opOffset = op.offset + i; 141 | var opToRemove = op.toRemove - i; 142 | var opToInsert = op.toInsert.substring(i); 143 | var ropToInsert = rop.toInsert.substring(i); 144 | 145 | if (ropToInsert.length === opToInsert.length) { 146 | for (i = ropToInsert.length-1; i >= 0 && ropToInsert[i] === opToInsert[i]; i--) ; 147 | opToInsert = opToInsert.substring(0, i+1); 148 | opToRemove = i+1; 149 | } 150 | 151 | if (opToRemove === 0 && opToInsert.length === 0) { return null; } 152 | return create(opOffset, opToRemove, opToInsert); 153 | }; 154 | 155 | Operation.equals = function (opA /*:Operation_t*/, opB /*:Operation_t*/) { 156 | return (opA.toRemove === opB.toRemove 157 | && opA.toInsert === opB.toInsert 158 | && opA.offset === opB.offset); 159 | }; 160 | 161 | Operation.lengthChange = function (op /*:Operation_t*/) 162 | { 163 | if (Common.PARANOIA) { check(op); } 164 | return op.toInsert.length - op.toRemove; 165 | }; 166 | 167 | /* 168 | * @return the merged operation OR null if the result of the merger is a noop. 169 | */ 170 | Operation.merge = function (oldOpOrig /*:Operation_t*/, newOpOrig /*:Operation_t*/) { 171 | if (Common.PARANOIA) { 172 | check(newOpOrig); 173 | check(oldOpOrig); 174 | } 175 | 176 | var oldOp_offset = oldOpOrig.offset; 177 | var oldOp_toRemove = oldOpOrig.toRemove; 178 | var oldOp_toInsert = oldOpOrig.toInsert; 179 | 180 | var newOp_offset = newOpOrig.offset; 181 | var newOp_toRemove = newOpOrig.toRemove; 182 | var newOp_toInsert = newOpOrig.toInsert; 183 | 184 | var offsetDiff = newOp_offset - oldOp_offset; 185 | 186 | if (newOp_toRemove > 0) { 187 | var origOldInsert = oldOp_toInsert; 188 | oldOp_toInsert = ( 189 | oldOp_toInsert.substring(0,offsetDiff) 190 | + oldOp_toInsert.substring(offsetDiff + newOp_toRemove) 191 | ); 192 | newOp_toRemove -= (origOldInsert.length - oldOp_toInsert.length); 193 | if (newOp_toRemove < 0) { newOp_toRemove = 0; } 194 | 195 | oldOp_toRemove += newOp_toRemove; 196 | newOp_toRemove = 0; 197 | } 198 | 199 | if (offsetDiff < 0) { 200 | oldOp_offset += offsetDiff; 201 | oldOp_toInsert = newOp_toInsert + oldOp_toInsert; 202 | 203 | } else if (oldOp_toInsert.length === offsetDiff) { 204 | oldOp_toInsert = oldOp_toInsert + newOp_toInsert; 205 | 206 | } else if (oldOp_toInsert.length > offsetDiff) { 207 | oldOp_toInsert = ( 208 | oldOp_toInsert.substring(0,offsetDiff) 209 | + newOp_toInsert 210 | + oldOp_toInsert.substring(offsetDiff) 211 | ); 212 | } else { 213 | throw new Error("should never happen\n" + 214 | JSON.stringify([oldOpOrig,newOpOrig], null, ' ')); 215 | } 216 | 217 | if (oldOp_toInsert === '' && oldOp_toRemove === 0) { return null; } 218 | 219 | return create(oldOp_offset, oldOp_toRemove, oldOp_toInsert); 220 | }; 221 | 222 | /** 223 | * If the new operation deletes what the old op inserted or inserts content in the middle of 224 | * the old op's content or if they abbut one another, they should be merged. 225 | */ 226 | Operation.shouldMerge = function (oldOp /*:Operation_t*/, newOp /*:Operation_t*/) 227 | { 228 | if (Common.PARANOIA) { 229 | check(oldOp); 230 | check(newOp); 231 | } 232 | if (newOp.offset < oldOp.offset) { 233 | return (oldOp.offset <= (newOp.offset + newOp.toRemove)); 234 | } else { 235 | return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length)); 236 | } 237 | }; 238 | 239 | /** 240 | * Rebase newOp against oldOp. 241 | * 242 | * @param oldOp the eariler operation to have happened. 243 | * @param newOp the later operation to have happened (in time). 244 | * @return either the untouched newOp if it need not be rebased, 245 | * the rebased clone of newOp if it needs rebasing, or 246 | * null if newOp and oldOp must be merged. 247 | */ 248 | Operation.rebase = function (oldOp /*:Operation_t*/, newOp /*:Operation_t*/) { 249 | if (Common.PARANOIA) { 250 | check(oldOp); 251 | check(newOp); 252 | } 253 | if (newOp.offset < oldOp.offset) { return newOp; } 254 | return create( 255 | newOp.offset + oldOp.toRemove - oldOp.toInsert.length, 256 | newOp.toRemove, 257 | newOp.toInsert 258 | ); 259 | }; 260 | 261 | /** Used for testing. */ 262 | Operation.random = function (docLength /*:number*/) { 263 | Common.assert(Common.isUint(docLength)); 264 | var offset = Math.floor(Math.random() * 100000000 % docLength) || 0; 265 | var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0; 266 | var toInsert = ''; 267 | do { 268 | toInsert = Common.randomASCII(Math.floor(Math.random() * 20)); 269 | } while (toRemove === 0 && toInsert === ''); 270 | return create(offset, toRemove, toInsert); 271 | }; 272 | 273 | Object.freeze(module.exports); 274 | -------------------------------------------------------------------------------- /client/Patch.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | var Common = require('./Common'); 22 | var Operation = require('./Operation'); 23 | var Sha = require('./sha256'); 24 | 25 | var Patch = module.exports; 26 | 27 | /*:: 28 | import type { 29 | Operation_t, 30 | Operation_Packed_t, 31 | Operation_Simplify_t, 32 | Operation_Transform_t 33 | } from './Operation'; 34 | import type { Sha256_t } from './sha256'; 35 | export type Patch_t = { 36 | type: 'Patch', 37 | operations: Array, 38 | parentHash: Sha256_t, 39 | isCheckpoint: boolean, 40 | mut: { 41 | inverseOf: ?Patch_t, 42 | } 43 | }; 44 | export type Patch_Packed_t = Array; 45 | export type Patch_Transform_t = ( 46 | toTransform:Array, 47 | transformBy:Array, 48 | state0:string 49 | ) => Array; 50 | */ 51 | 52 | var create = Patch.create = function (parentHash /*:Sha256_t*/, isCheckpoint /*:?boolean*/) { 53 | var out = Object.freeze({ 54 | type: 'Patch', 55 | operations: [], 56 | parentHash: parentHash, 57 | isCheckpoint: !!isCheckpoint, 58 | mut: { 59 | inverseOf: undefined 60 | } 61 | }); 62 | if (isCheckpoint) { 63 | out.mut.inverseOf = out; 64 | } 65 | return out; 66 | }; 67 | 68 | var check = Patch.check = function (patch /*:any*/, docLength_opt /*:?number*/) /*:Patch_t*/ { 69 | Common.assert(patch.type === 'Patch'); 70 | Common.assert(Array.isArray(patch.operations)); 71 | Common.assert(/^[0-9a-f]{64}$/.test(patch.parentHash)); 72 | for (var i = patch.operations.length - 1; i >= 0; i--) { 73 | Operation.check(patch.operations[i], docLength_opt); 74 | if (i > 0) { 75 | Common.assert(!Operation.shouldMerge(patch.operations[i], patch.operations[i-1])); 76 | } 77 | if (typeof(docLength_opt) === 'number') { 78 | docLength_opt += Operation.lengthChange(patch.operations[i]); 79 | } 80 | } 81 | if (patch.isCheckpoint) { 82 | Common.assert(patch.operations.length === 1); 83 | Common.assert(patch.operations[0].offset === 0); 84 | if (typeof(docLength_opt) === 'number') { 85 | Common.assert(!docLength_opt || patch.operations[0].toRemove === docLength_opt); 86 | } 87 | } 88 | return patch; 89 | }; 90 | 91 | Patch.toObj = function (patch /*:Patch_t*/) { 92 | if (Common.PARANOIA) { check(patch); } 93 | var out /*:Array*/ = new Array(patch.operations.length+1); 94 | var i; 95 | for (i = 0; i < patch.operations.length; i++) { 96 | out[i] = Operation.toObj(patch.operations[i]); 97 | } 98 | out[i] = patch.parentHash; 99 | return out; 100 | }; 101 | 102 | Patch.fromObj = function (obj /*:Patch_Packed_t*/, isCheckpoint /*:?boolean*/) { 103 | Common.assert(Array.isArray(obj) && obj.length > 0); 104 | var patch = create(Sha.check(obj[obj.length-1]), isCheckpoint); 105 | var i; 106 | for (i = 0; i < obj.length-1; i++) { 107 | patch.operations[i] = Operation.fromObj(obj[i]); 108 | } 109 | if (Common.PARANOIA) { check(patch); } 110 | return patch; 111 | }; 112 | 113 | var hash = function (text) { 114 | return Sha.hex_sha256(text); 115 | }; 116 | 117 | var addOperation = Patch.addOperation = function (patch /*:Patch_t*/, op /*:Operation_t*/) { 118 | if (Common.PARANOIA) { 119 | check(patch); 120 | Operation.check(op); 121 | } 122 | for (var i = 0; i < patch.operations.length; i++) { 123 | if (Operation.shouldMerge(patch.operations[i], op)) { 124 | var maybeOp = Operation.merge(patch.operations[i], op); 125 | patch.operations.splice(i,1); 126 | if (maybeOp === null) { return; } 127 | op = maybeOp; 128 | i--; 129 | } else { 130 | var out = Operation.rebase(patch.operations[i], op); 131 | if (out === op) { 132 | // op could not be rebased further, insert it here to keep the list ordered. 133 | patch.operations.splice(i,0,op); 134 | return; 135 | } else { 136 | op = out; 137 | // op was rebased, try rebasing it against the next operation. 138 | } 139 | } 140 | } 141 | patch.operations.push(op); 142 | if (Common.PARANOIA) { check(patch); } 143 | }; 144 | 145 | Patch.createCheckpoint = function ( 146 | parentContent /*:string*/, 147 | checkpointContent /*:string*/, 148 | parentContentHash_opt /*:?string*/) 149 | { 150 | var op = Operation.create(0, parentContent.length, checkpointContent); 151 | if (Common.PARANOIA && parentContentHash_opt) { 152 | Common.assert(parentContentHash_opt === hash(parentContent)); 153 | } 154 | parentContentHash_opt = parentContentHash_opt || hash(parentContent); 155 | var out = create(parentContentHash_opt, true); 156 | out.operations[0] = op; 157 | return out; 158 | }; 159 | 160 | var clone = Patch.clone = function (patch /*:Patch_t*/) { 161 | if (Common.PARANOIA) { check(patch); } 162 | var out = create(patch.parentHash, patch.isCheckpoint); 163 | for (var i = 0; i < patch.operations.length; i++) { 164 | out.operations[i] = patch.operations[i]; 165 | } 166 | return out; 167 | }; 168 | 169 | Patch.merge = function (oldPatch /*:Patch_t*/, newPatch /*:Patch_t*/) { 170 | if (Common.PARANOIA) { 171 | check(oldPatch); 172 | check(newPatch); 173 | } 174 | if (oldPatch.isCheckpoint) { 175 | Common.assert(newPatch.parentHash === oldPatch.parentHash); 176 | if (newPatch.isCheckpoint) { 177 | return create(oldPatch.parentHash); 178 | } 179 | return clone(newPatch); 180 | } else if (newPatch.isCheckpoint) { 181 | return clone(oldPatch); 182 | } 183 | oldPatch = clone(oldPatch); 184 | for (var i = newPatch.operations.length-1; i >= 0; i--) { 185 | addOperation(oldPatch, newPatch.operations[i]); 186 | } 187 | return oldPatch; 188 | }; 189 | 190 | Patch.apply = function (patch /*:Patch_t*/, doc /*:string*/) 191 | { 192 | if (Common.PARANOIA) { 193 | check(patch); 194 | Common.assert(typeof(doc) === 'string'); 195 | Common.assert(Sha.hex_sha256(doc) === patch.parentHash); 196 | } 197 | var newDoc = doc; 198 | for (var i = patch.operations.length-1; i >= 0; i--) { 199 | newDoc = Operation.apply(patch.operations[i], newDoc); 200 | } 201 | return newDoc; 202 | }; 203 | 204 | Patch.lengthChange = function (patch /*:Patch_t*/) 205 | { 206 | if (Common.PARANOIA) { check(patch); } 207 | var out = 0; 208 | for (var i = 0; i < patch.operations.length; i++) { 209 | out += Operation.lengthChange(patch.operations[i]); 210 | } 211 | return out; 212 | }; 213 | 214 | Patch.invert = function (patch /*:Patch_t*/, doc /*:string*/) 215 | { 216 | if (Common.PARANOIA) { 217 | check(patch); 218 | Common.assert(typeof(doc) === 'string'); 219 | Common.assert(Sha.hex_sha256(doc) === patch.parentHash); 220 | } 221 | var newDoc = doc; 222 | var operations = new Array(patch.operations.length); 223 | for (var i = patch.operations.length-1; i >= 0; i--) { 224 | operations[i] = Operation.invert(patch.operations[i], newDoc); 225 | newDoc = Operation.apply(patch.operations[i], newDoc); 226 | } 227 | var opOffsets = new Array(patch.operations.length); 228 | (function () { 229 | for (var i = operations.length-1; i >= 0; i--) { 230 | opOffsets[i] = operations[i].offset; 231 | for (var j = i - 1; j >= 0; j--) { 232 | opOffsets[i] += operations[j].toRemove - operations[j].toInsert.length; 233 | } 234 | } 235 | }()); 236 | var rpatch = create(Sha.hex_sha256(newDoc), patch.isCheckpoint); 237 | rpatch.operations.splice(0, rpatch.operations.length); 238 | for (var j = 0; j < operations.length; j++) { 239 | rpatch.operations[j] = 240 | Operation.create(opOffsets[j], operations[j].toRemove, operations[j].toInsert); 241 | } 242 | if (Common.PARANOIA) { check(rpatch); } 243 | return rpatch; 244 | }; 245 | 246 | Patch.simplify = function ( 247 | patch /*:Patch_t*/, 248 | doc /*:string*/, 249 | operationSimplify /*:Operation_Simplify_t*/ ) 250 | { 251 | if (Common.PARANOIA) { 252 | check(patch); 253 | Common.assert(typeof(doc) === 'string'); 254 | Common.assert(Sha.hex_sha256(doc) === patch.parentHash); 255 | } 256 | var spatch = create(patch.parentHash); 257 | var newDoc = doc; 258 | var outOps = []; 259 | var j = 0; 260 | for (var i = patch.operations.length-1; i >= 0; i--) { 261 | var outOp = operationSimplify(patch.operations[i], newDoc, Operation.simplify); 262 | if (outOp) { 263 | newDoc = Operation.apply(outOp, newDoc); 264 | outOps[j++] = outOp; 265 | } 266 | } 267 | Array.prototype.push.apply(spatch.operations, outOps.reverse()); 268 | if (!spatch.operations[0]) { 269 | spatch.operations.shift(); 270 | } 271 | if (Common.PARANOIA) { 272 | check(spatch); 273 | } 274 | return spatch; 275 | }; 276 | 277 | Patch.equals = function (patchA /*:Patch_t*/, patchB /*:Patch_t*/) { 278 | if (patchA.operations.length !== patchB.operations.length) { return false; } 279 | for (var i = 0; i < patchA.operations.length; i++) { 280 | if (!Operation.equals(patchA.operations[i], patchB.operations[i])) { return false; } 281 | } 282 | return true; 283 | }; 284 | 285 | var isCheckpointOp = function (op, text) { 286 | return op.offset === 0 && op.toRemove === text.length && op.toInsert === text; 287 | }; 288 | 289 | Patch.transform = function ( 290 | toTransform /*:Patch_t*/, 291 | transformBy /*:Patch_t*/, 292 | doc /*:string*/, 293 | patchTransformer /*:Patch_Transform_t*/ ) 294 | { 295 | if (Common.PARANOIA) { 296 | check(toTransform, doc.length); 297 | check(transformBy, doc.length); 298 | if (Sha.hex_sha256(doc) !== toTransform.parentHash) { throw new Error("wrong hash"); } 299 | } 300 | if (toTransform.parentHash !== transformBy.parentHash) { throw new Error(); } 301 | 302 | var afterTransformBy = Patch.apply(transformBy, doc); 303 | var out = create(transformBy.mut.inverseOf 304 | ? transformBy.mut.inverseOf.parentHash 305 | : Sha.hex_sha256(afterTransformBy), 306 | toTransform.isCheckpoint 307 | ); 308 | 309 | if (transformBy.operations.length === 0) { return clone(toTransform); } 310 | if (toTransform.operations.length === 0) { 311 | if (toTransform.isCheckpoint) { throw new Error(); } 312 | return out; 313 | } 314 | 315 | if (toTransform.isCheckpoint || 316 | (toTransform.operations.length === 1 && isCheckpointOp(toTransform.operations[0], doc))) 317 | { 318 | throw new Error("Attempting to transform a checkpoint, this should not happen"); 319 | } 320 | 321 | if (transformBy.operations.length === 1 && isCheckpointOp(transformBy.operations[0], doc)) { 322 | if (!transformBy.isCheckpoint) { throw new Error(); } 323 | return toTransform; 324 | } 325 | 326 | if (transformBy.isCheckpoint) { throw new Error(); } 327 | 328 | var ops = patchTransformer(toTransform.operations, transformBy.operations, doc); 329 | Array.prototype.push.apply(out.operations, ops); 330 | 331 | if (Common.PARANOIA) { 332 | check(out, afterTransformBy.length); 333 | } 334 | 335 | return out; 336 | }; 337 | 338 | Patch.random = function (doc /*:string*/, opCount /*:?number*/) { 339 | Common.assert(typeof(doc) === 'string'); 340 | opCount = opCount || (Math.floor(Math.random() * 30) + 1); 341 | var patch = create(Sha.hex_sha256(doc)); 342 | var docLength = doc.length; 343 | while (opCount-- > 0) { 344 | var op = Operation.random(docLength); 345 | docLength += Operation.lengthChange(op); 346 | addOperation(patch, op); 347 | } 348 | check(patch); 349 | return patch; 350 | }; 351 | 352 | Object.freeze(module.exports); 353 | -------------------------------------------------------------------------------- /client/transform/SmartJSONTransformer_test.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | var SmartJSONTransformer = require("./SmartJSONTransformer"); 23 | //var NaiveJSONTransformer = require("./NaiveJSONTransformer"); 24 | var TextTransformer = require('./TextTransformer'); 25 | var Diff = require('../Diff'); 26 | //var Sortify = require("json.sortify"); 27 | var Operation = require('../Operation'); 28 | 29 | var OT = SmartJSONTransformer._; 30 | 31 | 32 | var assertions = 0; 33 | var failed = false; 34 | var failedOn; 35 | var failMessages = []; 36 | 37 | var ASSERTS = []; 38 | 39 | var runASSERTS = function (jsonTransformer) { 40 | ASSERTS.forEach(function (f, index) { 41 | console.log("Running " + f.name); 42 | f.f(index + 1, jsonTransformer); 43 | }); 44 | }; 45 | 46 | var assert = function (test, msg, expected) { 47 | ASSERTS.push({ 48 | f: function (i, jsonTransformer) { 49 | test = (test /*:function*/); 50 | var returned = test(expected, jsonTransformer); 51 | if (returned === true) { 52 | assertions++; 53 | return; 54 | } 55 | failed = true; 56 | failedOn = assertions; 57 | 58 | console.log("\n" + Array(64).fill("=").join("")); 59 | console.log(JSON.stringify({ 60 | test: i, 61 | message: msg, 62 | output: returned, 63 | expected: typeof(expected) !== 'undefined'? expected: true, 64 | }, null, 2)); 65 | failMessages.push(1); 66 | }, 67 | name: msg 68 | }); 69 | }; 70 | 71 | assert(function () { 72 | var O = {x:5}; 73 | var C = OT.clone(O); 74 | 75 | return O !== C; 76 | }, "Expected object identity to fail on cloned objects"); 77 | 78 | assert(function () { 79 | return OT.pathOverlaps(['a', 'b', 'c'], 80 | ['a', 'b', 'c', 'd']); 81 | }, "child elements have overlapping paths"); 82 | 83 | assert(function () { 84 | return !OT.pathOverlaps(['a', 'b', 'c'], 85 | ['a', 'b', 'd', 'e']); 86 | }, "sibling elements do not overlap"); 87 | 88 | assert(function () { 89 | var A = [ 90 | { 91 | x: 5, 92 | y: [ 93 | 1, 94 | 2, 95 | 3, 96 | ], 97 | z: 15 98 | }, 99 | "pewpew", 100 | 23 101 | ]; 102 | 103 | var B = OT.clone(A); 104 | 105 | return OT.deepEqual(A, B); 106 | }, "Expected deep equality"); 107 | 108 | assert(function () { 109 | var A = [ 110 | { 111 | x: 5, 112 | y: [ 113 | 1, 114 | 2, 115 | 3, 116 | ], 117 | z: 15 118 | }, 119 | "pewpew", 120 | 23 121 | ]; 122 | 123 | var B = OT.clone(A); 124 | B[0].z = 9; 125 | 126 | return !OT.deepEqual(A, B); 127 | }, "Expected deep inequality"); 128 | 129 | assert(function () { 130 | var A = [1, 2, { 131 | x: 7 132 | }, 4, 5, undefined]; 133 | 134 | var B = [1, 2, { 135 | x: 7, 136 | }, 4, 5]; 137 | 138 | return !OT.deepEqual(A, B); 139 | }, "Expected deep inequality"); 140 | 141 | assert(function () { 142 | var A = { 143 | x: 5, 144 | y: 7 145 | }; 146 | 147 | var B = { 148 | x: 5, 149 | y: 7, 150 | z: 9 151 | }; 152 | return !OT.deepEqual(A, B); 153 | }, "Expected deep inequality"); 154 | 155 | assert(function (expected) { 156 | var O = { 157 | x: [], 158 | y: { }, 159 | z: "pew", 160 | }; 161 | 162 | var A = OT.clone(O); 163 | var B = OT.clone(O); 164 | 165 | A.x.push("a"); 166 | B.x.push("b"); 167 | 168 | A.y.a = 5; 169 | B.y.a = 7; 170 | 171 | A.z = "bang"; 172 | B.z = "bam!"; 173 | 174 | var d_A = OT.diff(O, A); 175 | var d_B = OT.diff(O, B); 176 | 177 | var changes = OT.resolve(d_A, d_B); 178 | 179 | var C = OT.clone(O); 180 | 181 | OT.patch(C, d_A); 182 | OT.patch(C, changes); 183 | 184 | if (!OT.deepEqual(C, expected)) { 185 | return changes; 186 | } 187 | return true; 188 | }, "Incorrect merge", { 189 | x: ['a', 'b'], 190 | y: { 191 | a: 5, 192 | }, 193 | // This would result in "bam!bang" if the arbitor was passed. 194 | z: 'bang', 195 | }); 196 | 197 | var transformText = function (O, A, B) { 198 | var tfb = Diff.diff(O, A); 199 | var ttf = Diff.diff(O, B); 200 | var r = TextTransformer(ttf, tfb, O); 201 | var out = Operation.applyMulti(tfb, O); 202 | out = Operation.applyMulti(r, out); 203 | return out; 204 | }; 205 | 206 | assert(function (expected) { 207 | var O = "pewpew"; 208 | var A = "pewpew bang"; 209 | var B = "powpow"; 210 | 211 | return transformText(O, A, B) === expected; 212 | }, "Check transform text", "powpow bang"); 213 | 214 | assert(function (expected) { 215 | var O = ["pewpew"]; 216 | var A = ["pewpew bang"]; 217 | var B = ["powpow"]; 218 | 219 | var d_A = OT.diff(O, A); 220 | var d_B = OT.diff(O, B); 221 | 222 | var changes = OT.resolve(d_A, d_B, function (a, b) { 223 | a.value = transformText(a.prev, a.value, b.value); 224 | return true; 225 | }); 226 | 227 | OT.patch(O, d_A); 228 | OT.patch(O, changes); 229 | 230 | if (!OT.deepEqual(O, expected)) { 231 | return { 232 | result: O, 233 | changes: changes, 234 | }; 235 | } 236 | return true; 237 | }, "diff/patching strings with overlaps", ["powpow bang"]); 238 | 239 | // TODO 240 | assert(function () { 241 | var O = { 242 | v: { 243 | x: [], 244 | }, 245 | }; 246 | 247 | var OO = OT.clone(O); 248 | 249 | var A = {}; 250 | var B = {b: 19}; 251 | 252 | var d_A = OT.diff(O, A); 253 | var d_B = OT.diff(O, B); 254 | 255 | var changes = OT.resolve(d_A, d_B); 256 | 257 | var C = OT.clone(O); 258 | 259 | OT.patch(C, d_A); 260 | OT.patch(C, changes); 261 | 262 | if (!OT.deepEqual(O, OO)) { 263 | return [O, OO]; 264 | } 265 | 266 | return true; 267 | }, "Expected original objects to be unaffected. all operations must be pure"); 268 | 269 | assert(function () { 270 | var O = { Y: ['pewpew', 'bangbang'], Z: 7, }; 271 | var A = { Y: ['bangbang'], Z: 7, }; 272 | var B = { Y: [ 'bangbang'], Z: 7, }; 273 | 274 | var d_A = OT.diff(O, A); 275 | var d_B = OT.diff(O, B); 276 | 277 | var changes = OT.resolve(d_A, d_B); 278 | 279 | var C = OT.clone(O); 280 | 281 | OT.patch(C, d_A); 282 | OT.patch(C, changes); 283 | 284 | var expected = { 285 | Y: ['bangbang'], 286 | Z: 7, 287 | }; 288 | 289 | if (!OT.deepEqual(C, expected)) { 290 | console.log('diff of A', d_A); 291 | console.log('diff of B', d_B); 292 | return C; 293 | } 294 | 295 | return true; 296 | }, 'the second of two identical array splices should be ignored'); 297 | 298 | assert(function () { 299 | var O = { Y: ['pewpew', 'bangbang', 'boom']}; 300 | var A = { Y: ['boom']}; 301 | var B = { Y: ['pewpew', 'boom']}; 302 | 303 | var d_A = OT.diff(O, A); 304 | var d_B = OT.diff(O, B); 305 | 306 | var changes = OT.resolve(d_A, d_B); 307 | 308 | var C = OT.clone(O); 309 | 310 | OT.patch(C, d_A); 311 | OT.patch(C, changes); 312 | 313 | var expected = { 314 | Y: ['boom'], 315 | }; 316 | 317 | if (!OT.deepEqual(C, expected)) { 318 | console.log('diff of A', d_A); 319 | console.log('diff of B', d_B); 320 | return C; 321 | } 322 | 323 | return true; 324 | }, 'overlapping splices did not preserve intent #1'); 325 | 326 | assert(function () { 327 | var O = { Y: ['pewpew', 'bangbang', 'boom', 'blam']}; 328 | var A = { Y: ['boom', 'blam']}; // remove the first two elements of an array 329 | var B = { Y: ['pewpew', 'boom', 'blam']}; // remove the second element of an array 330 | 331 | var d_A = OT.diff(O, A); 332 | var d_B = OT.diff(O, B); 333 | 334 | var changes = OT.resolve(d_A, d_B); 335 | 336 | var C = OT.clone(O); 337 | 338 | OT.patch(C, d_A); 339 | OT.patch(C, changes); 340 | 341 | var expected = { 342 | Y: ['boom', 'blam'], 343 | }; 344 | 345 | if (!OT.deepEqual(C, expected)) { 346 | console.log('diff of A', d_A); 347 | console.log('diff of B', d_B); 348 | return C; 349 | } 350 | 351 | return true; 352 | }, 'overlapping splices did not preserve intent #2'); 353 | 354 | assert(function () { 355 | var O = { Y: '12345'.split("")}; 356 | var A = { Y: '15'.split("")}; 357 | var B = { Y: '1245'.split("")}; 358 | 359 | var d_A = OT.diff(O, A); 360 | var d_B = OT.diff(O, B); 361 | 362 | var changes = OT.resolve(d_A, d_B); 363 | 364 | var C = OT.clone(O); 365 | 366 | OT.patch(C, d_A); 367 | OT.patch(C, changes); 368 | 369 | var expected = { 370 | Y: '15'.split(""), 371 | }; 372 | 373 | if (!OT.deepEqual(C, expected)) { 374 | console.log('diff of A', d_A); 375 | console.log('diff of B', d_B); 376 | return C; 377 | } 378 | 379 | return true; 380 | }, 'overlapping splices did not preserve intent #3'); 381 | 382 | assert(function () { 383 | var O = { Y: '12345'.split("")}; 384 | var A = { Y: '15'.split("")}; // remove the middle three elements 385 | var B = { Y: '1245'.split("")}; // remove one element from the middle 386 | 387 | var d_A = OT.diff(O, A); 388 | var d_B = OT.diff(O, B); 389 | 390 | var changes = OT.resolve(d_A, d_B); 391 | 392 | var C = OT.clone(O); 393 | 394 | OT.patch(C, d_A); 395 | OT.patch(C, changes); 396 | 397 | var expected = { 398 | Y: '15'.split(""), // the contained removal should have been cancelled out 399 | }; 400 | 401 | if (!OT.deepEqual(C, expected)) { 402 | console.log('diff of A', d_A); 403 | console.log('diff of B', d_B); 404 | return C; 405 | } 406 | 407 | return true; 408 | }, 'overlapping splices did not preserve intent #4'); 409 | 410 | assert(function () { 411 | var O = { Y: '12345'.split("")}; 412 | var A = { Y: '125'.split("")}; // remove two of the middle elements 413 | var B = { Y: '145'.split("")}; // remove elements earlier in the array with some overlap 414 | 415 | var d_A = OT.diff(O, A); 416 | var d_B = OT.diff(O, B); 417 | 418 | var changes = OT.resolve(d_A, d_B); 419 | 420 | var C = OT.clone(O); 421 | 422 | OT.patch(C, d_A); 423 | OT.patch(C, changes); 424 | 425 | var expected = { 426 | Y: '15'.split(""), // the contained removal should have been cancelled out 427 | }; 428 | 429 | if (!OT.deepEqual(C, expected)) { 430 | console.log('diff of A', d_A); 431 | console.log('diff of B', d_B); 432 | return C; 433 | } 434 | 435 | return true; 436 | }, 'overlapping splices did not preserve intent #5'); 437 | 438 | assert(function () { 439 | return true; // TODO unstub this, make it work 440 | /* 441 | var O = { Y: '12345'.split("")}; 442 | var A = { Y: '125'.split("")}; // remove 3, 4 443 | var B = { Y: '013456'.split("")}; // remove 2, insert 0, 6 444 | 445 | var d_A = OT.diff(O, A); 446 | var d_B = OT.diff(O, B); 447 | 448 | var changes = OT.resolve(d_A, d_B); 449 | 450 | var C = OT.clone(O); 451 | 452 | OT.patch(C, d_A); 453 | OT.patch(C, changes); 454 | 455 | var expected = { 456 | Y: '0156'.split(""), // the contained removal should have been cancelled out 457 | }; 458 | 459 | if (!OT.deepEqual(C, expected)) { 460 | console.log('diff of A', d_A); 461 | console.log('diff of B', d_B); 462 | return C; 463 | } 464 | 465 | return true; 466 | */ 467 | }, 'overlapping splices did not preserve intent #6'); 468 | 469 | module.exports.main = function (cycles /*:number*/, callback /*:()=>void*/) { 470 | runASSERTS(SmartJSONTransformer); 471 | if (failed) { 472 | console.log("\n%s assertions passed and %s failed", assertions, failMessages.length); 473 | throw new Error(); 474 | } 475 | console.log("[SUCCESS] %s tests passed", assertions); 476 | 477 | callback(); 478 | }; 479 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ChainPad 2 | 3 | [![XWiki labs logo](https://raw.githubusercontent.com/xwiki-labs/xwiki-labs-logo/master/projects/xwikilabs/xwikilabsproject.png "XWiki labs")](https://labs.xwiki.com/xwiki/bin/view/Projects/XWikiLabsProject) 4 | 5 | ChainPad Algorithm is a Realtime Collaborative Editor algorithm based on 6 | [Nakamoto Blockchains](https://en.bitcoin.it/wiki/Block_chain). This implementation is designed 7 | to run with a dumb broadcasting server but with minimal effort, the algorithm could be ported to 8 | full peer-to-peer. Because the ChainPad server need not be aware of the content which is being 9 | edited, different types of editors can exist in harmony on the same system. 10 | 11 | This library is currently licensed as LGPL-2.1. Previous versions of this library (v5.2.7 and below) were licensed as AGPL-3.0. 12 | 13 | ## Getting Started 14 | 15 | To embed ChainPad in your web application, it is recommended that you use the contained node.js 16 | websocket server. You may examine `test.html` to see how to bind the editor to a simple textarea. 17 | 18 | ### Building 19 | 20 | To compile the code into `chainpad.js` run the following: 21 | 22 | npm install 23 | node make 24 | 25 | This will run the tests and concatenate the js files into the resulting `chainpad.js` output file. 26 | 27 | ## The API 28 | 29 | ```javascript 30 | var chainpad = ChainPad.create(config); 31 | 32 | // The bindings are not included in the engine, see below. 33 | bindToDataTransport(chainpad); 34 | bindToUserInterface(chainpad); 35 | 36 | chainpad.start(); 37 | ``` 38 | 39 | ### Configuration Parameters 40 | 41 | Config is an *optional* object parameter which may have one or more of the following contents. 42 | **NOTE:** it's critical that every ChainPad instance in the session has the same values for these 43 | parameters. 44 | 45 | * **initialState** (string) content to start off the pad with, default is empty-string. 46 | * **checkpointInterval** (number) the number of patches which should be allowed to go across the 47 | wire before sending a *checkpoint*. A small number will result in lots of sending of *checkpoints* 48 | which are necessarily large because they send the whole document in the message. A large number 49 | will result in more patches to download for a new person joining the pad. 50 | * **avgSyncMilliseconds** (number) the number of milliseconds to wait before sending to the server 51 | if there is anything to be sent. Making this number smaller will cause lots of patches to be sent 52 | (however the number will be limited by the RTT to the server because ChainPad will only keep one 53 | unacknowledged message on the wire at a time). 54 | * **validateContent** (function) if specified, this function will be called during each patch and 55 | receive the content of the document after the patch, if the document has semantic requirements 56 | then this function can validate them if they are broken then the patch will be rejected. 57 | * **strictCheckpointValidation** (boolean) if true then we will fail any checkpoint which comes 58 | at an interval which is not in agreement with **checkpointInterval**. Default: *false*. 59 | * **patchTransformer** (function) if specified, this function will be used for Operational 60 | Transformation. You have 3 options which are packaged with ChainPad or you can implement your own. 61 | * `ChainPad.TextTransformer` (this is default so you need not pass anything) if you're using 62 | ChainPad on plain text, you probably want to use this. 63 | * `ChainPad.SmartJSONTransformer` if you are using ChainPad to patch JSON data, you probably 64 | want this. 65 | * `ChainPad.NaiveJSONTransformer` this is effectively just TextTransformer with a 66 | validation step to make sure the result is JSON, using this is not recommended. 67 | * **operationSimplify** (function) This is an optional function which will override the function 68 | `ChainPad.Operation.simplify` in case you want to use a different one. Simplify is used for "fixing" 69 | operations which remove content and then put back the same content. The default simplify will not 70 | create patches containing strings with single characters from 71 | [surrogate pairs](https://en.wikipedia.org/wiki/UTF-16#U.2B0000_to_U.2BD7FF_and_U.2BE000_to_U.2BFFFF). 72 | * **logLevel** (number) If this is zero, none of the normal logs will be printed. 73 | * **userName** (string) This is a string which will appear at the beginning of all logs in the 74 | console, if multiple ChainPad instances are running at the same time, this will help differentiate 75 | them. 76 | * **noPrune** (boolean) If this is true, history will not be pruned when a checkpoint is encountered. 77 | Caution: this can end up occupying a lot of memory! 78 | * **diffFunction** (function) This is a function which takes 2 strings and outputs and array of 79 | Operations. If unspecified, ChainPad will use the `ChainPad.Diff` which is a smart diff algorithm 80 | based on the one used by Fossel. The default diff function will not create patches containing strings 81 | with single characters from 82 | [surrogate pairs](https://en.wikipedia.org/wiki/UTF-16#U.2B0000_to_U.2BD7FF_and_U.2BE000_to_U.2BFFFF). 83 | * **diffBlockSize** (number) This is an optional number which will inform the default diff function 84 | `ChainPad.Diff` how big the rolling window should be. Smaller numbers imply more resource usage but 85 | common areas within a pair of documents which are smaller than this number will not be seen. 86 | The default is 8. 87 | * **transformFunction** (function) This parameter has been removed, if you attempt to pass this 88 | argument ChainPad will fail to start and throw an error. 89 | 90 | 91 | ## Binding the ChainPad Session to the Data Transport 92 | 93 | To bind the session to a data transport such as a websocket, you'll need to use the `message()` 94 | and `onMessage()` methods of the ChainPad session object as follows: 95 | 96 | * **message**: Function which takes a String and signals the ChainPad engine of an incoming 97 | message. 98 | * **onMessage**: Function which takes a function taking a String, called by the ChainPad engine 99 | when a message is to be sent. 100 | 101 | ```javascript 102 | var socket = new WebSocket("ws://your.server:port/"); 103 | socket.onopen = function(evt) { 104 | socket.onmessage = function (evt) { chainpad.message(evt.data); }; 105 | chainpad.onMessage(function (message, cb) { 106 | socket.send(message); 107 | // Really the callback should only be called after you are sure the server has the patch. 108 | cb(); 109 | }); 110 | }); 111 | ``` 112 | 113 | ### Binding the ChainPad Session to the User Interface 114 | 115 | * Register a function to handle *changes* to the document, a change comprises an offset, a number 116 | of characters to be removed and a number of characters to be inserted. This is the easiest way 117 | to interact with ChainPad. 118 | ```javascript 119 | var myContent = ''; 120 | chainpad.onChange(function (offset, toRemove, toInsert) { 121 | myContent = myContent.substring(0, offset) + toInsert + myContent.substring(offset + toRemove); 122 | }); 123 | ``` 124 | 125 | * Signal to chainpad engine that the user has inserted and/or removed content with the *change()* 126 | function. 127 | ```javascript 128 | var chainpad = ChainPad.create(); 129 | chainpad.change(0, 0, "Hello world"); 130 | console.log(chainpad.getUserDoc()); // -> "Hello world" 131 | chainpad.change(0, 5, "Goodbye cruel"); 132 | console.log(chainpad.getUserDoc()); // -> "Goodbye cruel world" 133 | ``` 134 | 135 | * Register a function to handle a patch to the document, a patch is a series of insertions and 136 | deletions which may must be applied atomically. When applying, the operations in the patch must 137 | be applied in *decending* order, from highest index to zero. For more information about Patch, 138 | see `chainpad.Patch`. 139 | ```javascript 140 | chainpad.onPatch(function(patch) {}); 141 | ``` 142 | 143 | * Signal the chainpad engine that the user has inserted and/or removed content to/from the document. 144 | The Patch object can be constructed using Patch.create and Operations can be added to the patch 145 | using Operation.create and Patch.addOperation(). See **ChainPad Internals** for more information. 146 | ```javascript 147 | chainpad.patch(patch); 148 | ``` 149 | 150 | ## Block Object 151 | 152 | A block object is an internal representation of a message sent on the wire, each block contains a 153 | **Patch** which itself contains one or more **Operations**. You can access **Blocks** using 154 | `chainpad.getAuthBlock()` or `chainpad.getBlockForHash()`. 155 | 156 | ### Fields/Functions 157 | 158 | * **hashOf**: Calculated SHA256 of the on-wire representation of this **Block** (as a **Message**). 159 | * **lastMsgHash**: SHA256 of previous/parent **Block** in the chain. If this is all zeros then this 160 | **Block** is the initial block. 161 | * **isCheckpoint**: True if this **Block** represents a *checkpoint*. A *checkpoint* always removes 162 | all of the content from the document and then adds it back, leaving the document as it was. 163 | * **getParent**`() -> Block`: Get the parent block of this block, this is fast because the blocks 164 | are already in the chain in memory. 165 | * **getContent**`() -> string`: Get the content of the *Authoritative Document* at the point in the 166 | history represented by this block. This takes time because it requires replaying part of the chain. 167 | * **getPatch**`() -> Patch`: Get a clone of the **Patch** which is contained in this block. 168 | * **getInversePatch**`() -> Patch`: Get a clone of the inverse **Patch** (the **Patch** which would 169 | undo the **Patch** provided by **getPatch**). This is calculated when the **Message** comes in to 170 | ChainPad. 171 | * **equals**`(Block) -> Boolean`: Find out if another **Block** is representing the same underlying 172 | structure, since **Blocks** are created whenever one is requested, using triple-equals is not ok. 173 | 174 | ## Control Functions 175 | 176 | ### chainpad.start() 177 | 178 | Start the engine, this will cause the engine to setup a setInterval to sync back the changes 179 | reported. Before start() is called, you can still inform chainpad of changes from the network. 180 | 181 | ### chainpad.abort() 182 | 183 | Stop the engine, no more messages will be sent, even if there is *Uncommitted Work*. 184 | 185 | ### chainpad.sync() 186 | 187 | Flush the *Uncommitted Work* back to the server, there is no guarantee that the work is actually 188 | committed, just that it has attempted to send it to the server. 189 | 190 | ### chainpad.getAuthDoc() 191 | 192 | Access the *Authoritative Document*, this is the content which everybody has agreed upon and has 193 | been entered into the chain. 194 | 195 | ### chainpad.getAuthBlock() 196 | 197 | Access the blockchain block which is at the head of the chain, this block contains the last patch 198 | which made the *Authoritative Document* what it is. This returns a *Block Object*. 199 | 200 | ### chainpad.getBlockForHash() 201 | 202 | Access the stored block which based on the SHA-256 hash. 203 | 204 | ### chainpad.getUserDoc() 205 | 206 | Access the document which the engine believes is in the user interface, this is equivilant to 207 | the *Authoritative Document* with the *Uncommitted Work* patch applied. Useful for debugging. 208 | This should be equivilant to the string representation of the content which is in the UI. 209 | 210 | ### chainpad.getDepthOfState(state [,minDepth]) 211 | 212 | Determine how deep a particular state is in the chain _relative to the current state_. Depth means 213 | the number of patches. 214 | 215 | ```javascript 216 | // the authDoc is 0 patches deep, by definition 217 | 0 === chainpad.getDepthOfState(chainpad.getAuthDoc()); 218 | 219 | // if a state never existed in the chain, return value is -1 220 | -1 === chainpad.getDepthOfState("said no one ever"); 221 | // ^^ assuming the state of the document was never "said no one ever" 222 | ``` 223 | 224 | You can specify a minimum depth to traverse, skip forward (down) this number of patches before 225 | starting to try to match the specified content. This allows you to see multiple times in history 226 | when the content was equal to the specified content. This function will not detect depth of states 227 | older than the second checkpoint because this is pruned. 228 | 229 | ```javascript 230 | // determine the last time the userDoc was 'pewpew' 231 | var firstEncounter = chainpad.getDepthOfState('pewpew'); 232 | 233 | // check if it was ever previously in that state 234 | if (chainpad.getDepthOfState('pewpew', firstEncounter) !== -1) { 235 | // use this pattern to check if the document state was 'pewpew' 236 | // at more than one point in its history 237 | console.log("the state 'pewpew' exists in the chain in at least two states"); 238 | } 239 | ``` 240 | 241 | ### chainpad.onSettle() 242 | 243 | Register a handler to be called *once* when there is no *Uncommitted Work* left. This does not 244 | prove that no patch will be reverted because of a chain fork, but it does verify that the message 245 | has hit the server and been acknowledged. The handler will be called only once the next time the 246 | state is settled but you can re-register inside of the handler. 247 | 248 | ### chainpad.getLag() 249 | 250 | Tells the amount of lag between the last onMessage events being fired by chainpad and the callback. 251 | Specifically this returns an object with lag and pending properties. Pending is true if a message 252 | has been sent which has not yet been acknowledged. Lag is the amount of time between the previous 253 | sent message and it's response or if the previously send message has not yet been acknowledged, it 254 | is the amount of time since it was sent. 255 | 256 | # Internals 257 | 258 | ## Data Types 259 | 260 | * **Operation**: An atomic insertion and/or deletion of a string at an offset in the document. 261 | An Operation can contain both insertion and deletion and in this case, the deletion will occur 262 | first. 263 | * **Patch**: A list of **Operations** to be applied to the document in order and a hash of the 264 | document content at the previous state (before the patch is applied). 265 | * **Message**: Either a request to register the user, an announcement of a user having joined the 266 | document or an encapsulation of a **Patch** to be sent over the wire. 267 | * **Block**: This is an API encapsulation of the **Message** when it is in the chain. 268 | 269 | ## Functions 270 | 271 | * **apply**`(Patch, Document) -> Document`: This function is fairly self-explanatory, a new document 272 | is returned which reflects the result of applying the **Patch** to the document. The hash of the 273 | document must be equal to `patch.parentHash`, otherwise an error will result. 274 | * **merge**`(Patch, Patch) -> Patch`: Merging of two mergable **Patches** yields a **Patch** which 275 | does the equivilant of applying the first **Patch**, then the second. Any two **Operations** which 276 | act upon overlapping or abutting sections of a document can (and must) be merged. A **Patch** 277 | containing mergable operations in invalid. 278 | * **invert**`(Patch, Document) -> Patch`: Given a **Patch** and the document to which it could be 279 | applied, calculate the *inverse* **Patch**, IE: the **Patch** which would un-do the operation of 280 | applying the original **Patch**. 281 | * **simplify**`(Patch, Document) -> Patch`: After **merging** of **Patches**, it is possible to end 282 | up with a **Patch** which contains some redundant or partially redundant **Operations**, a redundant 283 | **Operation** is one which removes some content from the document and then adds back the very same 284 | content. Since the actual content to be removed is not stored in the **Operation** or **Patch**, the 285 | **simplify** function exists to find and remove any redundancy in the **Patch**. Any **Patch** which 286 | is sent over the wire which can still be **simplified** is invalid. 287 | * **transform**`(Patch, Patch, Document) -> Patch`: This is the traditional Operational Transform 288 | function. This is the only function which can *lose information*, for example if Alice and Bob both 289 | delete the same text at the same time, **transform** will merge those two deletions. It is critical 290 | to note that **transform** is only carried out upon the user's *Uncommitted Work*, never on any 291 | other user's work so **transform's** decision making cannot possibly lead to de-synchronization. 292 | 293 | ## Mechanics 294 | 295 | Internally the client stores a document known as the *Authoritative Document* this is the last known 296 | state of the document which is agreed upon by all of the clients and the *Authoritative Document* 297 | can only be changed as a result of an incoming **Patch** from the server. The difference between 298 | what the user sees in their screen and the *Authoritative Document* is represented by a **Patch** 299 | known as the *Uncommitted Work*. 300 | 301 | When the user types in the document, onInsert() and onRemove() are called, creating **Operations** 302 | which are **merged** into the *Uncommitted Work*. As the user adds and removes text, this **Patch** 303 | grows. Periodically the engine transmits the *Uncommitted Work* to the server. 304 | When the *Uncommitted Work* is transmitted to the server which will broadcast it out to all clients. 305 | 306 | When a **Patch** is received from the server, it is first examined for validity and discarded if it 307 | is obviously invalid. If this **Patch** is rooted in the current *Authoritative Document*, the 308 | **Patch** is applied to the *Authoritative Document* and the user's *Uncommitted Work* is 309 | **transformed** by that patch. If the **Patch** happens to be created by the current user, the 310 | inverse of the **Patch** is merged with the user's *Uncommitted Work*, thus removing the committed 311 | part. 312 | 313 | If a **Patch** is received which does not root in the *Authoritative Document*, it is stored 314 | by the client in case it is actually part of the chain but other patches have not yet been filled 315 | in. If a **Patch** is rooted in a previous state of the document which is not the 316 | *Authoritative Document*, the patch is stored in case it might be part of a fork of the patch-chain 317 | which proves longer than the chain which the engine currently is aware of. 318 | 319 | In the event that a fork of the chain becomes longer than the currently accepted chain, a 320 | "reorganization" (Bitcoin term) will occur which will cause the *Authoritative Document* to be 321 | rolled back to a previous state and then rolled forward along the winning chain. In the event of a 322 | "reorganization", work which the user wrote which was committed may be reverted and as the engine 323 | detects that it's own patch has been reverted, the content will be re-added to the user's 324 | *Uncommitted Work* to be pushed to the server next time it is synced. 325 | 326 | The initial startup of the engine, the server is asked for all of the **Messages** to date. These 327 | are filtered through the engine as with any other incoming **Message** in a process which Bitcoin 328 | developers will recognize as "syncing the chain". 329 | 330 | A special type of **Patch** is known as a **Checkpoint** and a checkpoint always removes and re-adds 331 | all content to the pad. The server may detect checkpoint patches because they are represented on 332 | the wire as an array with a 4 as the first element. In order to improve performance of new users 333 | joining the pad and "syncing" the chain, the server may send only the second most recent checkpoint 334 | and all patches newer than that. 335 | 336 | 337 | ## Relationship to Bitcoin 338 | 339 | Those with knowledge of Bitcoin will recognize this consensus protocol as inherently a 340 | Nakamoto Chain. Whereas Bitcoin uses blocks, each of which point to the previous block, ChainPad 341 | uses **Patches** each of which point to the previous state of the document. In the case of ChainPad 342 | there is of course no mining or difficulty as security is not intended by this protocol. Obviously 343 | it would be trivial to generate ever longer side-chains, causing all work to be reverted and 344 | jamming the document. 345 | 346 | A more subtle difference is the use of "lowest hash wins" as a tie-breaker. Bitcoin very cleverly 347 | does *not* use "lowest hash wins" in order to prevent miners from withholding valid blocks with 348 | particularly low hashes in order to gain advantages by mining against their own block before anyone 349 | else gets a chance. Again since security is not a consideration in this design, "lowest hash wins" 350 | is used in order to expediate convergence in the event of a split. 351 | -------------------------------------------------------------------------------- /client/ChainPad_test.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | global.localStorage = { TESTING: true }; 23 | 24 | var ChainPad = require('./ChainPad'); 25 | var Common = require('./Common'); 26 | var Operation = require('./Operation'); 27 | var nThen = require('nthen'); 28 | 29 | var xsetInterval = function (call, ms) { 30 | var inter = setInterval(function () { 31 | try { call(); } catch (e) { clearInterval(inter); throw e; } 32 | }, ms); 33 | return inter; 34 | }; 35 | 36 | var startup = function (callback) { 37 | var rt = ChainPad.create({ 38 | userName: 'x', 39 | initialState: 'abc' 40 | }); 41 | rt.abort(); 42 | callback(); 43 | }; 44 | 45 | var runOperation = function (realtimeFacade, op) { 46 | realtimeFacade.rt.change(op.offset, op.toRemove, op.toInsert); 47 | }; 48 | 49 | /* 50 | var insert = function (doc, offset, chars) { 51 | return doc.substring(0,offset) + chars + doc.substring(offset); 52 | }; 53 | 54 | var remove = function (doc, offset, count) { 55 | return doc.substring(0,offset) + doc.substring(offset+count); 56 | };*/ 57 | 58 | var registerNode = function (name, initialDoc, conf) { 59 | conf = ((conf || {}) /*:Object*/); 60 | conf.userName = conf.userName || name; 61 | conf.initialState = initialDoc; 62 | var rt = ChainPad.create(conf); 63 | //rt.change(0, 0, initialDoc); 64 | 65 | var handlers = []; 66 | rt.onMessage(function (msg, cb) { 67 | setTimeout(function () { 68 | handlers.forEach(function (handler) { handler(msg, cb); }); 69 | }); 70 | }); 71 | 72 | var out = { 73 | onMessage: function (handler /*:(string,()=>void)=>void*/) { handlers.push(handler); }, 74 | change: rt.change, 75 | start: rt.start, 76 | sync: rt.sync, 77 | abort: rt.abort, 78 | message: rt.message, 79 | getUserDoc: rt.getUserDoc, 80 | getAuthDoc: rt.getAuthDoc, 81 | getDepthOfState: rt.getDepthOfState, 82 | getAuthBlock: rt.getAuthBlock, 83 | getBlockForHash: rt.getBlockForHash, 84 | 85 | queue: [], 86 | rt: rt, 87 | doc: initialDoc, 88 | }; 89 | rt.onPatch(function () { out.doc = rt.getUserDoc(); }); 90 | return out; 91 | }; 92 | 93 | var editing = function (callback) { 94 | var doc = ''; 95 | var rt = registerNode('editing()', ''); 96 | var messages = 0; 97 | rt.onMessage(function (msg, cb) { 98 | messages++; 99 | //rt.message(msg); 100 | cb(); 101 | }); 102 | rt.start(); 103 | 104 | var i = 0; 105 | var to = xsetInterval(function () { 106 | if (i++ > 10) { 107 | clearTimeout(to); 108 | for (var j = 0; j < 100; j++) { 109 | var m = messages; 110 | rt.sync(); 111 | if (m === messages) { 112 | rt.abort(); 113 | callback(); 114 | return; 115 | } 116 | } 117 | throw new Error(); 118 | } 119 | // fire off another operation 120 | var op = Operation.random(doc.length); 121 | doc = Operation.apply(op, doc); 122 | runOperation(rt, op); 123 | rt.sync(); 124 | },1); 125 | 126 | }; 127 | 128 | var fakeSetTimeout = function (func, time) { 129 | var i = time; 130 | var tick = function () { if (i-- <= 0) { func(); } else { setTimeout(tick); } }; 131 | setTimeout(tick); 132 | }; 133 | 134 | var twoClientsCycle = function (callback, origDocA, origDocB) { 135 | var rtA = registerNode('twoClients(rtA)', origDocA); 136 | var rtB = registerNode('twoClients(rtB)', origDocB); 137 | rtA.queue = []; 138 | rtB.queue = []; 139 | var messages = 0; 140 | 141 | var onMsg = function (rt, msg, cb) { 142 | messages++; 143 | var destRt = (rt === rtA) ? rtB : rtA; 144 | fakeSetTimeout(function () { 145 | messages--; 146 | destRt.queue.push(msg); 147 | fakeSetTimeout(function () { 148 | destRt.message(destRt.queue.shift()); 149 | cb(); 150 | }, Math.random() * 100); 151 | }, Math.random() * 100); 152 | }; 153 | [rtA, rtB].forEach(function (rt) { 154 | rt.onMessage(function (msg, cb) { onMsg(rt, msg, cb); }); 155 | rt.start(); 156 | }); 157 | //[rtA, rtB].forEach(function (rt) { rt.start(); }); 158 | 159 | var i = 0; 160 | var to = xsetInterval(function () { 161 | if (i++ > 100) { 162 | clearTimeout(to); 163 | var j = 0; 164 | var flushCounter = 0; 165 | var again = function () { 166 | if (++j > 10000) { throw new Error("never synced"); } 167 | rtA.sync(); 168 | rtB.sync(); 169 | if (messages === 0 && rtA.queue.length === 0 && rtB.queue.length === 0 && flushCounter++ > 100) { 170 | console.log(rtA.getUserDoc()); 171 | console.log(rtB.getUserDoc()); 172 | Common.assert(rtA.doc === rtB.doc); 173 | rtA.abort(); 174 | rtB.abort(); 175 | callback(); 176 | return; 177 | } else { 178 | setTimeout(again); 179 | } 180 | }; 181 | again(); 182 | } 183 | 184 | //console.log(JSON.stringify([rtA.doc, rtB.doc])); 185 | 186 | var rt = (Math.random() > 0.5) ? rtA : rtB; 187 | 188 | var op = Operation.random(rt.doc.length); 189 | rt.doc = Operation.apply(op, rt.doc); 190 | runOperation(rt, op); 191 | 192 | if (Math.random() > 0.8) { 193 | rt.sync(); 194 | } 195 | },1); 196 | 197 | }; 198 | 199 | var twoClients = function (cycles, callback) { 200 | var i = 0; 201 | var again = function () { 202 | if (++i >= cycles) { again = callback; } 203 | var docA = Common.randomASCII(Math.floor(Math.random()*20)); 204 | var docB = Common.randomASCII(Math.floor(Math.random()*20)); 205 | twoClientsCycle(again, docA, docB); 206 | }; 207 | again(); 208 | }; 209 | 210 | var syncCycle = function (messages, finalDoc, name, callback) { 211 | var rt = registerNode(name, ''); 212 | for (var i = 0; i < messages.length; i++) { 213 | rt.message(messages[i]); 214 | } 215 | setTimeout(function () { 216 | Common.assert(rt.doc === finalDoc); 217 | rt.abort(); 218 | callback(); 219 | }); 220 | }; 221 | 222 | var outOfOrderSync = function (callback) { 223 | var messages = []; 224 | var rtA = registerNode('outOfOrderSync()', '', { checkpointInterval: 1000 }); 225 | rtA.onMessage(function (msg, cb) { 226 | setTimeout(cb); 227 | messages.push(msg); 228 | }); 229 | var i = 0; 230 | rtA.start(); 231 | 232 | var finish = function () { 233 | rtA.abort(); 234 | var i = 0; 235 | var cycle = function () { 236 | if (i++ > 10) { 237 | callback(); 238 | return; 239 | } 240 | // first sync is in order 241 | syncCycle(messages, rtA.doc, 'outOfOrderSync(rt'+i+')', function () { 242 | for (var j = 0; j < messages.length; j++) { 243 | var k = Math.floor(Math.random() * messages.length); 244 | var m = messages[k]; 245 | messages[k] = messages[j]; 246 | messages[j] = m; 247 | } 248 | cycle(); 249 | }); 250 | }; 251 | cycle(); 252 | }; 253 | 254 | var again = function () { 255 | setTimeout( (i++ < 150) ? again : finish ); 256 | if (i < 100) { 257 | var op = Operation.random(rtA.doc.length); 258 | rtA.doc = Operation.apply(op, rtA.doc); 259 | runOperation(rtA, op); 260 | } 261 | rtA.sync(); 262 | }; 263 | again(); 264 | }; 265 | 266 | var checkVersionInChain = function (callback) { 267 | var doc = ''; 268 | // create a chainpad 269 | var rt = registerNode('checkVersionInChain()', '', { checkpointInterval: 1000 }); 270 | var messages = 0; 271 | rt.onMessage(function (msg, cb) { 272 | messages++; 273 | cb(); // must be sync because of the xsetInterval below 274 | }); 275 | rt.start(); 276 | 277 | var i = 0; 278 | //var oldUserDoc; 279 | var oldAuthDoc; 280 | var to = xsetInterval(function () { 281 | // on the 51st change, grab the doc 282 | if (i === 50) { 283 | oldAuthDoc = rt.getAuthDoc(); 284 | } 285 | // on the 100th random change, check whether the 50th existed before 286 | if (i++ > 100) { 287 | clearTimeout(to); 288 | Common.assert(rt.getDepthOfState(oldAuthDoc) !== -1); 289 | Common.assert(rt.getDepthOfState(rt.getAuthDoc()) !== -1); 290 | rt.abort(); 291 | callback(); 292 | return; 293 | } 294 | 295 | // fire off another operation 296 | var op = Operation.random(doc.length); 297 | doc = Operation.apply(op, doc); 298 | runOperation(rt, op); 299 | rt.sync(); 300 | },1); 301 | 302 | }; 303 | 304 | var whichStateIsDeeper = function (callback) { 305 | // create a chainpad 306 | var rt = registerNode('whichStateIsDeeper()', '', { checkpointInterval: 1000 }); 307 | var messages = 0; 308 | var next = function () { }; 309 | rt.onMessage(function (msg, cb) { 310 | messages++; 311 | cb(); 312 | next(); 313 | }); 314 | rt.start(); 315 | 316 | var doc = '', 317 | docO = doc, 318 | docA, 319 | docB; 320 | 321 | var i = 0; 322 | 323 | next = function () { 324 | if (i === 25) { 325 | // grab docO 326 | docO = rt.getAuthDoc(); 327 | } else if (i === 50) { 328 | // grab docA 329 | docA = rt.getAuthDoc(); 330 | console.log("Got Document A"); 331 | console.log(docA); 332 | 333 | Common.assert(rt.getDepthOfState(docA) === 0); 334 | Common.assert(rt.getDepthOfState(docO) === 25); 335 | } else if (i === 75) { 336 | // grab docB 337 | docB = rt.getAuthDoc(); 338 | console.log("Got Document B"); 339 | console.log(docB); 340 | 341 | // state assertions 342 | Common.assert(rt.getDepthOfState(docB) === 0); 343 | Common.assert(rt.getDepthOfState(docA) === 25); 344 | Common.assert(rt.getDepthOfState(docO) === 50); 345 | } else if (i >= 100) { 346 | console.log("Completed"); 347 | // finish 348 | next = function () { }; 349 | 350 | Common.assert(rt.getDepthOfState(docB) === 25); 351 | Common.assert(rt.getDepthOfState(docA) === 50); 352 | Common.assert(rt.getDepthOfState(docO) === 75); 353 | 354 | rt.abort(); 355 | callback(); 356 | return; 357 | } 358 | 359 | i++; 360 | var op; 361 | do { 362 | op = Operation.random(doc.length); 363 | // we can't have the same state multiple times for this test. 364 | } while (op.toInsert.length <= op.toRemove); 365 | doc = Operation.apply(op,doc); 366 | runOperation(rt, op); 367 | rt.sync(); 368 | }; 369 | next(); 370 | }; 371 | 372 | var checkpointOT = function (callback) { 373 | var rtA = registerNode('checkpointOT(rtA)', '', { checkpointInterval: 10 }); 374 | var rtB = registerNode('checkpointOT(rtB)', '', { checkpointInterval: 10 }); 375 | rtA.queue = []; 376 | rtB.queue = []; 377 | var messages = 0; 378 | var syncing = 0; 379 | 380 | var onMsg = function (rt, msg, cb) { 381 | if (syncing) { 382 | setTimeout(function () { onMsg(rt, msg, cb); }); 383 | return; 384 | } 385 | messages++; 386 | var destRt = (rt === rtA) ? rtB : rtA; 387 | syncing++; 388 | setTimeout(function () { 389 | messages--; 390 | destRt.queue.push(msg); 391 | setTimeout(function () { 392 | destRt.message(destRt.queue.shift()); 393 | syncing--; 394 | cb(); 395 | }); 396 | }); 397 | }; 398 | [rtA, rtB].forEach(function (rt) { 399 | rt.onMessage(function (msg, cb) { onMsg(rt, msg, cb); }); 400 | rt.start(); 401 | }); 402 | 403 | var i = 0; 404 | var to = xsetInterval(function () { 405 | if (syncing) { return; } 406 | i++; 407 | if (i < 20) { 408 | var op = Operation.random(rtA.doc.length); 409 | rtA.doc = Operation.apply(op, rtA.doc); 410 | runOperation(rtA, op); 411 | } else if (i === 25) { 412 | //console.log(rtA.getUserDoc() + ' ==x= ' + rtB.getUserDoc()); 413 | Common.assert(rtA.getUserDoc() === rtB.getAuthDoc()); 414 | Common.assert(rtA.getUserDoc() === rtB.getUserDoc()); 415 | Common.assert(rtA.getAuthDoc() === rtB.getAuthDoc()); 416 | var opA = Operation.create(0, 0, 'A'); 417 | var opB = Operation.create(1, 0, 'B'); 418 | runOperation(rtA, opA); 419 | runOperation(rtB, opB); 420 | } else if (i > 35) { 421 | console.log("rtA authDoc " + rtA.getAuthDoc()); 422 | console.log("rtB authDoc " + rtB.getAuthDoc()); 423 | Common.assert(rtA.getUserDoc() === rtB.getUserDoc()); 424 | Common.assert(rtA.getAuthDoc() === rtB.getAuthDoc()); 425 | Common.assert(rtA.getAuthDoc()[0] === 'A'); 426 | Common.assert(rtA.getAuthDoc()[2] === 'B'); 427 | 428 | clearTimeout(to); 429 | rtA.abort(); 430 | rtB.abort(); 431 | callback(); 432 | return; 433 | } 434 | 435 | rtA.sync(); 436 | rtB.sync(); 437 | //console.log(rtA.getUserDoc() + ' === ' + rtB.getUserDoc()); 438 | }); 439 | 440 | }; 441 | 442 | var getAuthBlock = function (callback) { 443 | var doc = ''; 444 | // create a chainpad 445 | var rt = registerNode('getAuthBlock()', '', { checkpointInterval: 1000 }); 446 | var messages = 0; 447 | rt.onMessage(function (msg, cb) { 448 | messages++; 449 | cb(); // must be sync because of the xsetInterval below 450 | }); 451 | rt.start(); 452 | 453 | var i = 0; 454 | //var oldUserDoc; 455 | var oldAuthBlock; 456 | var oldAuthDoc; 457 | var to = xsetInterval(function () { 458 | // on the 51st change, grab the block 459 | if (i === 50) { 460 | oldAuthBlock = rt.getAuthBlock(); 461 | oldAuthDoc = rt.getAuthDoc(); 462 | Common.assert(oldAuthBlock.getContent().doc === oldAuthDoc); 463 | } 464 | // on the 100th random change, check if getting the state at oldAuthBlock works... 465 | if (i++ > 100) { 466 | clearTimeout(to); 467 | Common.assert(oldAuthBlock.getContent().doc === oldAuthDoc); 468 | Common.assert(oldAuthBlock.equals(rt.getBlockForHash(oldAuthBlock.hashOf))); 469 | rt.abort(); 470 | callback(); 471 | return; 472 | } 473 | 474 | // fire off another operation 475 | var op = Operation.random(doc.length); 476 | doc = Operation.apply(op, doc); 477 | runOperation(rt, op); 478 | rt.sync(); 479 | },1); 480 | }; 481 | 482 | var benchmarkSyncCycle = function (messageList, authDoc, callback) { 483 | var rt = registerNode('benchmarkSyncCycle()', '', { checkpointInterval: 1000, logLevel: 0 }); 484 | rt.start(); 485 | for (var i = 0; i < messageList.length; i++) { 486 | rt.message(messageList[i]); 487 | } 488 | var intr = setInterval(function () { 489 | if (rt.getAuthDoc() === authDoc) { 490 | clearInterval(intr); 491 | rt.abort(); 492 | callback(); 493 | } else { 494 | console.log('waiting'); 495 | } 496 | }); 497 | }; 498 | 499 | var benchmarkSync = function (callback) { 500 | var doc = ''; 501 | // create a chainpad 502 | var rt = registerNode('benchmarkSync()', '', { checkpointInterval: 1000 }); 503 | var messages = 0; 504 | var messageList = []; 505 | rt.onMessage(function (msg, cb) { 506 | messages++; 507 | messageList.push(msg); 508 | cb(); // must be sync because of the xsetInterval below 509 | }); 510 | rt.start(); 511 | 512 | var i = 0; 513 | //var oldUserDoc; 514 | var to = xsetInterval(function () { 515 | // on the 100th random change, check if getting the state at oldAuthBlock works... 516 | if (i++ > 200) { 517 | clearTimeout(to); 518 | rt.abort(); 519 | var wait = function () { 520 | if (messages !== i) { setTimeout(wait); return; } 521 | var ad = rt.getAuthDoc(); 522 | var again = function (cycles) { 523 | var t0 = +new Date(); 524 | var times = []; 525 | benchmarkSyncCycle(messageList, ad, function () { 526 | var time = (+new Date()) - t0; 527 | times.push(time); 528 | console.log('cycle ' + cycles + ' ' + time + 'ms'); 529 | if (cycles >= 10) { 530 | var avg = times.reduce(function (x, y) { return x+y; }) / times.length; 531 | console.log(avg + 'ms average time to sync'); 532 | callback(); 533 | return; 534 | } 535 | again(cycles+1); 536 | }); 537 | }; 538 | again(0); 539 | }; 540 | wait(); 541 | } 542 | 543 | // fire off another operation 544 | var op = Operation.create(doc.length, 0, 'A'); 545 | doc = Operation.apply(op, doc); 546 | runOperation(rt, op); 547 | rt.sync(); 548 | },10); 549 | }; 550 | 551 | /* Insert an emoji in the document, then replace it by another emoji; 552 | * Check that the resulting patch is not containing a broken half-emoji 553 | * by trying to "encodeURIComonent" it. 554 | */ 555 | var emojiTest = function (callback) { 556 | var rt = ChainPad.create({ 557 | userName: 'x', 558 | initialState: '' 559 | }); 560 | rt.start(); 561 | 562 | // Check if the pacthes are encryptable 563 | rt.onMessage(function (message, cb) { 564 | console.log(message); 565 | try { 566 | encodeURIComponent(message); 567 | } catch (e) { 568 | console.log('Error'); 569 | console.log(e.message); 570 | Common.assert(false); 571 | } 572 | setTimeout(cb); 573 | }); 574 | 575 | nThen(function (waitFor) { 576 | // Insert first emoji in the userdoc 577 | var emoji1 = "\uD83D\uDE00"; 578 | rt.contentUpdate(emoji1); 579 | rt.onSettle(waitFor()); 580 | }).nThen(function (waitFor) { 581 | // Replace the emoji by a different one 582 | var emoji2 = "\uD83D\uDE11"; 583 | rt.contentUpdate(emoji2); 584 | rt.onSettle(waitFor()); 585 | }).nThen(function () { 586 | rt.abort(); 587 | callback(); 588 | }); 589 | }; 590 | 591 | module.exports.main = function (cycles /*:number*/, callback /*:()=>void*/) { 592 | nThen(function (waitFor) { 593 | startup(waitFor()); 594 | }).nThen(function (waitFor) { 595 | editing(waitFor()); 596 | }).nThen(function (waitFor) { 597 | twoClients(cycles, waitFor()); 598 | }).nThen(function (waitFor) { 599 | outOfOrderSync(waitFor()); 600 | }).nThen(function (waitFor) { 601 | checkVersionInChain(waitFor()); 602 | }).nThen(function (waitFor) { 603 | whichStateIsDeeper(waitFor()); 604 | }).nThen(function (waitFor) { 605 | checkpointOT(waitFor()); 606 | }).nThen(function (waitFor) { 607 | getAuthBlock(waitFor()); 608 | }).nThen(function (waitFor) { 609 | benchmarkSync(waitFor()); 610 | }).nThen(function (waitFor) { 611 | emojiTest(waitFor()); 612 | }).nThen(callback); 613 | }; 614 | -------------------------------------------------------------------------------- /client/transform/SmartJSONTransformer.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | var Sortify = require('json.sortify'); 23 | var Diff = require('../Diff'); 24 | //var Patch = require('../Patch'); 25 | var Operation = require('../Operation'); 26 | var TextTransformer = require('./TextTransformer'); 27 | //var Sha = require('../sha256'); 28 | 29 | /*:: 30 | import type { Operation_t } from '../Operation'; 31 | */ 32 | 33 | var isArray = function (obj) { 34 | return Object.prototype.toString.call(obj)==='[object Array]'; 35 | }; 36 | 37 | /* Arrays and nulls both register as 'object' when using native typeof 38 | we need to distinguish them as their own types, so use this instead. */ 39 | var type = function (dat) { 40 | return dat === null? 'null': isArray(dat)?'array': typeof(dat); 41 | }; 42 | 43 | var find = function (map, path) { 44 | var l = path.length; 45 | for (var i = 0; i < l; i++) { 46 | if (typeof(map[path[i]]) === 'undefined') { return; } 47 | map = map[path[i]]; 48 | } 49 | return map; 50 | }; 51 | 52 | var clone = function (val) { 53 | return JSON.parse(JSON.stringify(val)); 54 | }; 55 | 56 | var deepEqual = function (A /*:any*/, B /*:any*/) { 57 | var t_A = type(A); 58 | var t_B = type(B); 59 | if (t_A !== t_B) { return false; } 60 | if (t_A === 'object') { 61 | var k_A = Object.keys(A); 62 | var k_B = Object.keys(B); 63 | return k_A.length === k_B.length && 64 | !k_A.some(function (a) { return !deepEqual(A[a], B[a]); }) && 65 | !k_B.some(function (b) { return !(b in A); }); 66 | } else if (t_A === 'array') { 67 | return A.length === B.length && 68 | !A.some(function (a, i) { return !deepEqual(a, B[i]); }); 69 | } else { 70 | return A === B; 71 | } 72 | }; 73 | 74 | /*:: 75 | export type SmartJSONTransformer_Replace_t = { 76 | type: 'replace', 77 | path: Array, 78 | value: any, 79 | prev: any 80 | }; 81 | export type SmartJSONTransformer_Splice_t = { 82 | type: 'splice', 83 | path: Array, 84 | value: any, 85 | offset: number, 86 | removals: number 87 | }; 88 | export type SmartJSONTransformer_Remove_t = { 89 | type: 'remove', 90 | path: Array, 91 | value: any 92 | }; 93 | export type SmartJSONTransformer_Operation_t = 94 | SmartJSONTransformer_Replace_t | SmartJSONTransformer_Splice_t | SmartJSONTransformer_Remove_t; 95 | */ 96 | 97 | var operation = function (type, path, value, prev, other) /*:SmartJSONTransformer_Operation_t*/ { 98 | if (type === 'replace') { 99 | return ({ 100 | type: 'replace', 101 | path: path, 102 | value: value, 103 | prev: prev, 104 | } /*:SmartJSONTransformer_Replace_t*/); 105 | } else if (type === 'splice') { 106 | if (typeof(prev) !== 'number') { throw new Error(); } 107 | if (typeof(other) !== 'number') { throw new Error(); } 108 | return ({ 109 | type: 'splice', 110 | path: path, 111 | value: value, 112 | offset: prev, 113 | removals: other 114 | } /*:SmartJSONTransformer_Splice_t*/); 115 | } else if (type !== 'remove') { throw new Error('expected a removal'); } 116 | // if it's not a replace or splice, it's a 'remove' 117 | return ({ 118 | type: 'remove', 119 | path: path, 120 | value: value, 121 | } /*:SmartJSONTransformer_Remove_t*/); 122 | }; 123 | 124 | var replace = function (ops, path, to, from) { 125 | ops.push(operation('replace', path, to, from)); 126 | }; 127 | 128 | var remove = function (ops, path, val) { 129 | ops.push(operation('remove', path, val)); 130 | }; 131 | 132 | 133 | // HERE 134 | var splice = function (ops, path, value, offset, removals) { 135 | ops.push(operation('splice', path, value, offset, removals)); 136 | }; 137 | 138 | /* 139 | all of A's path is at the beginning of B 140 | roughly: B.indexOf(A) === 0 141 | */ 142 | var pathOverlaps = function (A /*:Array*/, B /*:Array*/) { 143 | return !A.some(function (a, i) { 144 | return a !== B[i]; 145 | }); 146 | }; 147 | 148 | // OT Case #1 replace->replace ✔ 149 | // OT Case #2 replace->remove ✔ 150 | // OT Case #3 replace->splice ✔ 151 | // OT Case #4 remove->replace ✔ 152 | // OT Case #5 remove->remove ✔ 153 | // OT Case #6 remove->splice ✔ 154 | // OT Case #7 splice->replace ✔ 155 | // OT Case #8 splice->remove ✔ 156 | // OT Case #9 splice->splice ✔ 157 | var CASES = (function () { 158 | var types = ['replace', 'remove', 'splice']; 159 | 160 | var matrix = {}; 161 | var i = 1; 162 | 163 | types.forEach(function (a) { 164 | matrix[a] = {}; 165 | return types.forEach(function (b) { matrix[a][b] = i++; }); 166 | }); 167 | return matrix; 168 | }()); 169 | 170 | // A and B are lists of operations which result from calling diff 171 | 172 | var resolve = function (A /*:any*/, B /*:any*/, arbiter /*:?function*/) { 173 | if (!(type(A) === 'array' && type(B) === 'array')) { 174 | throw new Error("[resolve] expected two arrays"); 175 | } 176 | 177 | /* OVERVIEW 178 | * B 179 | * 1. filter removals at identical paths 180 | * 181 | */ 182 | 183 | B = B.filter(function (b) { 184 | // if A removed part of the tree you were working on... 185 | if (A.some(function (a) { 186 | if (a.type === 'remove') { 187 | if (pathOverlaps(a.path, b.path)) { 188 | if (b.path.length - a.path.length > 1) { return true; } 189 | } 190 | } 191 | })) { 192 | // this is weird... FIXME 193 | return false; 194 | } 195 | 196 | /* remove operations which would no longer make sense 197 | for instance, if a replaces an array with a string, 198 | that would invalidate a splice operation at that path */ 199 | if (b.type === 'splice' && A.some(function (a) { 200 | if (a.type === 'splice' && pathOverlaps(a.path, b.path)) { 201 | if (a.path.length - b.path.length < 0) { 202 | if (!a.removals) { return; } 203 | 204 | var start = a.offset; 205 | var end = a.offset + a.removals; 206 | 207 | for (;start < end; start++) { 208 | if (start === b.path[a.path.length]) { 209 | /* 210 | if (typeof(arbiter) === 'function' && 211 | deepEqual(a.path, b.path) && 212 | a.value.length === 1 && 213 | b.value.length === 1 && 214 | typeof(a.value[0]) === 'string' && 215 | typeof(b.value[0]) === 'string') { 216 | console.log('strings'); 217 | 218 | return arbiter(a, b, CASES.splice.splice); 219 | } 220 | */ 221 | 222 | // b is a descendant of a removal 223 | return true; 224 | } 225 | } 226 | } 227 | } 228 | })) { return false; } 229 | 230 | if (!A.some(function (a) { 231 | return b.type === 'remove' && deepEqual(a.path, b.path); 232 | })) { return true; } 233 | }) 234 | .filter(function (b) { 235 | // let A win conflicts over b if no arbiter is supplied here 236 | 237 | // Arbiter is required here 238 | return !A.some(function (a) { 239 | if (b.type === 'replace' && a.type === 'replace') { 240 | // remove any operations which return true 241 | if (deepEqual(a.path, b.path)) { 242 | if (typeof(a.value) === 'string' && typeof(b.value) === 'string') { 243 | if (arbiter && a.prev === b.prev && a.value !== b.value) { 244 | return arbiter(a, b, CASES.replace.replace); 245 | } 246 | return true; 247 | } 248 | return true; 249 | } 250 | } 251 | }); 252 | }) 253 | .map(function (b) { 254 | // if a splice in A modifies the path to b 255 | // update b's path to reflect that 256 | 257 | A.forEach(function (a) { 258 | if (a.type === 'splice') { 259 | // TODO 260 | // what if a.path == b.path 261 | 262 | // resolve insertion overlaps array.push conflicts 263 | // iterate over A such that each overlapping splice 264 | // adjusts the path/offset/removals of b 265 | 266 | if (deepEqual(a.path, b.path)) { 267 | if (b.type === 'splice') { 268 | // if b's offset is outside of a's range 269 | // decrease its offset by a delta length 270 | if (b.offset > (a.offset + b.removals)) { 271 | b.offset += a.value.length - a.removals; 272 | return; 273 | } 274 | 275 | if (b.offset < a.offset) { 276 | // shorten the list of removals to before a's offset 277 | // TODO this is probably wrong, but it's making tests pass... 278 | b.removals = Math.max(a.offset - b.offset, 0); 279 | return; 280 | } 281 | 282 | // otherwise, a and b have the same offset 283 | // substract a's removals from your own 284 | b.removals = Math.max(b.removals - (b.offset + a.removals - b.offset), 0); 285 | // and adjust your offset by the change in length introduced by a 286 | b.offset += (a.value.length - a.removals); 287 | } else { 288 | // adjust the path of b to account for the splice 289 | // TODO 290 | } 291 | return; 292 | } 293 | 294 | if (pathOverlaps(a.path, b.path)) { 295 | // TODO validate that this isn't an off-by-one error 296 | var pos = a.path.length; 297 | if (typeof(b.path[pos]) === 'number' && a.offset <= b.path[pos]) { // FIXME a.value is undefined 298 | b.path[pos] += (a.value.length - a.removals); 299 | } 300 | } 301 | } 302 | }); 303 | 304 | return b; 305 | }); 306 | 307 | return B; 308 | }; 309 | 310 | // A, B, f, path, ops 311 | var objects = function (A, B, path, ops) { 312 | var Akeys = Object.keys(A); 313 | var Bkeys = Object.keys(B); 314 | 315 | Bkeys.forEach(function (b) { 316 | var t_b = type(B[b]); 317 | var old = A[b]; 318 | 319 | var nextPath = path.concat(b); 320 | 321 | if (Akeys.indexOf(b) === -1) { 322 | // there was an insertion 323 | 324 | // mind the fallthrough behaviour 325 | if (t_b === 'undefined') { 326 | throw new Error("undefined type has key. this shouldn't happen?"); 327 | } 328 | if (old) { throw new Error("no such key existed in b, so 'old' should be falsey"); } 329 | replace(ops, nextPath, B[b], old); 330 | return; 331 | } 332 | 333 | // else the key already existed 334 | var t_a = type(old); 335 | if (t_a !== t_b) { 336 | // its type changed! 337 | console.log("type changed from [%s] to [%s]", t_a, t_b); 338 | // type changes always mean a change happened 339 | if (t_b === 'undefined') { 340 | throw new Error("first pass should never reveal undefined keys"); 341 | } 342 | replace(ops, nextPath, B[b], old); 343 | return; 344 | } 345 | 346 | if (t_a === 'object') { 347 | // it's an object 348 | objects(A[b], B[b], nextPath, ops); 349 | } else if (t_a === 'array') { 350 | // it's an array 351 | arrays(A[b], B[b], nextPath, ops); 352 | } else if (A[b] !== B[b]) { 353 | // it's not an array or object, so we can do === comparison 354 | replace(ops, nextPath, B[b], old); 355 | } 356 | }); 357 | Akeys.forEach(function (a) { 358 | // the key was deleted 359 | if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { 360 | remove(ops, path.concat(a), A[a]); 361 | } 362 | }); 363 | }; 364 | 365 | var arrayShallowEquality = function (A, B) { 366 | if (A.length !== B.length) { return false; } 367 | for (var i = 0; i < A.length; i++) { 368 | if (type(A[i]) !== type(B[i])) { return false; } 369 | } 370 | return true; 371 | }; 372 | 373 | // When an element in an array (number, string, bool) is changed, instead of a replace we 374 | // will do a splice(offset, [element], 1) 375 | var arrays = function (A_orig, B, path, ops) { 376 | var A = A_orig.slice(0); // shallow clone 377 | 378 | if (A.length === 0) { 379 | // A is zero length, this is going to be easy... 380 | splice(ops, path, B, 0, 0); 381 | 382 | } else if (arrayShallowEquality(A, B)) { 383 | // This is a relatively simple case, the elements in A and B are all of the same type and if 384 | // that type happens to be a primitive type, they are also equal. 385 | // This means no change will be needed at the level of this array, only it's children. 386 | A.forEach(function (a, i) { 387 | var b = B[i]; 388 | if (b === a) { return; } 389 | var old = a; 390 | var nextPath = path.concat(i); 391 | 392 | var t_a = type(a); 393 | switch (t_a) { 394 | case 'undefined': 395 | throw new Error('existing key had type `undefined`. this should never happen'); 396 | case 'object': 397 | objects(a, b, nextPath, ops); 398 | break; 399 | case 'array': 400 | arrays(a, b, nextPath, ops); 401 | break; 402 | default: 403 | //console.log('replace: ' + t_a); 404 | //splice(ops, path, [b], i, 1); 405 | replace(ops, nextPath, b, old); 406 | } 407 | }); 408 | } else { 409 | // Something was changed in the length of the array or one of the primitives so we're going 410 | // to make an actual change to this array, not only it's children. 411 | var commonStart = 0; 412 | var commonEnd = 0; 413 | while (commonStart < A.length && deepEqual(A[commonStart], B[commonStart])) { commonStart++; } 414 | while (deepEqual(A[A.length - 1 - commonEnd], B[B.length - 1 - commonEnd]) && 415 | commonEnd + commonStart < A.length && commonEnd + commonStart < B.length) 416 | { 417 | commonEnd++; 418 | } 419 | var toRemove = A.length - commonStart - commonEnd; 420 | var toInsert = []; 421 | if (B.length !== commonStart + commonEnd) { 422 | toInsert = B.slice(commonStart, B.length - commonEnd); 423 | } 424 | splice(ops, path, toInsert, commonStart, toRemove); 425 | } 426 | }; 427 | 428 | var diff = function (A, B) { 429 | var ops = []; 430 | 431 | var t_A = type(A); 432 | var t_B = type(B); 433 | 434 | if (t_A !== t_B) { 435 | throw new Error("Can't merge two objects of differing types"); 436 | } 437 | 438 | if (t_B === 'array') { 439 | arrays(A, B, [], ops); 440 | } else if (t_B === 'object') { 441 | objects(A, B, [], ops); 442 | } else { 443 | throw new Error("unsupported datatype" + t_B); 444 | } 445 | return ops; 446 | }; 447 | 448 | var applyOp = function (O, op /*:SmartJSONTransformer_Operation_t*/) { 449 | var path; 450 | var key; 451 | var result; 452 | switch (op.type) { 453 | case "replace": 454 | key = op.path[op.path.length -1]; 455 | path = op.path.slice(0, op.path.length - 1); 456 | 457 | var parent = find(O, path); 458 | 459 | if (!parent) { 460 | throw new Error("cannot apply change to non-existent element"); 461 | } 462 | parent[key] = op.value; 463 | break; 464 | case "splice": 465 | var found = find(O, op.path); 466 | if (!found) { 467 | console.error("[applyOp] expected path [%s] to exist in object", op.path.join(',')); 468 | throw new Error("Path did not exist"); 469 | } 470 | 471 | if (type(found) !== 'array') { 472 | throw new Error("Can't splice non-array"); 473 | } 474 | 475 | Array.prototype.splice.apply(found, [op.offset, op.removals].concat(op.value)); 476 | break; 477 | case "remove": 478 | key = op.path[op.path.length -1]; 479 | path = op.path.slice(0, op.path.length - 1); 480 | result = find(O, path); 481 | if (typeof(result) !== 'undefined') { delete result[key]; } 482 | break; 483 | default: 484 | throw new Error('unsupported operation type'); 485 | } 486 | }; 487 | 488 | var patch = function (O, ops) { 489 | ops.forEach(function (op) { 490 | applyOp(O, op); 491 | }); 492 | return O; 493 | }; 494 | 495 | 496 | ///// 497 | 498 | // We mutate b in this function 499 | // Our operation is p_b and the other person's operation is p_a. 500 | // If we return true here, it means our operation will die off. 501 | var arbiter = function (p_a, p_b, c) { 502 | if (p_a.prev !== p_b.prev) { throw new Error("Parent values don't match!"); } 503 | 504 | if (c === CASES.splice.splice) { 505 | // We and the other person are both pushing strings to an array so 506 | // we'll just accept both of them into the array. 507 | console.log(p_a); 508 | console.log(p_b); 509 | console.log('\n\n\n\n\n\n\n\n\n'); 510 | // TODO: do we really want to kill off our operation in this case ? 511 | return true; 512 | } 513 | var o = p_a.prev; 514 | 515 | var ops_a = Diff.diff(o, p_a.value); 516 | var ops_b = Diff.diff(o, p_b.value); 517 | 518 | /* given the parent text, the op to transform, and the incoming op 519 | return a transformed operation which takes the incoming 520 | op into account */ 521 | var ops_x = TextTransformer(ops_b, ops_a, o); 522 | 523 | /* Apply the incoming operation to the parent text 524 | */ 525 | var x2 = Operation.applyMulti(ops_a, o); 526 | 527 | /* Apply the transformed operation to the result of the incoming op 528 | */ 529 | var x3 = Operation.applyMulti(ops_x, x2); 530 | 531 | p_b.value = x3; 532 | }; 533 | 534 | module.exports = function ( 535 | opsToTransform /*:Array*/, 536 | opsTransformBy /*:Array*/, 537 | s_orig /*:string*/ ) /*:Array*/ 538 | { 539 | var o_orig = JSON.parse(s_orig); 540 | var s_transformBy = Operation.applyMulti(opsTransformBy, s_orig); 541 | var o_transformBy = JSON.parse(s_transformBy); 542 | // try whole patch at a time, see how it goes... 543 | var s_toTransform = Operation.applyMulti(opsToTransform, s_orig); 544 | var o_toTransform = JSON.parse(s_toTransform); 545 | 546 | try { 547 | var diffTTF = diff(o_orig, o_toTransform); 548 | var diffTFB = diff(o_orig, o_transformBy); 549 | var newDiffTTF = resolve(diffTFB, diffTTF, arbiter); 550 | 551 | // mutates orig 552 | patch(o_orig, diffTFB); 553 | patch(o_orig, newDiffTTF); 554 | 555 | var result = Sortify(o_orig); 556 | var ret = Diff.diff(s_transformBy, result); 557 | return ret; 558 | 559 | } catch (err) { 560 | console.error(err); // FIXME Path did not exist... 561 | } 562 | return []; 563 | }; 564 | 565 | 566 | module.exports._ = { 567 | clone: clone, 568 | pathOverlaps: pathOverlaps, 569 | deepEqual: deepEqual, 570 | diff: diff, 571 | resolve: resolve, 572 | patch: patch, 573 | arbiter: arbiter, 574 | }; 575 | -------------------------------------------------------------------------------- /client/transform/GenericJSONTransformer_test.js: -------------------------------------------------------------------------------- 1 | /*@flow*/ 2 | /* 3 | * Copyright 2024 XWiki SAS 4 | * 5 | * This is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU Lesser General Public License as 7 | * published by the Free Software Foundation; either version 2.1 of 8 | * the License, or (at your option) any later version. 9 | * 10 | * This software is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this software; if not, write to the Free 17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 19 | */ 20 | "use strict"; 21 | 22 | var SmartJSONTransformer = require("./SmartJSONTransformer"); 23 | var NaiveJSONTransformer = require("./NaiveJSONTransformer"); 24 | //var TextTransformer = require('./TextTransformer'); 25 | var Diff = require('../Diff'); 26 | var Sortify = require("json.sortify"); 27 | var Operation = require('../Operation'); 28 | 29 | var assertions = 0; 30 | var failed = false; 31 | var failedOn; 32 | var failMessages = []; 33 | 34 | var ASSERTS = []; 35 | 36 | var runASSERTS = function (jsonTransformer) { 37 | ASSERTS.forEach(function (f, index) { 38 | f.f(index + 1, jsonTransformer); 39 | }); 40 | }; 41 | 42 | var assert = function (test, msg, expected, skipIfNaive) { 43 | ASSERTS.push({ 44 | f: function (i, jsonTransformer) { 45 | if (skipIfNaive && jsonTransformer === NaiveJSONTransformer) { 46 | console.log("Skipping " + msg + " because it fails with NaiveJSONTransformer"); 47 | return; 48 | } 49 | console.log("Running " + msg); 50 | var returned = test(expected, jsonTransformer); 51 | if (returned === true) { 52 | assertions++; 53 | return; 54 | } 55 | failed = true; 56 | failedOn = assertions; 57 | 58 | console.log("\n" + Array(64).fill("=").join("")); 59 | console.log(JSON.stringify({ 60 | test: i, 61 | message: msg, 62 | output: returned, 63 | expected: typeof(expected) !== 'undefined'? expected: true, 64 | }, null, 2)); 65 | failMessages.push(1); 66 | }, 67 | name: msg 68 | }); 69 | }; 70 | 71 | var clone = function (x) { return JSON.parse(JSON.stringify(x)); }; 72 | var deepEqual = function (x, y) { return Sortify(x) === Sortify(y); }; 73 | 74 | var basicTest = function (obj) { 75 | assert(function (expected, jsonTransformer) { 76 | var s_O = obj.doc; 77 | var s_ttf = obj.s_toTransform; 78 | var s_tfb = obj.s_transformBy; 79 | var toTransform = Diff.diff(s_O, s_ttf); 80 | var transformBy = Diff.diff(s_O, s_tfb); 81 | var transformed = jsonTransformer(toTransform, transformBy, s_O); 82 | var temp = Operation.applyMulti(transformed, s_tfb); 83 | if (obj.comment) { 84 | console.log(); 85 | console.log(obj.comment); 86 | console.log(); 87 | } 88 | if (temp === expected) { return true; } 89 | return temp; 90 | }, obj.name, obj.expected, obj.skipIfNaive || false); 91 | }; 92 | 93 | (function () { 94 | var O = { a: 5 }; 95 | var A = { a: 6 }; 96 | var B = { a: 7 }; 97 | basicTest({ 98 | doc: Sortify(O), 99 | s_toTransform: Sortify(B), 100 | s_transformBy: Sortify(A), 101 | expected: Sortify({a: 6}), 102 | name: "replace->replace (Case #1 Conflicting)", 103 | skipIfNaive: true 104 | }); 105 | }()); 106 | 107 | // independent replace -> replace 108 | (function () { 109 | var O = {x:5}; 110 | var A = {x:7}; 111 | var B = {x:5, y: 9}; 112 | basicTest({ 113 | doc: Sortify(O), 114 | s_toTransform: Sortify(B), 115 | s_transformBy: Sortify(A), 116 | expected: Sortify({ 117 | x: 7, 118 | y: 9 119 | }), 120 | name: "Expected transform to result in two operations", 121 | }); 122 | }()); 123 | 124 | (function () { 125 | var O = { 126 | x: 5, 127 | }; 128 | var A = { 129 | x: 5, 130 | y: [ 131 | "one", 132 | "two", 133 | ] 134 | }; 135 | var B = {z: 23}; 136 | basicTest({ 137 | doc: Sortify(O), 138 | s_toTransform: Sortify(B), 139 | s_transformBy: Sortify(A), 140 | expected: Sortify({ 141 | y: ["one", "two"], 142 | z: 23 143 | }), 144 | name: "wat", 145 | skipIfNaive: true 146 | }); 147 | }()); 148 | 149 | (function () { 150 | var O = [[]]; 151 | var A = [[1]]; 152 | var B = [[2]]; 153 | basicTest({ 154 | doc: Sortify(O), 155 | s_toTransform: Sortify(B), 156 | s_transformBy: Sortify(A), 157 | expected: Sortify([[1, 2]]), 158 | name: "Expected A to take precedence over B when both push", 159 | skipIfNaive: true 160 | }); 161 | }()); 162 | 163 | (function () { 164 | var O = [{x: 5}]; 165 | 166 | var A = clone(O); 167 | A.unshift("unshifted"); // ["unshifted",{"x":5}] 168 | 169 | var B = clone(O); 170 | B[0].x = 7; // [{"x":7}] 171 | 172 | basicTest({ 173 | doc: Sortify(O), 174 | s_toTransform: Sortify(B), 175 | s_transformBy: Sortify(A), 176 | expected: Sortify([ "unshifted", { x: 7} ]), 177 | name: "Expected unshift to result in a splice operation" 178 | }); 179 | }()); 180 | 181 | (function () { 182 | var O = { }; 183 | var A = {x:5}; 184 | var B = {y: 7}; 185 | basicTest({ 186 | doc: Sortify(O), 187 | s_toTransform: Sortify(B), 188 | s_transformBy: Sortify(A), 189 | expected: Sortify({x:5, y: 7}), 190 | name: "replace->replace (Case #1 No conflict)", 191 | skipIfNaive: true 192 | }); 193 | }()); 194 | 195 | (function () { // simple merge with deletions 196 | var O = {z: 17}; 197 | var A = {x:5}; 198 | var B = {y: 7}; 199 | basicTest({ 200 | doc: Sortify(O), 201 | s_toTransform: Sortify(B), 202 | s_transformBy: Sortify(A), 203 | expected: Sortify({x:5, y: 7}), 204 | name: "simple merge with deletions", 205 | skipIfNaive: true 206 | }); 207 | }()); 208 | 209 | // remove->remove 210 | (function () { 211 | var O = { x: 5, }; 212 | var A = {}; 213 | var B = {}; 214 | basicTest({ 215 | doc: Sortify(O), 216 | s_toTransform: Sortify(B), 217 | s_transformBy: Sortify(A), 218 | expected: Sortify({}), 219 | name: "Identical removals should be deduplicated" 220 | }); 221 | }()); 222 | 223 | // replace->remove 224 | (function () { 225 | var O = { 226 | x: 5, 227 | }; 228 | var A = { 229 | x: 7, 230 | }; 231 | var B = { }; 232 | basicTest({ 233 | doc: Sortify(O), 234 | s_toTransform: Sortify(B), 235 | s_transformBy: Sortify(A), 236 | expected: Sortify({x: 7}), 237 | name: "replacements should override removals. (Case #2)", 238 | skipIfNaive: true // outputs the right thing but makes a stack trace. 239 | }); 240 | }()); 241 | 242 | // replace->splice 243 | (function () { 244 | var O = [{x:5}]; 245 | var A = clone(O); 246 | A[0].x = 7; 247 | var B = clone(O); 248 | B.unshift(3); 249 | 250 | basicTest({ 251 | doc: Sortify(O), 252 | s_toTransform: Sortify(B), 253 | s_transformBy: Sortify(A), 254 | expected: Sortify([3, {x: 7}]), 255 | name: "replace->splice (Case #3)" 256 | }); 257 | }()); 258 | 259 | // remove->replace 260 | (function () { 261 | var O = { x: 5, }; 262 | var A = { }; 263 | var B = { x: 7, }; 264 | basicTest({ 265 | doc: Sortify(O), 266 | s_toTransform: Sortify(B), 267 | s_transformBy: Sortify(A), 268 | expected: Sortify({x:7}), 269 | name: "removals should not override replacements. (Case #4)", 270 | skipIfNaive: true 271 | }); 272 | }()); 273 | 274 | // remove->remove 275 | (function () { 276 | var O = { x: 5, }; 277 | var A = {}; 278 | var B = {}; 279 | basicTest({ 280 | doc: Sortify(O), 281 | s_toTransform: Sortify(B), 282 | s_transformBy: Sortify(A), 283 | expected: Sortify({}), 284 | name: "identical removals should be deduped. (Case #5)" 285 | }); 286 | }()); 287 | 288 | // remove->splice 289 | // TODO 290 | (function () { 291 | var O = [{x:5}]; 292 | var A = [{}]; 293 | var B = [2, {x: 5}]; 294 | basicTest({ 295 | doc: Sortify(O), 296 | s_toTransform: Sortify(B), 297 | s_transformBy: Sortify(A), 298 | expected: Sortify([2, {}]), 299 | name: "remove->splice (Case #6)" 300 | }); 301 | }()); 302 | 303 | (function () { 304 | var O = [ 305 | { 306 | x:5, 307 | } 308 | ]; 309 | 310 | var A = clone(O); 311 | A.unshift(7); 312 | 313 | var B = clone(O); 314 | 315 | basicTest({ 316 | doc: Sortify(O), 317 | s_toTransform: Sortify(B), 318 | s_transformBy: Sortify(A), 319 | expected: Sortify([ 7, { x:5 } ]), 320 | name: "splice->replace (Case #7)" 321 | }); 322 | }()); 323 | 324 | // splice->remove 325 | (function () { 326 | var O = [ 327 | 1, 328 | { 329 | x: 5, 330 | } 331 | ]; 332 | 333 | var A = [ 334 | 1, 335 | 2, 336 | { 337 | x: 5, 338 | } 339 | ]; 340 | 341 | var B = [ 342 | 1, 343 | {} 344 | ]; 345 | basicTest({ 346 | doc: Sortify(O), 347 | s_toTransform: Sortify(B), 348 | s_transformBy: Sortify(A), 349 | expected: Sortify([1, 2, {}]), 350 | name: "splice->remove (Case #8)" 351 | }); 352 | }()); 353 | 354 | basicTest({ 355 | doc: '[]', s_toTransform: '["two"]', s_transformBy: '["one"]', expected: '["one","two"]', 356 | name: "splice->splice (Case #9)", 357 | skipIfNaive: true 358 | }); 359 | 360 | (function () { 361 | var O = { 362 | x: [], 363 | y: { }, 364 | z: "pew", 365 | }; 366 | 367 | var A = clone(O); 368 | var B = clone(O); 369 | 370 | A.x.push("a"); 371 | B.x.push("b"); 372 | 373 | A.y.a = 5; 374 | B.y.a = 7; 375 | 376 | A.z = "bang"; 377 | B.z = "bam!"; 378 | 379 | basicTest({ 380 | doc: Sortify(O), 381 | s_toTransform: Sortify(B), 382 | s_transformBy: Sortify(A), 383 | expected: Sortify({ 384 | x: ['a', 'b'], 385 | y: { 386 | a: 5, 387 | }, 388 | z: 'bam!bang', 389 | }), 390 | name: "Incorrect merge", 391 | //comment: 'Caleb: Without the arbitor, the string is just "bang"', 392 | skipIfNaive: true 393 | }); 394 | }()); 395 | 396 | assert(function (expected, jsonTransformer) { 397 | var O = '[]'; 398 | var A = '["a"]'; 399 | var B = '["b"]'; 400 | 401 | var actual = jsonTransformer( 402 | Diff.diff(O, B), 403 | Diff.diff(O, A), 404 | O 405 | ); 406 | 407 | //console.log(Operation.applyMulti(actual, A)); 408 | 409 | if (!deepEqual(actual, expected)) { return actual; } 410 | return true; 411 | }, "ot is incorrect", 412 | [ { type: 'Operation', offset: 4, toInsert: ',"b"', toRemove: 0 } ], 413 | true); // skipIfNaive 414 | 415 | assert(function (E, jsonTransformer) { 416 | var O = Sortify(['pewpew']); 417 | var A = Sortify(['pewpew bang']); 418 | 419 | var o_A = Diff.diff(O, A); 420 | 421 | var B = Sortify(['powpow']); 422 | var o_B = Diff.diff(O, B); 423 | 424 | var actual = jsonTransformer(o_A, o_B, O); //, true); 425 | 426 | var R = Operation.applyMulti(o_B, O); 427 | R = Operation.applyMulti(actual, R); 428 | 429 | if (R !== E) { 430 | return R; 431 | } 432 | 433 | return true; 434 | }, "transforming concurrent edits to a single string didn't work", '["powpow bang"]'); 435 | 436 | assert(function (expected, jsonTransformer) { 437 | var O = '{}'; 438 | var A = Diff.diff(O, Sortify({y: 7})); 439 | var B = Diff.diff(O, Sortify({x: 5})); 440 | 441 | var actual = jsonTransformer(A, B, O); 442 | 443 | var temp = Operation.applyMulti(A, O); 444 | temp = Operation.applyMulti(actual, temp); 445 | 446 | try { JSON.parse(temp); } 447 | catch (e) { return temp; } 448 | 449 | if (!deepEqual(actual, expected)) { 450 | console.log(actual); 451 | console.log(expected); 452 | return actual; 453 | } 454 | return true; 455 | }, 'ot on empty maps is incorrect (#1)', [ { 456 | // this is incorrect! // FIXME 457 | type: 'Operation', toInsert: ',"y":7', toRemove: 0, offset: 6 458 | } ], true); // skipIfNaive 459 | 460 | assert(function (expected, jsonTransformer) { 461 | var O = '{}'; 462 | var A = Diff.diff(O, Sortify({x: 7})); 463 | var B = Diff.diff(O, Sortify({y: 5})); 464 | 465 | var actual = jsonTransformer(A, B, O); 466 | 467 | var temp = Operation.applyMulti(A, O); 468 | temp = Operation.applyMulti(actual, temp); 469 | 470 | try { JSON.parse(temp); } 471 | catch (e) { 472 | console.log(temp); 473 | throw e; 474 | } 475 | 476 | if (!deepEqual(actual, expected)) { 477 | return actual; 478 | } 479 | return true; 480 | }, 'ot on empty maps is incorrect (#2)', 481 | [ { type: 'Operation', toInsert: 'x":7,"', toRemove: 0, offset: 2 } ], 482 | true); // skipIfNaive 483 | 484 | var checkTransform = function (O, A, B, E, M) { 485 | assert(function (expected, jsonTransformer) { 486 | var s_O = Sortify(O); 487 | 488 | var o_a = Diff.diff(s_O, Sortify(A)); 489 | var o_b = Diff.diff(s_O, Sortify(B)); 490 | 491 | var o_c = jsonTransformer(o_b, o_a, s_O); 492 | 493 | var doc = Operation.applyMulti(o_a, s_O); 494 | doc = Operation.applyMulti(o_c, doc); 495 | 496 | var result; 497 | try { result = JSON.parse(doc); } 498 | catch (e) { return e; } 499 | 500 | if (!deepEqual(result, E)) { 501 | return result; 502 | } 503 | return true; 504 | }, M || "", E); 505 | }; 506 | 507 | var goesBothWays = function (O, A, B, E, M) { 508 | checkTransform(O, A, B, E, M); 509 | checkTransform(O, B, A, E, M); 510 | }; 511 | 512 | goesBothWays( 513 | ['BODY', {}, [ 514 | ['P', {}, [['BR', {}, []]], 515 | ['P', {}, ['quick red fox']] 516 | ]]], 517 | ['BODY', {}, [ 518 | ['P', {}, [['BR', {}, []]], 519 | ['P', {}, ['The quick red fox']] 520 | ]]], 521 | 522 | ['BODY', {}, [ 523 | ['P', {}, [['BR', {}, []]], 524 | ['P', {}, ['quick red fox jumped over the lazy brown dog']] 525 | ]]], 526 | 527 | ['BODY', {}, [ 528 | ['P', {}, [['BR', {}, []]], 529 | ['P', {}, 530 | [ 'The quick red fox jumped over the lazy brown dog'], 531 | ] 532 | ]]], 533 | 534 | 'ot on the same paragraph failed'); 535 | 536 | 537 | assert(function (E, jsonTransformer) { 538 | // define a parent state and create a string representation of it 539 | var O = ['BODY', {}, [ 540 | ['P', {}, ['the quick red']] 541 | ]]; 542 | var s_O = Sortify(O); 543 | 544 | // append text into a text node 545 | var A = JSON.parse(s_O); 546 | A[2][0][2][0] = 'the quick red fox'; 547 | 548 | // insert a new paragraph at the top 549 | var B = JSON.parse(s_O); 550 | B[2].unshift(['P', {}, [ 551 | 'pewpew', 552 | ]]); 553 | 554 | // infer necessary text operations 555 | var o_A = Diff.diff(s_O, Sortify(A)); 556 | var o_B = Diff.diff(s_O, Sortify(B)); 557 | 558 | // construct a transformed text operation which takes into account the fact 559 | // that we are working with JSON 560 | var o_X = jsonTransformer(o_A, o_B, s_O); 561 | 562 | if (!o_X) { 563 | console.log(o_A); 564 | console.log(o_B); 565 | console.log(o_X); 566 | throw new Error("Expected ot to result in a patch"); 567 | } 568 | 569 | // apply both ops to the original document in the right order 570 | var doc = Operation.applyMulti(o_B, s_O); 571 | doc = Operation.applyMulti(o_X, doc); 572 | 573 | // parse the result 574 | var parsed = JSON.parse(doc); 575 | 576 | // make sure it checks out 577 | if (!deepEqual(parsed, E)) { return parsed; } 578 | return true; 579 | }, "failed to transform paragraph insertion and text node update in hyperjson", 580 | ['BODY', {}, [ 581 | ['P', {}, ['pewpew']], 582 | ['P', {}, ['the quick red fox']], 583 | ]] 584 | ); 585 | 586 | assert(function (E, jsonTransformer) { 587 | var O = ['BODY', {}, 588 | ['P', {}, [ 589 | ['STRONG', {}, ['bold']] 590 | ]] 591 | ]; 592 | var s_O = Sortify(O); 593 | 594 | var A = JSON.parse(s_O); 595 | A[2][2][0] = 'pewpew'; 596 | var s_A = Sortify(A); 597 | 598 | var d_A = Diff.diff(s_O, s_A); 599 | 600 | var B = JSON.parse(s_O); 601 | B[2][2][0][2] = 'bolded text'; 602 | 603 | var s_B = Sortify(B); 604 | var d_B = Diff.diff(s_O, s_B); 605 | 606 | var ops = jsonTransformer(d_B, d_A, s_O); 607 | 608 | if (!ops.length) { 609 | /* Your outgoing operation was cancelled by the incoming one 610 | so just apply the incoming one and DEAL WITH IT */ 611 | var temp = Operation.applyMulti(d_A, s_O); 612 | if (temp !== Sortify(E)) { return temp; } 613 | return true; 614 | } 615 | }, "failed OT on removing parent branch", 616 | ['BODY', {}, 617 | ['P', {}, ["pewpew"]] 618 | ], 619 | true); // skipIfNaive -> it outputs the right thing but it makes a stack trace. 620 | 621 | assert(function (expected, jsonTransformer) { 622 | var s_O = '["BODY",{},["P",{},["pewpew pezpew"]]]'; 623 | 624 | var toTransform = [ { type: "Operation", offset: 27, toRemove: 0, toInsert: "pew" } ]; 625 | var transformBy = [ { type: "Operation", offset: 33, toRemove: 1, toInsert: 'z' } ]; 626 | 627 | var d_C = jsonTransformer(toTransform, transformBy, s_O); 628 | 629 | //var s_A = Operation.applyMulti(toTransform, s_O); 630 | var s_B = Operation.applyMulti(transformBy, s_O); 631 | 632 | var temp = Operation.applyMulti(d_C, s_B); 633 | 634 | if (temp !== expected) { return temp; } 635 | return true; 636 | }, "failed ot with 2 operations in the same text node", 637 | '["BODY",{},["P",{},["pewpewpew pezpez"]]]'); 638 | 639 | basicTest({ 640 | doc: '["BODY",{},["P",{},["pewpew pezpew end"]]]', 641 | s_toTransform: '["BODY",{},["P",{},["pewpewpew pezpew end"]]]', 642 | s_transformBy: '["BODY",{},["P",{},["pewpe pezpez"," end"]]]', 643 | expected: '["BODY",{},["P",{},["pewpe pezpez","pewpewpew pezpew end"]]]', 644 | name: "failed ot with concurrent operations in the same text nod", 645 | skipIfNaive: true, 646 | comment: [ 647 | 'TODO This test is passing but only to document the behavior of JSON-OT', 648 | 'Yanns expected output of this test is: ["BODY",{},["P",{},["pewpewpew pezpez"," end"]]]', 649 | 'NaiveJSONTransformer results in: ["BODY",{},["P",{},["pewpe pezpez","pew end"]]]', 650 | 'The output is: ["BODY",{},["P",{},["pewpe pezpez","pewpewpew pezpew end"]]]' 651 | ].join('\n') 652 | }); 653 | 654 | basicTest({ 655 | doc: '["a"]', s_toTransform: '["b"]', s_transformBy: '["c"]', expected: '["bc"]', 656 | name: "multiple intersecting array splices (replace replace)", 657 | }); 658 | 659 | basicTest({ 660 | doc: '["a","b"]', s_toTransform: '["a","c"]', s_transformBy: '["a","d"]', expected: '["a","cd"]', 661 | name: "multiple intersecting array splices (replace replace) with element before", 662 | }); 663 | 664 | basicTest({ 665 | doc: '["b","a"]', s_toTransform: '["c","a"]', s_transformBy: '["b","a"]', expected: '["c","a"]', 666 | name: "multiple intersecting array splices (replace replace) with element after" 667 | }); 668 | 669 | basicTest({ 670 | doc: '["a"]', s_toTransform: '["b"]', s_transformBy: '["a","c"]', expected: '["b","c"]', 671 | name: "multiple intersecting array splices (replace push)" 672 | }); 673 | 674 | basicTest({ 675 | doc: '["a"]', s_toTransform: '["a","c"]', s_transformBy: '["b"]', expected: '["b","c"]', 676 | name: "multiple intersecting array splices (push replace)" 677 | }); 678 | 679 | basicTest({ 680 | doc: '["a"]', s_toTransform: '["a","b"]', s_transformBy: '["a","c"]', expected: '["a","c","b"]', 681 | name: "multiple intersecting array splices (push push)", 682 | skipIfNaive: true 683 | }); 684 | 685 | basicTest({ 686 | doc: '["a"]', s_toTransform: '[]', s_transformBy: '["b"]', expected: '[]', 687 | name: "multiple intersecting array splices (remove replace)", 688 | skipIfNaive: true 689 | }); 690 | 691 | basicTest({ 692 | doc: '["a"]', s_toTransform: '["b"]', s_transformBy: '[]', expected: '[]', 693 | name: "multiple intersecting array splices (replace remove)", 694 | skipIfNaive: true 695 | }); 696 | 697 | basicTest({ 698 | doc: '["a"]', s_toTransform: '[]', s_transformBy: '["a","c"]', expected: '["c"]', 699 | name: "multiple intersecting array splices (remove push)", 700 | // expected value was set to an incorrect value, but we now produce the correct value 701 | // this test has since been un-stubbed 702 | comment: 'Caleb: This should result in ["c"]', 703 | skipIfNaive: true 704 | }); 705 | 706 | basicTest({ 707 | doc: '["a"]', s_toTransform: '[]', s_transformBy: '["c","a"]', expected: '["c"]', 708 | name: "multiple intersecting array splices (remove unshift)", 709 | skipIfNaive: true 710 | }); 711 | 712 | module.exports.main = function (cycles /*:number*/, callback /*:()=>void*/) { 713 | runASSERTS(SmartJSONTransformer); 714 | if (failed) { 715 | console.log("\n%s assertions passed and %s failed", assertions, failMessages.length); 716 | throw new Error(); 717 | } 718 | console.log("[SUCCESS] %s tests passed", assertions); 719 | 720 | runASSERTS(NaiveJSONTransformer); 721 | if (failed) { 722 | console.log("\n%s assertions passed and %s failed", assertions, failMessages.length); 723 | throw new Error(); 724 | } 725 | console.log("[SUCCESS] %s tests passed", assertions); 726 | callback(); 727 | }; 728 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | (This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.) 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | {description} 474 | Copyright (C) {year} {fullname} 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | {signature of Ty Coon}, 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | 506 | --------------------------------------------------------------------------------