@dra2020/baseclient 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/all/all.d.ts +18 -0
- package/dist/baseclient.js +9567 -0
- package/dist/baseclient.js.map +1 -0
- package/dist/context/all.d.ts +1 -0
- package/dist/context/context.d.ts +13 -0
- package/dist/filterexpr/all.d.ts +1 -0
- package/dist/filterexpr/filterexpr.d.ts +64 -0
- package/dist/fsm/all.d.ts +1 -0
- package/dist/fsm/fsm.d.ts +118 -0
- package/dist/logabstract/all.d.ts +1 -0
- package/dist/logabstract/log.d.ts +26 -0
- package/dist/logclient/all.d.ts +1 -0
- package/dist/logclient/log.d.ts +6 -0
- package/dist/ot-editutil/all.d.ts +2 -0
- package/dist/ot-editutil/oteditutil.d.ts +14 -0
- package/dist/ot-editutil/otmaputil.d.ts +21 -0
- package/dist/ot-js/all.d.ts +9 -0
- package/dist/ot-js/otarray.d.ts +111 -0
- package/dist/ot-js/otclientengine.d.ts +38 -0
- package/dist/ot-js/otcomposite.d.ts +37 -0
- package/dist/ot-js/otcounter.d.ts +17 -0
- package/dist/ot-js/otengine.d.ts +22 -0
- package/dist/ot-js/otmap.d.ts +19 -0
- package/dist/ot-js/otserverengine.d.ts +38 -0
- package/dist/ot-js/otsession.d.ts +111 -0
- package/dist/ot-js/ottypes.d.ts +29 -0
- package/dist/poly/all.d.ts +15 -0
- package/dist/poly/blend.d.ts +1 -0
- package/dist/poly/boundbox.d.ts +16 -0
- package/dist/poly/cartesian.d.ts +5 -0
- package/dist/poly/graham-scan.d.ts +8 -0
- package/dist/poly/hash.d.ts +1 -0
- package/dist/poly/matrix.d.ts +24 -0
- package/dist/poly/minbound.d.ts +1 -0
- package/dist/poly/poly.d.ts +52 -0
- package/dist/poly/polybin.d.ts +5 -0
- package/dist/poly/polylabel.d.ts +7 -0
- package/dist/poly/polypack.d.ts +30 -0
- package/dist/poly/polyround.d.ts +1 -0
- package/dist/poly/polysimplify.d.ts +1 -0
- package/dist/poly/quad.d.ts +48 -0
- package/dist/poly/selfintersect.d.ts +1 -0
- package/dist/poly/shamos.d.ts +1 -0
- package/dist/poly/simplify.d.ts +2 -0
- package/dist/poly/topo.d.ts +46 -0
- package/dist/poly/union.d.ts +48 -0
- package/dist/util/all.d.ts +5 -0
- package/dist/util/bintrie.d.ts +93 -0
- package/dist/util/countedhash.d.ts +19 -0
- package/dist/util/gradient.d.ts +15 -0
- package/dist/util/indexedarray.d.ts +15 -0
- package/dist/util/util.d.ts +68 -0
- package/docs/context.md +2 -0
- package/docs/fsm.md +243 -0
- package/docs/logabstract.md +2 -0
- package/docs/logclient.md +2 -0
- package/docs/ot-editutil.md +2 -0
- package/docs/ot-js.md +95 -0
- package/docs/poly.md +103 -0
- package/docs/util.md +2 -0
- package/lib/all/all.ts +19 -0
- package/lib/context/all.ts +1 -0
- package/lib/context/context.ts +82 -0
- package/lib/filterexpr/all.ts +1 -0
- package/lib/filterexpr/filterexpr.ts +625 -0
- package/lib/fsm/all.ts +1 -0
- package/lib/fsm/fsm.ts +549 -0
- package/lib/logabstract/all.ts +1 -0
- package/lib/logabstract/log.ts +55 -0
- package/lib/logclient/all.ts +1 -0
- package/lib/logclient/log.ts +105 -0
- package/lib/ot-editutil/all.ts +2 -0
- package/lib/ot-editutil/oteditutil.ts +180 -0
- package/lib/ot-editutil/otmaputil.ts +209 -0
- package/lib/ot-js/all.ts +9 -0
- package/lib/ot-js/otarray.ts +1168 -0
- package/lib/ot-js/otclientengine.ts +327 -0
- package/lib/ot-js/otcomposite.ts +247 -0
- package/lib/ot-js/otcounter.ts +145 -0
- package/lib/ot-js/otengine.ts +71 -0
- package/lib/ot-js/otmap.ts +144 -0
- package/lib/ot-js/otserverengine.ts +329 -0
- package/lib/ot-js/otsession.ts +199 -0
- package/lib/ot-js/ottypes.ts +98 -0
- package/lib/poly/all.ts +15 -0
- package/lib/poly/blend.ts +27 -0
- package/lib/poly/boundbox.ts +102 -0
- package/lib/poly/cartesian.ts +130 -0
- package/lib/poly/graham-scan.ts +401 -0
- package/lib/poly/hash.ts +15 -0
- package/lib/poly/matrix.ts +309 -0
- package/lib/poly/minbound.ts +211 -0
- package/lib/poly/poly.ts +767 -0
- package/lib/poly/polybin.ts +218 -0
- package/lib/poly/polylabel.ts +204 -0
- package/lib/poly/polypack.ts +458 -0
- package/lib/poly/polyround.ts +30 -0
- package/lib/poly/polysimplify.ts +24 -0
- package/lib/poly/quad.ts +272 -0
- package/lib/poly/selfintersect.ts +87 -0
- package/lib/poly/shamos.ts +297 -0
- package/lib/poly/simplify.ts +119 -0
- package/lib/poly/topo.ts +525 -0
- package/lib/poly/union.ts +371 -0
- package/lib/util/all.ts +5 -0
- package/lib/util/bintrie.ts +603 -0
- package/lib/util/countedhash.ts +83 -0
- package/lib/util/gradient.ts +108 -0
- package/lib/util/indexedarray.ts +80 -0
- package/lib/util/util.ts +695 -0
- package/package.json +52 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Shared libraries
|
|
2
|
+
import * as LogAbstract from "../logabstract/all";
|
|
3
|
+
|
|
4
|
+
import * as OT from "./ottypes";
|
|
5
|
+
import * as OTC from "./otcomposite";
|
|
6
|
+
import * as OTE from "./otengine";
|
|
7
|
+
|
|
8
|
+
export class OTClientEngine extends OTE.OTEngine
|
|
9
|
+
{
|
|
10
|
+
// Data members
|
|
11
|
+
clientID: string;
|
|
12
|
+
resourceID: string;
|
|
13
|
+
isNeedAck: boolean;
|
|
14
|
+
isNeedResend: boolean;
|
|
15
|
+
bReadOnly: boolean;
|
|
16
|
+
clientSequenceNo: number;
|
|
17
|
+
stateServer: OTC.OTCompositeResource;
|
|
18
|
+
stateLocal: OTC.OTCompositeResource;
|
|
19
|
+
valCache: any;
|
|
20
|
+
prefailCache: any;
|
|
21
|
+
|
|
22
|
+
actionAllClient: OTC.OTCompositeResource;
|
|
23
|
+
actionAllPendingClient: OTC.OTCompositeResource;
|
|
24
|
+
actionSentClient: OTC.OTCompositeResource;
|
|
25
|
+
actionSentClientOriginal: OTC.OTCompositeResource;
|
|
26
|
+
actionServerInterposedSentClient: OTC.OTCompositeResource;
|
|
27
|
+
|
|
28
|
+
// Constructor
|
|
29
|
+
constructor(ilog: LogAbstract.ILog, rid: string, cid: string)
|
|
30
|
+
{
|
|
31
|
+
super(ilog);
|
|
32
|
+
|
|
33
|
+
this.resourceID = rid;
|
|
34
|
+
this.clientID = cid;
|
|
35
|
+
this.initialize();
|
|
36
|
+
this.bReadOnly = false;
|
|
37
|
+
this.valCache = {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
initialize(): void
|
|
41
|
+
{
|
|
42
|
+
if (this.prefailCache === undefined && this.clientSequenceNo > 0)
|
|
43
|
+
this.prefailCache = this.valCache;
|
|
44
|
+
this.clientSequenceNo = 0;
|
|
45
|
+
this.isNeedAck = false;
|
|
46
|
+
this.isNeedResend = false;
|
|
47
|
+
this.actionAllClient = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
48
|
+
this.actionAllPendingClient = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
49
|
+
this.actionSentClient = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
50
|
+
this.actionSentClientOriginal = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
51
|
+
this.actionServerInterposedSentClient = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
52
|
+
this.stateServer = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
53
|
+
this.stateLocal = new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Members
|
|
57
|
+
serverClock(): number
|
|
58
|
+
{
|
|
59
|
+
return this.stateServer.clock;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
rid(): string
|
|
63
|
+
{
|
|
64
|
+
return this.resourceID;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
cid(): string
|
|
68
|
+
{
|
|
69
|
+
return this.resourceID;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toPartialValue(resourceName: string): any
|
|
73
|
+
{
|
|
74
|
+
return this.valCache[resourceName];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toValue(): any
|
|
78
|
+
{
|
|
79
|
+
return this.valCache;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toPrefailValue(): any
|
|
83
|
+
{
|
|
84
|
+
return this.prefailCache;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
clearPrefail(): void
|
|
88
|
+
{
|
|
89
|
+
delete this.prefailCache;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setReadOnly(b: boolean): void
|
|
93
|
+
{
|
|
94
|
+
if (b != this.bReadOnly)
|
|
95
|
+
{
|
|
96
|
+
this.bReadOnly = b;
|
|
97
|
+
if (this.bReadOnly)
|
|
98
|
+
this.failbackToServerState();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
startLocalEdit(): OTC.OTCompositeResource
|
|
103
|
+
{
|
|
104
|
+
return new OTC.OTCompositeResource(this.resourceID, this.clientID);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isPending(): boolean
|
|
108
|
+
{
|
|
109
|
+
return this.isNeedResend || !this.actionAllPendingClient.isEmpty();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getPending(): OTC.OTCompositeResource
|
|
113
|
+
{
|
|
114
|
+
if (!this.isNeedResend && this.actionAllPendingClient.isEmpty())
|
|
115
|
+
return null;
|
|
116
|
+
else
|
|
117
|
+
{
|
|
118
|
+
// If "isNeedResend" I need to send the exact same event (instead of aggregating all pending)
|
|
119
|
+
// because the server might have actually received and processed the event and I just didn't
|
|
120
|
+
// receive acknowledgement. If I merge that event into others I'll lose ability to distinguish
|
|
121
|
+
// that. Eventually when I re-establish communication with server I will get that event response
|
|
122
|
+
// and can then move on.
|
|
123
|
+
if (! this.isNeedResend)
|
|
124
|
+
{
|
|
125
|
+
this.actionSentClient = this.actionAllPendingClient.copy();
|
|
126
|
+
//console.log(`ClientEngine:getPending: bump sequence count from ${this.clientSequenceNo}`);
|
|
127
|
+
this.actionSentClient.clientSequenceNo = this.clientSequenceNo++;
|
|
128
|
+
this.actionAllPendingClient.empty();
|
|
129
|
+
}
|
|
130
|
+
this.actionSentClient.clock = this.stateServer.clock;
|
|
131
|
+
this.actionSentClientOriginal = this.actionSentClient.copy();
|
|
132
|
+
this.actionServerInterposedSentClient.empty();
|
|
133
|
+
this.isNeedAck = true;
|
|
134
|
+
this.isNeedResend = false;
|
|
135
|
+
return this.actionSentClient.copy();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// When I fail to send, I need to reset to resend the event again
|
|
140
|
+
resetPending(): void
|
|
141
|
+
{
|
|
142
|
+
if (this.isNeedAck)
|
|
143
|
+
{
|
|
144
|
+
this.isNeedAck = false;
|
|
145
|
+
this.isNeedResend = true;
|
|
146
|
+
//console.log('otclientengine: resetPending');
|
|
147
|
+
}
|
|
148
|
+
//else
|
|
149
|
+
// console.log('otclientengine: resetPending ignored because isNeedAck false');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// When I don't accurately have server state - will then refresh from server
|
|
153
|
+
failbackToInitialState(): void
|
|
154
|
+
{
|
|
155
|
+
console.log('otclientengine: failbackToInitialState');
|
|
156
|
+
if (this.prefailCache === undefined)
|
|
157
|
+
this.prefailCache = this.valCache;
|
|
158
|
+
this.initialize();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// When I have server state but my state got mixed up
|
|
162
|
+
failbackToServerState(): void
|
|
163
|
+
{
|
|
164
|
+
console.log('otclientengine: failbackToServerState');
|
|
165
|
+
if (this.prefailCache === undefined)
|
|
166
|
+
this.prefailCache = this.valCache;
|
|
167
|
+
this.stateLocal = this.stateServer.copy();
|
|
168
|
+
this.isNeedAck = false;
|
|
169
|
+
this.actionSentClient.empty();
|
|
170
|
+
this.actionSentClientOriginal.empty();
|
|
171
|
+
this.actionServerInterposedSentClient.empty();
|
|
172
|
+
this.actionAllPendingClient.empty();
|
|
173
|
+
this.actionAllClient.empty();
|
|
174
|
+
this.valCache = this.stateLocal.toValue();
|
|
175
|
+
this.emit('state');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//
|
|
179
|
+
// Function: OTClientEngine.addRemote
|
|
180
|
+
//
|
|
181
|
+
// Description:
|
|
182
|
+
// This function is really where the action is in managing the dynamic logic of applying OT. This is run
|
|
183
|
+
// on each end point and handles the events received from the server. This includes server acknowledgements
|
|
184
|
+
// (both success and failure) of locally generated events as well as all the events generated from other
|
|
185
|
+
// clients.
|
|
186
|
+
//
|
|
187
|
+
// The key things that happen here are:
|
|
188
|
+
// 1. Track server state.
|
|
189
|
+
// 2. Respond to server acknowledgement of locally generated events. This also includes validation
|
|
190
|
+
// (with failback code) in case where server transformed my event in a way that was inconsistent
|
|
191
|
+
// with what I expected (due to insert collision that arose due to multiple independent events).
|
|
192
|
+
// 3. Transform the incoming event (by local events) so it can be applied to local state.
|
|
193
|
+
// 4. Transform pending local events so they can be dispatched to the service once the service
|
|
194
|
+
// is ready for another event.
|
|
195
|
+
//
|
|
196
|
+
|
|
197
|
+
addRemote(orig: OTC.OTCompositeResource): void
|
|
198
|
+
{
|
|
199
|
+
// Reset if server forces restart
|
|
200
|
+
if (orig.clock == OTC.clockInitialValue)
|
|
201
|
+
{
|
|
202
|
+
this.failbackToInitialState();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Reset if server restarted and we don't sync up
|
|
207
|
+
if (orig.clock < 0)
|
|
208
|
+
{
|
|
209
|
+
// If server didn't lose anything I can just keep going...
|
|
210
|
+
if (this.stateServer.clock+1 == -orig.clock)
|
|
211
|
+
orig.clock = - orig.clock
|
|
212
|
+
else
|
|
213
|
+
{
|
|
214
|
+
this.failbackToInitialState();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ignore if I've seen this event already
|
|
220
|
+
if (orig.clock <= this.serverClock())
|
|
221
|
+
{
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let bMine: boolean = orig.clientID == this.clientID;
|
|
226
|
+
let bResend: boolean = bMine && orig.clock == OTC.clockFailureValue;
|
|
227
|
+
let a: OTC.OTCompositeResource = orig.copy();
|
|
228
|
+
|
|
229
|
+
if (bResend)
|
|
230
|
+
{
|
|
231
|
+
// Service failed my request. Retry with currently outstanding content.
|
|
232
|
+
this.resetPending();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try
|
|
237
|
+
{
|
|
238
|
+
// Track server state and clock
|
|
239
|
+
this.stateServer.compose(a);
|
|
240
|
+
|
|
241
|
+
if (bMine)
|
|
242
|
+
{
|
|
243
|
+
// Validate that I didn't run into unresolvable conflict
|
|
244
|
+
if (! this.actionServerInterposedSentClient.isEmpty())
|
|
245
|
+
{
|
|
246
|
+
this.actionSentClientOriginal.transform(this.actionServerInterposedSentClient, true);
|
|
247
|
+
if (! this.actionSentClient.effectivelyEqual(this.actionSentClientOriginal))
|
|
248
|
+
{
|
|
249
|
+
this.failbackToServerState();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// I don't need to apply to local state since it has already been applied - this is just an ack.
|
|
254
|
+
this.isNeedAck = false;
|
|
255
|
+
this.actionSentClient.empty();
|
|
256
|
+
this.actionSentClientOriginal.empty();
|
|
257
|
+
this.actionServerInterposedSentClient.empty();
|
|
258
|
+
this.actionAllClient = this.actionAllPendingClient.copy();
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
{
|
|
262
|
+
// Transform server action to apply locally by transforming by all pending client actions
|
|
263
|
+
a.transform(this.actionAllClient, false);
|
|
264
|
+
|
|
265
|
+
// And then compose with local state
|
|
266
|
+
this.stateLocal.compose(a);
|
|
267
|
+
|
|
268
|
+
// Transform pending client by server action so it is rooted off the server state.
|
|
269
|
+
// This ensures that I can convert the next server action I receive.
|
|
270
|
+
this.actionAllClient.transform(orig, true);
|
|
271
|
+
|
|
272
|
+
// Transform server action to be after previously sent client action and then
|
|
273
|
+
// transform the unsent actions so they are ready to be sent.
|
|
274
|
+
let aServerTransformed: OTC.OTCompositeResource = orig.copy();
|
|
275
|
+
aServerTransformed.transform(this.actionSentClient, false);
|
|
276
|
+
this.actionAllPendingClient.transform(aServerTransformed, true);
|
|
277
|
+
|
|
278
|
+
// And then transform the sent client action so ready to be used for transforming next server event
|
|
279
|
+
this.actionSentClient.transform(orig, true);
|
|
280
|
+
|
|
281
|
+
// Track server operations interposed between a sent action
|
|
282
|
+
if (this.isNeedAck)
|
|
283
|
+
this.actionServerInterposedSentClient.compose(orig);
|
|
284
|
+
|
|
285
|
+
// Let clients know
|
|
286
|
+
this.valCache = this.stateLocal.toValue();
|
|
287
|
+
this.emit('state');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (err)
|
|
291
|
+
{
|
|
292
|
+
this.ilog.error("OTClientEngine.addRemote: unexpected exception: " + err);
|
|
293
|
+
this.failbackToInitialState();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//
|
|
298
|
+
// Function: addLocalEdit
|
|
299
|
+
//
|
|
300
|
+
// Description:
|
|
301
|
+
// This is the logic for adding an action to the local state. The logic is straight-forward
|
|
302
|
+
// as we need to track:
|
|
303
|
+
// 1. The composed set of unacknowledged locally generated events.
|
|
304
|
+
// 2. The composed set of unsent locally generated events (queued until sent event is acknowledged).
|
|
305
|
+
// 3. The local state.
|
|
306
|
+
// 4. An undo operation.
|
|
307
|
+
//
|
|
308
|
+
addLocalEdit(orig: OTC.OTCompositeResource): void
|
|
309
|
+
{
|
|
310
|
+
if (! this.bReadOnly)
|
|
311
|
+
{
|
|
312
|
+
try
|
|
313
|
+
{
|
|
314
|
+
this.actionAllClient.compose(orig);
|
|
315
|
+
this.actionAllPendingClient.compose(orig);
|
|
316
|
+
this.stateLocal.compose(orig);
|
|
317
|
+
this.valCache = this.stateLocal.toValue();
|
|
318
|
+
this.emit('state');
|
|
319
|
+
}
|
|
320
|
+
catch (err)
|
|
321
|
+
{
|
|
322
|
+
this.ilog.error("OTClientEngine.addLocalEdit: unexpected exception: " + err);
|
|
323
|
+
this.failbackToInitialState();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import * as OT from "./ottypes";
|
|
2
|
+
import * as OTA from "./otarray";
|
|
3
|
+
import * as OTM from "./otmap";
|
|
4
|
+
import * as OTC from "./otcounter";
|
|
5
|
+
|
|
6
|
+
export const clockInitialValue: number = -1; // Initial value
|
|
7
|
+
export const clockTerminateValue: number = -2; // Terminal action from client.
|
|
8
|
+
export const clockRandomizeValue: number = -3; // Fill in with random data.
|
|
9
|
+
export const clockFailureValue: number = -4; // Server failed to apply
|
|
10
|
+
export const clockInitializeValue: number = -5; // Used to initialize client to a specific string value.
|
|
11
|
+
export const clockUndoValue: number = -6; // Used to indicate we should generate an undo event.
|
|
12
|
+
export const clockSeenValue: number = -7; // Server has already seen this event
|
|
13
|
+
|
|
14
|
+
export class OTCompositeResource extends OT.OTResourceBase
|
|
15
|
+
{
|
|
16
|
+
resourceID: string;
|
|
17
|
+
clientID: string;
|
|
18
|
+
clock: number;
|
|
19
|
+
clientSequenceNo: number;
|
|
20
|
+
static typeRegistry: any;
|
|
21
|
+
|
|
22
|
+
constructor(rid: string, cid: string)
|
|
23
|
+
{
|
|
24
|
+
super('root', 'composite');
|
|
25
|
+
this.resourceID = rid;
|
|
26
|
+
this.clientID = cid;
|
|
27
|
+
this.clock = clockInitialValue;
|
|
28
|
+
this.clientSequenceNo = 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static registerType(underlyingType: string, factory: (resourceName: string) => OT.OTResourceBase): void
|
|
32
|
+
{
|
|
33
|
+
if (OTCompositeResource.typeRegistry == null)
|
|
34
|
+
OTCompositeResource.typeRegistry = { };
|
|
35
|
+
OTCompositeResource.typeRegistry[underlyingType] = factory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
findResource(rname: string, utype: string = '', bConstruct: boolean = false): OT.IOTResource
|
|
39
|
+
{
|
|
40
|
+
for (let i: number = this.length-1; i >= 0; i--)
|
|
41
|
+
if (this.edits[i].resourceName === rname)
|
|
42
|
+
return this.edits[i];
|
|
43
|
+
if (bConstruct)
|
|
44
|
+
{
|
|
45
|
+
let edit: OT.IOTResource = OTCompositeResource.constructResource(rname, utype);
|
|
46
|
+
this.edits.push(edit);
|
|
47
|
+
return edit;
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
map(rid: string): OTM.OTMapResource
|
|
54
|
+
{
|
|
55
|
+
return this.findResource(rid, 'map', true) as OTM.OTMapResource;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
array(rid: string): OTA.OTArrayResource
|
|
59
|
+
{
|
|
60
|
+
return this.findResource(rid, 'array', true) as OTA.OTArrayResource;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
counter(rid: string): OTC.OTCounterResource
|
|
64
|
+
{
|
|
65
|
+
return this.findResource(rid, 'counter', true) as OTC.OTCounterResource;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
garbageCollect(map: any): boolean
|
|
69
|
+
{
|
|
70
|
+
if (map)
|
|
71
|
+
{
|
|
72
|
+
let bDirty: boolean = false;
|
|
73
|
+
for (let i: number = this.length-1; i >= 0; i--)
|
|
74
|
+
{
|
|
75
|
+
if (map[this.edits[i].resourceName] === undefined)
|
|
76
|
+
{
|
|
77
|
+
this.edits.splice(i, 1);
|
|
78
|
+
bDirty = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return bDirty;
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
return false; // If no resource map, we don't garbage collect
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isEmpty(): boolean
|
|
88
|
+
{
|
|
89
|
+
// Canonical empty is an empty edits array, but an array of empty edits is always considered empty
|
|
90
|
+
for (let i: number = 0; i < this.length; i++)
|
|
91
|
+
if (! this.edits[i].isEmpty())
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Copy an instance
|
|
97
|
+
copy(): OTCompositeResource
|
|
98
|
+
{
|
|
99
|
+
let c: OTCompositeResource = new OTCompositeResource(this.resourceID, this.clientID);
|
|
100
|
+
c.clock = this.clock;
|
|
101
|
+
c.clientSequenceNo = this.clientSequenceNo;
|
|
102
|
+
for (let i: number = 0; i < this.length; i++)
|
|
103
|
+
c.edits.push(this.edits[i].copy());
|
|
104
|
+
return c;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Test whether two operations are effectively equivalent
|
|
108
|
+
effectivelyEqual(rhs: OTCompositeResource): boolean
|
|
109
|
+
{
|
|
110
|
+
// This should really be a structural error
|
|
111
|
+
if (this.length != rhs.length)
|
|
112
|
+
return false;
|
|
113
|
+
for (let i: number = 0; i < this.length; i++)
|
|
114
|
+
{
|
|
115
|
+
let lhsEdit: OT.IOTResource = this.edits[i];
|
|
116
|
+
let rhsEdit: OT.IOTResource = rhs.findResource(lhsEdit.resourceName);
|
|
117
|
+
|
|
118
|
+
if ((rhsEdit == null && !lhsEdit.isEmpty()) || ! lhsEdit.effectivelyEqual(rhsEdit))
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Core OT algorithm for this type
|
|
125
|
+
transform(rhs: OTCompositeResource, bPriorIsService: boolean): void
|
|
126
|
+
{
|
|
127
|
+
for (let i: number = 0; i < rhs.length; i++)
|
|
128
|
+
{
|
|
129
|
+
let rhsEdit: OT.IOTResource = rhs.edits[i];
|
|
130
|
+
let lhsEdit: OT.IOTResource = this.findResource(rhsEdit.resourceName, rhsEdit.underlyingType, false);
|
|
131
|
+
if (lhsEdit)
|
|
132
|
+
lhsEdit.transform(rhsEdit, bPriorIsService);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// compose two edit actions
|
|
137
|
+
compose(rhs: OTCompositeResource): void // throws on error
|
|
138
|
+
{
|
|
139
|
+
for (let i: number = 0; i < rhs.length; i++)
|
|
140
|
+
{
|
|
141
|
+
let rhsEdit: OT.IOTResource = rhs.edits[i];
|
|
142
|
+
|
|
143
|
+
let lhsEdit: OT.IOTResource = this.findResource(rhsEdit.resourceName, rhsEdit.underlyingType, !rhsEdit.isEmpty());
|
|
144
|
+
if (lhsEdit)
|
|
145
|
+
lhsEdit.compose(rhsEdit);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.clock = rhs.clock;
|
|
149
|
+
this.clientSequenceNo = rhs.clientSequenceNo;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// apply this edit to an existing value, returning new value (if underlying type is mutable, may modify input)
|
|
153
|
+
// For composite, takes array of values, returns array of results, one for each underlying resource.
|
|
154
|
+
apply(runningValue: any): any
|
|
155
|
+
{
|
|
156
|
+
if (runningValue == null)
|
|
157
|
+
runningValue = { };
|
|
158
|
+
for (let i: number = 0; i < this.length; i++)
|
|
159
|
+
{
|
|
160
|
+
let e: OT.IOTResource = this.edits[i];
|
|
161
|
+
runningValue[e.resourceName] = e.apply(runningValue[e.resourceName]);
|
|
162
|
+
}
|
|
163
|
+
return runningValue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toPartialValue(resourceName: string): any
|
|
167
|
+
{
|
|
168
|
+
let e = this.edits.find(e => e.resourceName === resourceName);
|
|
169
|
+
return e ? e.apply(null) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
toValue(): any
|
|
173
|
+
{
|
|
174
|
+
return this.apply(null);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
minimize(): void
|
|
178
|
+
{
|
|
179
|
+
for (let i: number = 0; i < this.length; i++)
|
|
180
|
+
this.edits[i].minimize();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static constructResource(rname: string, utype: string): OT.IOTResource
|
|
184
|
+
{
|
|
185
|
+
if (OTCompositeResource.typeRegistry == null)
|
|
186
|
+
{
|
|
187
|
+
//throw "OTCompositeResource.constructResource: no registered factories";
|
|
188
|
+
// This is only place where Composite type knows of other types - could hoist to outer level
|
|
189
|
+
OTCompositeResource.registerType('string', OTA.OTStringResource.factory);
|
|
190
|
+
OTCompositeResource.registerType('array', OTA.OTArrayResource.factory);
|
|
191
|
+
OTCompositeResource.registerType('map', OTM.OTMapResource.factory);
|
|
192
|
+
OTCompositeResource.registerType('counter', OTC.OTCounterResource.factory);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let factory: (resourceName: string) => OT.OTResourceBase = OTCompositeResource.typeRegistry[utype];
|
|
196
|
+
if (factory == null)
|
|
197
|
+
throw "OTCompositeResource.constructResource: no registered factory for " + utype;
|
|
198
|
+
return factory(rname);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Deserialization
|
|
202
|
+
static constructFromObject(o: any): OTCompositeResource
|
|
203
|
+
{
|
|
204
|
+
let cedit: OTCompositeResource = new OTCompositeResource("", "");
|
|
205
|
+
if (o['resourceID'] !== undefined)
|
|
206
|
+
cedit.resourceID = o['resourceID'];
|
|
207
|
+
if (o['clientID'] !== undefined)
|
|
208
|
+
cedit.clientID = o['clientID'];
|
|
209
|
+
if (o['clock'] !== undefined)
|
|
210
|
+
cedit.clock = Number(o['clock']);
|
|
211
|
+
if (o['clientSequenceNo'] !== undefined)
|
|
212
|
+
cedit.clientSequenceNo = Number(o['clientSequenceNo']);
|
|
213
|
+
if (o['edits'] !== undefined)
|
|
214
|
+
{
|
|
215
|
+
let arrEdits: any = o['edits'];
|
|
216
|
+
for (let i: number = 0; i < arrEdits.length; i++)
|
|
217
|
+
{
|
|
218
|
+
let a: any = arrEdits[i];
|
|
219
|
+
let rname: string = a['resourceName'];
|
|
220
|
+
let utype: string = a['underlyingType'];
|
|
221
|
+
let edit: OT.IOTResource = this.constructResource(rname, utype);
|
|
222
|
+
edit.edits = a['edits'];
|
|
223
|
+
cedit.edits.push(edit);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return cedit;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Serialization
|
|
230
|
+
toJSON(): any
|
|
231
|
+
{
|
|
232
|
+
let o: any = {
|
|
233
|
+
"resourceID": this.resourceID,
|
|
234
|
+
"clientID": this.clientID,
|
|
235
|
+
"clock": this.clock,
|
|
236
|
+
"clientSequenceNo": this.clientSequenceNo,
|
|
237
|
+
"edits": [] };
|
|
238
|
+
for (let i: number = 0; i < this.length; i++)
|
|
239
|
+
{
|
|
240
|
+
let edit: OT.IOTResource = this.edits[i];
|
|
241
|
+
let oEdit: any = { "resourceName": edit.resourceName, "underlyingType": edit.underlyingType, "edits": edit.edits };
|
|
242
|
+
o["edits"].push(oEdit);
|
|
243
|
+
}
|
|
244
|
+
return o;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as OT from "./ottypes";
|
|
2
|
+
|
|
3
|
+
// This implements OT for a simple map of counters. Instead of a new value replacing the
|
|
4
|
+
// keyed value, values are added together. This allows a simple accumulating counter.
|
|
5
|
+
// Possible future additions:
|
|
6
|
+
// Add additional semantics for how the values accumulate. Examples from DropBox's datastore API
|
|
7
|
+
// included "min" and "max" as alternate rules to "sum".
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
export const OpCounterAdd = 1;
|
|
11
|
+
export const OpCounterDel = 2;
|
|
12
|
+
export type CounterEdit = [ number, string, any ]; // Op, Key, Value
|
|
13
|
+
|
|
14
|
+
export class OTCounterResource extends OT.OTResourceBase
|
|
15
|
+
{
|
|
16
|
+
constructor(rid: string)
|
|
17
|
+
{
|
|
18
|
+
super(rid, 'counter');
|
|
19
|
+
}
|
|
20
|
+
static factory(rid: string): OTCounterResource { return new OTCounterResource(rid); }
|
|
21
|
+
|
|
22
|
+
// copy an instance
|
|
23
|
+
copy(): OTCounterResource
|
|
24
|
+
{
|
|
25
|
+
let c: OTCounterResource = new OTCounterResource(this.resourceName);
|
|
26
|
+
for (let i: number = 0; i < this.length; i++)
|
|
27
|
+
{
|
|
28
|
+
let e: CounterEdit = this.edits[i];
|
|
29
|
+
c.edits.push([ e[0], e[1], e[2] ]);
|
|
30
|
+
}
|
|
31
|
+
return c;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Test whether two operations are effectively equivalent
|
|
35
|
+
effectivelyEqual(rhs: OTCounterResource): boolean
|
|
36
|
+
{
|
|
37
|
+
// This should really be a structural error
|
|
38
|
+
if (this.length != rhs.length)
|
|
39
|
+
return false;
|
|
40
|
+
|
|
41
|
+
// This checks for exact structural equivalency. Really the ordering shouldn't matter for Counter so
|
|
42
|
+
// an improvement to this algorithm would be to be more robust to ordering differences.
|
|
43
|
+
for (let i: number = 0; i < this.length; i++)
|
|
44
|
+
{
|
|
45
|
+
let e1: CounterEdit = this.edits[i];
|
|
46
|
+
let e2: CounterEdit = rhs.edits[i];
|
|
47
|
+
if (e1[0] != e2[0] || e1[1] != e2[1] || e1[2] != e2[2])
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Core OT algorithm for this type
|
|
54
|
+
transform(prior: OTCounterResource, bPriorIsService: boolean): void
|
|
55
|
+
{
|
|
56
|
+
// Last wins - if I'm last, my adds and deletes are all preserved
|
|
57
|
+
if (bPriorIsService)
|
|
58
|
+
return;
|
|
59
|
+
|
|
60
|
+
// Deletes in prior will delete mine. Implement by loading up properties rather than
|
|
61
|
+
// using N^2 lookup through Edits array.
|
|
62
|
+
let myEdits: any = this.toObject();
|
|
63
|
+
let bEdited: boolean = false;
|
|
64
|
+
|
|
65
|
+
// Now delete any that are deleted by prior.
|
|
66
|
+
for (let i: number = 0; i < prior.length; i++)
|
|
67
|
+
{
|
|
68
|
+
let eP: CounterEdit = prior.edits[i];
|
|
69
|
+
if (eP[0] == OpCounterDel)
|
|
70
|
+
{
|
|
71
|
+
delete myEdits[eP[1]];
|
|
72
|
+
bEdited = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Now restore edit array from edited object
|
|
77
|
+
if (bEdited)
|
|
78
|
+
this.fromObject(myEdits);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// compose two edit actions
|
|
82
|
+
compose(rhs: OTCounterResource): void // throws on error
|
|
83
|
+
{
|
|
84
|
+
let lhsKeys: any = this.toObject();
|
|
85
|
+
let rhsKeys: any = rhs.toObject();
|
|
86
|
+
for (let i: number = 0; i < rhs.length; i++)
|
|
87
|
+
{
|
|
88
|
+
let eR: CounterEdit = rhs.edits[i];
|
|
89
|
+
let eL: CounterEdit = lhsKeys[eR[1]];
|
|
90
|
+
if (eL === undefined)
|
|
91
|
+
lhsKeys[eR[1]] = [ eR[0], eR[1], eR[2] ];
|
|
92
|
+
else
|
|
93
|
+
eL[2] += eR[2];
|
|
94
|
+
}
|
|
95
|
+
this.fromObject(lhsKeys);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
apply(startValue: any): any
|
|
99
|
+
{
|
|
100
|
+
if (startValue == null)
|
|
101
|
+
startValue = { };
|
|
102
|
+
for (let i: number = 0; i < this.length; i++)
|
|
103
|
+
{
|
|
104
|
+
let e: CounterEdit = this.edits[i];
|
|
105
|
+
switch (e[0])
|
|
106
|
+
{
|
|
107
|
+
case OpCounterAdd:
|
|
108
|
+
if (startValue[e[1]] === undefined)
|
|
109
|
+
startValue[e[1]] = e[2];
|
|
110
|
+
else
|
|
111
|
+
startValue[e[1]] += e[2];
|
|
112
|
+
break;
|
|
113
|
+
case OpCounterDel:
|
|
114
|
+
delete startValue[e[1]];
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return startValue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
minimize(): any
|
|
122
|
+
{
|
|
123
|
+
// No-op
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
loadObject(o: any): any
|
|
127
|
+
{
|
|
128
|
+
for (let i: number = 0; i < this.length; i++)
|
|
129
|
+
o[(this.edits[i])[1]] = this.edits[i];
|
|
130
|
+
return o;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
toObject(): any
|
|
134
|
+
{
|
|
135
|
+
return this.loadObject({ });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fromObject(o: any): void
|
|
139
|
+
{
|
|
140
|
+
this.edits = [];
|
|
141
|
+
for (var p in o)
|
|
142
|
+
if (o.hasOwnProperty(p))
|
|
143
|
+
this.edits.push(o[p]);
|
|
144
|
+
}
|
|
145
|
+
}
|