Comparing version 0.2.2 to 0.2.3
97
canop.js
@@ -14,5 +14,6 @@ (function (root, factory) { | ||
var actions = { | ||
set: 0, | ||
stringAdd: 7, | ||
stringRemove: 8 | ||
pass: 0, | ||
set: 1, | ||
stringAdd: 8, | ||
stringRemove: 9 | ||
}; | ||
@@ -59,4 +60,12 @@ var PROTOCOL_VERSION = 0; | ||
}, | ||
// Is this operation's type rebaseable (ie, its indices get reshifted)? | ||
// If it is, then key = index in a list. | ||
rebaseable: function rebaseable() { | ||
return this.action === actions.stringAdd || | ||
this.action === actions.stringRemove; | ||
}, | ||
// Get this operation modified by a list of PosChanges. | ||
getModifiedBy: function getModifiedBy(changes) { | ||
if (!this.rebaseable()) { return; } | ||
var oldEnd = this.key + this.value.length; | ||
@@ -79,4 +88,3 @@ this.original = { | ||
if (key === undefined || end === undefined) { | ||
this.key = 0; // Nulled to avoid adding spaces at the end in toString(). | ||
this.value = ""; // Null operation. | ||
this.action = actions.pass; // Nulled to avoid adding spaces at the end in toString(). | ||
} else { | ||
@@ -97,8 +105,11 @@ this.key = key; | ||
inverse: function inverse() { | ||
var op = this.dup(); | ||
if (this.action === actions.stringAdd) { | ||
var op = this.dup(); | ||
op.action = actions.stringRemove; | ||
} else if (this.action === actions.stringRemove) { | ||
var op = this.dup(); | ||
op.action = actions.stringAdd; | ||
} else if (this.action === actions.set) { | ||
var newValue = op.key; | ||
op.key = op.value; // Old value | ||
op.value = newValue;// New value | ||
} | ||
@@ -170,2 +181,6 @@ return op; | ||
var change = changes[i]; | ||
if (change === undefined) { | ||
if (bestGuess) { return key; } | ||
else { return; } | ||
} | ||
var newKey = change.update(key, originalKey); | ||
@@ -271,7 +286,4 @@ var contextFound = false; | ||
var change = this.list[i].change(); | ||
if (change !== undefined) { | ||
posChanges.push(change); | ||
} else { | ||
posChanges = []; | ||
} | ||
if (change === undefined) { return posChanges; } | ||
posChanges.push(change); | ||
} | ||
@@ -284,8 +296,7 @@ return posChanges; | ||
for (var i = this.list.length - 1; i >= 0; i--) { | ||
var change = this.list[i].change().inverse(); | ||
if (change !== undefined) { | ||
posChanges.push(change); | ||
} else { | ||
return posChanges; | ||
} | ||
var change = this.list[i].change(); | ||
if (change === undefined) { return posChanges; } | ||
change = change.inverse(); | ||
if (change === undefined) { return posChanges; } | ||
posChanges.push(change); | ||
} | ||
@@ -306,2 +317,8 @@ return posChanges; | ||
}, | ||
// Change the whole value. Mutates this. | ||
set: function setOp(path, newVal, oldVal, base, local) { | ||
var aop = new AtomicOperation(actions.set, newVal, oldVal, base, local); | ||
this.list.push(aop); | ||
return aop; | ||
}, | ||
// Assume we start with the empty value. | ||
@@ -394,3 +411,3 @@ toString: function toString() { | ||
this.base = params.base || 0; // Most recent known canon operation index. | ||
this.localId = 0; // Identifier of the current machine. | ||
this.id = 0; // Identifier of the current machine. | ||
this.local = new Operation(); // Operation for local changes. | ||
@@ -405,5 +422,3 @@ this.sent = new Operation(); // Operation for changes sent but not acknowledged. | ||
// Note: servers should never disable data. | ||
if (params.disableData !== undefined) { | ||
this.disableData = params.disableData; | ||
} | ||
this.disableData = !!params.disableData; | ||
this.data = undefined; // Holds the current data including local operations. | ||
@@ -433,6 +448,6 @@ // Also, clients should never have params.data. | ||
try { | ||
if (self.localId === 0) { | ||
if (self.id === 0) { | ||
self.send(JSON.stringify([PROTOCOL_PLEASE, PROTOCOL_VERSION])); | ||
} else { | ||
self.send(JSON.stringify([PROTOCOL_SINCE, self.localId, self.base])); | ||
self.send(JSON.stringify([PROTOCOL_SINCE, self.id, self.base])); | ||
} | ||
@@ -558,4 +573,6 @@ self.clientState = STATE_LOADING; | ||
} | ||
if (delta[1][0] === 0) { // set | ||
} else if ((delta[1][0] === 7) || (delta[1][0] === 8)) { | ||
if (delta[1][0] === actions.pass) { // pass | ||
} else if (delta[1][0] === actions.set) { // set | ||
} else if ((delta[1][0] === actions.stringAdd) || | ||
(delta[1][0] === actions.stringRemove)) { | ||
// string add / remove | ||
@@ -707,9 +724,9 @@ if (typeof delta[1][1] !== "number") { // offset | ||
reset: function(data, base, localId) { | ||
reset: function(data, base, id) { | ||
this.local = new Operation(); | ||
this.sent = new Operation(); | ||
this.canon = new Operation(); | ||
this.localId = localId; | ||
this.id = id; | ||
if (!this.disableData) { this.data = data; } | ||
this.receiveChange([PROTOCOL_DELTA, [], [[[base, localId, 0], [actions.set, data]]]]); | ||
this.receiveChange([PROTOCOL_DELTA, [], [[[base, id, 0], [actions.set, data]]]]); | ||
}, | ||
@@ -862,3 +879,6 @@ localToSent: function() { | ||
var aops = []; // AtomicOperations | ||
if (actionType === actions.stringAdd) { | ||
if (actionType === actions.pass) { | ||
} else if (actionType === actions.set) { | ||
aops.push(this.set(action)); | ||
} else if (actionType === actions.stringAdd) { | ||
aops.push(this.stringAdd(action)); | ||
@@ -876,3 +896,3 @@ } else if (actionType === actions.stringRemove) { | ||
return this.local.add(action[1], action[2], action[3], | ||
this.base, this.localId); | ||
this.base, this.id); | ||
}, | ||
@@ -883,4 +903,10 @@ // action: [actions.stringAdd, path, …params] | ||
return this.local.remove(action[1], action[2], action[3], | ||
this.base, this.localId); | ||
this.base, this.id); | ||
}, | ||
// action: [actions.set, path, new value, old value] | ||
// Return an AtomicOperation. | ||
set: function(action) { | ||
return this.local.set(action[1], action[2], action[3], | ||
this.base, this.localId); | ||
}, | ||
@@ -893,3 +919,3 @@ // Send a signal to all other nodes of the network. | ||
try { | ||
this.send(JSON.stringify([PROTOCOL_SIGNAL, this.localId, content])); | ||
this.send(JSON.stringify([PROTOCOL_SIGNAL, this.id, content])); | ||
} catch(e) { | ||
@@ -935,2 +961,3 @@ this.emit('unsyncable', e); | ||
self.removeClient(newClient); | ||
var oldClientId = newClient.id; | ||
newClient.id = protocol[1]; | ||
@@ -950,2 +977,6 @@ newClient.base = base; | ||
} | ||
// The client may have had its id changed. | ||
delete self.signalFromClient[oldClientId]; | ||
self.signalFromClient[newClient.id] = self.signalFromClient[newClient.id] || | ||
Object.create(null); | ||
self.sendSignalsToClient(newClient); | ||
@@ -952,0 +983,0 @@ } else if (messageType === PROTOCOL_DELTA) { |
@@ -33,11 +33,9 @@ # Canop Protocol | ||
- `action`: set = 0 | ||
- `action`: pass = 0, set = 1 | ||
- `key`: any JSON value, possibly of a different type. | ||
- `value`: old value, if any. | ||
(Note: `[0]` "set nothing to nothing" is the empty operation.) | ||
Delta for objects: | ||
- `action`: add = 1, remove = 2, move = 3. | ||
- `action`: add = 2, remove = 3, move = 4. | ||
- `key`: key (in the object) as a string. | ||
@@ -48,3 +46,3 @@ - `value`: value (for move, path of the new location). | ||
- `action`: add = 4, remove = 5, move = 6. | ||
- `action`: add = 5, remove = 6, move = 7. | ||
- `key`: index (in the list). | ||
@@ -55,3 +53,3 @@ - `value`: value (for move, path of the new location). | ||
- `action`: add = 7, remove = 8. | ||
- `action`: add = 8, remove = 9. | ||
- `key`: offset (in the string). | ||
@@ -58,0 +56,0 @@ - `value`: string. |
{ | ||
"name": "canop", | ||
"version": "0.2.2", | ||
"version": "0.2.3", | ||
"description": "Convergent algorithm for collaborative text.", | ||
@@ -5,0 +5,0 @@ "main": "canop.js", |
@@ -146,8 +146,7 @@ `canop` | ||
- Signal a list of all currently connected clients and of disconnections | ||
- Separate wire and widget hooks. | ||
- Textarea adapter | ||
- Customizable UI sync debouncing | ||
- JSON-compatible protocol | ||
- Array index rebasing | ||
- Autosave of operations to disk | ||
- Separate display hooks from transport hooks | ||
- Allow creating user-defined operations |
@@ -128,6 +128,6 @@ var canop = require('../canop.js'); | ||
// localId | ||
// id | ||
var star = new Star(''); | ||
assert.equal(star.clients[0].localId, 1, 'Client 0 localId'); | ||
assert.equal(star.clients[1].localId, 2, 'Client 1 localId'); | ||
assert.equal(star.clients[0].id, 1, 'Client 0 id'); | ||
assert.equal(star.clients[1].id, 2, 'Client 1 id'); | ||
@@ -140,1 +140,5 @@ // clientCount | ||
assert.equal(star.clients[1].clientCount, 2, 'Client 1 clientCount'); | ||
star.server.removeClient(star.clients[0]); | ||
sendChange(star, 1); | ||
assert.equal(star.server.clientCount, 1, 'Server clientCount after disconnection'); | ||
assert.equal(star.clients[1].clientCount, 1, 'Client 1 clientCount after disconnection'); |
@@ -29,22 +29,71 @@ (function(exports, undefined) { | ||
editorChange: function canopCodemirrorEditorChange(editor, change, actions) { | ||
var actions = actions || []; | ||
var from = change.from; | ||
var to = change.to; | ||
var added = change.text.join('\n'); | ||
var removed = change.removed.join('\n'); | ||
var fromIdx = editor.indexFromPos(from); | ||
if (removed.length > 0) { | ||
actions.push([canop.action.stringRemove, [], fromIdx, removed]); | ||
editorChange: function canopCodemirrorEditorChange(editor, changes) { | ||
// The codemirror positions in `changes` correspond to before the change's | ||
// execution. However, the `editor` has the changes applied, so that | ||
// `indexFromPos` only returns the right answer for positions after the | ||
// changes are applied. | ||
var actions = []; | ||
var indexFromPos = function(pos) { return editor.indexFromPos(pos); }; | ||
for (var i = changes.length - 1; i >= 0; i--) { | ||
var change = changes[i]; | ||
var from = change.from; | ||
var to = change.to; | ||
var added = change.text.join('\n'); | ||
var removed = change.removed.join('\n'); | ||
indexFromPos = this.updateIdxFromCmPos(indexFromPos, change, added, removed); | ||
var fromIdx = indexFromPos(from); | ||
if (removed.length > 0) { | ||
actions.push([canop.action.stringRemove, [], fromIdx, removed]); | ||
} | ||
if (added.length > 0) { | ||
actions.push([canop.action.stringAdd, [], fromIdx, added]); | ||
} | ||
} | ||
if (added.length > 0) { | ||
actions.push([canop.action.stringAdd, [], fromIdx, added]); | ||
} | ||
if (change.next) { | ||
this.editorChange(editor, change.next, actions); | ||
} else { | ||
this.canopClient.actAtomically(actions); | ||
} | ||
this.canopClient.actAtomically(actions.reverse()); | ||
}, | ||
// Compare codemirror positions a and b. | ||
cmpCmPos: function canopCompareCodemirrorPosition(a, b) { | ||
if (a.line < b.line) { return -1; } | ||
if (a.line > b.line) { return 1; } | ||
if (a.ch < b.ch) { return -1; } | ||
if (a.ch > b.ch) { return 1; } | ||
return 0; | ||
}, | ||
// Is the codemirror position a before b? | ||
cmPosLe: function canopCodemirrorPositionLessOrEqual(a, b) { | ||
return this.cmpCmPos(a, b) <= 0; | ||
}, | ||
updateIdxFromCmPos: function canopUpdateIndexFromCodemirrorPosition( | ||
indexFromPos, change, added, removed) { | ||
var self = this; | ||
return function(pos) { | ||
if (self.cmPosLe(pos, change.from)) { return indexFromPos(pos); } | ||
else if (self.cmPosLe(change.to, pos)) { | ||
var ch = 0; | ||
if (change.to.line < pos.line) { | ||
ch = pos.ch; | ||
} else if (change.text.length <= 1) { | ||
ch = pos.ch - (change.to.ch - change.from.ch) + added.length; | ||
} else { | ||
var lastLineAdded = change.text[change.text.length - 1]; | ||
ch = pos.ch - change.to.ch + lastLineAdded.length; | ||
} | ||
return indexFromPos({ | ||
line: pos.line + change.text.length - 1 | ||
- (change.to.line - change.from.line), | ||
ch: ch, | ||
}) + removed.length - added.length; | ||
} else if (change.from.line === pos.line) { | ||
return indexFromPos(change.from) - change.from.ch + pos.ch; | ||
} else { | ||
return indexFromPos(change.from) | ||
+ change.removed.slice(0, pos.line - change.from.line).join('\n').length | ||
+ pos.ch + 1; | ||
} | ||
}; | ||
}, | ||
cursorActivity: function canopCodemirrorCursorActivity(editor) { | ||
@@ -62,3 +111,3 @@ var self = this; | ||
resetEditor: function canopCodemirrorResetEditor() { | ||
this.editor.off('change', this.editorChange); | ||
this.editor.off('changes', this.editorChange); | ||
this.editor.off('cursorActivity', this.cursorActivity); | ||
@@ -69,3 +118,3 @@ var cursor = this.editor.getCursor(); | ||
this.editor.on('cursorActivity', this.cursorActivity); | ||
this.editor.on('change', this.editorChange); | ||
this.editor.on('changes', this.editorChange); | ||
}, | ||
@@ -75,9 +124,9 @@ | ||
updateEditor: function canopCodemirrorUpdateEditor(delta, posChanges) { | ||
this.editor.off('change', this.editorChange); | ||
this.editor.off('changes', this.editorChange); | ||
this.editor.off('cursorActivity', this.cursorActivity); | ||
var cursor = this.editor.indexFromPos(this.editor.getCursor()); | ||
var selections = this.getSelections(); | ||
this.applyDelta(delta); | ||
this.updateCursor(posChanges, cursor); | ||
this.setSelections(posChanges, selections); | ||
this.editor.on('cursorActivity', this.cursorActivity); | ||
this.editor.on('change', this.editorChange); | ||
this.editor.on('changes', this.editorChange); | ||
}, | ||
@@ -100,7 +149,27 @@ | ||
updateCursor: function canopCodemirrorUpdateCursor(posChanges, oldCursor) { | ||
cursor = canop.changePosition(oldCursor, posChanges, true); | ||
this.editor.setCursor(this.editor.posFromIndex(cursor)); | ||
getSelections: function canopCodemirrorGetSelections() { | ||
var cmSelections = this.editor.listSelections(); | ||
var selections = []; | ||
for (var i = 0; i < cmSelections.length; i++) { | ||
var cmSelection = cmSelections[i]; | ||
selections.push({ | ||
anchor: this.editor.indexFromPos(cmSelection.anchor), | ||
head: this.editor.indexFromPos(cmSelection.head), | ||
}); | ||
} | ||
return selections; | ||
}, | ||
setSelections: function canopCodemirrorSetSelections(posChanges, oldSelections) { | ||
var cmSelections = []; | ||
for (var i = 0; i < oldSelections.length; i++) { | ||
var oldSelection = oldSelections[i]; | ||
cmSelections.push({ | ||
anchor: canop.changePosition(this.editor.posFromIndex(oldSelection.anchor), posChanges, true), | ||
head: canop.changePosition(this.editor.posFromIndex(oldSelection.head), posChanges, true), | ||
}); | ||
} | ||
this.editor.setSelections(cmSelections); | ||
}, | ||
// UI management to show selection from other participants. | ||
@@ -235,6 +304,6 @@ | ||
canop.ui = canop.ui || {}; | ||
canop.ui.codemirror = function(client, options) { | ||
return new CanopCodemirror(client, options); | ||
canop.ui.codemirror = function(client, editor) { | ||
return new CanopCodemirror(client, editor); | ||
}; | ||
}(this)); |
204422
17
1655
152