/
Operation.js
296 lines (282 loc) · 10.8 KB
/
Operation.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
//
// Defines the base class for operations.
//
// @todo: probably shouldn't be a class for performance; a bunch of functions
// that act on raw js objects representing ops would cut out the serialize
// steps and make copy simpler most likely
//
// Copyright (c) The Dojo Foundation 2011. All Rights Reserved.
// Copyright (c) IBM Corporation 2008, 2011. All Rights Reserved.
//
/*jslint white:false, bitwise:true, eqeqeq:true, immed:true, nomen:false,
onevar:false, plusplus:false, undef:true, browser:true, devel:true,
forin:false, sub:false*/
/*global define*/
define([
'coweb/jsoe/ContextVector'
], function(ContextVector) {
/**
* Contains information about a local or remote event for transformation.
*
* Initializes the operation from serialized state or individual props if
* state is not defined in the args parameter.
*
* @param {Object[]} args.state Array in format returned by getState
* bundling the following individual parameter values
* @param {Number} args.siteId Integer site ID where the op originated
* @param {ContextVector} args.contextVector Context in which the op
* occurred
* @param {String} args.key Name of the property the op affected
* @param {String} args.value Value of the op
* @param {Number} args.position Integer position of the op in a linear
* collection
* @param {Number} args.order Integer sequence number of the op in the
* total op order across all sites
* @param {Number} args.seqId Integer sequence number of the op at its
* originating site. If undefined, computed from the context vector and
* site ID.
* @param {Boolean} args.immutable True if the op cannot be changed, most
* likely because it is in a history buffer somewhere
* to this instance
*/
var Operation = function(args) {
if(args === undefined) {
// abstract
this.type = null;
return;
} else if(args.state) {
// restore from state alone
this.setState(args.state);
// never local when building from serialized state
this.local = false;
} else {
// use individual properties
this.siteId = args.siteId;
this.contextVector = args.contextVector;
this.key = args.key;
this.value = args.value;
this.position = args.position;
this.order = (args.order === undefined || args.order === null) ?
Infinity : args.order;
if(args.seqId !== undefined) {
this.seqId = args.seqId;
} else if(this.contextVector) {
this.seqId = this.contextVector.getSeqForSite(this.siteId) + 1;
} else {
throw new Error('missing sequence id for new operation');
}
this.xCache = args.xCache;
this.local = args.local || false;
}
// always mutable to start
this.immutable = false;
// define the xcache if not set elsewhere
if(!this.xCache) {
this.xCache = [];
}
// always mutable to start
this.immutable = false;
};
/**
* Serializes the operation as an array of values for transmission.
*
* @return {Object[]} Array with the name of the operation type and all
* of its instance variables as primitive JS types
*/
Operation.prototype.getState = function() {
// use an array to minimize the wire format
var arr = [this.type, this.key, this.value, this.position,
this.contextVector.sites, this.seqId, this.siteId,
this.order];
return arr;
};
/**
* Unserializes operation data and sets it as the instance data. Throws an
* exception if the state is not from an operation of the same type.
*
* @param {Object[]} arr Array in the format returned by getState
*/
Operation.prototype.setState = function(arr) {
if(arr[0] !== this.type) {
throw new Error('setState invoked with state from wrong op type');
} else if(this.immutable) {
throw new Error('op is immutable');
}
// name args as required by constructor
this.key = arr[1];
this.value = arr[2];
this.position = arr[3];
this.contextVector = new ContextVector({state : arr[4]});
this.seqId = arr[5];
this.siteId = arr[6];
this.order = arr[7] || Infinity;
};
/**
* Makes a copy of this operation object. Takes a shortcut and returns
* a ref to this instance if the op is marked as mutable.
*
* @returns {Operation} Operation object
*/
Operation.prototype.copy = function() {
var args = {
siteId : this.siteId,
seqId : this.seqId,
contextVector : this.contextVector.copy(),
key : this.key,
value : this.value,
position : this.position,
order : this.order,
local : this.local,
// reference existing xCache
xCache : this.xCache
};
// respect subclasses
var op = new this.constructor(args);
return op;
};
/**
* Gets a version of the given operation previously transformed into the
* given context if available.
*
* @param {ContextVector} cv Context of the transformed op to seek
* @returns {Operation|null} Copy of the transformed operation from the
* cache or null if not found in the cache
*/
Operation.prototype.getFromCache = function(cv) {
// check if the cv is a key in the xCache
var cache = this.xCache,
xop, i, l;
for(i=0, l=cache.length; i<l; i++) {
xop = cache[i];
if(xop.contextVector.equals(cv)) {
return xop.copy();
}
}
return null;
};
/**
* Caches a transformed copy of this original operation for faster future
* transformations.
*
* @param {Number} Integer count of active sites, including the local one
*/
Operation.prototype.addToCache = function(siteCount) {
// pull some refs local
var cache = this.xCache,
cop = this.copy();
// mark copy as immutable
cop.immutable = true;
// add a copy of this transformed op to the history
cache.push(cop);
// check the count of cached ops against number of sites - 1
var diff = cache.length - (siteCount-1);
if(diff > 0) {
// if overflow, remove oldest op(s)
cache = cache.slice(diff);
}
};
/**
* Computes an ordered comparison of this op and another based on their
* context vectors. Used for sorting operations by their contexts.
*
* @param {Operation} op Other operation
* @returns {Number} -1 if this op is ordered before the other, 0 if they
* are in the same context, and 1 if this op is ordered after the other
*/
Operation.prototype.compareByContext = function(op) {
var rv = this.contextVector.compare(op.contextVector);
if(rv === 0) {
if(this.siteId < op.siteId) {
return -1;
} else if(this.siteId > op.siteId) {
return 1;
} else {
return 0;
}
}
return rv;
};
/**
* Computes an ordered comparison of this op and another based on their
* position in the total op order.
*
* @param {Operation} op Other operation
* @returns {Number} -1 if this op is ordered before the other, 0 if they
* are in the same context, and 1 if this op is ordered after the other
*/
Operation.prototype.compareByOrder = function(op) {
if(this.order === op.order) {
// both unknown total order so next check if both ops are from
// the same site or if one is from the local site and the other
// remote
if(this.local === op.local) {
// compare sequence ids for local-local or remote-remote order
return (this.seqId < op.seqId) ? -1 : 1;
} else if(this.local && !op.local) {
// this local op must appear after the remote one in the total
// order as the remote one was included in the late joining
// state sent by the remote site to this one meaning it was
// sent before this site finished joining
return 1;
} else if(!this.local && op.local) {
// same as above, but this op is the remote one now
return -1;
}
} else if(this.order < op.order) {
return -1;
} else if(this.order > op.order) {
return 1;
}
};
/**
* Transforms this operation to include the effects of the operation
* provided as a parameter IT(this, op). Upgrade the context of this
* op to reflect the inclusion of the other.
*
* @returns {Operation|null} This operation, transformed in-place, or null
* if its effects are nullified by the transform
* @throws {Error} If this op to be transformed is immutable or if the
* this operation subclass does not implement the transform method needed
* to handle the passed op
*/
Operation.prototype.transformWith = function(op) {
if(this.immutable) {
throw new Error('attempt to transform immutable op');
}
var func = this[op.transformMethod()], rv;
if(!func) {
throw new Error('operation cannot handle transform with type: '+ op.type);
}
// do the transform
rv = func.apply(this, arguments);
// check if op effects nullified
if(rv) {
// upgrade the context of this op to include the other
this.upgradeContextTo(op);
}
return rv;
};
/**
* Upgrades the context of this operation to reflect the inclusion of a
* single other operation from some site.
*
* @param {Operation} The operation to include in the context of this op
* @throws {Error} If this op to be upgraded is immutable
*/
Operation.prototype.upgradeContextTo = function(op) {
if(this.immutable) {
throw new Error('attempt to upgrade context of immutable op');
}
this.contextVector.setSeqForSite(op.siteId, op.seqId);
};
/**
* Gets the name of the method to use to transform this operation with
* another based on the type of this operation defined by a subclass.
*
* Abstract implementation always throws an exception if not overriden.
*/
Operation.prototype.getTransformMethod = function() {
throw new Error('transformMethod not implemented');
};
return Operation;
});