Comparing version 1.1.2 to 2.0.0
@@ -33,3 +33,3 @@ /** | ||
this.master = null | ||
this.queue = queue() | ||
@@ -70,7 +70,6 @@ this.queue.concurrency = 1 | ||
* Creates a new Link and attaches it as a slave | ||
* @param authorizeFn (optional) function(msg, credentials, cb) that authorizes the message | ||
* @param opts Options to be passed to Link constructor | ||
*/ | ||
Document.prototype.slaveLink = function(authorizeFn) { | ||
var link = new Link | ||
link.authorizeFn = authorizeFn | ||
Document.prototype.slaveLink = function(opts) { | ||
var link = new Link(opts) | ||
this.attachSlaveLink(link) | ||
@@ -83,7 +82,6 @@ return link | ||
* (You will want to listen to the link's 'close' event) | ||
* @param credentials (optional) A string or object containing the client'S credentials | ||
* @param opts Options to be passed to Link constructor | ||
*/ | ||
Document.prototype.masterLink = function(credentials) { | ||
var link = new Link | ||
link.credentials = credentials | ||
Document.prototype.masterLink = function(opts) { | ||
var link = new Link(opts) | ||
this.attachMasterLink(link) | ||
@@ -101,3 +99,3 @@ return link | ||
this.attachLink(link) | ||
link.on('editError', function() { | ||
@@ -118,3 +116,3 @@ link.send('requestInit') | ||
this.attachLink(link) | ||
link.on('editError', function() { | ||
@@ -171,3 +169,3 @@ this.history.latest(function(er, latest) { | ||
* | ||
* @param data {Object} Example: {content: "", initialEdit: <Edit..>} | ||
* @param data {Object} Example: {contents: "", edit: <Edit..>} | ||
*/ | ||
@@ -197,3 +195,5 @@ Document.prototype.receiveInit = function(data, fromLink) { | ||
}.bind(this)) | ||
this.emit('init') | ||
return content | ||
@@ -249,3 +249,3 @@ } | ||
} | ||
// Check integrity of this edit | ||
@@ -264,3 +264,3 @@ this.history.remembers(edit.parent, function(er, remembersParent) { | ||
} | ||
try { | ||
@@ -267,0 +267,0 @@ this.applyEdit(edit) |
@@ -51,3 +51,3 @@ /** | ||
var edit = Edit.newFromChangeset(cs, this.ottype) | ||
this.history.latest(function(er, latestSnapshot) { | ||
@@ -57,3 +57,5 @@ if(er) throw er | ||
edit.parent = latestSnapshot.edit.id | ||
this.emit('update', edit) | ||
// Merge into the queue for increased collab speed | ||
@@ -60,0 +62,0 @@ if(false && this.master.queue.length == 1) { |
@@ -27,7 +27,9 @@ /** | ||
*/ | ||
// XX Must have the same terminology of #awaitingAck and #sent as prismIO.connection | ||
function Link () { | ||
this.credentials | ||
this.authorizeFn | ||
function Link (opts) { | ||
if(!opts) opts = {} | ||
this.credentials = opts.credentials | ||
this.sentCredentials | ||
this.receivedCredentials | ||
this.authorizeReadFn = opts.authorizeRead | ||
this.authorizeWriteFn = opts.authorizeWrite | ||
this.sentEdit | ||
@@ -42,3 +44,3 @@ this.queue | ||
}.bind(this)) | ||
this.on('editError', function(er) { | ||
@@ -65,7 +67,13 @@ console.warn('EditError in link', 'undefined'!=typeof window? er : er.stack || er) | ||
Link.prototype.send = function(event/*, args..*/) { | ||
var data = JSON.stringify(Array.prototype.slice.call(arguments).concat([this.credentials])) | ||
console.log('->', data) | ||
//console.trace() | ||
var data = JSON.stringify(Array.prototype.slice.call(arguments)) | ||
this.authorizeRead(Array.prototype.slice.apply(arguments), function(er, authorized) { | ||
if(er) return this.emit('error', er) | ||
if(!authorized) return this.sendUnauthorized() | ||
console.log('->', data) | ||
this.push(data) | ||
}.bind(this)) | ||
} | ||
this.push(data) | ||
Link.prototype.sendUnauthorized = function() { | ||
this.push(JSON.stringify(['unauthorized'])) | ||
} | ||
@@ -103,6 +111,25 @@ | ||
this.authorize(args, args[args.length-1], function(er, authorized) { | ||
if(args[0] === 'authenticate') { | ||
this.receivedCredentials = args[1] | ||
cb() | ||
return | ||
} | ||
if(args[0] === 'unauthorized') { | ||
if(this.sentCredentials) return this.emit('error', new Error('Authentication failed')) | ||
this.send('authenticate', this.credentials) | ||
this.send('requestInit') | ||
this.sentCredentials = true | ||
cb() | ||
return | ||
} | ||
this.authorizeWrite(args, function(er, authorized) { | ||
if(er) this.emit('error', er) | ||
if(!authorized) return | ||
if(!authorized) { | ||
this.sendUnauthorized() | ||
cb() | ||
return | ||
} | ||
@@ -139,5 +166,10 @@ // Intercept acks for shifting the queue and calling callbacks | ||
Link.prototype.authorize = function(msg, credentials, cb) { | ||
if(!this.authorizeFn) return cb(null, true) | ||
this.authorizeFn(msg, credentials, cb) | ||
Link.prototype.authorizeWrite = function(msg, cb) { | ||
if(!this.authorizeWriteFn) return cb(null, true) | ||
this.authorizeWriteFn(msg, this.receivedCredentials, cb) | ||
} | ||
Link.prototype.authorizeRead = function(msg, cb) { | ||
if(!this.authorizeReadFn) return cb(null, true) | ||
this.authorizeReadFn(msg, this.receivedCredentials, cb) | ||
} |
{ | ||
"name": "gulf", | ||
"version": "1.1.2", | ||
"version": "2.0.0", | ||
"description": "transport-agnostic operational transformation control layer", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -148,2 +148,94 @@ # Gulf [![Build Status](https://travis-ci.org/marcelklehr/gulf.png)](https://travis-ci.org/marcelklehr/gulf) | ||
## API | ||
### Class: gulf.Link | ||
#### new gulf.Link([opts:Object]) | ||
Instantiates a new link, optionally with some options: | ||
* `opts.credentials` The credentials to be sent to the other end for authentication purposes. | ||
* `opts.authorizeWrite` A function which gets called when the other end writes a message, and has the following signature: `function (msg, receivedCredentials, cb)` | ||
* `opts.authorizeRead` A function which gets called when this side of the link writes a message, and has the following signature: `function (msg, receivedCredentials, cb)` | ||
### Class: gulf.Document | ||
#### new gulf.Document(adapter, ottype) | ||
Instantiates a new, empty Document. | ||
#### gulf.Document.create(adapter, ottype, contents, cb) | ||
Creates a documents with pre-set contents. `cb` will be called with `(er, doc)`. | ||
#### gulf.Document.load(adapter, ottype, cb) | ||
Loads a document from the storage. Since there's one instance of a storage adapter per document, you need to pass the information *which* document to load to the adapter instance. `cb` will be called with `(er, doc)`. | ||
#### gulf.Document#slaveLink(opts:Object) : Link | ||
Creates a link with `opts` passed to the Link constructor and attaches it as a slave link. | ||
#### gulf.Document#masterLink(opts:Object) : Link | ||
Creates a link with `opts` passed to the Link constructor and attaches it as a master link. | ||
#### gulf.Document#attachMasterLink(link:Link) | ||
Attaches an existing link as master. | ||
#### gulf.Document#attachSlaveLink(link:Link) | ||
Attaches an existing link as a slave. | ||
#### gulf.Document#receiveInit(data:Object, fromLink:Link) | ||
Listener function that gets called when a link attached to this document receives an `init` message. `data` could look like this: `{contents: 'abc', edit: '<Edit>'}` | ||
#### gulf.Document#receiveEdit(edit:String, fromLink:Link, [callback:Function]) | ||
A listener function that gets called when a link receives an `edit` message. Adds the edit to the queue (after checking with a possible master) and calls Document#dispatchEdit() when ready. | ||
#### gulf.Document#dispatchEdit(edit:Edit, fromLink:Link, cb:Function) | ||
Checks with the document's History whether we know this edit already, and if not, whether we know its parent. If so, it calls Document#sanitizeEdit(), applies the edit to this document with Document#applyEdit(), add it to this document's History, send's an `ack` message to the link the edit came from, distributes the edit to any slaves and emits an `edit` event. | ||
#### gulf.Document#sanitizeEdit(edit:Edit, fromLink:Link, cb:Function) | ||
Transforms the passed edit against missed edits according to this document's history and the edit's parent information. | ||
#### gulf.Document#applyEdit(edit:Edit) | ||
Applies an edit to the document's content. | ||
#### gulf.Document#distributeEdit(edit:Edit, [fromLink:Link]) | ||
Sends the passed edit to all attached links, except to `fromLink`. | ||
### Class: gulf.EditableDocument | ||
This class extends `gulf.Document` and overrides some of its methods.Most important among those are `gulf.EditableDocument#sanitizeEdit()` which is changed to transform the incoming edit against the ones queued in the master link and transform those against the incoming one, and `glf.EditableDocument#applyEdit` which is changed to call `_change` with the changes and the resulting contents. | ||
#### gulf.EditableDocument#update(changes:mixed) | ||
Update an editable document with local changes provided in `changes`. This wraps the changes in an Edit and sends them to master. | ||
### Class: gulf.Edit | ||
#### new Edit(ottype) | ||
instantiates a new edit without parent, changes or id. Thus it's pretty useless. | ||
#### gulf.Edit.unpack(json:String, ottype) : Edit | ||
Deserializes an edit that was serialized using `gulf.Edit#pack()`. | ||
#### gulf.Edit.newInitial(ottype) : Edit | ||
Creates a new initial edit with a random id. Initial edits carry an id but no changes. | ||
#### gulf.Edit.newFromChangeset(cs:mixed, ottype) : Edit | ||
Creates a new edit with a random id and changes set to `cs`. | ||
#### gulf.Edit.fromSnapshot(snapshot, ottype) : Edit | ||
Recreates an edit from a Snapshot. A snapshot is how edits are stored in gulf. It's an object with {id, changes, parent, contents}, which should all be pretty self-explanatory. | ||
#### gulf.Edit#apply(documentContents) | ||
Applies this edit on a document snapshot. | ||
#### gulf.Edit#folow(edit:Edit) | ||
transforms this edit against the passed one and sets the other as this edit's parent. (This operation rewrites history.) | ||
#### gulf.Edit#transformAgainst(edit:Edit) | ||
transforms this edit against the passed one and without resetting this edit's parent. | ||
#### gulf.Edit#merge(edit:Edit) : Edit | ||
merges the passed edit with this one. Returns the new edit. | ||
#### gulf.Edit#pack() : String | ||
Serializes this edit. | ||
#### gulf.Edit#clone() : Edit | ||
Returns a new edit instance that has exactly the same properties as this one. | ||
## FAQ | ||
@@ -150,0 +242,0 @@ |
@@ -19,3 +19,3 @@ /* global xdescribe, describe, it, xit */ | ||
var linkA, linkB | ||
before(function(cb) { | ||
@@ -63,4 +63,4 @@ gulf.Document.create(new gulf.MemoryAdapter, ottype, 'abc', function(er, doc) { | ||
}) | ||
/*linkA.on('link:edit', console.log.bind(console, 'edit in linkA')) | ||
@@ -109,6 +109,6 @@ linkA.on('link:ack', console.log.bind(console, 'ack in linkA')) | ||
}) | ||
it('should re-init on error', function(done) { | ||
docB.update([10, 'e']) // an obviously corrupt edit | ||
setTimeout(function() { | ||
@@ -120,7 +120,7 @@ console.log('DocB:', docB.content, 'DocA', docA.content) | ||
}) | ||
it('should propagate edits correctly after re-init', function(done) { | ||
content = 'abcdefgh' | ||
docB.update([7, 'h']) // an correct edit | ||
setTimeout(function() { | ||
@@ -134,3 +134,3 @@ console.log('DocB:', docB.content, 'DocA', docA.content) | ||
}) | ||
describe('Linking two editable documents via a master', function() { | ||
@@ -167,7 +167,7 @@ var initialContent = 'abc' | ||
}) | ||
it('should correctly propagate the initial contents', function(cb) { | ||
linkA.pipe(masterDoc.slaveLink()).pipe(linkA) | ||
linkB.pipe(masterDoc.slaveLink()).pipe(linkB) | ||
setTimeout(function() { | ||
@@ -179,7 +179,7 @@ expect(contentA).to.eql(initialContent) | ||
}) | ||
it('should correctly propagate the first edit from one end to the other end', function(cb) { | ||
contentA = 'abcd' | ||
docA.update([3, 'd']) | ||
setTimeout(function() { | ||
@@ -192,3 +192,3 @@ expect(docA.content).to.eql(contentA) | ||
}) | ||
it('should correctly propagate edits from one end to the other end', function(cb) { | ||
@@ -202,9 +202,9 @@ linkA.unpipe() | ||
docA.update([4, '1']) | ||
contentB = 'abcd2' | ||
docB.update([4, '2']) | ||
linkA.pipe(masterDoc.slaveLink()).pipe(linkA) | ||
linkB.pipe(masterDoc.slaveLink()).pipe(linkB) | ||
setTimeout(function() { | ||
@@ -216,2 +216,45 @@ expect(contentB).to.eql(contentA) | ||
}) | ||
describe('Linking to protected documents', function() { | ||
var docA, docB | ||
var linkA, linkB | ||
beforeEach(function(cb) { | ||
gulf.Document.create(new gulf.MemoryAdapter, ottype, 'abc', function(er, doc) { | ||
docA = doc | ||
docB = new gulf.Document(new gulf.MemoryAdapter, ottype) | ||
linkA = docA.slaveLink({ | ||
authorizeRead: function(msg, credentials, cb) { | ||
if(credentials == 'rightCredentials') return cb(null, true) | ||
else return cb(null, false) | ||
} | ||
, authorizeWrite: function(msg, credentials, cb) { | ||
if(credentials == 'rightCredentials') return cb(null, true) | ||
else return cb(null, false) | ||
} | ||
}) | ||
cb() | ||
}) | ||
}) | ||
it('should adopt the current document state correctly', function(done) { | ||
linkB = docB.masterLink({credentials: 'rightCredentials'}) | ||
linkA.pipe(linkB).pipe(linkA) | ||
setTimeout(function() { | ||
expect(docA.content).to.eql(docB.content) | ||
done() | ||
}, 100) | ||
}) | ||
it('should not adopt the current document state if athentication failed', function(done) { | ||
linkB = docB.masterLink({credentials: 'wrongCredentials'}) | ||
linkA.pipe(linkB).pipe(linkA) | ||
setTimeout(function() { | ||
expect(docB.content).to.eql(null) | ||
done() | ||
}, 100) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
80814
1000
257