sn-collab-editor 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- :message => doc.content,
16
- :iv => doc.iv,
17
- :auth => doc.auth,
15
+ :patches => doc.patches,
18
16
  :client_id => data['client_id'],
19
- :retrieve => true
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
- doc.content = message
27
- doc.iv = data["iv"]
28
- doc.auth = data["auth"]
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, {:message => message, :iv => data["iv"], :auth => data["auth"], :client_id => data['client_id']})
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
@@ -1,4 +1,5 @@
1
1
  module Collab
2
2
  class Document < ApplicationRecord
3
+ has_many :patches
3
4
  end
4
5
  end
@@ -0,0 +1,5 @@
1
+ module Collab
2
+ class Patch < ApplicationRecord
3
+
4
+ end
5
+ end
@@ -2,11 +2,18 @@
2
2
 
3
3
  <div id="wrapper">
4
4
  <% if @doc %>
5
- <div id="url">
6
- <strong>Sharing URL</strong>:
7
- <span><%= @url %></span><span id="url-key"></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
- <textarea id="editor"></textarea>
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
- # config.assets.paths << File.expand_path("../../../vendor/assets/javascripts", __FILE__)
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
@@ -1,3 +1,3 @@
1
1
  module Collab
2
- VERSION = '0.1.6'
2
+ VERSION = '0.1.7'
3
3
  end
@@ -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");}());