@sanity/mutator
Advanced tools
Comparing version 0.1.6 to 0.1.7
@@ -52,5 +52,5 @@ 'use strict'; | ||
var BufferedDocument = function () { | ||
// Local mutations that are not scheduled to be committed yet | ||
// Commits that are waiting to be delivered to the server | ||
// The Document with local changes applied | ||
// The Document we are wrapping | ||
function BufferedDocument(doc) { | ||
@@ -73,10 +73,20 @@ var _this = this; | ||
// Add a change to the buffer | ||
// Used to reset the state of the local document model. If the model has been inconsistent | ||
// for too long, it has probably missed a notification, and should reload the document from the server | ||
// Commits that are waiting to be delivered to the server | ||
// Local mutations that are not scheduled to be committed yet | ||
// The Document we are wrapping | ||
// The Document with local changes applied | ||
_createClass(BufferedDocument, [{ | ||
key: 'reset', | ||
value: function reset(doc) { | ||
this.document.reset(doc); | ||
this.rebase(); | ||
} | ||
// Add a change to the buffer | ||
}, { | ||
key: 'add', | ||
@@ -162,3 +172,7 @@ value: function add(mutation) { | ||
commit.tries += 1; | ||
_this2.commits.unshift(commit); | ||
if (_this2.LOCAL !== null) { | ||
// Only schedule this commit for a retry of the document still exist to avoid looping | ||
// indefinitely when the document was deleted from under our noses | ||
_this2.commits.unshift(commit); | ||
} | ||
docResponder.failure(); | ||
@@ -184,2 +198,14 @@ // Retry | ||
}, { | ||
key: 'handleDocumentDeleted', | ||
value: function handleDocumentDeleted() { | ||
// If the document was just deleted, fire the onDelete event with the absolutely latest version of the document | ||
// before someone deleted it so that the client may revive the document in the last state the user saw it, should | ||
// she so desire. | ||
if (this.LOCAL !== null && this.onDelete) { | ||
this.onDelete(this.LOCAL); | ||
} | ||
this.commits = []; | ||
this.mutations = []; | ||
} | ||
}, { | ||
key: 'handleDocMutation', | ||
@@ -194,3 +220,9 @@ value: function handleDocMutation(msg) { | ||
} | ||
// It wasn't. Need to rebase | ||
// If there are local edits, and the document was deleted, we need to purge those local edits now | ||
if (this.document.EDGE === null) { | ||
this.handleDocumentDeleted(); | ||
} | ||
// We had local changes, so need to signal rebase | ||
this.rebase(); | ||
@@ -201,2 +233,6 @@ } | ||
value: function rebase() { | ||
if (this.document.EDGE === null) { | ||
this.handleDocumentDeleted(); | ||
} | ||
var oldLocal = this.LOCAL; | ||
@@ -208,3 +244,5 @@ this.LOCAL = this.commits.reduce(function (doc, commit) { | ||
// Copy over rev, since we don't care if it changed, we only care about the content | ||
oldLocal._rev = this.LOCAL._rev; | ||
if (oldLocal !== null && this.LOCAL !== null) { | ||
oldLocal._rev = this.LOCAL._rev; | ||
} | ||
var changed = !(0, _isEqual3.default)(this.LOCAL, oldLocal); | ||
@@ -211,0 +249,0 @@ if (changed && this.onRebase) { |
@@ -49,11 +49,6 @@ 'use strict'; | ||
this.incoming = []; | ||
this.submitted = []; | ||
this.pending = []; | ||
this.inconsistentAt = null; | ||
this.HEAD = doc; | ||
this.EDGE = doc; | ||
this.reset(doc); | ||
} | ||
// Call when a mutation arrives from Sanity | ||
// Reset the state of the Document, used to recover from unsavory states by reloading the document | ||
@@ -77,2 +72,17 @@ // The last time we staged a patch of our own. If we have been inconsistent for a while, but it hasn't been long since | ||
_createClass(Document, [{ | ||
key: 'reset', | ||
value: function reset(doc) { | ||
this.incoming = []; | ||
this.submitted = []; | ||
this.pending = []; | ||
this.inconsistentAt = null; | ||
this.HEAD = doc; | ||
this.EDGE = doc; | ||
this.considerIncoming(); | ||
this.updateConsistencyFlag(); | ||
} | ||
// Call when a mutation arrives from Sanity | ||
}, { | ||
key: 'arrive', | ||
@@ -147,7 +157,30 @@ value: function arrive(mutation) { | ||
var nextMut = void 0; | ||
// Filter mutations that are older than the document | ||
if (this.HEAD) { | ||
(function () { | ||
var updatedAt = new Date(_this2.HEAD._updatedAt); | ||
if (_this2.incoming.find(function (mut) { | ||
return mut.timestamp && mut.timestamp < updatedAt; | ||
})) { | ||
_this2.incoming = _this2.incoming.filter(function (mut) { | ||
return mut.timestamp < updatedAt; | ||
}); | ||
} | ||
})(); | ||
} | ||
// Keep applying mutations as long as any apply | ||
do { | ||
// Find next mutation that can be applied to HEAD (if any) | ||
nextMut = this.incoming.find(function (mut) { | ||
return mut.previousRev == _this2.HEAD._rev; | ||
}); | ||
if (this.HEAD) { | ||
nextMut = this.incoming.find(function (mut) { | ||
return mut.previousRev == _this2.HEAD._rev; | ||
}); | ||
} else { | ||
// When HEAD is null, that means the document is currently deleted. Only mutations that start with a create | ||
// operation will be considered. | ||
nextMut = this.incoming.find(function (mut) { | ||
return mut.appliesToMissingDocument(); | ||
}); | ||
} | ||
mustRebase = mustRebase || this.applyIncoming(nextMut); | ||
@@ -191,2 +224,9 @@ } while (nextMut); | ||
if (this.HEAD === null) { | ||
// If the document was deleted, clear all upcoming pending edits that do no apply to missing documents | ||
while (this.pending.length > 0 && !this.pending[0].appliesToMissingDocument()) { | ||
this.pending.shift(); | ||
} | ||
} | ||
// Eliminate from incoming set | ||
@@ -193,0 +233,0 @@ this.incoming = this.incoming.filter(function (m) { |
@@ -37,2 +37,17 @@ 'use strict'; | ||
} | ||
}, { | ||
key: 'appliesToMissingDocument', | ||
value: function appliesToMissingDocument() { | ||
if (typeof this._appliesToMissingDocument != 'undefined') { | ||
return this._appliesToMissingDocument; | ||
} | ||
// Only mutations starting with a create operation apply to documents that do not exist ... | ||
var firstMut = this.mutations[0]; | ||
if (firstMut) { | ||
this._appliesToMissingDocument = firstMut.create || firstMut.createIfNotExist || firstMut.createOrReplace; | ||
} else { | ||
this._appliesToMissingDocument = true; | ||
} | ||
return this._appliesToMissingDocument; | ||
} | ||
// Compiles all mutations into a handy function | ||
@@ -46,4 +61,12 @@ | ||
if (mutation.create) { | ||
operations.push(function (doc) { | ||
return doc === null ? mutation.create : doc; | ||
}); | ||
} else if (mutation.createIfNotExist) { | ||
operations.push(function (doc) { | ||
return doc === null ? mutation.createIfNotExist : doc; | ||
}); | ||
} else if (mutation.createOrReplace) { | ||
operations.push(function () { | ||
return mutation.create; | ||
return mutation.createOrReplace; | ||
}); | ||
@@ -118,2 +141,10 @@ } else if (mutation.delete) { | ||
} | ||
}, { | ||
key: 'timestamp', | ||
get: function get() { | ||
if (typeof this.params.timestamp == 'string') { | ||
return new Date(this.params.timestamp); | ||
} | ||
return undefined; | ||
} | ||
}], [{ | ||
@@ -120,0 +151,0 @@ key: 'applyAll', |
{ | ||
"name": "@sanity/mutator", | ||
"version": "0.1.6", | ||
"version": "0.1.7", | ||
"description": "A set of models to make it easier to utilize the powerful real time collaborative features of Sanity", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
@@ -37,2 +37,3 @@ // A wrapper for Document that allows the client to gather mutations on the client side and commit them | ||
onRebase : Function | ||
onDelete : Function | ||
commitHandler : Function | ||
@@ -48,2 +49,9 @@ constructor(doc) { | ||
// Used to reset the state of the local document model. If the model has been inconsistent | ||
// for too long, it has probably missed a notification, and should reload the document from the server | ||
reset(doc) { | ||
this.document.reset(doc) | ||
this.rebase() | ||
} | ||
// Add a change to the buffer | ||
@@ -115,3 +123,7 @@ add(mutation : Mutation) { | ||
commit.tries += 1 | ||
this.commits.unshift(commit) | ||
if (this.LOCAL !== null) { | ||
// Only schedule this commit for a retry of the document still exist to avoid looping | ||
// indefinitely when the document was deleted from under our noses | ||
this.commits.unshift(commit) | ||
} | ||
docResponder.failure() | ||
@@ -134,2 +146,13 @@ // Retry | ||
handleDocumentDeleted() { | ||
// If the document was just deleted, fire the onDelete event with the absolutely latest version of the document | ||
// before someone deleted it so that the client may revive the document in the last state the user saw it, should | ||
// she so desire. | ||
if (this.LOCAL !== null && this.onDelete) { | ||
this.onDelete(this.LOCAL) | ||
} | ||
this.commits = [] | ||
this.mutations = [] | ||
} | ||
handleDocMutation(msg) { | ||
@@ -143,3 +166,9 @@ // If we have no local changes, we can just pass this on to the client | ||
} | ||
// It wasn't. Need to rebase | ||
// If there are local edits, and the document was deleted, we need to purge those local edits now | ||
if (this.document.EDGE === null) { | ||
this.handleDocumentDeleted() | ||
} | ||
// We had local changes, so need to signal rebase | ||
this.rebase() | ||
@@ -149,2 +178,6 @@ } | ||
rebase() { | ||
if (this.document.EDGE === null) { | ||
this.handleDocumentDeleted() | ||
} | ||
const oldLocal = this.LOCAL | ||
@@ -154,3 +187,5 @@ this.LOCAL = this.commits.reduce((doc, commit) => commit.apply(doc), this.document.EDGE) | ||
// Copy over rev, since we don't care if it changed, we only care about the content | ||
oldLocal._rev = this.LOCAL._rev | ||
if (oldLocal !== null && this.LOCAL !== null) { | ||
oldLocal._rev = this.LOCAL._rev | ||
} | ||
const changed = !isEqual(this.LOCAL, oldLocal) | ||
@@ -157,0 +192,0 @@ if (changed && this.onRebase) { |
@@ -55,2 +55,7 @@ // @flow | ||
constructor(doc : Object) { | ||
this.reset(doc) | ||
} | ||
// Reset the state of the Document, used to recover from unsavory states by reloading the document | ||
reset(doc : Object) { | ||
this.incoming = [] | ||
@@ -62,2 +67,4 @@ this.submitted = [] | ||
this.EDGE = doc | ||
this.considerIncoming() | ||
this.updateConsistencyFlag() | ||
} | ||
@@ -121,5 +128,20 @@ | ||
let nextMut : Mutation | ||
// Filter mutations that are older than the document | ||
if (this.HEAD) { | ||
const updatedAt = new Date(this.HEAD._updatedAt) | ||
if (this.incoming.find(mut => mut.timestamp && mut.timestamp < updatedAt)) { | ||
this.incoming = this.incoming.filter(mut => mut.timestamp < updatedAt) | ||
} | ||
} | ||
// Keep applying mutations as long as any apply | ||
do { | ||
// Find next mutation that can be applied to HEAD (if any) | ||
nextMut = this.incoming.find(mut => mut.previousRev == this.HEAD._rev) | ||
if (this.HEAD) { | ||
nextMut = this.incoming.find(mut => mut.previousRev == this.HEAD._rev) | ||
} else { | ||
// When HEAD is null, that means the document is currently deleted. Only mutations that start with a create | ||
// operation will be considered. | ||
nextMut = this.incoming.find(mut => mut.appliesToMissingDocument()) | ||
} | ||
mustRebase = mustRebase || this.applyIncoming(nextMut) | ||
@@ -157,2 +179,9 @@ } while (nextMut) | ||
if (this.HEAD === null) { | ||
// If the document was deleted, clear all upcoming pending edits that do no apply to missing documents | ||
while (this.pending.length > 0 && !this.pending[0].appliesToMissingDocument()) { | ||
this.pending.shift() | ||
} | ||
} | ||
// Eliminate from incoming set | ||
@@ -159,0 +188,0 @@ this.incoming = this.incoming.filter(m => m.transactionId != mut.transactionId) |
@@ -17,3 +17,4 @@ // @flow | ||
resultRev : string, | ||
mutations : Array<Object> | ||
mutations : Array<Object>, | ||
timestamp: String, | ||
} | ||
@@ -43,5 +44,24 @@ compiled : Function | ||
} | ||
get timestamp() : Date { | ||
if (typeof this.params.timestamp == 'string') { | ||
return new Date(this.params.timestamp) | ||
} | ||
return undefined | ||
} | ||
assignRandomTransactionId() { | ||
this.params.resultRev = this.params.transactionId = luid() | ||
} | ||
appliesToMissingDocument() { | ||
if (typeof this._appliesToMissingDocument != 'undefined') { | ||
return this._appliesToMissingDocument | ||
} | ||
// Only mutations starting with a create operation apply to documents that do not exist ... | ||
const firstMut = this.mutations[0] | ||
if (firstMut) { | ||
this._appliesToMissingDocument = (firstMut.create || firstMut.createIfNotExist || firstMut.createOrReplace) | ||
} else { | ||
this._appliesToMissingDocument = true | ||
} | ||
return this._appliesToMissingDocument | ||
} | ||
// Compiles all mutations into a handy function | ||
@@ -52,3 +72,7 @@ compile() { | ||
if (mutation.create) { | ||
operations.push(() => mutation.create) | ||
operations.push(doc => (doc === null ? mutation.create : doc)) | ||
} else if (mutation.createIfNotExist) { | ||
operations.push(doc => (doc === null ? mutation.createIfNotExist : doc)) | ||
} else if (mutation.createOrReplace) { | ||
operations.push(() => mutation.createOrReplace) | ||
} else if (mutation.delete) { | ||
@@ -55,0 +79,0 @@ operations.push(() => null) |
@@ -98,1 +98,49 @@ // @flow | ||
}) | ||
test('document being deleted by remote', tap => { | ||
(new BufferedDocumentTester(tap, { | ||
_id: 'a', | ||
_rev: '1', | ||
text: 'hello' | ||
})) | ||
.hasNoLocalEdits() | ||
.stage('when applying first local edit') | ||
.localPatch({ | ||
set: { | ||
text: 'goodbye' | ||
} | ||
}) | ||
.onMutationFired() | ||
.hasLocalEdits() | ||
.assertLOCAL('text', 'goodbye') | ||
.stage('when remote patch appear') | ||
.remoteMutation('1', '2', { | ||
delete: {id: '1'} | ||
}) | ||
.didRebase() | ||
.onDeleteDidFire() | ||
.hasNoLocalEdits() | ||
.assertALLDeleted() | ||
.stage('when local user creates document') | ||
.localMutation(null, '3', { | ||
create: {_id: 'a', text: 'good morning'} | ||
}) | ||
.assertLOCAL('text', 'good morning') | ||
.assertHEADDeleted() | ||
.assertEDGEDeleted() | ||
.stage('when committing local create') | ||
.commit() | ||
.assertHEADDeleted() | ||
.assertEDGE('text', 'good morning') | ||
.stage('when commit succeeds') | ||
.commitSucceeds() | ||
.assertALL('text', 'good morning') | ||
.end() | ||
}) |
@@ -20,2 +20,5 @@ // @flow | ||
} | ||
this.doc.onDelete = (local) => { | ||
this.onDeleteCalled = true | ||
} | ||
this.doc.commitHandler = opts => { | ||
@@ -28,6 +31,11 @@ this.pendingCommit = opts | ||
} | ||
reset() { | ||
resetState() { | ||
this.onMutationCalled = false | ||
this.onRebaseCalled = false | ||
this.onDeleteCalled = false | ||
} | ||
resetDocument(doc) { | ||
this.resetState() | ||
this.doc.reset(doc) | ||
} | ||
stage(title) { | ||
@@ -38,3 +46,3 @@ this.context = title | ||
remotePatch(fromRev, toRev, patch) { | ||
this.reset() | ||
this.resetState() | ||
const mut = new Mutation({ | ||
@@ -49,4 +57,15 @@ transactionId: toRev, | ||
} | ||
remoteMutation(fromRev, toRev, operation) { | ||
this.resetState() | ||
const mut = new Mutation({ | ||
transactionId: toRev, | ||
resultRev: toRev, | ||
previousRev: fromRev, | ||
mutations: [operation] | ||
}) | ||
this.doc.arrive(mut) | ||
return this | ||
} | ||
localPatch(patch) { | ||
this.reset() | ||
this.resetState() | ||
const mut = new Mutation({ | ||
@@ -58,4 +77,15 @@ mutations: [{patch}] | ||
} | ||
localMutation(fromRev, toRev, operation) { | ||
this.resetState() | ||
const mut = new Mutation({ | ||
transactionId: toRev, | ||
resultRev: toRev, | ||
previousRev: fromRev, | ||
mutations: [operation] | ||
}) | ||
this.doc.add(mut) | ||
return this | ||
} | ||
commit() { | ||
this.reset() | ||
this.resetState() | ||
this.doc.commit() | ||
@@ -65,6 +95,8 @@ return this | ||
commitSucceeds() { | ||
this.reset() | ||
this.resetState() | ||
this.pendingCommit.success() | ||
// Magically this commit is based on the current HEAD revision | ||
this.pendingCommit.mutation.params.previousRev = this.doc.document.HEAD._rev | ||
if (this.doc.document.HEAD) { | ||
this.pendingCommit.mutation.params.previousRev = this.doc.document.HEAD._rev | ||
} | ||
this.doc.arrive(this.pendingCommit.mutation) | ||
@@ -75,3 +107,3 @@ this.pendingCommit = null | ||
commitFails() { | ||
this.reset() | ||
this.resetState() | ||
this.pendingCommit.failure() | ||
@@ -99,2 +131,20 @@ this.pendingCommit = null | ||
} | ||
assertLOCALDeleted() { | ||
this.tap.ok(this.doc.LOCAL === null, `LOCAL should be deleted ${this.context}`) | ||
return this | ||
} | ||
assertEDGEDeleted() { | ||
this.tap.ok(this.doc.document.EDGE === null, `EDGE should be deleted ${this.context}`) | ||
return this | ||
} | ||
assertHEADDeleted() { | ||
this.tap.ok(this.doc.document.HEAD === null, `HEAD should be deleted ${this.context}`) | ||
return this | ||
} | ||
assertALLDeleted() { | ||
this.assertLOCALDeleted() | ||
this.assertEDGEDeleted() | ||
this.assertHEADDeleted() | ||
return this | ||
} | ||
didRebase() { | ||
@@ -116,2 +166,10 @@ this.tap.ok(this.onRebaseCalled, `should rebase ${this.context}`) | ||
} | ||
onDeleteDidFire() { | ||
this.tap.ok(this.onDeleteCalled, `should fire onDelete event ${this.context}`) | ||
return this | ||
} | ||
onDeleteDidNotFire() { | ||
this.tap.notOk(this.onDeleteCalled, `should not fire onDelete event ${this.context}`) | ||
return this | ||
} | ||
isConsistent() { | ||
@@ -118,0 +176,0 @@ this.tap.ok(this.doc.document.isConsistent(), `should be consistent ${this.context}`) |
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
327718
97
6947