Comparing version 0.2.1 to 0.2.2
34
canop.js
@@ -28,2 +28,5 @@ (function (root, factory) { | ||
var PROTOCOL_WARN_UNKNOWN_BASE = 0; | ||
var warnNamesFromCode = [ | ||
"UnknownBaseError", | ||
]; | ||
@@ -382,2 +385,3 @@ // The last incrementable integer in IEEE754. | ||
var self = this; | ||
params = params || {}; | ||
this.base = params.base || 0; // Most recent known canon operation index. | ||
@@ -414,2 +418,3 @@ this.localId = 0; // Identifier of the current machine. | ||
this.nextClientId = 1; | ||
this.clientCount = 0; // Number of clients connected. | ||
this.signalFromClient = Object.create(null); | ||
@@ -427,2 +432,3 @@ | ||
self.clientState = STATE_LOADING; | ||
self.clientCount++; | ||
} catch(e) { | ||
@@ -437,3 +443,10 @@ self.emit('unsyncable', e); | ||
}); | ||
self.once('syncing', initiateLoading); | ||
this.once('syncing', initiateLoading); | ||
this.on('signal', function(event) { | ||
if (event.data.connected !== undefined) { | ||
if (event.data.connected) { | ||
self.clientCount++; | ||
} else { self.clientCount--; } | ||
} | ||
}); | ||
} | ||
@@ -632,3 +645,5 @@ } | ||
// local changes are applied on top of if. | ||
self.emit('unsyncable'); | ||
var error = new Error(error[1]); | ||
error.name = warnNamesFromCode[error[0]]; | ||
self.emit('unsyncable', error); | ||
} | ||
@@ -885,2 +900,3 @@ }); | ||
self.clients[newClient.id] = newClient; | ||
self.clientCount++; | ||
@@ -935,3 +951,2 @@ newClient.onReceive(function receiveFromClient(message) { | ||
var data = protocol[2]; | ||
self.signalFromClient[clientId] = self.signalFromClient[clientId] || {}; | ||
if (data !== undefined) { | ||
@@ -952,2 +967,6 @@ for (var key in data) { | ||
}); | ||
// Send the connection signal. | ||
this.signalFromClient[newClient.id] = this.signalFromClient[newClient.id] || | ||
Object.create(null); | ||
this.signalFromClient[newClient.id].connected = true; | ||
}, | ||
@@ -958,5 +977,8 @@ | ||
for (var clientId in this.clients) { | ||
if (this.signalFromClient[clientId] !== undefined) { | ||
if (client.id !== +clientId) { | ||
client.send(JSON.stringify([PROTOCOL_SIGNAL, +clientId, | ||
this.signalFromClient[clientId]])); | ||
var otherClient = this.clients[clientId]; | ||
otherClient.send(JSON.stringify([PROTOCOL_SIGNAL, client.id, | ||
this.signalFromClient[client.id]])); | ||
} | ||
@@ -972,5 +994,7 @@ } | ||
var aClient = this.clients[aClientId]; | ||
aClient.send(JSON.stringify([PROTOCOL_SIGNAL, +clientId])); | ||
aClient.send(JSON.stringify([PROTOCOL_SIGNAL, +clientId, | ||
{connected: false}])); | ||
} | ||
delete this.signalFromClient[clientId]; | ||
this.clientCount--; | ||
}, | ||
@@ -977,0 +1001,0 @@ |
{ | ||
"name": "canop", | ||
"version": "0.2.1", | ||
"version": "0.2.2", | ||
"description": "Convergent algorithm for collaborative text.", | ||
@@ -5,0 +5,0 @@ "main": "canop.js", |
@@ -9,2 +9,8 @@ `canop` | ||
A simple [client](./web/index.html)-[server](./app.js) example is available, | ||
using adapters for [WebSocket](./web/canop-websocket.js) and | ||
[CodeMirror](./web/canop-codemirror.js). | ||
Exploratory API description: | ||
```js | ||
@@ -30,3 +36,5 @@ var canop = require('canop'); | ||
// cursor (which moves the selection with shift+arrow). | ||
// Also, connected is signaled when a node joins or leaves. | ||
client.signal({ name: 'Grace', focus: ['some'], sel: [[9,9]] }); | ||
client.clientCount // Number of clients currently connected. | ||
@@ -141,2 +149,3 @@ // This event has the following keys: | ||
- Signal a list of all currently connected clients and of disconnections | ||
- Separate wire and widget hooks. | ||
- JSON-compatible protocol | ||
@@ -143,0 +152,0 @@ - Array index rebasing |
@@ -127,1 +127,13 @@ var canop = require('../canop.js'); | ||
assert.equal(result, 'a', 'Inserting a character immediately removed'); | ||
// localId | ||
var star = new Star(''); | ||
assert.equal(star.clients[0].localId, 1, 'Client 0 localId'); | ||
assert.equal(star.clients[1].localId, 2, 'Client 1 localId'); | ||
// clientCount | ||
var star = new Star(''); | ||
sendChange(star, 0); | ||
assert.equal(star.server.clientCount, 2, 'Server clientCount'); | ||
assert.equal(star.clients[0].clientCount, 2, 'Client 0 clientCount'); | ||
assert.equal(star.clients[1].clientCount, 2, 'Client 1 clientCount'); |
(function(exports, undefined) { | ||
var canop = exports.canop; | ||
var RECONNECTION_INTERVAL = 256; // in ms. Increases exponentially to 10min. | ||
// client: a canop.Client instance. | ||
// editor: a CodeMirror instance. | ||
// options: | ||
// - url: location of websocket. Defaults to ws://<dns>/websocket. | ||
// - error: function triggered when the websocket errors. | ||
// - open: function triggered when the websocket opens. | ||
// - close: function triggered when the websocket closes. | ||
// (You can also rely on .canopClient.on('unsyncable', …) | ||
// - reconnect: if true, automatically reconnect when disconnected. | ||
function CanopCodemirrorHook(editor, options) { | ||
options = options || {}; | ||
options.url = options.url || | ||
// Trick: use the end of either http: or https:. | ||
'ws' + window.location.protocol.slice(4) + '//' + | ||
window.location.host + '/websocket'; | ||
// options: (planned) | ||
// - path: list determining the location of the corresponding string in the JSON | ||
// object. | ||
function CanopCodemirror(client, editor) { | ||
this.canopClient = client; | ||
this.editor = editor; | ||
var self = this; | ||
this.canopClient = new canop.Client({ | ||
send: function(msg) { | ||
if (self.socket === undefined || | ||
self.socket.readyState !== WebSocket.OPEN) { | ||
throw new Error("WebSocket is not open for business"); | ||
} | ||
self.socket.send(msg); | ||
} | ||
}); | ||
this.url = "" + options.url; | ||
this.socket = null; | ||
this.reconnect = (options.reconnect === undefined)? true: !!options.reconnect; | ||
this.reconnectionInterval = RECONNECTION_INTERVAL; | ||
this.socketReceive = this.socketReceive.bind(this); | ||
this.editorChange = this.editorChange.bind(this); | ||
@@ -46,38 +22,10 @@ this.remoteChange = this.remoteChange.bind(this); | ||
this.clientSelectionWidgets = Object.create(null); | ||
this.connect(options); | ||
} | ||
CanopCodemirrorHook.prototype = { | ||
connect: function CCHconnect(options) { | ||
var self = this; | ||
this.socket = new WebSocket(this.url); | ||
this.socket.addEventListener('message', this.socketReceive); | ||
this.socket.addEventListener('close', function(e) { | ||
self.canopClient.emit('unsyncable'); | ||
if (self.reconnect) { | ||
setTimeout(function() { self.connect(options); }, | ||
self.reconnectionInterval); | ||
if (self.reconnectionInterval <= 1000 * 60 * 10) { | ||
self.reconnectionInterval *= 2; | ||
} | ||
} | ||
}); | ||
this.socket.addEventListener('open', function() { | ||
self.reconnectionInterval = RECONNECTION_INTERVAL; | ||
self.canopClient.emit('syncing'); | ||
}); | ||
if (options.error) { this.socket.addEventListener('error', options.error); } | ||
if (options.open) { this.socket.addEventListener('open', options.open); } | ||
if (options.close) { this.socket.addEventListener('close', options.close); } | ||
}, | ||
socketReceive: function CCHsocketReceive(event) { | ||
this.canopClient.receive('' + event.data); | ||
}, | ||
remoteChange: function CCHremoteUpdate(event) { | ||
CanopCodemirror.prototype = { | ||
remoteChange: function canopCodemirrorRemoteUpdate(event) { | ||
this.updateEditor(event.changes, event.posChanges); | ||
}, | ||
editorChange: function CCHeditorChange(editor, change, actions) { | ||
editorChange: function canopCodemirrorEditorChange(editor, change, actions) { | ||
var actions = actions || []; | ||
@@ -102,3 +50,3 @@ var from = change.from; | ||
cursorActivity: function CCHcursorActivity(editor) { | ||
cursorActivity: function canopCodemirrorCursorActivity(editor) { | ||
var self = this; | ||
@@ -114,3 +62,3 @@ var selections = self.editor.listSelections().map(function(selection) { | ||
resetEditor: function CCHresetEditor() { | ||
resetEditor: function canopCodemirrorResetEditor() { | ||
this.editor.off('change', this.editorChange); | ||
@@ -126,3 +74,3 @@ this.editor.off('cursorActivity', this.cursorActivity); | ||
// Takes a list of AtomicOperations and a list of PosChanges. | ||
updateEditor: function CCHupdateEditor(delta, posChanges) { | ||
updateEditor: function canopCodemirrorUpdateEditor(delta, posChanges) { | ||
this.editor.off('change', this.editorChange); | ||
@@ -137,3 +85,3 @@ this.editor.off('cursorActivity', this.cursorActivity); | ||
applyDelta: function CCHapplyDelta(delta) { | ||
applyDelta: function canopCodemirrorApplyDelta(delta) { | ||
for (var i = 0; i < delta.length; i++) { | ||
@@ -153,3 +101,3 @@ var change = delta[i]; | ||
updateCursor: function CCHupdateCursor(posChanges, oldCursor) { | ||
updateCursor: function canopCodemirrorUpdateCursor(posChanges, oldCursor) { | ||
cursor = canop.changePosition(oldCursor, posChanges, true); | ||
@@ -161,3 +109,3 @@ this.editor.setCursor(this.editor.posFromIndex(cursor)); | ||
signalReceive: function CCHsignalReceive(event) { | ||
signalReceive: function canopCodemirrorSignalReceive(event) { | ||
var clientId = event.clientId; | ||
@@ -189,3 +137,3 @@ var data = event.data; | ||
// Return a list of widgets that got added. | ||
addSelection: function CCHaddSelection(selection, name) { | ||
addSelection: function canopCodemirrorAddSelection(selection, name) { | ||
var widgets = [this.addUiCursor(selection[0], name)]; | ||
@@ -199,3 +147,3 @@ if (selection[0] !== selection[1]) { | ||
// Return the CodeMirror bookmark associated with the cursor. | ||
addUiCursor: function CCHaddUiCursor(offset, name) { | ||
addUiCursor: function canopCodemirrorAddUiCursor(offset, name) { | ||
var pos = this.editor.posFromIndex(offset); | ||
@@ -229,3 +177,3 @@ var coords = this.editor.cursorCoords(pos); | ||
// Returns a CodeMirror mark. | ||
addUiSelection: function CCHaddUiSelection(selection, name) { | ||
addUiSelection: function canopCodemirrorAddUiSelection(selection, name) { | ||
var color = this.colorFromName(name.toString(), 0.9); | ||
@@ -247,3 +195,3 @@ if (selection[0] < selection[1]) { | ||
// Return a CSS rgb(…) string. | ||
rgbFromLch: function CCHrgbFromLch(luma, chroma, hue) { | ||
rgbFromLch: function canopCodemirrorRgbFromLch(luma, chroma, hue) { | ||
var hue6 = hue / 60; | ||
@@ -274,3 +222,3 @@ var x = chroma * (1 - Math.abs((hue6 % 2) - 1)); | ||
// Small differences in the string yield very different colors. | ||
hueFromName: function CCHcolorFromName(name) { | ||
hueFromName: function canopCodemirrorColorFromName(name) { | ||
var hue = 0; | ||
@@ -286,3 +234,3 @@ for (var i = 0; i < name.length; i++) { | ||
// such that small differences in the string yield very different colors. | ||
colorFromName: function CCHcolorFromName(name, luma) { | ||
colorFromName: function canopCodemirrorColorFromName(name, luma) { | ||
if (luma === undefined) { luma = 0.7; } | ||
@@ -294,3 +242,7 @@ return this.rgbFromLch(luma, 0.6, this.hueFromName(name)); | ||
exports.CanopCodemirrorHook = CanopCodemirrorHook; | ||
canop.ui = canop.ui || {}; | ||
canop.ui.codemirror = function(client, options) { | ||
return new CanopCodemirror(client, options); | ||
}; | ||
}(this)); |
Sorry, the diff of this file is not supported yet
1480
153
160623