sn-collab-editor 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/collab/cable.js +121 -11
- data/app/assets/javascripts/collab/documents.js +142 -52
- data/app/assets/stylesheets/collab/{application.css → application.css.scss} +31 -4
- data/app/channels/edit_channel.rb +16 -10
- data/app/controllers/collab/documents_controller.rb +2 -2
- data/app/models/collab/document.rb +1 -0
- data/app/models/collab/patch.rb +5 -0
- data/app/views/collab/documents/show.html.erb +11 -4
- data/app/views/layouts/collab/application.html.erb +5 -0
- data/db/migrate/20170222133537_create_collab_documents.rb +8 -3
- data/lib/collab/engine.rb +1 -2
- data/lib/collab/version.rb +1 -1
- data/vendor/assets/javascripts/TextPatcher.js +154 -0
- data/vendor/assets/javascripts/chainpad.js +1588 -0
- data/vendor/assets/javascripts/codemirror.js +9231 -0
- data/vendor/assets/javascripts/markdown.js +816 -0
- data/vendor/assets/stylesheets/codemirror.css +341 -0
- metadata +9 -3
@@ -12,21 +12,27 @@ class EditChannel < ApplicationCable::Channel
|
|
12
12
|
def retrieve(data)
|
13
13
|
doc = Collab::Document.find_by_uuid(data['doc_id'])
|
14
14
|
ActionCable.server.broadcast("doc_#{doc.uuid}_#{params['client_id']}", {
|
15
|
-
:
|
16
|
-
:iv => doc.iv,
|
17
|
-
:auth => doc.auth,
|
15
|
+
:patches => doc.patches,
|
18
16
|
:client_id => data['client_id'],
|
19
|
-
:
|
20
|
-
|
17
|
+
:initial_retrieve => true
|
18
|
+
})
|
21
19
|
end
|
22
20
|
|
23
21
|
def post(data)
|
24
|
-
message = data['message']
|
25
22
|
doc = Collab::Document.find_by_uuid(data['doc_id'])
|
26
|
-
|
27
|
-
|
28
|
-
doc.
|
23
|
+
|
24
|
+
edit_token = data["edit_token"]
|
25
|
+
if edit_token != doc.edit_token
|
26
|
+
puts "Edit token not authorized, returning"
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
patch = doc.patches.create({
|
31
|
+
:content => data['content'],
|
32
|
+
:iv => data["iv"],
|
33
|
+
:auth => data["auth"]
|
34
|
+
})
|
29
35
|
doc.save
|
30
|
-
EditChannel.broadcast_to(doc, {:
|
36
|
+
EditChannel.broadcast_to(doc, {:patch => patch, :client_id => data['client_id']})
|
31
37
|
end
|
32
38
|
end
|
@@ -24,11 +24,11 @@ module Collab
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def new
|
27
|
-
puts "\n\nCreating new document\n\n"
|
28
27
|
@doc = Document.new
|
29
28
|
@doc.uuid = SecureRandom.uuid
|
29
|
+
@doc.edit_token = Digest::SHA256.hexdigest(SecureRandom.random_bytes())[0,24]
|
30
30
|
@doc.save
|
31
|
-
redirect_to(url)
|
31
|
+
redirect_to(url + "?et=#{@doc.edit_token}")
|
32
32
|
end
|
33
33
|
|
34
34
|
end
|
@@ -2,11 +2,18 @@
|
|
2
2
|
|
3
3
|
<div id="wrapper">
|
4
4
|
<% if @doc %>
|
5
|
-
|
6
|
-
|
7
|
-
<span
|
5
|
+
|
6
|
+
<div id="viewing-url-wrapper" class="url-wrapper">
|
7
|
+
<span class="label">Viewing URL</span>:
|
8
|
+
<a id="viewing-url" class="url" target="_blank"></a>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div id="editing-url-wrapper" class="url-wrapper">
|
12
|
+
<span class="label">Editing URL</span>:
|
13
|
+
<a id="editing-url" class="url" target="_blank"></a>
|
8
14
|
</div>
|
9
|
-
|
15
|
+
|
16
|
+
<textarea id="editor" name="editor"></textarea>
|
10
17
|
<% else %>
|
11
18
|
|
12
19
|
<% end %>
|
@@ -3,9 +3,14 @@
|
|
3
3
|
<head>
|
4
4
|
<title>Standard Notes Collaborative Editor</title>
|
5
5
|
<%= stylesheet_link_tag "collab/application", media: "all" %>
|
6
|
+
<%= stylesheet_link_tag "codemirror" %>
|
6
7
|
<%= javascript_include_tag "collab/application" %>
|
7
8
|
<%= javascript_include_tag "aes" %>
|
8
9
|
<%= javascript_include_tag "hmac-sha256" %>
|
10
|
+
<%= javascript_include_tag "chainpad" %>
|
11
|
+
<%= javascript_include_tag "codemirror" %>
|
12
|
+
<%= javascript_include_tag "TextPatcher" %>
|
13
|
+
<%= javascript_include_tag "markdown" %>
|
9
14
|
<%= csrf_meta_tags %>
|
10
15
|
</head>
|
11
16
|
<body>
|
@@ -1,11 +1,16 @@
|
|
1
1
|
class CreateCollabDocuments < ActiveRecord::Migration[5.0]
|
2
2
|
def change
|
3
3
|
create_table :collab_documents do |t|
|
4
|
-
t.text :content
|
5
|
-
t.string :iv
|
6
|
-
t.string :auth
|
7
4
|
t.string :uuid
|
5
|
+
t.string :edit_token
|
8
6
|
t.timestamps
|
9
7
|
end
|
8
|
+
|
9
|
+
create_table :collab_patches do |t|
|
10
|
+
t.text :content, :limit => 16.megabytes - 1
|
11
|
+
t.string :iv
|
12
|
+
t.string :auth
|
13
|
+
t.integer :document_id
|
14
|
+
end
|
10
15
|
end
|
11
16
|
end
|
data/lib/collab/engine.rb
CHANGED
@@ -2,7 +2,6 @@ module Collab
|
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace Collab
|
4
4
|
|
5
|
-
|
6
|
-
config.assets.precompile += %w( aes.js hmac-sha256.js )
|
5
|
+
config.assets.precompile += %w( aes.js hmac-sha256.js chainpad.js codemirror.js codemirror.css TextPatcher markdown )
|
7
6
|
end
|
8
7
|
end
|
data/lib/collab/version.rb
CHANGED
@@ -0,0 +1,154 @@
|
|
1
|
+
(function () {
|
2
|
+
|
3
|
+
var TextPatcher = {};
|
4
|
+
|
5
|
+
/* diff takes two strings, the old content, and the desired content
|
6
|
+
it returns the difference between these two strings in the form
|
7
|
+
of an 'Operation' (as defined in chainpad.js).
|
8
|
+
|
9
|
+
diff is purely functional.
|
10
|
+
*/
|
11
|
+
var diff = TextPatcher.diff = function (oldval, newval) {
|
12
|
+
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
|
13
|
+
if (oldval === newval) {
|
14
|
+
return;
|
15
|
+
}
|
16
|
+
|
17
|
+
var commonStart = 0;
|
18
|
+
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
|
19
|
+
commonStart++;
|
20
|
+
}
|
21
|
+
|
22
|
+
var commonEnd = 0;
|
23
|
+
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
|
24
|
+
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
|
25
|
+
commonEnd++;
|
26
|
+
}
|
27
|
+
|
28
|
+
var toRemove = 0;
|
29
|
+
var toInsert = '';
|
30
|
+
|
31
|
+
/* throw some assertions in here before dropping patches into the realtime */
|
32
|
+
if (oldval.length !== commonStart + commonEnd) {
|
33
|
+
toRemove = oldval.length - commonStart - commonEnd;
|
34
|
+
}
|
35
|
+
if (newval.length !== commonStart + commonEnd) {
|
36
|
+
toInsert = newval.slice(commonStart, newval.length - commonEnd);
|
37
|
+
}
|
38
|
+
|
39
|
+
return {
|
40
|
+
type: 'Operation',
|
41
|
+
offset: commonStart,
|
42
|
+
toInsert: toInsert,
|
43
|
+
toRemove: toRemove
|
44
|
+
};
|
45
|
+
};
|
46
|
+
|
47
|
+
/* patch accepts a realtime facade and an operation (which might be falsey)
|
48
|
+
it applies the operation to the realtime as components (remove/insert)
|
49
|
+
|
50
|
+
patch has no return value, and operates solely through side effects on
|
51
|
+
the realtime facade.
|
52
|
+
*/
|
53
|
+
var patch = TextPatcher.patch = function (ctx, op) {
|
54
|
+
if (!op) { return; }
|
55
|
+
|
56
|
+
if (ctx.patch) {
|
57
|
+
ctx.patch(op.offset, op.toRemove, op.toInsert);
|
58
|
+
} else {
|
59
|
+
console.log("chainpad.remove and chainpad.insert are deprecated. "+
|
60
|
+
"update your chainpad installation to the latest version.");
|
61
|
+
if (op.toRemove) { ctx.remove(op.offset, op.toRemove); }
|
62
|
+
if (op.toInsert) { ctx.insert(op.offset, op.toInsert); }
|
63
|
+
}
|
64
|
+
};
|
65
|
+
|
66
|
+
/* format has the same signature as log, but doesn't log to the console
|
67
|
+
use it to get the pretty version of a diff */
|
68
|
+
var format = TextPatcher.format = function (text, op) {
|
69
|
+
return op?{
|
70
|
+
insert: op.toInsert,
|
71
|
+
remove: text.slice(op.offset, op.offset + op.toRemove)
|
72
|
+
}: { insert: '', remove: '' };
|
73
|
+
};
|
74
|
+
|
75
|
+
/* log accepts a string and an operation, and prints an object to the console
|
76
|
+
the object will display the content which is to be removed, and the content
|
77
|
+
which will be inserted in its place.
|
78
|
+
|
79
|
+
log is useful for debugging, but can otherwise be disabled.
|
80
|
+
*/
|
81
|
+
var log = TextPatcher.log = function (text, op) {
|
82
|
+
if (!op) { return; }
|
83
|
+
console.log(format(text, op));
|
84
|
+
};
|
85
|
+
|
86
|
+
/* applyChange takes:
|
87
|
+
ctx: the context (aka the realtime)
|
88
|
+
oldval: the old value
|
89
|
+
newval: the new value
|
90
|
+
|
91
|
+
it performs a diff on the two values, and generates patches
|
92
|
+
which are then passed into `ctx.remove` and `ctx.insert`.
|
93
|
+
|
94
|
+
Due to its reliance on patch, applyChange has side effects on the supplied
|
95
|
+
realtime facade.
|
96
|
+
*/
|
97
|
+
var applyChange = TextPatcher.applyChange = function(ctx, oldval, newval, logging) {
|
98
|
+
var op = diff(oldval, newval);
|
99
|
+
if (logging) { log(oldval, op); }
|
100
|
+
patch(ctx, op);
|
101
|
+
};
|
102
|
+
|
103
|
+
var transformCursor = TextPatcher.transformCursor = function (cursor, op) {
|
104
|
+
if (!op) { return cursor; }
|
105
|
+
var pos = op.offset;
|
106
|
+
var remove = op.toRemove;
|
107
|
+
var insert = op.toInsert.length;
|
108
|
+
if (typeof cursor === 'undefined') { return; }
|
109
|
+
if (typeof remove === 'number' && pos < cursor) {
|
110
|
+
cursor -= Math.min(remove, cursor - pos);
|
111
|
+
}
|
112
|
+
if (typeof insert === 'number' && pos < cursor) {
|
113
|
+
cursor += insert;
|
114
|
+
}
|
115
|
+
return cursor;
|
116
|
+
};
|
117
|
+
|
118
|
+
var create = TextPatcher.create = function(config) {
|
119
|
+
var ctx = config.realtime;
|
120
|
+
var logging = config.logging;
|
121
|
+
|
122
|
+
// initial state will always fail the !== check in genop.
|
123
|
+
// because nothing will equal this object
|
124
|
+
var content = {};
|
125
|
+
|
126
|
+
// *** remote -> local changes
|
127
|
+
ctx.onPatch(function(pos, length) {
|
128
|
+
content = ctx.getUserDoc();
|
129
|
+
});
|
130
|
+
|
131
|
+
// propogate()
|
132
|
+
return function (newContent, force) {
|
133
|
+
if (newContent !== content || force) {
|
134
|
+
applyChange(ctx, ctx.getUserDoc(), newContent, logging);
|
135
|
+
if (ctx.getUserDoc() !== newContent) {
|
136
|
+
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
|
137
|
+
}
|
138
|
+
else { content = ctx.getUserDoc(); }
|
139
|
+
return true;
|
140
|
+
}
|
141
|
+
return false;
|
142
|
+
};
|
143
|
+
};
|
144
|
+
|
145
|
+
if (typeof(module) !== 'undefined' && module.exports) {
|
146
|
+
module.exports = TextPatcher;
|
147
|
+
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
148
|
+
define(function () {
|
149
|
+
return TextPatcher;
|
150
|
+
});
|
151
|
+
} else {
|
152
|
+
window.TextPatcher = TextPatcher;
|
153
|
+
}
|
154
|
+
}());
|
@@ -0,0 +1,1588 @@
|
|
1
|
+
(function(){
|
2
|
+
var r=function(){var e="function"==typeof require&&require,r=function(i,o,u){o||(o=0);var n=r.resolve(i,o),t=r.m[o][n];if(!t&&e){if(t=e(n))return t}else if(t&&t.c&&(o=t.c,n=t.m,t=r.m[o][t.m],!t))throw new Error('failed to require "'+n+'" from '+o);if(!t)throw new Error('failed to require "'+i+'" from '+u);return t.exports||(t.exports={},t.call(t.exports,t,t.exports,r.relative(n,o))),t.exports};return r.resolve=function(e,n){var i=e,t=e+".js",o=e+"/index.js";return r.m[n][t]&&t?t:r.m[n][o]&&o?o:i},r.relative=function(e,t){return function(n){if("."!=n.charAt(0))return r(n,t,e);var o=e.split("/"),f=n.split("/");o.pop();for(var i=0;i<f.length;i++){var u=f[i];".."==u?o.pop():"."!=u&&o.push(u)}return r(o.join("/"),t,e)}},r}();r.m = [];
|
3
|
+
r.m[0] = {
|
4
|
+
"Patch.js": function(module, exports, require){
|
5
|
+
/*
|
6
|
+
* Copyright 2014 XWiki SAS
|
7
|
+
*
|
8
|
+
* This program is free software: you can redistribute it and/or modify
|
9
|
+
* it under the terms of the GNU Affero General Public License as published by
|
10
|
+
* the Free Software Foundation, either version 3 of the License, or
|
11
|
+
* (at your option) any later version.
|
12
|
+
*
|
13
|
+
* This program is distributed in the hope that it will be useful,
|
14
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
* GNU Affero General Public License for more details.
|
17
|
+
*
|
18
|
+
* You should have received a copy of the GNU Affero General Public License
|
19
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
20
|
+
*/
|
21
|
+
var Common = require('./Common');
|
22
|
+
var Operation = require('./Operation');
|
23
|
+
var Sha = require('./SHA256');
|
24
|
+
|
25
|
+
var Patch = module.exports;
|
26
|
+
|
27
|
+
var create = Patch.create = function (parentHash) {
|
28
|
+
return {
|
29
|
+
type: 'Patch',
|
30
|
+
operations: [],
|
31
|
+
parentHash: parentHash,
|
32
|
+
isCheckpoint: false
|
33
|
+
};
|
34
|
+
};
|
35
|
+
|
36
|
+
var check = Patch.check = function (patch, docLength_opt) {
|
37
|
+
Common.assert(patch.type === 'Patch');
|
38
|
+
Common.assert(Array.isArray(patch.operations));
|
39
|
+
Common.assert(/^[0-9a-f]{64}$/.test(patch.parentHash));
|
40
|
+
for (var i = patch.operations.length - 1; i >= 0; i--) {
|
41
|
+
Operation.check(patch.operations[i], docLength_opt);
|
42
|
+
if (i > 0) {
|
43
|
+
Common.assert(!Operation.shouldMerge(patch.operations[i], patch.operations[i-1]));
|
44
|
+
}
|
45
|
+
if (typeof(docLength_opt) === 'number') {
|
46
|
+
docLength_opt += Operation.lengthChange(patch.operations[i]);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
if (patch.isCheckpoint) {
|
50
|
+
Common.assert(patch.operations.length === 1);
|
51
|
+
Common.assert(patch.operations[0].offset === 0);
|
52
|
+
if (typeof(docLength_opt) === 'number') {
|
53
|
+
Common.assert(!docLength_opt || patch.operations[0].toRemove === docLength_opt);
|
54
|
+
}
|
55
|
+
}
|
56
|
+
};
|
57
|
+
|
58
|
+
var toObj = Patch.toObj = function (patch) {
|
59
|
+
if (Common.PARANOIA) { check(patch); }
|
60
|
+
var out = new Array(patch.operations.length+1);
|
61
|
+
var i;
|
62
|
+
for (i = 0; i < patch.operations.length; i++) {
|
63
|
+
out[i] = Operation.toObj(patch.operations[i]);
|
64
|
+
}
|
65
|
+
out[i] = patch.parentHash;
|
66
|
+
return out;
|
67
|
+
};
|
68
|
+
|
69
|
+
var fromObj = Patch.fromObj = function (obj) {
|
70
|
+
Common.assert(Array.isArray(obj) && obj.length > 0);
|
71
|
+
var patch = create();
|
72
|
+
var i;
|
73
|
+
for (i = 0; i < obj.length-1; i++) {
|
74
|
+
patch.operations[i] = Operation.fromObj(obj[i]);
|
75
|
+
}
|
76
|
+
patch.parentHash = obj[i];
|
77
|
+
if (Common.PARANOIA) { check(patch); }
|
78
|
+
return patch;
|
79
|
+
};
|
80
|
+
|
81
|
+
var hash = function (text) {
|
82
|
+
return Sha.hex_sha256(text);
|
83
|
+
};
|
84
|
+
|
85
|
+
var addOperation = Patch.addOperation = function (patch, op) {
|
86
|
+
if (Common.PARANOIA) {
|
87
|
+
check(patch);
|
88
|
+
Operation.check(op);
|
89
|
+
}
|
90
|
+
for (var i = 0; i < patch.operations.length; i++) {
|
91
|
+
if (Operation.shouldMerge(patch.operations[i], op)) {
|
92
|
+
op = Operation.merge(patch.operations[i], op);
|
93
|
+
patch.operations.splice(i,1);
|
94
|
+
if (op === null) {
|
95
|
+
//console.log("operations cancelled eachother");
|
96
|
+
return;
|
97
|
+
}
|
98
|
+
i--;
|
99
|
+
} else {
|
100
|
+
var out = Operation.rebase(patch.operations[i], op);
|
101
|
+
if (out === op) {
|
102
|
+
// op could not be rebased further, insert it here to keep the list ordered.
|
103
|
+
patch.operations.splice(i,0,op);
|
104
|
+
return;
|
105
|
+
} else {
|
106
|
+
op = out;
|
107
|
+
// op was rebased, try rebasing it against the next operation.
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
patch.operations.push(op);
|
112
|
+
if (Common.PARANOIA) { check(patch); }
|
113
|
+
};
|
114
|
+
|
115
|
+
var createCheckpoint = Patch.createCheckpoint =
|
116
|
+
function (parentContent, checkpointContent, parentContentHash_opt)
|
117
|
+
{
|
118
|
+
var op = Operation.create(0, parentContent.length, checkpointContent);
|
119
|
+
if (Common.PARANOIA && parentContentHash_opt) {
|
120
|
+
Common.assert(parentContentHash_opt === hash(parentContent));
|
121
|
+
}
|
122
|
+
parentContentHash_opt = parentContentHash_opt || hash(parentContent);
|
123
|
+
var out = create(parentContentHash_opt);
|
124
|
+
addOperation(out, op);
|
125
|
+
out.isCheckpoint = true;
|
126
|
+
return out;
|
127
|
+
};
|
128
|
+
|
129
|
+
var clone = Patch.clone = function (patch) {
|
130
|
+
if (Common.PARANOIA) { check(patch); }
|
131
|
+
var out = create();
|
132
|
+
out.parentHash = patch.parentHash;
|
133
|
+
for (var i = 0; i < patch.operations.length; i++) {
|
134
|
+
out.operations[i] = Operation.clone(patch.operations[i]);
|
135
|
+
}
|
136
|
+
return out;
|
137
|
+
};
|
138
|
+
|
139
|
+
var merge = Patch.merge = function (oldPatch, newPatch) {
|
140
|
+
if (Common.PARANOIA) {
|
141
|
+
check(oldPatch);
|
142
|
+
check(newPatch);
|
143
|
+
}
|
144
|
+
if (oldPatch.isCheckpoint) {
|
145
|
+
Common.assert(newPatch.parentHash === oldPatch.parentHash);
|
146
|
+
if (newPatch.isCheckpoint) {
|
147
|
+
return create(oldPatch.parentHash)
|
148
|
+
}
|
149
|
+
return clone(newPatch);
|
150
|
+
} else if (newPatch.isCheckpoint) {
|
151
|
+
return clone(oldPatch);
|
152
|
+
}
|
153
|
+
oldPatch = clone(oldPatch);
|
154
|
+
for (var i = newPatch.operations.length-1; i >= 0; i--) {
|
155
|
+
addOperation(oldPatch, newPatch.operations[i]);
|
156
|
+
}
|
157
|
+
return oldPatch;
|
158
|
+
};
|
159
|
+
|
160
|
+
var apply = Patch.apply = function (patch, doc)
|
161
|
+
{
|
162
|
+
if (Common.PARANOIA) {
|
163
|
+
check(patch);
|
164
|
+
Common.assert(typeof(doc) === 'string');
|
165
|
+
Common.assert(Sha.hex_sha256(doc) === patch.parentHash);
|
166
|
+
}
|
167
|
+
var newDoc = doc;
|
168
|
+
for (var i = patch.operations.length-1; i >= 0; i--) {
|
169
|
+
newDoc = Operation.apply(patch.operations[i], newDoc);
|
170
|
+
}
|
171
|
+
return newDoc;
|
172
|
+
};
|
173
|
+
|
174
|
+
var lengthChange = Patch.lengthChange = function (patch)
|
175
|
+
{
|
176
|
+
if (Common.PARANOIA) { check(patch); }
|
177
|
+
var out = 0;
|
178
|
+
for (var i = 0; i < patch.operations.length; i++) {
|
179
|
+
out += Operation.lengthChange(patch.operations[i]);
|
180
|
+
}
|
181
|
+
return out;
|
182
|
+
};
|
183
|
+
|
184
|
+
var invert = Patch.invert = function (patch, doc)
|
185
|
+
{
|
186
|
+
if (Common.PARANOIA) {
|
187
|
+
check(patch);
|
188
|
+
Common.assert(typeof(doc) === 'string');
|
189
|
+
Common.assert(Sha.hex_sha256(doc) === patch.parentHash);
|
190
|
+
}
|
191
|
+
var rpatch = create();
|
192
|
+
var newDoc = doc;
|
193
|
+
for (var i = patch.operations.length-1; i >= 0; i--) {
|
194
|
+
rpatch.operations[i] = Operation.invert(patch.operations[i], newDoc);
|
195
|
+
newDoc = Operation.apply(patch.operations[i], newDoc);
|
196
|
+
}
|
197
|
+
for (var i = rpatch.operations.length-1; i >= 0; i--) {
|
198
|
+
for (var j = i - 1; j >= 0; j--) {
|
199
|
+
rpatch.operations[i].offset += rpatch.operations[j].toRemove;
|
200
|
+
rpatch.operations[i].offset -= rpatch.operations[j].toInsert.length;
|
201
|
+
}
|
202
|
+
}
|
203
|
+
rpatch.parentHash = Sha.hex_sha256(newDoc);
|
204
|
+
rpatch.isCheckpoint = patch.isCheckpoint;
|
205
|
+
if (Common.PARANOIA) { check(rpatch); }
|
206
|
+
return rpatch;
|
207
|
+
};
|
208
|
+
|
209
|
+
var simplify = Patch.simplify = function (patch, doc, operationSimplify)
|
210
|
+
{
|
211
|
+
if (Common.PARANOIA) {
|
212
|
+
check(patch);
|
213
|
+
Common.assert(typeof(doc) === 'string');
|
214
|
+
Common.assert(Sha.hex_sha256(doc) === patch.parentHash);
|
215
|
+
}
|
216
|
+
operationSimplify = operationSimplify || Operation.simplify;
|
217
|
+
var spatch = create(patch.parentHash);
|
218
|
+
var newDoc = doc;
|
219
|
+
var outOps = [];
|
220
|
+
var j = 0;
|
221
|
+
for (var i = patch.operations.length-1; i >= 0; i--) {
|
222
|
+
outOps[j] = operationSimplify(patch.operations[i], newDoc, Operation.simplify);
|
223
|
+
if (outOps[j]) {
|
224
|
+
newDoc = Operation.apply(outOps[j], newDoc);
|
225
|
+
j++;
|
226
|
+
}
|
227
|
+
}
|
228
|
+
spatch.operations = outOps.reverse();
|
229
|
+
if (!spatch.operations[0]) {
|
230
|
+
spatch.operations.shift();
|
231
|
+
}
|
232
|
+
if (Common.PARANOIA) {
|
233
|
+
check(spatch);
|
234
|
+
}
|
235
|
+
return spatch;
|
236
|
+
};
|
237
|
+
|
238
|
+
var equals = Patch.equals = function (patchA, patchB) {
|
239
|
+
if (patchA.operations.length !== patchB.operations.length) { return false; }
|
240
|
+
for (var i = 0; i < patchA.operations.length; i++) {
|
241
|
+
if (!Operation.equals(patchA.operations[i], patchB.operations[i])) { return false; }
|
242
|
+
}
|
243
|
+
return true;
|
244
|
+
};
|
245
|
+
|
246
|
+
var isCheckpointOp = function (op, text) {
|
247
|
+
return op.offset === 0 && op.toRemove === text.length && op.toInsert === text;
|
248
|
+
};
|
249
|
+
|
250
|
+
var transform = Patch.transform = function (origToTransform, transformBy, doc, transformFunction) {
|
251
|
+
if (Common.PARANOIA) {
|
252
|
+
check(origToTransform, doc.length);
|
253
|
+
check(transformBy, doc.length);
|
254
|
+
Common.assert(Sha.hex_sha256(doc) === origToTransform.parentHash);
|
255
|
+
}
|
256
|
+
Common.assert(origToTransform.parentHash === transformBy.parentHash);
|
257
|
+
var resultOfTransformBy = apply(transformBy, doc);
|
258
|
+
|
259
|
+
var toTransform = clone(origToTransform);
|
260
|
+
var text = doc;
|
261
|
+
for (var i = toTransform.operations.length-1; i >= 0; i--) {
|
262
|
+
if (isCheckpointOp(toTransform.operations[i], text)) { continue; }
|
263
|
+
for (var j = transformBy.operations.length-1; j >= 0; j--) {
|
264
|
+
if (isCheckpointOp(transformBy.operations[j], text)) { console.log('cpo'); continue; }
|
265
|
+
if (Common.DEBUG) {
|
266
|
+
console.log(
|
267
|
+
['TRANSFORM', text, toTransform.operations[i], transformBy.operations[j]]
|
268
|
+
);
|
269
|
+
}
|
270
|
+
try {
|
271
|
+
toTransform.operations[i] = Operation.transform(text,
|
272
|
+
toTransform.operations[i],
|
273
|
+
transformBy.operations[j],
|
274
|
+
transformFunction);
|
275
|
+
} catch (e) {
|
276
|
+
console.error("The pluggable transform function threw an error, " +
|
277
|
+
"failing operational transformation");
|
278
|
+
return create(Sha.hex_sha256(resultOfTransformBy));
|
279
|
+
}
|
280
|
+
if (!toTransform.operations[i]) {
|
281
|
+
break;
|
282
|
+
}
|
283
|
+
}
|
284
|
+
if (Common.PARANOIA && toTransform.operations[i]) {
|
285
|
+
Operation.check(toTransform.operations[i], resultOfTransformBy.length);
|
286
|
+
}
|
287
|
+
}
|
288
|
+
var out = create(transformBy.parentHash);
|
289
|
+
for (var i = toTransform.operations.length-1; i >= 0; i--) {
|
290
|
+
if (toTransform.operations[i]) {
|
291
|
+
addOperation(out, toTransform.operations[i]);
|
292
|
+
}
|
293
|
+
}
|
294
|
+
|
295
|
+
out.parentHash = Sha.hex_sha256(resultOfTransformBy);
|
296
|
+
|
297
|
+
if (Common.PARANOIA) {
|
298
|
+
check(out, resultOfTransformBy.length);
|
299
|
+
}
|
300
|
+
return out;
|
301
|
+
};
|
302
|
+
|
303
|
+
var random = Patch.random = function (doc, opCount) {
|
304
|
+
Common.assert(typeof(doc) === 'string');
|
305
|
+
opCount = opCount || (Math.floor(Math.random() * 30) + 1);
|
306
|
+
var patch = create(Sha.hex_sha256(doc));
|
307
|
+
var docLength = doc.length;
|
308
|
+
while (opCount-- > 0) {
|
309
|
+
var op = Operation.random(docLength);
|
310
|
+
docLength += Operation.lengthChange(op);
|
311
|
+
addOperation(patch, op);
|
312
|
+
}
|
313
|
+
check(patch);
|
314
|
+
return patch;
|
315
|
+
};
|
316
|
+
|
317
|
+
},
|
318
|
+
"SHA256.js": function(module, exports, require){
|
319
|
+
/* A JavaScript implementation of the Secure Hash Algorithm, SHA-256
|
320
|
+
* Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/
|
321
|
+
* Distributed under the BSD License
|
322
|
+
* Some bits taken from Paul Johnston's SHA-1 implementation
|
323
|
+
*/
|
324
|
+
(function () {
|
325
|
+
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
|
326
|
+
function safe_add (x, y) {
|
327
|
+
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
328
|
+
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
329
|
+
return (msw << 16) | (lsw & 0xFFFF);
|
330
|
+
}
|
331
|
+
function S (X, n) {return ( X >>> n ) | (X << (32 - n));}
|
332
|
+
function R (X, n) {return ( X >>> n );}
|
333
|
+
function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));}
|
334
|
+
function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));}
|
335
|
+
function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));}
|
336
|
+
function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));}
|
337
|
+
function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));}
|
338
|
+
function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));}
|
339
|
+
function newArray (n) {
|
340
|
+
var a = [];
|
341
|
+
for (;n>0;n--) {
|
342
|
+
a.push(undefined);
|
343
|
+
}
|
344
|
+
return a;
|
345
|
+
}
|
346
|
+
function core_sha256 (m, l) {
|
347
|
+
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];
|
348
|
+
var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19];
|
349
|
+
var W = newArray(64);
|
350
|
+
var a, b, c, d, e, f, g, h, i, j;
|
351
|
+
var T1, T2;
|
352
|
+
/* append padding */
|
353
|
+
m[l >> 5] |= 0x80 << (24 - l % 32);
|
354
|
+
m[((l + 64 >> 9) << 4) + 15] = l;
|
355
|
+
for ( var i = 0; i<m.length; i+=16 ) {
|
356
|
+
a = HASH[0]; b = HASH[1]; c = HASH[2]; d = HASH[3];
|
357
|
+
e = HASH[4]; f = HASH[5]; g = HASH[6]; h = HASH[7];
|
358
|
+
for ( var j = 0; j<64; j++) {
|
359
|
+
if (j < 16) {
|
360
|
+
W[j] = m[j + i];
|
361
|
+
} else {
|
362
|
+
W[j] = safe_add(safe_add(safe_add(Gamma1256(
|
363
|
+
W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
|
364
|
+
}
|
365
|
+
T1 = safe_add(safe_add(safe_add(
|
366
|
+
safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
|
367
|
+
T2 = safe_add(Sigma0256(a), Maj(a, b, c));
|
368
|
+
h = g; g = f; f = e; e = safe_add(d, T1);
|
369
|
+
d = c; c = b; b = a; a = safe_add(T1, T2);
|
370
|
+
}
|
371
|
+
HASH[0] = safe_add(a, HASH[0]); HASH[1] = safe_add(b, HASH[1]);
|
372
|
+
HASH[2] = safe_add(c, HASH[2]); HASH[3] = safe_add(d, HASH[3]);
|
373
|
+
HASH[4] = safe_add(e, HASH[4]); HASH[5] = safe_add(f, HASH[5]);
|
374
|
+
HASH[6] = safe_add(g, HASH[6]); HASH[7] = safe_add(h, HASH[7]);
|
375
|
+
}
|
376
|
+
return HASH;
|
377
|
+
}
|
378
|
+
function str2binb (str) {
|
379
|
+
var bin = Array();
|
380
|
+
var mask = (1 << chrsz) - 1;
|
381
|
+
for(var i = 0; i < str.length * chrsz; i += chrsz)
|
382
|
+
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
|
383
|
+
return bin;
|
384
|
+
}
|
385
|
+
function binb2hex (binarray) {
|
386
|
+
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
|
387
|
+
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
|
388
|
+
var str = "";
|
389
|
+
for (var i = 0; i < binarray.length * 4; i++) {
|
390
|
+
str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
|
391
|
+
hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
|
392
|
+
}
|
393
|
+
return str;
|
394
|
+
}
|
395
|
+
function hex_sha256(s){
|
396
|
+
return binb2hex(core_sha256(str2binb(s),s.length * chrsz));
|
397
|
+
}
|
398
|
+
module.exports.hex_sha256 = hex_sha256;
|
399
|
+
}());
|
400
|
+
|
401
|
+
},
|
402
|
+
"Common.js": function(module, exports, require){
|
403
|
+
/*
|
404
|
+
* Copyright 2014 XWiki SAS
|
405
|
+
*
|
406
|
+
* This program is free software: you can redistribute it and/or modify
|
407
|
+
* it under the terms of the GNU Affero General Public License as published by
|
408
|
+
* the Free Software Foundation, either version 3 of the License, or
|
409
|
+
* (at your option) any later version.
|
410
|
+
*
|
411
|
+
* This program is distributed in the hope that it will be useful,
|
412
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
413
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
414
|
+
* GNU Affero General Public License for more details.
|
415
|
+
*
|
416
|
+
* You should have received a copy of the GNU Affero General Public License
|
417
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
418
|
+
*/
|
419
|
+
|
420
|
+
var DEBUG = module.exports.debug =
|
421
|
+
(typeof(localStorage) !== 'undefined' && localStorage['ChainPad_DEBUG']);
|
422
|
+
|
423
|
+
var PARANOIA = module.exports.PARANOIA =
|
424
|
+
(typeof(localStorage) !== 'undefined' && localStorage['ChainPad_PARANOIA']);
|
425
|
+
|
426
|
+
/* Good testing but slooooooooooow */
|
427
|
+
var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG =
|
428
|
+
(typeof(localStorage) !== 'undefined' && localStorage['ChainPad_VALIDATE_ENTIRE_CHAIN_EACH_MSG']);
|
429
|
+
|
430
|
+
/* throw errors over non-compliant messages which would otherwise be treated as invalid */
|
431
|
+
var TESTING = module.exports.TESTING =
|
432
|
+
(typeof(localStorage) !== 'undefined' && localStorage['ChainPad_TESTING']);
|
433
|
+
|
434
|
+
var assert = module.exports.assert = function (expr) {
|
435
|
+
if (!expr) { throw new Error("Failed assertion"); }
|
436
|
+
};
|
437
|
+
|
438
|
+
var isUint = module.exports.isUint = function (integer) {
|
439
|
+
return (typeof(integer) === 'number') &&
|
440
|
+
(Math.floor(integer) === integer) &&
|
441
|
+
(integer >= 0);
|
442
|
+
};
|
443
|
+
|
444
|
+
var randomASCII = module.exports.randomASCII = function (length) {
|
445
|
+
var content = [];
|
446
|
+
for (var i = 0; i < length; i++) {
|
447
|
+
content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 );
|
448
|
+
}
|
449
|
+
return content.join('');
|
450
|
+
};
|
451
|
+
|
452
|
+
var strcmp = module.exports.strcmp = function (a, b) {
|
453
|
+
if (PARANOIA && typeof(a) !== 'string') { throw new Error(); }
|
454
|
+
if (PARANOIA && typeof(b) !== 'string') { throw new Error(); }
|
455
|
+
return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) );
|
456
|
+
}
|
457
|
+
|
458
|
+
},
|
459
|
+
"Message.js": function(module, exports, require){
|
460
|
+
/*
|
461
|
+
* Copyright 2014 XWiki SAS
|
462
|
+
*
|
463
|
+
* This program is free software: you can redistribute it and/or modify
|
464
|
+
* it under the terms of the GNU Affero General Public License as published by
|
465
|
+
* the Free Software Foundation, either version 3 of the License, or
|
466
|
+
* (at your option) any later version.
|
467
|
+
*
|
468
|
+
* This program is distributed in the hope that it will be useful,
|
469
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
470
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
471
|
+
* GNU Affero General Public License for more details.
|
472
|
+
*
|
473
|
+
* You should have received a copy of the GNU Affero General Public License
|
474
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
475
|
+
*/
|
476
|
+
var Common = require('./Common');
|
477
|
+
var Operation = require('./Operation');
|
478
|
+
var Patch = require('./Patch');
|
479
|
+
var Sha = require('./SHA256');
|
480
|
+
|
481
|
+
var Message = module.exports;
|
482
|
+
|
483
|
+
var REGISTER = Message.REGISTER = 0;
|
484
|
+
var REGISTER_ACK = Message.REGISTER_ACK = 1;
|
485
|
+
var PATCH = Message.PATCH = 2;
|
486
|
+
var DISCONNECT = Message.DISCONNECT = 3;
|
487
|
+
var CHECKPOINT = Message.CHECKPOINT = 4;
|
488
|
+
|
489
|
+
var check = Message.check = function(msg) {
|
490
|
+
Common.assert(msg.type === 'Message');
|
491
|
+
if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) {
|
492
|
+
Patch.check(msg.content);
|
493
|
+
Common.assert(typeof(msg.lastMsgHash) === 'string');
|
494
|
+
} else {
|
495
|
+
throw new Error("invalid message type [" + msg.messageType + "]");
|
496
|
+
}
|
497
|
+
};
|
498
|
+
|
499
|
+
var create = Message.create = function (type, content, lastMsgHash) {
|
500
|
+
var msg = {
|
501
|
+
type: 'Message',
|
502
|
+
messageType: type,
|
503
|
+
content: content,
|
504
|
+
lastMsgHash: lastMsgHash
|
505
|
+
};
|
506
|
+
if (Common.PARANOIA) { check(msg); }
|
507
|
+
return msg;
|
508
|
+
};
|
509
|
+
|
510
|
+
var toString = Message.toString = function (msg) {
|
511
|
+
if (Common.PARANOIA) { check(msg); }
|
512
|
+
if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) {
|
513
|
+
return JSON.stringify([msg.messageType, Patch.toObj(msg.content), msg.lastMsgHash]);
|
514
|
+
} else {
|
515
|
+
throw new Error();
|
516
|
+
}
|
517
|
+
};
|
518
|
+
|
519
|
+
var discardBencode = function (msg, arr) {
|
520
|
+
var len = msg.substring(0,msg.indexOf(':'));
|
521
|
+
msg = msg.substring(len.length+1);
|
522
|
+
var value = msg.substring(0,Number(len));
|
523
|
+
msg = msg.substring(value.length);
|
524
|
+
|
525
|
+
if (arr) { arr.push(value); }
|
526
|
+
return msg;
|
527
|
+
};
|
528
|
+
|
529
|
+
var fromString = Message.fromString = function (str) {
|
530
|
+
var m = JSON.parse(str);
|
531
|
+
if (m[0] !== CHECKPOINT && m[0] !== PATCH) { throw new Error("invalid message type " + m[0]); }
|
532
|
+
var msg = create(m[0], Patch.fromObj(m[1]), m[2]);
|
533
|
+
if (m[0] === CHECKPOINT) { msg.content.isCheckpoint = true; }
|
534
|
+
return msg;
|
535
|
+
};
|
536
|
+
|
537
|
+
var hashOf = Message.hashOf = function (msg) {
|
538
|
+
if (Common.PARANOIA) { check(msg); }
|
539
|
+
var hash = Sha.hex_sha256(toString(msg));
|
540
|
+
return hash;
|
541
|
+
};
|
542
|
+
|
543
|
+
},
|
544
|
+
"ChainPad.js": function(module, exports, require){
|
545
|
+
/*
|
546
|
+
* Copyright 2014 XWiki SAS
|
547
|
+
*
|
548
|
+
* This program is free software: you can redistribute it and/or modify
|
549
|
+
* it under the terms of the GNU Affero General Public License as published by
|
550
|
+
* the Free Software Foundation, either version 3 of the License, or
|
551
|
+
* (at your option) any later version.
|
552
|
+
*
|
553
|
+
* This program is distributed in the hope that it will be useful,
|
554
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
555
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
556
|
+
* GNU Affero General Public License for more details.
|
557
|
+
*
|
558
|
+
* You should have received a copy of the GNU Affero General Public License
|
559
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
560
|
+
*/
|
561
|
+
var Common = module.exports.Common = require('./Common');
|
562
|
+
var Operation = module.exports.Operation = require('./Operation');
|
563
|
+
var Patch = module.exports.Patch = require('./Patch');
|
564
|
+
var Message = module.exports.Message = require('./Message');
|
565
|
+
var Sha = module.exports.Sha = require('./SHA256');
|
566
|
+
|
567
|
+
var ChainPad = {};
|
568
|
+
|
569
|
+
// hex_sha256('')
|
570
|
+
var EMPTY_STR_HASH = module.exports.EMPTY_STR_HASH =
|
571
|
+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
572
|
+
var ZERO = '0000000000000000000000000000000000000000000000000000000000000000';
|
573
|
+
|
574
|
+
// Default number of patches between checkpoints (patches older than this will be pruned)
|
575
|
+
// default for realtime.config.checkpointInterval
|
576
|
+
var DEFAULT_CHECKPOINT_INTERVAL = 50;
|
577
|
+
|
578
|
+
// Default number of milliseconds to wait before syncing to the server
|
579
|
+
var DEFAULT_AVERAGE_SYNC_MILLISECONDS = 300;
|
580
|
+
|
581
|
+
// By default, we allow checkpoints at any place but if this is set true, we will blow up on chains
|
582
|
+
// which have checkpoints not where we expect them to be.
|
583
|
+
var DEFAULT_STRICT_CHECKPOINT_VALIDATION = false;
|
584
|
+
|
585
|
+
var enterChainPad = function (realtime, func) {
|
586
|
+
return function () {
|
587
|
+
if (realtime.failed) { return; }
|
588
|
+
func.apply(null, arguments);
|
589
|
+
};
|
590
|
+
};
|
591
|
+
|
592
|
+
var debug = function (realtime, msg) {
|
593
|
+
if (realtime.logLevel > 0) {
|
594
|
+
console.log("[" + realtime.userName + "] " + msg);
|
595
|
+
}
|
596
|
+
};
|
597
|
+
|
598
|
+
var warn = function (realtime, msg) {
|
599
|
+
if (realtime.logLevel > 0) {
|
600
|
+
console.error("[" + realtime.userName + "] " + msg);
|
601
|
+
}
|
602
|
+
};
|
603
|
+
|
604
|
+
var schedule = function (realtime, func, timeout) {
|
605
|
+
if (realtime.aborted) { return; }
|
606
|
+
if (!timeout) {
|
607
|
+
timeout = Math.floor(Math.random() * 2 * realtime.config.avgSyncMilliseconds);
|
608
|
+
}
|
609
|
+
var to = setTimeout(enterChainPad(realtime, function () {
|
610
|
+
realtime.schedules.splice(realtime.schedules.indexOf(to), 1);
|
611
|
+
func();
|
612
|
+
}), timeout);
|
613
|
+
realtime.schedules.push(to);
|
614
|
+
return to;
|
615
|
+
};
|
616
|
+
|
617
|
+
var unschedule = function (realtime, schedule) {
|
618
|
+
var index = realtime.schedules.indexOf(schedule);
|
619
|
+
if (index > -1) {
|
620
|
+
realtime.schedules.splice(index, 1);
|
621
|
+
}
|
622
|
+
clearTimeout(schedule);
|
623
|
+
};
|
624
|
+
|
625
|
+
var onMessage = function (realtime, message, callback) {
|
626
|
+
if (!realtime.messageHandlers.length) {
|
627
|
+
callback("no onMessage() handler registered");
|
628
|
+
}
|
629
|
+
for (var i = 0; i < realtime.messageHandlers.length; i++) {
|
630
|
+
realtime.messageHandlers[i](message, function () {
|
631
|
+
callback.apply(null, arguments);
|
632
|
+
callback = function () { };
|
633
|
+
});
|
634
|
+
}
|
635
|
+
};
|
636
|
+
|
637
|
+
var sendMessage = function (realtime, msg, callback) {
|
638
|
+
var strMsg = Message.toString(msg);
|
639
|
+
|
640
|
+
onMessage(realtime, strMsg, function (err) {
|
641
|
+
if (err) {
|
642
|
+
debug(realtime, "Posting to server failed [" + err + "]");
|
643
|
+
realtime.pending = null;
|
644
|
+
} else {
|
645
|
+
var pending = realtime.pending;
|
646
|
+
realtime.pending = null;
|
647
|
+
Common.assert(pending.hash === msg.hashOf);
|
648
|
+
handleMessage(realtime, strMsg, true);
|
649
|
+
pending.callback();
|
650
|
+
}
|
651
|
+
});
|
652
|
+
|
653
|
+
msg.hashOf = msg.hashOf || Message.hashOf(msg);
|
654
|
+
|
655
|
+
var timeout = schedule(realtime, function () {
|
656
|
+
debug(realtime, "Failed to send message [" + msg.hashOf + "] to server");
|
657
|
+
sync(realtime);
|
658
|
+
}, 10000 + (Math.random() * 5000));
|
659
|
+
|
660
|
+
if (realtime.pending) { throw new Error("there is already a pending message"); }
|
661
|
+
realtime.pending = {
|
662
|
+
hash: msg.hashOf,
|
663
|
+
callback: function () {
|
664
|
+
unschedule(realtime, timeout);
|
665
|
+
realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0);
|
666
|
+
callback();
|
667
|
+
}
|
668
|
+
};
|
669
|
+
if (Common.PARANOIA) { check(realtime); }
|
670
|
+
};
|
671
|
+
|
672
|
+
var sync = function (realtime) {
|
673
|
+
if (Common.PARANOIA) { check(realtime); }
|
674
|
+
if (realtime.syncSchedule && !realtime.pending) {
|
675
|
+
unschedule(realtime, realtime.syncSchedule);
|
676
|
+
realtime.syncSchedule = null;
|
677
|
+
} else {
|
678
|
+
//debug(realtime, "already syncing...");
|
679
|
+
// we're currently waiting on something from the server.
|
680
|
+
return;
|
681
|
+
}
|
682
|
+
|
683
|
+
realtime.uncommitted = Patch.simplify(
|
684
|
+
realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify);
|
685
|
+
|
686
|
+
if (realtime.uncommitted.operations.length === 0) {
|
687
|
+
//debug(realtime, "No data to sync to the server, sleeping");
|
688
|
+
realtime.syncSchedule = schedule(realtime, function () { sync(realtime); });
|
689
|
+
return;
|
690
|
+
}
|
691
|
+
|
692
|
+
if (((parentCount(realtime, realtime.best) + 1) % realtime.config.checkpointInterval) === 0) {
|
693
|
+
var best = realtime.best;
|
694
|
+
debug(realtime, "Sending checkpoint");
|
695
|
+
var cpp = Patch.createCheckpoint(realtime.authDoc,
|
696
|
+
realtime.authDoc,
|
697
|
+
realtime.best.content.inverseOf.parentHash);
|
698
|
+
var cp = Message.create(Message.CHECKPOINT, cpp, realtime.best.hashOf);
|
699
|
+
sendMessage(realtime, cp, function () {
|
700
|
+
debug(realtime, "Checkpoint sent and accepted");
|
701
|
+
});
|
702
|
+
return;
|
703
|
+
}
|
704
|
+
|
705
|
+
var msg;
|
706
|
+
if (realtime.setContentPatch) {
|
707
|
+
msg = realtime.setContentPatch;
|
708
|
+
} else {
|
709
|
+
msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf);
|
710
|
+
}
|
711
|
+
|
712
|
+
sendMessage(realtime, msg, function () {
|
713
|
+
//debug(realtime, "patch sent");
|
714
|
+
if (realtime.setContentPatch) {
|
715
|
+
debug(realtime, "initial Ack received [" + msg.hashOf + "]");
|
716
|
+
realtime.setContentPatch = null;
|
717
|
+
}
|
718
|
+
});
|
719
|
+
};
|
720
|
+
|
721
|
+
var storeMessage = function (realtime, msg) {
|
722
|
+
Common.assert(msg.lastMsgHash);
|
723
|
+
Common.assert(msg.hashOf);
|
724
|
+
realtime.messages[msg.hashOf] = msg;
|
725
|
+
(realtime.messagesByParent[msg.lastMsgHash] =
|
726
|
+
realtime.messagesByParent[msg.lastMsgHash] || []).push(msg);
|
727
|
+
};
|
728
|
+
|
729
|
+
var forgetMessage = function (realtime, msg) {
|
730
|
+
Common.assert(msg.lastMsgHash);
|
731
|
+
Common.assert(msg.hashOf);
|
732
|
+
delete realtime.messages[msg.hashOf];
|
733
|
+
var list = realtime.messagesByParent[msg.lastMsgHash];
|
734
|
+
Common.assert(list.indexOf(msg) > -1);
|
735
|
+
list.splice(list.indexOf(msg), 1);
|
736
|
+
if (list.length === 0) {
|
737
|
+
delete realtime.messagesByParent[msg.lastMsgHash];
|
738
|
+
}
|
739
|
+
var children = realtime.messagesByParent[msg.hashOf];
|
740
|
+
if (children) {
|
741
|
+
for (var i = 0; i < children.length; i++) {
|
742
|
+
delete children[i].parent;
|
743
|
+
}
|
744
|
+
}
|
745
|
+
};
|
746
|
+
|
747
|
+
var create = ChainPad.create = function (config) {
|
748
|
+
config = config || {};
|
749
|
+
var initialState = config.initialState || '';
|
750
|
+
config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL;
|
751
|
+
config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS;
|
752
|
+
config.strictCheckpointValidation =
|
753
|
+
config.strictCheckpointValidation || DEFAULT_STRICT_CHECKPOINT_VALIDATION;
|
754
|
+
|
755
|
+
var realtime = {
|
756
|
+
type: 'ChainPad',
|
757
|
+
|
758
|
+
authDoc: '',
|
759
|
+
|
760
|
+
config: config,
|
761
|
+
|
762
|
+
logLevel: (typeof(config.logLevel) === 'number') ? config.logLevel : 1,
|
763
|
+
|
764
|
+
/** A patch representing all uncommitted work. */
|
765
|
+
uncommitted: null,
|
766
|
+
|
767
|
+
uncommittedDocLength: initialState.length,
|
768
|
+
|
769
|
+
patchHandlers: [],
|
770
|
+
changeHandlers: [],
|
771
|
+
|
772
|
+
messageHandlers: [],
|
773
|
+
|
774
|
+
schedules: [],
|
775
|
+
aborted: false,
|
776
|
+
|
777
|
+
syncSchedule: null,
|
778
|
+
|
779
|
+
registered: false,
|
780
|
+
|
781
|
+
// this is only used if PARANOIA is enabled.
|
782
|
+
userInterfaceContent: undefined,
|
783
|
+
|
784
|
+
// If we want to set the content to a particular thing, this patch will be sent across the
|
785
|
+
// wire. If the patch is not accepted we will not try to recover it. This is used for
|
786
|
+
// setting initial state.
|
787
|
+
setContentPatch: null,
|
788
|
+
|
789
|
+
failed: false,
|
790
|
+
|
791
|
+
// hash and callback for previously send patch, currently in flight.
|
792
|
+
pending: null,
|
793
|
+
|
794
|
+
messages: {},
|
795
|
+
messagesByParent: {},
|
796
|
+
|
797
|
+
rootMessage: null,
|
798
|
+
|
799
|
+
onSettle: [],
|
800
|
+
|
801
|
+
userName: config.userName || 'anonymous',
|
802
|
+
};
|
803
|
+
|
804
|
+
var zeroPatch = Patch.create(EMPTY_STR_HASH);
|
805
|
+
zeroPatch.inverseOf = Patch.invert(zeroPatch, '');
|
806
|
+
zeroPatch.inverseOf.inverseOf = zeroPatch;
|
807
|
+
var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO);
|
808
|
+
zeroMsg.hashOf = Message.hashOf(zeroMsg);
|
809
|
+
zeroMsg.parentCount = 0;
|
810
|
+
zeroMsg.isInitialMessage = true;
|
811
|
+
storeMessage(realtime, zeroMsg);
|
812
|
+
realtime.rootMessage = zeroMsg;
|
813
|
+
realtime.best = zeroMsg;
|
814
|
+
|
815
|
+
if (initialState !== '') {
|
816
|
+
var initPatch = Patch.create(EMPTY_STR_HASH);
|
817
|
+
Patch.addOperation(initPatch, Operation.create(0, 0, initialState));
|
818
|
+
initPatch.inverseOf = Patch.invert(initPatch, '');
|
819
|
+
initPatch.inverseOf.inverseOf = initPatch;
|
820
|
+
var initMsg = Message.create(Message.PATCH, initPatch, zeroMsg.hashOf);
|
821
|
+
initMsg.hashOf = Message.hashOf(initMsg);
|
822
|
+
initMsg.isInitialMessage = true;
|
823
|
+
storeMessage(realtime, initMsg);
|
824
|
+
realtime.best = initMsg;
|
825
|
+
realtime.authDoc = initialState;
|
826
|
+
realtime.setContentPatch = initMsg;
|
827
|
+
}
|
828
|
+
realtime.uncommitted = Patch.create(realtime.best.content.inverseOf.parentHash);
|
829
|
+
|
830
|
+
if (Common.PARANOIA) {
|
831
|
+
realtime.userInterfaceContent = initialState;
|
832
|
+
}
|
833
|
+
return realtime;
|
834
|
+
};
|
835
|
+
|
836
|
+
var getParent = function (realtime, message) {
|
837
|
+
return message.parent = message.parent || realtime.messages[message.lastMsgHash];
|
838
|
+
};
|
839
|
+
|
840
|
+
var check = ChainPad.check = function(realtime) {
|
841
|
+
Common.assert(realtime.type === 'ChainPad');
|
842
|
+
Common.assert(typeof(realtime.authDoc) === 'string');
|
843
|
+
|
844
|
+
Patch.check(realtime.uncommitted, realtime.authDoc.length);
|
845
|
+
|
846
|
+
var uiDoc = Patch.apply(realtime.uncommitted, realtime.authDoc);
|
847
|
+
if (uiDoc.length !== realtime.uncommittedDocLength) {
|
848
|
+
Common.assert(0);
|
849
|
+
}
|
850
|
+
if (realtime.userInterfaceContent !== '') {
|
851
|
+
Common.assert(uiDoc === realtime.userInterfaceContent);
|
852
|
+
}
|
853
|
+
|
854
|
+
if (!Common.VALIDATE_ENTIRE_CHAIN_EACH_MSG) { return; }
|
855
|
+
|
856
|
+
var doc = realtime.authDoc;
|
857
|
+
var patchMsg = realtime.best;
|
858
|
+
Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash);
|
859
|
+
var patches = [];
|
860
|
+
do {
|
861
|
+
patches.push(patchMsg);
|
862
|
+
doc = Patch.apply(patchMsg.content.inverseOf, doc);
|
863
|
+
} while ((patchMsg = getParent(realtime, patchMsg)));
|
864
|
+
Common.assert(doc === '');
|
865
|
+
while ((patchMsg = patches.pop())) {
|
866
|
+
doc = Patch.apply(patchMsg.content, doc);
|
867
|
+
}
|
868
|
+
Common.assert(doc === realtime.authDoc);
|
869
|
+
};
|
870
|
+
|
871
|
+
var doOperation = ChainPad.doOperation = function (realtime, op) {
|
872
|
+
if (Common.PARANOIA) {
|
873
|
+
check(realtime);
|
874
|
+
realtime.userInterfaceContent = Operation.apply(op, realtime.userInterfaceContent);
|
875
|
+
}
|
876
|
+
Operation.check(op, realtime.uncommittedDocLength);
|
877
|
+
Patch.addOperation(realtime.uncommitted, op);
|
878
|
+
realtime.uncommittedDocLength += Operation.lengthChange(op);
|
879
|
+
};
|
880
|
+
|
881
|
+
var doPatch = ChainPad.doPatch = function (realtime, patch) {
|
882
|
+
if (Common.PARANOIA) {
|
883
|
+
check(realtime);
|
884
|
+
Common.assert(Patch.invert(realtime.uncommitted).parentHash === patch.parentHash);
|
885
|
+
realtime.userInterfaceContent = Patch.apply(patch, realtime.userInterfaceContent);
|
886
|
+
}
|
887
|
+
Patch.check(patch, realtime.uncommittedDocLength);
|
888
|
+
realtime.uncommitted = Patch.merge(realtime.uncommitted, patch);
|
889
|
+
realtime.uncommittedDocLength += Patch.lengthChange(patch);
|
890
|
+
};
|
891
|
+
|
892
|
+
var isAncestorOf = function (realtime, ancestor, decendent) {
|
893
|
+
if (!decendent || !ancestor) { return false; }
|
894
|
+
if (ancestor === decendent) { return true; }
|
895
|
+
return isAncestorOf(realtime, ancestor, getParent(realtime, decendent));
|
896
|
+
};
|
897
|
+
|
898
|
+
var parentCount = function (realtime, message) {
|
899
|
+
if (typeof(message.parentCount) !== 'number') {
|
900
|
+
message.parentCount = parentCount(realtime, getParent(realtime, message)) + 1;
|
901
|
+
}
|
902
|
+
return message.parentCount;
|
903
|
+
};
|
904
|
+
|
905
|
+
var applyPatch = function (realtime, isFromMe, patch) {
|
906
|
+
Common.assert(patch);
|
907
|
+
Common.assert(patch.inverseOf);
|
908
|
+
if (isFromMe) {
|
909
|
+
// Case 1: We're applying a patch which we originally created (yay our work was accepted)
|
910
|
+
// We will merge the inverse of the patch with our uncommitted work in order that
|
911
|
+
// we do not try to commit that work over again.
|
912
|
+
// Case 2: We're reverting a patch which had originally come from us, a.k.a. we're applying
|
913
|
+
// the inverse of that patch.
|
914
|
+
//
|
915
|
+
// In either scenario, we want to apply the inverse of the patch we are applying, to the
|
916
|
+
// uncommitted work. Whatever we "add" to the authDoc we "remove" from the uncommittedWork.
|
917
|
+
//
|
918
|
+
Common.assert(patch.parentHash === realtime.uncommitted.parentHash);
|
919
|
+
realtime.uncommitted = Patch.merge(patch.inverseOf, realtime.uncommitted);
|
920
|
+
|
921
|
+
} else {
|
922
|
+
// It's someone else's patch which was received, we need to *transform* out uncommitted
|
923
|
+
// work over their patch in order to preserve intent as much as possible.
|
924
|
+
realtime.uncommitted =
|
925
|
+
Patch.transform(
|
926
|
+
realtime.uncommitted, patch, realtime.authDoc, realtime.config.transformFunction);
|
927
|
+
}
|
928
|
+
realtime.uncommitted.parentHash = patch.inverseOf.parentHash;
|
929
|
+
|
930
|
+
realtime.authDoc = Patch.apply(patch, realtime.authDoc);
|
931
|
+
|
932
|
+
if (Common.PARANOIA) {
|
933
|
+
Common.assert(realtime.uncommitted.parentHash === patch.inverseOf.parentHash);
|
934
|
+
Common.assert(Sha.hex_sha256(realtime.authDoc) === realtime.uncommitted.parentHash);
|
935
|
+
realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc);
|
936
|
+
}
|
937
|
+
};
|
938
|
+
|
939
|
+
var revertPatch = function (realtime, isFromMe, patch) {
|
940
|
+
applyPatch(realtime, isFromMe, patch.inverseOf);
|
941
|
+
};
|
942
|
+
|
943
|
+
var getBestChild = function (realtime, msg) {
|
944
|
+
var best = msg;
|
945
|
+
(realtime.messagesByParent[msg.hashOf] || []).forEach(function (child) {
|
946
|
+
Common.assert(child.lastMsgHash === msg.hashOf);
|
947
|
+
child = getBestChild(realtime, child);
|
948
|
+
if (parentCount(realtime, child) > parentCount(realtime, best)) { best = child; }
|
949
|
+
});
|
950
|
+
return best;
|
951
|
+
};
|
952
|
+
|
953
|
+
var pushUIPatch = function (realtime, patch) {
|
954
|
+
if (patch.operations.length) {
|
955
|
+
// push the uncommittedPatch out to the user interface.
|
956
|
+
for (var i = 0; i < realtime.patchHandlers.length; i++) {
|
957
|
+
realtime.patchHandlers[i](patch);
|
958
|
+
}
|
959
|
+
for (var i = 0; i < realtime.changeHandlers.length; i++) {
|
960
|
+
for (var j = patch.operations.length - 1; j >= 0; j--) {
|
961
|
+
var op = patch.operations[j];
|
962
|
+
realtime.changeHandlers[i](op.offset, op.toRemove, op.toInsert);
|
963
|
+
}
|
964
|
+
}
|
965
|
+
}
|
966
|
+
};
|
967
|
+
|
968
|
+
var validContent = function (realtime, contentGetter) {
|
969
|
+
if (!realtime.config.validateContent) { return true; }
|
970
|
+
try {
|
971
|
+
return realtime.config.validateContent(contentGetter());
|
972
|
+
} catch (e) {
|
973
|
+
warn(realtime, "Error in content validator [" + e.stack + "]");
|
974
|
+
}
|
975
|
+
return false;
|
976
|
+
};
|
977
|
+
|
978
|
+
var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) {
|
979
|
+
|
980
|
+
if (Common.PARANOIA) { check(realtime); }
|
981
|
+
var msg = Message.fromString(msgStr);
|
982
|
+
|
983
|
+
if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) {
|
984
|
+
debug(realtime, "unrecognized message type " + msg.messageType);
|
985
|
+
return;
|
986
|
+
}
|
987
|
+
|
988
|
+
msg.hashOf = Message.hashOf(msg);
|
989
|
+
|
990
|
+
if (Common.DEBUG) { debug(realtime, JSON.stringify([msg.hashOf, msg.content.operations])); }
|
991
|
+
|
992
|
+
if (realtime.messages[msg.hashOf]) {
|
993
|
+
if (realtime.setContentPatch && realtime.setContentPatch.hashOf === msg.hashOf) {
|
994
|
+
// We got the initial state patch, channel already has a pad, no need to send it.
|
995
|
+
realtime.setContentPatch = null;
|
996
|
+
} else {
|
997
|
+
debug(realtime, "Patch [" + msg.hashOf + "] is already known");
|
998
|
+
}
|
999
|
+
if (Common.PARANOIA) { check(realtime); }
|
1000
|
+
return;
|
1001
|
+
}
|
1002
|
+
|
1003
|
+
if (msg.content.isCheckpoint &&
|
1004
|
+
!validContent(realtime, function () { return msg.content.operations[0].toInsert }))
|
1005
|
+
{
|
1006
|
+
// If it's not a checkpoint, we verify it later on...
|
1007
|
+
debug(realtime, "Checkpoint [" + msg.hashOf + "] failed content validation");
|
1008
|
+
return;
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
storeMessage(realtime, msg);
|
1012
|
+
|
1013
|
+
if (!isAncestorOf(realtime, realtime.rootMessage, msg)) {
|
1014
|
+
if (msg.content.isCheckpoint && realtime.best.isInitialMessage) {
|
1015
|
+
// We're starting with a trucated chain from a checkpoint, we will adopt this
|
1016
|
+
// as the root message and go with it...
|
1017
|
+
var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc);
|
1018
|
+
Common.assert(!Common.PARANOIA || realtime.userInterfaceContent === userDoc);
|
1019
|
+
var fixUserDocPatch = Patch.invert(realtime.uncommitted, realtime.authDoc);
|
1020
|
+
Patch.addOperation(fixUserDocPatch,
|
1021
|
+
Operation.create(0, realtime.authDoc.length, msg.content.operations[0].toInsert));
|
1022
|
+
fixUserDocPatch =
|
1023
|
+
Patch.simplify(fixUserDocPatch, userDoc, realtime.config.operationSimplify);
|
1024
|
+
|
1025
|
+
msg.parentCount = 0;
|
1026
|
+
realtime.rootMessage = realtime.best = msg;
|
1027
|
+
|
1028
|
+
realtime.authDoc = msg.content.operations[0].toInsert;
|
1029
|
+
realtime.uncommitted = Patch.create(Sha.hex_sha256(realtime.authDoc));
|
1030
|
+
realtime.uncommittedDocLength = realtime.authDoc.length;
|
1031
|
+
pushUIPatch(realtime, fixUserDocPatch);
|
1032
|
+
|
1033
|
+
if (Common.PARANOIA) { realtime.userInterfaceContent = realtime.authDoc; }
|
1034
|
+
return;
|
1035
|
+
} else {
|
1036
|
+
// we'll probably find the missing parent later.
|
1037
|
+
debug(realtime, "Patch [" + msg.hashOf + "] not connected to root");
|
1038
|
+
if (Common.PARANOIA) { check(realtime); }
|
1039
|
+
return;
|
1040
|
+
}
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
// of this message fills in a hole in the chain which makes another patch better, swap to the
|
1044
|
+
// best child of this patch since longest chain always wins.
|
1045
|
+
msg = getBestChild(realtime, msg);
|
1046
|
+
msg.isFromMe = isFromMe;
|
1047
|
+
var patch = msg.content;
|
1048
|
+
|
1049
|
+
// Find the ancestor of this patch which is in the main chain, reverting as necessary
|
1050
|
+
var toRevert = [];
|
1051
|
+
var commonAncestor = realtime.best;
|
1052
|
+
if (!isAncestorOf(realtime, realtime.best, msg)) {
|
1053
|
+
var pcBest = parentCount(realtime, realtime.best);
|
1054
|
+
var pcMsg = parentCount(realtime, msg);
|
1055
|
+
if (pcBest < pcMsg
|
1056
|
+
|| (pcBest === pcMsg
|
1057
|
+
&& Common.strcmp(realtime.best.hashOf, msg.hashOf) > 0))
|
1058
|
+
{
|
1059
|
+
// switch chains
|
1060
|
+
while (commonAncestor && !isAncestorOf(realtime, commonAncestor, msg)) {
|
1061
|
+
toRevert.push(commonAncestor);
|
1062
|
+
commonAncestor = getParent(realtime, commonAncestor);
|
1063
|
+
}
|
1064
|
+
Common.assert(commonAncestor);
|
1065
|
+
debug(realtime, "Patch [" + msg.hashOf + "] better than best chain, switching");
|
1066
|
+
} else {
|
1067
|
+
debug(realtime, "Patch [" + msg.hashOf + "] chain is [" + pcMsg + "] best chain is [" +
|
1068
|
+
pcBest + "]");
|
1069
|
+
if (Common.PARANOIA) { check(realtime); }
|
1070
|
+
return;
|
1071
|
+
}
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
// Find the parents of this patch which are not in the main chain.
|
1075
|
+
var toApply = [];
|
1076
|
+
var current = msg;
|
1077
|
+
do {
|
1078
|
+
toApply.unshift(current);
|
1079
|
+
current = getParent(realtime, current);
|
1080
|
+
Common.assert(current);
|
1081
|
+
} while (current !== commonAncestor);
|
1082
|
+
|
1083
|
+
|
1084
|
+
var authDocAtTimeOfPatch = realtime.authDoc;
|
1085
|
+
|
1086
|
+
for (var i = 0; i < toRevert.length; i++) {
|
1087
|
+
Common.assert(typeof(toRevert[i].content.inverseOf) !== 'undefined');
|
1088
|
+
authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch);
|
1089
|
+
}
|
1090
|
+
|
1091
|
+
// toApply.length-1 because we do not want to apply the new patch.
|
1092
|
+
for (var i = 0; i < toApply.length-1; i++) {
|
1093
|
+
if (typeof(toApply[i].content.inverseOf) === 'undefined') {
|
1094
|
+
toApply[i].content.inverseOf = Patch.invert(toApply[i].content, authDocAtTimeOfPatch);
|
1095
|
+
toApply[i].content.inverseOf.inverseOf = toApply[i].content;
|
1096
|
+
}
|
1097
|
+
authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch);
|
1098
|
+
}
|
1099
|
+
|
1100
|
+
if (Sha.hex_sha256(authDocAtTimeOfPatch) !== patch.parentHash) {
|
1101
|
+
debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid");
|
1102
|
+
if (Common.PARANOIA) { check(realtime); }
|
1103
|
+
if (Common.TESTING) { throw new Error(); }
|
1104
|
+
forgetMessage(realtime, msg);
|
1105
|
+
return;
|
1106
|
+
}
|
1107
|
+
|
1108
|
+
if (patch.isCheckpoint) {
|
1109
|
+
// Ok, we have a checkpoint patch.
|
1110
|
+
// If the chain length is not equal to checkpointInterval then this patch is invalid.
|
1111
|
+
var i = 0;
|
1112
|
+
var checkpointP;
|
1113
|
+
for (var m = getParent(realtime, msg); m; m = getParent(realtime, m)) {
|
1114
|
+
if (m.content.isCheckpoint) {
|
1115
|
+
if (checkpointP) {
|
1116
|
+
checkpointP = m;
|
1117
|
+
break;
|
1118
|
+
}
|
1119
|
+
checkpointP = m;
|
1120
|
+
}
|
1121
|
+
}
|
1122
|
+
if (checkpointP && checkpointP !== realtime.rootMessage) {
|
1123
|
+
var point = parentCount(realtime, checkpointP);
|
1124
|
+
if (realtime.config.strictCheckpointValidation &&
|
1125
|
+
(point % realtime.config.checkpointInterval) !== 0)
|
1126
|
+
{
|
1127
|
+
debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]");
|
1128
|
+
if (Common.PARANOIA) { check(realtime); }
|
1129
|
+
if (Common.TESTING) { throw new Error(); }
|
1130
|
+
forgetMessage(realtime, msg);
|
1131
|
+
return;
|
1132
|
+
}
|
1133
|
+
|
1134
|
+
// Time to prune some old messages from the chain
|
1135
|
+
debug(realtime, "checkpoint [" + msg.hashOf + "]");
|
1136
|
+
for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) {
|
1137
|
+
debug(realtime, "pruning [" + m.hashOf + "]");
|
1138
|
+
forgetMessage(realtime, m);
|
1139
|
+
}
|
1140
|
+
realtime.rootMessage = checkpointP;
|
1141
|
+
}
|
1142
|
+
} else {
|
1143
|
+
var simplePatch =
|
1144
|
+
Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify);
|
1145
|
+
if (!Patch.equals(simplePatch, patch)) {
|
1146
|
+
debug(realtime, "patch [" + msg.hashOf + "] can be simplified");
|
1147
|
+
if (Common.PARANOIA) { check(realtime); }
|
1148
|
+
if (Common.TESTING) { throw new Error(); }
|
1149
|
+
forgetMessage(realtime, msg);
|
1150
|
+
return;
|
1151
|
+
}
|
1152
|
+
|
1153
|
+
if (!validContent(realtime,
|
1154
|
+
function () { return Patch.apply(patch, authDocAtTimeOfPatch); }))
|
1155
|
+
{
|
1156
|
+
debug(realtime, "Patch [" + msg.hashOf + "] failed content validation");
|
1157
|
+
return;
|
1158
|
+
}
|
1159
|
+
}
|
1160
|
+
|
1161
|
+
patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch);
|
1162
|
+
patch.inverseOf.inverseOf = patch;
|
1163
|
+
|
1164
|
+
realtime.uncommitted = Patch.simplify(
|
1165
|
+
realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify);
|
1166
|
+
var oldUserInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc);
|
1167
|
+
if (Common.PARANOIA) {
|
1168
|
+
Common.assert(oldUserInterfaceContent === realtime.userInterfaceContent);
|
1169
|
+
}
|
1170
|
+
|
1171
|
+
// Derive the patch for the user's uncommitted work
|
1172
|
+
var uncommittedPatch = Patch.invert(realtime.uncommitted, realtime.authDoc);
|
1173
|
+
|
1174
|
+
for (var i = 0; i < toRevert.length; i++) {
|
1175
|
+
debug(realtime, "reverting [" + toRevert[i].hashOf + "]");
|
1176
|
+
if (toRevert[i].isFromMe) { debug(realtime, "reverting patch 'from me' [" + JSON.stringify(toRevert[i].content.operations) + "]"); }
|
1177
|
+
uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf);
|
1178
|
+
revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content);
|
1179
|
+
}
|
1180
|
+
|
1181
|
+
for (var i = 0; i < toApply.length; i++) {
|
1182
|
+
debug(realtime, "applying [" + toApply[i].hashOf + "]");
|
1183
|
+
uncommittedPatch = Patch.merge(uncommittedPatch, toApply[i].content);
|
1184
|
+
applyPatch(realtime, toApply[i].isFromMe, toApply[i].content);
|
1185
|
+
}
|
1186
|
+
|
1187
|
+
uncommittedPatch = Patch.merge(uncommittedPatch, realtime.uncommitted);
|
1188
|
+
uncommittedPatch = Patch.simplify(
|
1189
|
+
uncommittedPatch, oldUserInterfaceContent, realtime.config.operationSimplify);
|
1190
|
+
|
1191
|
+
realtime.uncommittedDocLength += Patch.lengthChange(uncommittedPatch);
|
1192
|
+
realtime.best = msg;
|
1193
|
+
|
1194
|
+
if (Common.PARANOIA) {
|
1195
|
+
// apply the uncommittedPatch to the userInterface content.
|
1196
|
+
var newUserInterfaceContent = Patch.apply(uncommittedPatch, oldUserInterfaceContent);
|
1197
|
+
Common.assert(realtime.userInterfaceContent.length === realtime.uncommittedDocLength);
|
1198
|
+
Common.assert(newUserInterfaceContent === realtime.userInterfaceContent);
|
1199
|
+
}
|
1200
|
+
|
1201
|
+
pushUIPatch(realtime, uncommittedPatch);
|
1202
|
+
|
1203
|
+
if (!uncommittedPatch.operations.length) {
|
1204
|
+
var onSettle = realtime.onSettle;
|
1205
|
+
realtime.onSettle = [];
|
1206
|
+
onSettle.forEach(function (handler) { handler(); });
|
1207
|
+
}
|
1208
|
+
|
1209
|
+
if (Common.PARANOIA) { check(realtime); }
|
1210
|
+
};
|
1211
|
+
|
1212
|
+
var getDepthOfState = function (content, minDepth, realtime) {
|
1213
|
+
Common.assert(typeof(content) === 'string');
|
1214
|
+
|
1215
|
+
// minimum depth is an optional argument which defaults to zero
|
1216
|
+
var minDepth = minDepth || 0;
|
1217
|
+
|
1218
|
+
if (minDepth === 0 && realtime.authDoc === content) {
|
1219
|
+
return 0;
|
1220
|
+
}
|
1221
|
+
|
1222
|
+
var hash = Sha.hex_sha256(content);
|
1223
|
+
|
1224
|
+
var patchMsg = realtime.best;
|
1225
|
+
var depth = 0;
|
1226
|
+
|
1227
|
+
do {
|
1228
|
+
if (depth < minDepth) {
|
1229
|
+
// you haven't exceeded the minimum depth
|
1230
|
+
} else {
|
1231
|
+
//console.log("Exceeded minimum depth");
|
1232
|
+
// you *have* exceeded the minimum depth
|
1233
|
+
if (patchMsg.content.parentHash === hash) {
|
1234
|
+
// you found it!
|
1235
|
+
return depth + 1;
|
1236
|
+
}
|
1237
|
+
}
|
1238
|
+
depth++;
|
1239
|
+
} while ((patchMsg = getParent(realtime, patchMsg)));
|
1240
|
+
return -1;
|
1241
|
+
};
|
1242
|
+
|
1243
|
+
module.exports.create = function (conf) {
|
1244
|
+
var realtime = ChainPad.create(conf);
|
1245
|
+
var out = {
|
1246
|
+
onPatch: enterChainPad(realtime, function (handler) {
|
1247
|
+
Common.assert(typeof(handler) === 'function');
|
1248
|
+
realtime.patchHandlers.push(handler);
|
1249
|
+
}),
|
1250
|
+
patch: enterChainPad(realtime, function (patch, x, y) {
|
1251
|
+
if (typeof(patch) === 'number') {
|
1252
|
+
// Actually they meant to call realtime.change()
|
1253
|
+
out.change(patch, x, y);
|
1254
|
+
return;
|
1255
|
+
}
|
1256
|
+
doPatch(realtime, patch);
|
1257
|
+
}),
|
1258
|
+
|
1259
|
+
onChange: enterChainPad(realtime, function (handler) {
|
1260
|
+
Common.assert(typeof(handler) === 'function');
|
1261
|
+
realtime.changeHandlers.push(handler);
|
1262
|
+
}),
|
1263
|
+
change: enterChainPad(realtime, function (offset, count, chars) {
|
1264
|
+
if (count === 0 && chars === '') { return; }
|
1265
|
+
doOperation(realtime, Operation.create(offset, count, chars));
|
1266
|
+
}),
|
1267
|
+
|
1268
|
+
onMessage: enterChainPad(realtime, function (handler) {
|
1269
|
+
Common.assert(typeof(handler) === 'function');
|
1270
|
+
realtime.messageHandlers.push(handler);
|
1271
|
+
}),
|
1272
|
+
|
1273
|
+
message: enterChainPad(realtime, function (message) {
|
1274
|
+
handleMessage(realtime, message, false);
|
1275
|
+
}),
|
1276
|
+
|
1277
|
+
start: enterChainPad(realtime, function () {
|
1278
|
+
if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); }
|
1279
|
+
realtime.syncSchedule = schedule(realtime, function () { sync(realtime); });
|
1280
|
+
}),
|
1281
|
+
|
1282
|
+
abort: enterChainPad(realtime, function () {
|
1283
|
+
realtime.aborted = true;
|
1284
|
+
realtime.schedules.forEach(function (s) { clearTimeout(s) });
|
1285
|
+
}),
|
1286
|
+
|
1287
|
+
sync: enterChainPad(realtime, function () { sync(realtime); }),
|
1288
|
+
|
1289
|
+
getAuthDoc: function () { return realtime.authDoc; },
|
1290
|
+
|
1291
|
+
getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); },
|
1292
|
+
|
1293
|
+
getDepthOfState: function (content, minDepth) {
|
1294
|
+
return getDepthOfState(content, minDepth, realtime);
|
1295
|
+
},
|
1296
|
+
|
1297
|
+
onSettle: function (handler) {
|
1298
|
+
realtime.onSettle.push(handler);
|
1299
|
+
},
|
1300
|
+
};
|
1301
|
+
return out;
|
1302
|
+
};
|
1303
|
+
|
1304
|
+
},
|
1305
|
+
"Operation.js": function(module, exports, require){
|
1306
|
+
/*
|
1307
|
+
* Copyright 2014 XWiki SAS
|
1308
|
+
*
|
1309
|
+
* This program is free software: you can redistribute it and/or modify
|
1310
|
+
* it under the terms of the GNU Affero General Public License as published by
|
1311
|
+
* the Free Software Foundation, either version 3 of the License, or
|
1312
|
+
* (at your option) any later version.
|
1313
|
+
*
|
1314
|
+
* This program is distributed in the hope that it will be useful,
|
1315
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
1316
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
1317
|
+
* GNU Affero General Public License for more details.
|
1318
|
+
*
|
1319
|
+
* You should have received a copy of the GNU Affero General Public License
|
1320
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
1321
|
+
*/
|
1322
|
+
var Common = require('./Common');
|
1323
|
+
|
1324
|
+
var Operation = module.exports;
|
1325
|
+
|
1326
|
+
var check = Operation.check = function (op, docLength_opt) {
|
1327
|
+
Common.assert(op.type === 'Operation');
|
1328
|
+
Common.assert(Common.isUint(op.offset));
|
1329
|
+
Common.assert(Common.isUint(op.toRemove));
|
1330
|
+
Common.assert(typeof(op.toInsert) === 'string');
|
1331
|
+
Common.assert(op.toRemove > 0 || op.toInsert.length > 0);
|
1332
|
+
Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt);
|
1333
|
+
};
|
1334
|
+
|
1335
|
+
var create = Operation.create = function (offset, toRemove, toInsert) {
|
1336
|
+
var out = {
|
1337
|
+
type: 'Operation',
|
1338
|
+
offset: offset || 0,
|
1339
|
+
toRemove: toRemove || 0,
|
1340
|
+
toInsert: toInsert || '',
|
1341
|
+
};
|
1342
|
+
if (Common.PARANOIA) { check(out); }
|
1343
|
+
return out;
|
1344
|
+
};
|
1345
|
+
|
1346
|
+
var toObj = Operation.toObj = function (op) {
|
1347
|
+
if (Common.PARANOIA) { check(op); }
|
1348
|
+
return [op.offset,op.toRemove,op.toInsert];
|
1349
|
+
};
|
1350
|
+
|
1351
|
+
var fromObj = Operation.fromObj = function (obj) {
|
1352
|
+
Common.assert(Array.isArray(obj) && obj.length === 3);
|
1353
|
+
return create(obj[0], obj[1], obj[2]);
|
1354
|
+
};
|
1355
|
+
|
1356
|
+
var clone = Operation.clone = function (op) {
|
1357
|
+
return create(op.offset, op.toRemove, op.toInsert);
|
1358
|
+
};
|
1359
|
+
|
1360
|
+
/**
|
1361
|
+
* @param op the operation to apply.
|
1362
|
+
* @param doc the content to apply the operation on
|
1363
|
+
*/
|
1364
|
+
var apply = Operation.apply = function (op, doc)
|
1365
|
+
{
|
1366
|
+
if (Common.PARANOIA) {
|
1367
|
+
check(op);
|
1368
|
+
Common.assert(typeof(doc) === 'string');
|
1369
|
+
Common.assert(op.offset + op.toRemove <= doc.length);
|
1370
|
+
}
|
1371
|
+
return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove);
|
1372
|
+
};
|
1373
|
+
|
1374
|
+
var invert = Operation.invert = function (op, doc) {
|
1375
|
+
if (Common.PARANOIA) {
|
1376
|
+
check(op);
|
1377
|
+
Common.assert(typeof(doc) === 'string');
|
1378
|
+
Common.assert(op.offset + op.toRemove <= doc.length);
|
1379
|
+
}
|
1380
|
+
var rop = clone(op);
|
1381
|
+
rop.toInsert = doc.substring(op.offset, op.offset + op.toRemove);
|
1382
|
+
rop.toRemove = op.toInsert.length;
|
1383
|
+
return rop;
|
1384
|
+
};
|
1385
|
+
|
1386
|
+
var simplify = Operation.simplify = function (op, doc) {
|
1387
|
+
if (Common.PARANOIA) {
|
1388
|
+
check(op);
|
1389
|
+
Common.assert(typeof(doc) === 'string');
|
1390
|
+
Common.assert(op.offset + op.toRemove <= doc.length);
|
1391
|
+
}
|
1392
|
+
var rop = invert(op, doc);
|
1393
|
+
op = clone(op);
|
1394
|
+
|
1395
|
+
var minLen = Math.min(op.toInsert.length, rop.toInsert.length);
|
1396
|
+
var i;
|
1397
|
+
for (i = 0; i < minLen && rop.toInsert[i] === op.toInsert[i]; i++) ;
|
1398
|
+
op.offset += i;
|
1399
|
+
op.toRemove -= i;
|
1400
|
+
op.toInsert = op.toInsert.substring(i);
|
1401
|
+
rop.toInsert = rop.toInsert.substring(i);
|
1402
|
+
|
1403
|
+
if (rop.toInsert.length === op.toInsert.length) {
|
1404
|
+
for (i = rop.toInsert.length-1; i >= 0 && rop.toInsert[i] === op.toInsert[i]; i--) ;
|
1405
|
+
op.toInsert = op.toInsert.substring(0, i+1);
|
1406
|
+
op.toRemove = i+1;
|
1407
|
+
}
|
1408
|
+
|
1409
|
+
if (op.toRemove === 0 && op.toInsert.length === 0) { return null; }
|
1410
|
+
return op;
|
1411
|
+
};
|
1412
|
+
|
1413
|
+
var equals = Operation.equals = function (opA, opB) {
|
1414
|
+
return (opA.toRemove === opB.toRemove
|
1415
|
+
&& opA.toInsert === opB.toInsert
|
1416
|
+
&& opA.offset === opB.offset);
|
1417
|
+
};
|
1418
|
+
|
1419
|
+
var lengthChange = Operation.lengthChange = function (op)
|
1420
|
+
{
|
1421
|
+
if (Common.PARANOIA) { check(op); }
|
1422
|
+
return op.toInsert.length - op.toRemove;
|
1423
|
+
};
|
1424
|
+
|
1425
|
+
/*
|
1426
|
+
* @return the merged operation OR null if the result of the merger is a noop.
|
1427
|
+
*/
|
1428
|
+
var merge = Operation.merge = function (oldOpOrig, newOpOrig) {
|
1429
|
+
if (Common.PARANOIA) {
|
1430
|
+
check(newOpOrig);
|
1431
|
+
check(oldOpOrig);
|
1432
|
+
}
|
1433
|
+
|
1434
|
+
var newOp = clone(newOpOrig);
|
1435
|
+
var oldOp = clone(oldOpOrig);
|
1436
|
+
var offsetDiff = newOp.offset - oldOp.offset;
|
1437
|
+
|
1438
|
+
if (newOp.toRemove > 0) {
|
1439
|
+
var origOldInsert = oldOp.toInsert;
|
1440
|
+
oldOp.toInsert = (
|
1441
|
+
oldOp.toInsert.substring(0,offsetDiff)
|
1442
|
+
+ oldOp.toInsert.substring(offsetDiff + newOp.toRemove)
|
1443
|
+
);
|
1444
|
+
newOp.toRemove -= (origOldInsert.length - oldOp.toInsert.length);
|
1445
|
+
if (newOp.toRemove < 0) { newOp.toRemove = 0; }
|
1446
|
+
|
1447
|
+
oldOp.toRemove += newOp.toRemove;
|
1448
|
+
newOp.toRemove = 0;
|
1449
|
+
}
|
1450
|
+
|
1451
|
+
if (offsetDiff < 0) {
|
1452
|
+
oldOp.offset += offsetDiff;
|
1453
|
+
oldOp.toInsert = newOp.toInsert + oldOp.toInsert;
|
1454
|
+
|
1455
|
+
} else if (oldOp.toInsert.length === offsetDiff) {
|
1456
|
+
oldOp.toInsert = oldOp.toInsert + newOp.toInsert;
|
1457
|
+
|
1458
|
+
} else if (oldOp.toInsert.length > offsetDiff) {
|
1459
|
+
oldOp.toInsert = (
|
1460
|
+
oldOp.toInsert.substring(0,offsetDiff)
|
1461
|
+
+ newOp.toInsert
|
1462
|
+
+ oldOp.toInsert.substring(offsetDiff)
|
1463
|
+
);
|
1464
|
+
} else {
|
1465
|
+
throw new Error("should never happen\n" +
|
1466
|
+
JSON.stringify([oldOpOrig,newOpOrig], null, ' '));
|
1467
|
+
}
|
1468
|
+
|
1469
|
+
if (oldOp.toInsert === '' && oldOp.toRemove === 0) {
|
1470
|
+
return null;
|
1471
|
+
}
|
1472
|
+
if (Common.PARANOIA) { check(oldOp); }
|
1473
|
+
|
1474
|
+
return oldOp;
|
1475
|
+
};
|
1476
|
+
|
1477
|
+
/**
|
1478
|
+
* If the new operation deletes what the old op inserted or inserts content in the middle of
|
1479
|
+
* the old op's content or if they abbut one another, they should be merged.
|
1480
|
+
*/
|
1481
|
+
var shouldMerge = Operation.shouldMerge = function (oldOp, newOp) {
|
1482
|
+
if (Common.PARANOIA) {
|
1483
|
+
check(oldOp);
|
1484
|
+
check(newOp);
|
1485
|
+
}
|
1486
|
+
if (newOp.offset < oldOp.offset) {
|
1487
|
+
return (oldOp.offset <= (newOp.offset + newOp.toRemove));
|
1488
|
+
} else {
|
1489
|
+
return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length));
|
1490
|
+
}
|
1491
|
+
};
|
1492
|
+
|
1493
|
+
/**
|
1494
|
+
* Rebase newOp against oldOp.
|
1495
|
+
*
|
1496
|
+
* @param oldOp the eariler operation to have happened.
|
1497
|
+
* @param newOp the later operation to have happened (in time).
|
1498
|
+
* @return either the untouched newOp if it need not be rebased,
|
1499
|
+
* the rebased clone of newOp if it needs rebasing, or
|
1500
|
+
* null if newOp and oldOp must be merged.
|
1501
|
+
*/
|
1502
|
+
var rebase = Operation.rebase = function (oldOp, newOp) {
|
1503
|
+
if (Common.PARANOIA) {
|
1504
|
+
check(oldOp);
|
1505
|
+
check(newOp);
|
1506
|
+
}
|
1507
|
+
if (newOp.offset < oldOp.offset) { return newOp; }
|
1508
|
+
newOp = clone(newOp);
|
1509
|
+
newOp.offset += oldOp.toRemove;
|
1510
|
+
newOp.offset -= oldOp.toInsert.length;
|
1511
|
+
return newOp;
|
1512
|
+
};
|
1513
|
+
|
1514
|
+
/**
|
1515
|
+
* this is a lossy and dirty algorithm, everything else is nice but transformation
|
1516
|
+
* has to be lossy because both operations have the same base and they diverge.
|
1517
|
+
* This could be made nicer and/or tailored to a specific data type.
|
1518
|
+
*
|
1519
|
+
* @param toTransform the operation which is converted *MUTATED*.
|
1520
|
+
* @param transformBy an existing operation which also has the same base.
|
1521
|
+
* @return toTransform *or* null if the result is a no-op.
|
1522
|
+
*/
|
1523
|
+
|
1524
|
+
var transform0 = Operation.transform0 = function (text, toTransformOrig, transformByOrig) {
|
1525
|
+
// Cloning the original transformations makes this algorithm such that it
|
1526
|
+
// **DOES NOT MUTATE ANYMORE**
|
1527
|
+
var toTransform = Operation.clone(toTransformOrig);
|
1528
|
+
var transformBy = Operation.clone(transformByOrig);
|
1529
|
+
|
1530
|
+
if (toTransform.offset > transformBy.offset) {
|
1531
|
+
if (toTransform.offset > transformBy.offset + transformBy.toRemove) {
|
1532
|
+
// simple rebase
|
1533
|
+
toTransform.offset -= transformBy.toRemove;
|
1534
|
+
toTransform.offset += transformBy.toInsert.length;
|
1535
|
+
return toTransform;
|
1536
|
+
}
|
1537
|
+
// goto the end, anything you deleted that they also deleted should be skipped.
|
1538
|
+
var newOffset = transformBy.offset + transformBy.toInsert.length;
|
1539
|
+
toTransform.toRemove = 0; //-= (newOffset - toTransform.offset);
|
1540
|
+
if (toTransform.toRemove < 0) { toTransform.toRemove = 0; }
|
1541
|
+
toTransform.offset = newOffset;
|
1542
|
+
if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) {
|
1543
|
+
return null;
|
1544
|
+
}
|
1545
|
+
return toTransform;
|
1546
|
+
}
|
1547
|
+
if (toTransform.offset + toTransform.toRemove < transformBy.offset) {
|
1548
|
+
return toTransform;
|
1549
|
+
}
|
1550
|
+
toTransform.toRemove = transformBy.offset - toTransform.offset;
|
1551
|
+
if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) {
|
1552
|
+
return null;
|
1553
|
+
}
|
1554
|
+
return toTransform;
|
1555
|
+
};
|
1556
|
+
|
1557
|
+
/**
|
1558
|
+
* @param toTransform the operation which is converted
|
1559
|
+
* @param transformBy an existing operation which also has the same base.
|
1560
|
+
* @return a modified clone of toTransform *or* toTransform itself if no change was made.
|
1561
|
+
*/
|
1562
|
+
var transform = Operation.transform = function (text, toTransform, transformBy, transformFunction) {
|
1563
|
+
if (Common.PARANOIA) {
|
1564
|
+
check(toTransform);
|
1565
|
+
check(transformBy);
|
1566
|
+
}
|
1567
|
+
transformFunction = transformFunction || transform0;
|
1568
|
+
toTransform = clone(toTransform);
|
1569
|
+
var result = transformFunction(text, toTransform, transformBy);
|
1570
|
+
if (Common.PARANOIA && result) { check(result); }
|
1571
|
+
return result;
|
1572
|
+
};
|
1573
|
+
|
1574
|
+
/** Used for testing. */
|
1575
|
+
var random = Operation.random = function (docLength) {
|
1576
|
+
Common.assert(Common.isUint(docLength));
|
1577
|
+
var offset = Math.floor(Math.random() * 100000000 % docLength) || 0;
|
1578
|
+
var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0;
|
1579
|
+
var toInsert = '';
|
1580
|
+
do {
|
1581
|
+
var toInsert = Common.randomASCII(Math.floor(Math.random() * 20));
|
1582
|
+
} while (toRemove === 0 && toInsert === '');
|
1583
|
+
return create(offset, toRemove, toInsert);
|
1584
|
+
};
|
1585
|
+
|
1586
|
+
}
|
1587
|
+
};
|
1588
|
+
ChainPad = r("ChainPad.js");}());
|