├── .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 | [](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 |
--------------------------------------------------------------------------------