mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-23 14:06:52 +00:00
Add collab.concurrency.js
* Contains `Controller` class to handle the concurrency control flow. * Static methods for Composing and Transforming transactions.
This commit is contained in:
parent
6a72114d56
commit
e59420fedd
219
modules/collaboration/collab.concurrency.js
Normal file
219
modules/collaboration/collab.concurrency.js
Normal file
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* Concurrency control module for VisualEditor.
|
||||
*/
|
||||
|
||||
collab.concurrency.Controller = function ( buffer ) {
|
||||
// Initialize empty transactions log
|
||||
this.history = [];
|
||||
|
||||
// Buffered transactions
|
||||
this.buffer = buffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current parent index.
|
||||
*
|
||||
* @method
|
||||
* @return {Number} Current reference to the parent index.
|
||||
*/
|
||||
collab.concurrency.Controller.prototype.getParentId = function () {
|
||||
return this.history.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a transaction into the transaction history.
|
||||
*
|
||||
* @method
|
||||
* @param {Transaction} transaction Transaction to be pushed into history.
|
||||
* @return {Number} The index of the pushed transaction. Will be used as a reference to a parent state based at 1.
|
||||
*/
|
||||
collab.concurrency.Controller.prototype.pushIntoHistory = function ( transaction ) {
|
||||
this.history.push( transaction )
|
||||
return this.history.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the part of history that succeeds reference index.
|
||||
*
|
||||
* @method
|
||||
* @param {reference}
|
||||
* @return {Array} Transactions in the history succeeding the reference index.
|
||||
*/
|
||||
collab.concurrency.Controller.prototype.getHistoricalSet = function ( reference ) {
|
||||
var history = this.history;
|
||||
return history.slice( i ).join( this.buffer );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and output a transaction that is ready to be applied on the document.
|
||||
*
|
||||
* @param {Object} transaction Transaction to be prepared.
|
||||
* @return {Object} Transaction that is trasformed and ready to be applied to the document.
|
||||
*/
|
||||
collab.concurrency.Controller.prototype.process = function ( transaction ) {
|
||||
var refId = transaction.parentIndex;
|
||||
var transactions = this.getHistoricalSet( refId );
|
||||
|
||||
if ( transactions.length > 0 ) {
|
||||
historyTransaction = transactions[0];
|
||||
}
|
||||
|
||||
var transformed = collab.concurrency.transformTransaction( transaction, historyTransaction );
|
||||
var parentId = this.pushIntoHistory( transaction );
|
||||
|
||||
return { parentId: parentId, transformed: transformed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a transform of a transaction against another applied on same document state.
|
||||
* IMPORTANT: Both the transactions should have been applied on the same document state.
|
||||
*
|
||||
* @method
|
||||
* @static
|
||||
* @param {Transaction} t1 Transaction applied first.
|
||||
* @param {Transaction} t2 Transaction applied second.
|
||||
* @return {Transaction} Transform of t2.
|
||||
*/
|
||||
collab.concurrency.transformTransaction = function( t1, t2 ) {
|
||||
var ops1 = t1.operations,
|
||||
ops2 = t2.operations,
|
||||
pos1 = 0,
|
||||
pos2 = 0,
|
||||
retainPos2 = 0;
|
||||
|
||||
for ( var i = 0; i < ops1.length; i++ ) {
|
||||
var op1 = ops1[i];
|
||||
switch( op1.type ) {
|
||||
case 'retain':
|
||||
pos1 = pos1 + op1.length;
|
||||
for ( var j = 0; j < ops2.length; j++ ) {
|
||||
var op2 = ops2[j];
|
||||
if ( op2.type === 'retain' ) {
|
||||
//pos2 = pos2 + op2.length;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'replace':
|
||||
var changedRetains = [];
|
||||
pos2 = 0;
|
||||
for ( var j = 0; j < ops2.length; j++ ) {
|
||||
op2 = ops2[j];
|
||||
var lengthDifference = op1.insert.length - op1.remove.length;
|
||||
if ( op2.type === 'retain' ) {
|
||||
pos2 = pos2 + op2.length;
|
||||
var shift = 0;
|
||||
if ( pos2 >= pos1 ) {
|
||||
if ( pos2 <= pos1 + op1.insert.length ) {
|
||||
shift = pos1 + op1.insert.length - pos2;
|
||||
}
|
||||
else {
|
||||
shift = op1.insert.length;
|
||||
if( pos2 <= pos1 + op1.remove.length ) {
|
||||
shift -= pos2 - pos1;
|
||||
}
|
||||
else {
|
||||
shift -= op1.remove.length;
|
||||
}
|
||||
}
|
||||
op2.length = pos2 + shift - retainPos2;
|
||||
}
|
||||
retainPos2 = pos2 + shift;
|
||||
|
||||
}
|
||||
|
||||
else if ( op2.type === 'replace' ) {
|
||||
|
||||
// resolve remove vectors
|
||||
var removeSize = op2.remove.length;
|
||||
|
||||
// positions where the removal ends
|
||||
var endOf1 = pos1 + op1.remove.length;
|
||||
var endOf2 = pos2 + op2.remove.length;
|
||||
|
||||
if ( pos2 >= pos1 && pos2 <= endOf1) {
|
||||
// case when the remove vector of 2 is completely overlapped by the remove vector of 1
|
||||
if( endOf2 <= endOf1 ) {
|
||||
overlapSize = op2.remove.length;
|
||||
op2.remove = [];
|
||||
}
|
||||
else {
|
||||
op2.remove = op2.remove.slice( endOf1 - pos2 );
|
||||
}
|
||||
}
|
||||
else if ( pos1 >= pos2 && pos1 <= endOf2 ) {
|
||||
// case when the remove vector of 1 is completely overlapped by the remove vector of 2
|
||||
if ( endOf1 <= endOf2 ) {
|
||||
op2.remove = op2.remove.slice( 0, pos1 - pos2 )
|
||||
.concat( new Array( endOf1 - pos1 ).join( ',' ).split( ',' ) )
|
||||
.concat( op2.remove.slice( -( endOf2 - endOf1 ) ) );
|
||||
|
||||
}
|
||||
else {
|
||||
op2.remove = op2.remove.slice( 0, -( endOf2 - pos1 ) );
|
||||
}
|
||||
}
|
||||
var posShift = op2.remove.length;
|
||||
pos2 += removeSize;
|
||||
retainPos2 += posShift;
|
||||
}
|
||||
}
|
||||
pos1 += op1.remove.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return t2;
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to compose a number of transactions into a single transaction.
|
||||
*
|
||||
* @method
|
||||
* @static
|
||||
* @param {Array} transactions Array of transactions to be composed.
|
||||
* @return {Transaction} Composed transaction.
|
||||
*/
|
||||
collab.concurrency.composeTransactions = function ( transactions ) {
|
||||
// Prepare the operations map
|
||||
var opMap = {};
|
||||
var pos = 0;
|
||||
var lengthDifference = 0;
|
||||
for ( var i = 0; i < transactions.length; i++ ) {
|
||||
pos = 0;
|
||||
var transaction = transactions[i];
|
||||
lengthDifference += transaction.lengthDifference;
|
||||
for ( var j = 0; j < transaction.operations.length; j++ ) {
|
||||
var op = transaction.operations[j];
|
||||
if ( op.type === 'retain' ) {
|
||||
pos += op.length;
|
||||
}
|
||||
else if ( op.type === 'replace' ) {
|
||||
if ( pos in opMap ) ) {
|
||||
opMap[pos] = [op];
|
||||
} else {
|
||||
opMap[pos].push(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse the operations map and build the composed transaction
|
||||
pos = 0;
|
||||
var ops = [];
|
||||
for ( index in opMap ) {
|
||||
var retainLength = index - pos;
|
||||
var retainOp = { type: 'retain', length: retainLength };
|
||||
ops.push( retainOp );
|
||||
ops.push( opMap[index] );
|
||||
pos = index;
|
||||
}
|
||||
|
||||
return {
|
||||
operations: ops,
|
||||
lengthDifference: lengthDifference
|
||||
};
|
||||
}
|
||||
|
||||
if ( typeof module == 'object' ) {
|
||||
module.exports.concurrency = concurrency;
|
||||
}
|
Loading…
Reference in a new issue