New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

canop

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

canop - npm Package Compare versions

Comparing version 0.3.1 to 0.3.2

web/canop-textarea.js

155

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,11 +296,16 @@ 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);
}
return posChanges;
},
// actionType: actions.*
applyAtomicOperation: function applyAtomicOperation(actionType, key, value, base, local) {
var aop = new AtomicOperation(actionType, key, value, base, local);
this.list.push(aop);
return aop;
},
// Insert a value to the operation. Mutates this.

@@ -306,2 +323,8 @@ add: function addOp(path, offset, value, base, local) {

},
// 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.

@@ -392,2 +415,3 @@ toString: function toString() {

var self = this;
params = params || {};
this.base = params.base || 0; // Most recent known canon operation index.

@@ -400,2 +424,4 @@ this.id = 0; // Identifier of the current machine.

this.listeners = {};
this.undoStack = [];
this.redoStack = [];

@@ -405,5 +431,3 @@ this.on('change', function(event) { self.updateData(event); });

// 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.

@@ -557,4 +581,6 @@ // Also, clients should never have params.data.

}
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

@@ -667,7 +693,8 @@ if (typeof delta[1][1] !== "number") { // offset

emitChanges: function(eventName, changes, posChanges) {
var change = changes.map(function(change) {
return [[], change.action, change.key, change.value];
});
var change = changes.map(this.aopArrayFromAop);
this.emit(eventName, {changes: change, posChanges: posChanges});
},
aopArrayFromAop: function(change) {
return [[], change.action, change.key, change.value];
},
// Receive an external update conforming to the protocol, as a String.

@@ -842,9 +869,6 @@ // Emit the corresponding change, update and synced events.

act: function(action) {
var aops = this.commitAction(action);
// TODO: localUpdate event.
this.emitChanges('localChange', aops);
this.sendToServer();
this.registerAtomicOperations(this.commitAction(action));
},
// actions: list of [actionType, path, …params].
actAtomically: function(actions) {
actAtomically: function(actions, options) {
var aops = [];

@@ -854,4 +878,3 @@ for (var i = 0; i < actions.length; i++) {

}
this.emitChanges('localChange', aops);
this.sendToServer();
this.registerAtomicOperations(aops, options);
},

@@ -863,3 +886,6 @@ // action: [actions.*, path, …params]

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));

@@ -873,2 +899,7 @@ } else if (actionType === actions.stringRemove) {

},
// actionType: actions.*
applyAtomicOperation: function(actionType, key, value) {
return this.local.applyAtomicOperation(
actionType, key, value, this.base, this.id);
},
// action: [actions.stringAdd, path, …params]

@@ -886,3 +917,52 @@ // Return an AtomicOperation.

},
// 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);
},
// History management
registerAtomicOperations: function(aops, options) {
options = options || {};
if (!options.skipHistory) {
// Add to the local history.
this.undoStack.push(new Operation(aops));
this.redoStack = [];
}
// TODO: localUpdate event.
this.emitChanges('localChange', aops);
this.sendToServer();
},
undo: function() {
var lastOp = this.undoStack.pop();
var aops = [];
if (lastOp !== undefined) {
this.redoStack.push(lastOp.dup());
var op = lastOp.inverse();
for (var i = 0; i < op.list.length; i++) {
var aop = op.list[i];
aops.push(this.applyAtomicOperation(aop.action, aop.key, aop.value));
}
this.registerAtomicOperations(aops, {skipHistory: true});
}
return aops.map(this.aopArrayFromAop);
},
redo: function() {
var op = this.redoStack.pop();
var aops = [];
if (op !== undefined) {
this.undoStack.push(op.dup());
for (var i = 0; i < op.list.length; i++) {
var aop = op.list[i];
aops.push(this.applyAtomicOperation(aop.action, aop.key, aop.value));
}
this.registerAtomicOperations(aops, {skipHistory: true});
}
return aops.map(this.aopArrayFromAop);
},
// Send a signal to all other nodes of the network.

@@ -935,2 +1015,3 @@ // content: JSON-serializable value, sent to other nodes.

self.removeClient(newClient);
var oldClientId = newClient.id;
newClient.id = protocol[1];

@@ -950,2 +1031,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);

