Comparing version 0.3.0 to 0.4.0
@@ -7,2 +7,3 @@ module.exports = { | ||
, History: require('./lib/History') | ||
, MemoryAdapter: require('./lib/MemoryAdapter') | ||
} |
@@ -5,6 +5,7 @@ var Link = require('./Link') | ||
function Document(ottype) { | ||
function Document(adapter, ottype) { | ||
this.adapter = adapter | ||
this.ottype = ottype | ||
this.content = null | ||
this.history = new History | ||
this.history = new History(this) | ||
this.slaves = [] | ||
@@ -15,2 +16,3 @@ this.links = [] | ||
if(!this.ottype) throw new Error('Document: No ottype specified') | ||
if(!this.adapter) throw new Error('Document: No adapter specified') | ||
} | ||
@@ -23,7 +25,8 @@ | ||
*/ | ||
Document.create = function(ottype, content) { | ||
var doc = new Document(ottype) | ||
Document.create = function(adapter, ottype, content, cb) { | ||
var doc = new Document(adapter, ottype) | ||
doc.content = content | ||
doc.history.pushEdit(Edit.newInitial(ottype)) | ||
return doc | ||
doc.history.pushEdit(Edit.newInitial(ottype), function(er) { | ||
cb(er, doc) | ||
}) | ||
} | ||
@@ -86,3 +89,6 @@ | ||
} | ||
link.send('init', {content: this.content, initialEdit: this.history.latest().pack()}) | ||
this.history.latest(function(er, latest) { | ||
if(er) return link.emit('error', er) | ||
link.send('init', {content: this.content, initialEdit: latest.pack()}) | ||
}.bind(this)) | ||
}.bind(this)) | ||
@@ -124,7 +130,9 @@ | ||
this.history.reset() | ||
this.history.pushEdit(initialEdit) | ||
// I got an init, so my slaves get one, too | ||
this.slaves.forEach(function(slave) { | ||
slave.send('init', {content: this.content, initialEdit: this.history.latest().pack()}) | ||
this.history.pushEdit(initialEdit, function() { | ||
// I got an init, so my slaves get one, too | ||
this.slaves.forEach(function(slave) { | ||
this.history.latest(function(er, latest) { | ||
slave.send('init', {content: this.content, initialEdit: latest.pack()}) | ||
}) | ||
}.bind(this)) | ||
}.bind(this)) | ||
@@ -159,27 +167,31 @@ } | ||
Document.prototype.dispatchEdit = function(edit, fromLink) { | ||
if (this.history.remembers(edit.id)) { | ||
// We've got this edit already. | ||
if(fromLink) fromLink.send('ack', edit.id) | ||
return | ||
} | ||
this.history.remembers(edit.id, function(er, remembers) { | ||
if(er) return fromLink.emit('error',er) | ||
try { | ||
if (remembers) { | ||
// We've got this edit already. | ||
if(fromLink) fromLink.send('ack', edit.id) | ||
return | ||
} | ||
// Check integrity of this edit | ||
if (!this.history.remembers(edit.parent)) { | ||
throw new Error('Edit "'+edit.id+'" has unknown parent "'+edit.parent+'"') | ||
} | ||
this.history.remembers(edit.parent, function(er, remembersParent) { | ||
if (!remembersParent) { | ||
return fromLink.emit('error', new Error('Edit "'+edit.id+'" has unknown parent "'+edit.parent+'"')) | ||
} | ||
this.applyEdit(this.sanitizeEdit(edit, fromLink)) | ||
}catch(er) { | ||
if(!fromLink) throw er // XXX: In case of an error we can't just terminate the link, | ||
// what if it's using us as a master link and just passing on an | ||
// edit for us to verify? | ||
fromLink.emit('error', er) // ^^ | ||
} | ||
if(fromLink) fromLink.send('ack', edit.id) | ||
this.distributeEdit(edit, fromLink) | ||
this.sanitizeEdit(edit, fromLink, function(er, edit) { | ||
if(er) return fromLink.emit('error', er) | ||
try { | ||
this.applyEdit(edit) | ||
}catch(er) { | ||
fromLink.emit('error', er) | ||
} | ||
if(fromLink) fromLink.send('ack', edit.id) | ||
this.distributeEdit(edit, fromLink) | ||
}.bind(this)) | ||
}.bind(this)) | ||
}.bind(this)) | ||
} | ||
@@ -190,3 +202,3 @@ | ||
*/ | ||
Document.prototype.sanitizeEdit = function(edit, fromLink) { | ||
Document.prototype.sanitizeEdit = function(edit, fromLink, cb) { | ||
@@ -197,6 +209,7 @@ if(this.master === fromLink) { | ||
// add to history + set id | ||
this.history.pushEdit(edit) | ||
return edit | ||
// add to history | ||
this.history.pushEdit(edit, function(er) { | ||
if(er) return cb(er) | ||
cb(null, edit) | ||
}) | ||
}else { | ||
@@ -206,12 +219,14 @@ // We are master! | ||
// Transform against missed edits from history that have happened in the meantime | ||
var missed = this.history.getAllAfter(edit.parent) | ||
missed.forEach(function(oldEdit) { | ||
edit.follow(oldEdit) | ||
}) | ||
this.history.getAllAfter(edit.parent, function(er, missed) { | ||
if(er) return cb(er) | ||
// add to history + set id | ||
this.history.pushEdit(edit) | ||
missed.forEach(function(oldEdit) { | ||
edit.follow(oldEdit) | ||
}) | ||
return edit | ||
// add to history | ||
this.history.pushEdit(edit) | ||
cb(null, edit) | ||
}.bind(this)) | ||
} | ||
@@ -218,0 +233,0 @@ } |
@@ -35,22 +35,27 @@ var Document = require('./Document') | ||
var edit = Edit.newFromChangeset(cs, this.ottype) | ||
edit.parent = this.history.latest().id | ||
this.history.latest(function(er, latestEdit) { | ||
if(er) throw er | ||
// Merge into the queue for increased collab speed | ||
if(this.master.queue.length == 1) { | ||
var parent = this.master.queue[0].parent | ||
, callback =this.master.queue[0].callback | ||
this.master.queue[0] = this.master.queue[0].merge(edit) | ||
this.master.queue[0].callback = callback | ||
this.master.queue[0].parent = parent | ||
return | ||
} | ||
edit.parent = latestEdit.id | ||
// Merge into the queue for increased collab speed | ||
if(this.master.queue.length == 1) { | ||
var parent = this.master.queue[0].parent | ||
, callback =this.master.queue[0].callback | ||
this.master.queue[0] = this.master.queue[0].merge(edit) | ||
this.master.queue[0].callback = callback | ||
this.master.queue[0].parent = parent | ||
return | ||
} | ||
this.master.sendEdit(edit, function onack(err, edit) { | ||
// Update queue | ||
this.master.queue.forEach(function(queuedEdit) { | ||
queuedEdit.parent = edit.id | ||
}) | ||
this.applyEdit(edit, true) | ||
//this.distributeEdit(edit) // Unnecessary round trip | ||
this.history.pushEdit(edit) | ||
this.master.sendEdit(edit, function onack(err, edit) { | ||
// Update queue | ||
this.master.queue.forEach(function(queuedEdit) { | ||
queuedEdit.parent = edit.id | ||
}) | ||
this.applyEdit(edit, true) | ||
//this.distributeEdit(edit) // Unnecessary round trip | ||
this.history.pushEdit(edit) | ||
}.bind(this)) | ||
}.bind(this)) | ||
@@ -60,3 +65,3 @@ } | ||
// overrides Document#sanitizeEdit | ||
EditableDocument.prototype.sanitizeEdit = function(incoming, fromLink) { | ||
EditableDocument.prototype.sanitizeEdit = function(incoming, fromLink, cb) { | ||
// Collect undetected local changes, before applying the new edit | ||
@@ -96,5 +101,7 @@ this._collectChanges() | ||
// add edit to history | ||
this.history.pushEdit(incoming) | ||
return incoming | ||
this.history.pushEdit(incoming, function(er) { | ||
if(er) return cb(er) | ||
cb(null, incoming) | ||
}) | ||
} | ||
@@ -101,0 +108,0 @@ |
// Stores revisions that are synced with the server | ||
function History() { | ||
this.reset() | ||
function History(document) { | ||
this.document = document | ||
} | ||
module.exports = History | ||
History.prototype.earliest = function() { | ||
if(!this.history.length) return | ||
return this.edits[this.history[0]] | ||
History.prototype.reset = function() { | ||
} | ||
History.prototype.latest = function() { | ||
if(!this.history.length) return | ||
return this.edits[this.history[this.history.length-1]] | ||
History.prototype.earliest = function(cb) { | ||
this.document.adapter.getEarliestEdit(cb) | ||
} | ||
History.prototype.pushEdit = function(edit) { | ||
History.prototype.latest = function(cb) { | ||
this.document.adapter.getLatestEdit(cb) | ||
} | ||
History.prototype.pushEdit = function(edit, cb) { | ||
// Only Master Document may set ids | ||
if(!edit.id) edit.id = ++this.idCounter | ||
if(this.latest() && this.latest().id != edit.parent) throw new Error('This edit\'s parent is not the latest edit in history: '+JSON.stringify(edit), console.log(this.history)) | ||
this.history.push(edit.id) | ||
this.edits[edit.id] = edit | ||
this.latest(function(er, latest) { | ||
if(er) cb && cb(er) | ||
if(latest && latest.id != edit.parent) cb && cb(new Error('This edit\'s parent is not the latest edit in history: '+JSON.stringify(edit))) | ||
this.document.adapter.storeEdit(edit, function(er) { | ||
cb && cb(er) | ||
}) | ||
}.bind(this)) | ||
} | ||
History.prototype.reset = function() { | ||
this.edits = {} | ||
this.history = [] | ||
this.idCounter = 0 | ||
History.prototype.remembers = function(editId, cb) { | ||
this.document.adapter.existsEdit(editId, function(er, remembers) { | ||
cb && cb(er, remembers) | ||
}) | ||
} | ||
History.prototype.remembers = function(editId) { | ||
return (this.edits[editId] && true) | ||
History.prototype.getAllAfter = function(editId, cb) { | ||
this.document.adapter.getEditsAfter(editId, function(er, edits) { | ||
cb && cb(er, edits) | ||
}) | ||
} | ||
History.prototype.getAllAfter = function(editId) { | ||
var arr = [] | ||
for(var i = this.history.indexOf(editId)+1; i < this.history.length; i++) { | ||
arr.push(this.edits[this.history[i]]) | ||
} | ||
return arr | ||
} | ||
/** | ||
* Prunes an edit from a history of edits (supplied as a list of edit ids) | ||
* | ||
* @return a chronological list of edit ojects | ||
*/ | ||
History.prototype.pruneFrom = function(editId, history) { | ||
if(!this.edits[editId]) throw new Error('Can\'t prune unknown edit') | ||
if(!~history.indexOf(editId)) return history // Nothing to prune | ||
var pruningEdit = this.edits[editId].clone().invert() | ||
var prunedHistory = history.slice(history.indexOf(editId)+1) | ||
.map(function(otherEdit) { | ||
if(!this.edits[otherEdit]) throw new Error('Can\'t reconstruct edit '+otherEdit) | ||
otherEdit = this.edits[otherEdit].clone() | ||
otherEdit.transformAgainst(pruningEdit) | ||
pruningEdit.transformAgainst(otherEdit) | ||
return otherEdit | ||
}) | ||
return prunedHistory | ||
} |
{ | ||
"name": "gulf", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Sync anything!", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -19,3 +19,3 @@ # Gulf [![Build Status](https://travis-ci.org/marcelklehr/gulf.png)](https://travis-ci.org/marcelklehr/gulf) | ||
// Create a new master document | ||
var doc = gulf.Document.create(textOT, 'abc') | ||
var doc = gulf.Document.create(new gulf.MemoryAdapter, textOT, 'abc') | ||
@@ -42,3 +42,3 @@ // Set up a server | ||
// Create a new slave document (empty by default) | ||
var doc = new gulf.Document(textOT) | ||
var doc = new gulf.Document(new gulf.MemoryAdapter, textOT) | ||
@@ -85,3 +85,3 @@ // Connect to alice's server | ||
```js | ||
masterDoc = new gulf.Document(ot) | ||
masterDoc = new gulf.Document(adapter, ot) | ||
@@ -92,3 +92,3 @@ socket.pipe(masterDoc.slaveLink()).pipe(socket) | ||
```js | ||
slaveDoc = new gulf.Document(ot) | ||
slaveDoc = new gulf.Document(adapter, ot) | ||
@@ -103,3 +103,3 @@ socket.pipe(slaveDoc.masterLink()).pipe(socket) | ||
```js | ||
var document = new gulf.EditableDocument(ottype) | ||
var document = new gulf.EditableDocument(adapter, ottype) | ||
@@ -128,3 +128,3 @@ document._change = function(newcontent, cs) { | ||
You can use [shareJS's built in ottypes](https://github.com/share/ottypes) or [other](https://github.com/marcelklehr/changesets) [libraries](https://github.com/marcelklehr/dom-ot). | ||
You can use [shareJS's built in ottypes](https://github.com/share/ottypes) or [some other](https://github.com/marcelklehr/changesets) [libraries](https://github.com/marcelklehr/dom-ot). | ||
@@ -136,5 +136,11 @@ For example, you could use shareJS's OT engine for plain text. | ||
var document = new gulf.Document(textOT) | ||
var document = new gulf.Document(new gulf.MemoryAdapter, textOT) | ||
``` | ||
## Storage adapters | ||
Gulf allows you to store your data anywhere you like, if you can provide it with a storage adapter. It comes with an in-memory adapter, ready for you to test your app quickly, but when the time comes to get ready for production you will want to change to a persistent storage backend like mongoDB or redis. | ||
Currently implemented adapters are: | ||
* [In-memory adapter](https://github.com/marcelklehr/gulf/blob/master/lib/MemoryAdapter.js) | ||
## FAQ | ||
@@ -154,3 +160,3 @@ | ||
## Todo | ||
* per-document edit queue | ||
* Check whether objects might get ripped apart in raw streams | ||
@@ -157,0 +163,0 @@ * Catch misusage (i.e. attaching the same link twice to the same or different docs, employing a pipeline as master on both ends, piping a link twice -- is that even possible?) |
@@ -17,8 +17,15 @@ /* global xdescribe, describe, it, xit */ | ||
describe('Linking to new documents', function() { | ||
var docA = gulf.Document.create(ottype, 'abc') | ||
, docB = new gulf.Document(ottype) | ||
var docA, docB | ||
var linkA, linkB | ||
before(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() | ||
linkB = docB.masterLink() | ||
cb() | ||
}) | ||
}) | ||
var linkA = docA.slaveLink() | ||
, linkB = docB.masterLink() | ||
it('should adopt the current document state correctly', function(done) { | ||
@@ -36,14 +43,24 @@ linkA.pipe(linkB).pipe(linkA) | ||
var initialContent = 'abc' | ||
var docA = gulf.Document.create(ottype, initialContent) | ||
, docB = new gulf.EditableDocument(ottype) | ||
var docA, docB | ||
var linkA, linkB | ||
var content | ||
var content = '' | ||
docB._change = function(newcontent, cs) { | ||
content = newcontent | ||
console.log('_change: ', newcontent) | ||
} | ||
before(function(cb) { | ||
gulf.Document.create(new gulf.MemoryAdapter, ottype, initialContent, function(er, doc) { | ||
docA = doc | ||
docB = new gulf.EditableDocument(new gulf.MemoryAdapter, ottype) | ||
var linkA = docA.slaveLink() | ||
, linkB = docB.masterLink() | ||
content = '' | ||
docB._change = function(newcontent, cs) { | ||
content = newcontent | ||
console.log('_change: ', newcontent) | ||
} | ||
linkA = docA.slaveLink() | ||
linkB = docB.masterLink() | ||
cb() | ||
}) | ||
}) | ||
/*linkA.on('link:edit', console.log.bind(console, 'edit in linkA')) | ||
@@ -76,3 +93,3 @@ linkA.on('link:ack', console.log.bind(console, 'ack in linkA')) | ||
done() | ||
}, 100) | ||
}, 0) | ||
}) | ||
@@ -91,5 +108,5 @@ | ||
done() | ||
}, 100) | ||
}, 20) | ||
}) | ||
}) | ||
}) |
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
31067
15
656
160