@@ -965,2 +1050,4 @@ } else if (messageType === PROTOCOL_DELTA) {

for (var key in data) {
self.signalFromClient[clientId] = self.signalFromClient[clientId] ||
{};
self.signalFromClient[clientId][key] = data[key];

@@ -967,0 +1054,0 @@ }

@@ -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.3.1",
"version": "0.3.2",
"description": "Convergent algorithm for collaborative text.",

@@ -9,2 +9,5 @@ "main": "canop.js",

},
"devDependencies": {
"camp": "~17.2.1"
},
"repository": {

@@ -11,0 +14,0 @@ "type": "git",

@@ -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

@@ -47,2 +53,5 @@ var canop = require('canop');

client.removeListener('change', changeListener);
// Returns an array of [path, action type, parameters…]
client.undo()
client.redo()

@@ -142,6 +151,7 @@ // When all local operations are acknowledged by the server.

- 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

@@ -143,1 +143,55 @@ var canop = require('../canop.js');

assert.equal(star.clients[1].clientCount, 1, 'Client 1 clientCount after disconnection');
// Undo
var star = new Star('');
star.clients[0].add([], 0, 'ab');
sendChange(star, 0);
star.clients[0].add([], 2, 'cd');
sendChange(star, 0);
sendChange(star, 1);
// Undo wrong client
star.clients[1].undo();
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.clients[0]), 'abcd', 'Client 0 wrong undo');
assert.equal(String(star.clients[1]), 'abcd', 'Client 1 wrong undo');
assert.equal(String(star.server), 'abcd', 'Client 1 wrong undo');
// Undo right client
star.clients[0].undo();
assert.equal(String(star.clients[0]), 'ab', 'Client 0 undo');
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.server), 'ab', 'Server undo');
assert.equal(String(star.clients[1]), 'ab', 'Client 1 undo');
// Redo
star.clients[0].redo();
assert.equal(String(star.clients[0]), 'abcd', 'Client 0 redo');
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.server), 'abcd', 'Server redo');
assert.equal(String(star.clients[1]), 'abcd', 'Client 1 redo');
// Partial undo, edition, and redo
var star = new Star('');
star.clients[0].add([], 0, 'a');
star.clients[0].add([], 1, 'b');
star.clients[0].undo();
star.clients[0].add([], 1, 'c');
star.clients[0].redo();
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.server), 'ac', 'Server undo, edition, redo');
assert.equal(String(star.clients[0]), 'ac', 'Client 0 undo, edition, redo');
assert.equal(String(star.clients[1]), 'ac', 'Client 1 undo, edition, redo');
// undo, redo, undo
var star = new Star('');
star.clients[0].add([], 0, 'a');
star.clients[0].undo();
star.clients[0].redo();
star.clients[0].undo();
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.server), '', 'Server undo, redo, undo');
assert.equal(String(star.clients[0]), '', 'Client 0 undo, redo, undo');
assert.equal(String(star.clients[1]), '', 'Client 1 undo, redo, undo');
(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);

@@ -42,2 +18,5 @@ this.remoteChange = this.remoteChange.bind(this);

this.signalReceive = this.signalReceive.bind(this);
this.editorUndo = this.editorUndo.bind(this);
this.editorRedo = this.editorRedo.bind(this);
this.commit = this.commit.bind(this);

@@ -47,43 +26,43 @@ this.canopClient.on('change', this.remoteChange);

this.clientSelectionWidgets = Object.create(null);
this.connect(options);
this.actionBuffer = [];
this.actionBufferTimeout = null;
this.commitCallbacks = [];
this.editor.undo = this.editorUndo;
this.editor.redo = this.editorRedo;
}
CanopCodemirrorHook.prototype = {
connect: function CCHconnect(options) {
CanopCodemirror.prototype = {
remoteChange: function canopCodemirrorRemoteUpdate(event) {
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;
}
}
self.awaitCommit(function() {
self.updateEditor(event.changes, event.posChanges);
});
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);
awaitCommit: function(cb) {
if (this.actionBufferTimeout === null) {
cb();
} else {
this.commitCallbacks.push(cb);
}
},
remoteChange: function CCHremoteUpdate(event) {
this.updateEditor(event.changes, event.posChanges);
commit: function() {
this.canopClient.actAtomically(this.actionBuffer);
this.actionBuffer = [];
this.actionBufferTimeout = null;
for (var i = 0; i < this.commitCallbacks.length; i++) {
this.commitCallbacks[i]();
}
this.commitCallbacks = [];
},
editorChange: function CCHeditorChange(editor, change, actions) {
var actions = actions || [];
// Listen to UI changes and register them in canop.
editorChange: function canopCodemirrorEditorChange(editor, change) {
var actions = [];
var from = change.from;
var to = change.to;
var added = change.text.join('\n');
var removed = change.removed.join('\n');
var removed = editor.getRange(from, to, '\n');
var fromIdx = editor.indexFromPos(from);

@@ -96,10 +75,10 @@ if (removed.length > 0) {

}
if (change.next) {
this.editorChange(editor, change.next, actions);
} else {
this.canopClient.actAtomically(actions);
this.actionBuffer = this.actionBuffer.concat(actions);
if (this.actionBufferTimeout === null) {
this.actionBufferTimeout = setTimeout(this.commit, 100);
}
},
cursorActivity: function CCHcursorActivity(editor) {
cursorActivity: function canopCodemirrorCursorActivity(editor) {
var self = this;

@@ -115,41 +94,86 @@ var selections = self.editor.listSelections().map(function(selection) {

resetEditor: function CCHresetEditor() {
this.editor.off('change', this.editorChange);
// action: function
withoutEditorListeners: function(action) {
this.editor.off('beforeChange', this.editorChange);
this.editor.off('cursorActivity', this.cursorActivity);
var cursor = this.editor.getCursor();
this.editor.setValue('' + this.canopClient);
this.editor.setCursor(cursor);
action();
this.editor.on('cursorActivity', this.cursorActivity);
this.editor.on('change', this.editorChange);
this.editor.on('beforeChange', this.editorChange);
},
resetEditor: function canopCodemirrorResetEditor() {
var self = this;
self.withoutEditorListeners(function() {
var cursor = self.editor.getCursor();
self.editor.setValue('' + self.canopClient);
self.editor.setCursor(cursor);
});
},
// Takes a list of AtomicOperations and a list of PosChanges.
updateEditor: function CCHupdateEditor(delta, posChanges) {
this.editor.off('change', this.editorChange);
this.editor.off('cursorActivity', this.cursorActivity);
var cursor = this.editor.indexFromPos(this.editor.getCursor());
this.applyDelta(delta);
this.updateCursor(posChanges, cursor);
this.editor.on('cursorActivity', this.cursorActivity);
this.editor.on('change', this.editorChange);
updateEditor: function canopCodemirrorUpdateEditor(delta, posChanges) {
var self = this;
self.withoutEditorListeners(function() {
var selections = self.getSelections();
self.applyDelta(delta);
self.setSelections(posChanges, selections);
});
},
applyDelta: function CCHapplyDelta(delta) {
for (var i = 0; i < delta.length; i++) {
var change = delta[i];
if (change[1] === canop.action.set) {
this.editor.setValue(change[2]);
} else if (change[1] === canop.action.stringAdd) {
this.editor.replaceRange(change[3], this.editor.posFromIndex(change[2]));
} else if (change[1] === canop.action.stringRemove) {
var from = this.editor.posFromIndex(change[2]);
var to = this.editor.posFromIndex(change[2] + change[3].length);
this.editor.replaceRange('', from, to);
applyDelta: function canopCodemirrorApplyDelta(delta) {
var self = this;
self.editor.changeGeneration(true);
self.editor.operation(function() {
for (var i = 0; i < delta.length; i++) {
var change = delta[i];
if (change[1] === canop.action.set) {
self.editor.setValue(change[2]);
} else if (change[1] === canop.action.stringAdd) {
var addPos = self.editor.posFromIndex(change[2]);
self.editor.replaceRange(change[3], addPos, addPos, '*');
} else if (change[1] === canop.action.stringRemove) {
var from = self.editor.posFromIndex(change[2]);
var to = self.editor.posFromIndex(change[2] + change[3].length);
self.editor.replaceRange('', from, to, '*');
}
}
});
},
editorUndo: function() {
var self = this;
self.withoutEditorListeners(function() {
self.applyDelta(self.canopClient.undo());
});
},
editorRedo: function() {
var self = this;
self.withoutEditorListeners(function() {
self.applyDelta(self.canopClient.redo());
});
},
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;
},
updateCursor: function CCHupdateCursor(posChanges, oldCursor) {
cursor = canop.changePosition(oldCursor, posChanges, true);
this.editor.setCursor(this.editor.posFromIndex(cursor));
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);
},

@@ -159,3 +183,3 @@

signalReceive: function CCHsignalReceive(event) {
signalReceive: function canopCodemirrorSignalReceive(event) {
var clientId = event.clientId;

@@ -187,3 +211,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)];

@@ -197,3 +221,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);

@@ -227,3 +251,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);

@@ -245,3 +269,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;

@@ -272,3 +296,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;

@@ -284,3 +308,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; }

@@ -292,3 +316,7 @@ return this.rgbFromLch(luma, 0.6, this.hueFromName(name));

exports.CanopCodemirrorHook = CanopCodemirrorHook;
canop.ui = canop.ui || {};
canop.ui.codemirror = function(client, editor) {
return new CanopCodemirror(client, editor);
};
}(this));

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc