Skip to content

Commit 8976db3

Browse files
committed
Add spec annotations applying incoming OBJECT messages for Objects
Spec IDs from [1]. This also fixes a couple of minor spec implementation details. [1] ably/specification#343
1 parent 82b56af commit 8976db3

File tree

4 files changed

+83
-56
lines changed

4 files changed

+83
-56
lines changed

src/plugins/objects/livecounter.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
168168

169169
/**
170170
* @internal
171+
* @spec RTLC7, RTLC7a
171172
*/
172173
applyOperation(op: ObjectOperation<ObjectData>, msg: ObjectMessage): void {
173174
if (op.objectId !== this.getObjectId()) {
@@ -181,6 +182,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
181182
const opSerial = msg.serial!;
182183
const opSiteCode = msg.siteCode!;
183184
if (!this._canApplyOperation(opSerial, opSiteCode)) {
185+
// RTLC7b
184186
this._client.Logger.logAction(
185187
this._client.logger,
186188
this._client.Logger.LOG_MICRO,
@@ -191,17 +193,18 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
191193
}
192194
// should update stored site serial immediately. doesn't matter if we successfully apply the op,
193195
// as it's important to mark that the op was processed by the object
194-
this._siteTimeserials[opSiteCode] = opSerial;
196+
this._siteTimeserials[opSiteCode] = opSerial; // RTLC7c
195197

196198
if (this.isTombstoned()) {
197199
// this object is tombstoned so the operation cannot be applied
198200
return;
199201
}
200202

201203
let update: LiveCounterUpdate | LiveObjectUpdateNoop;
204+
// RTLC7d
202205
switch (op.action) {
203206
case ObjectOperationAction.COUNTER_CREATE:
204-
update = this._applyCounterCreate(op);
207+
update = this._applyCounterCreate(op); // RTLC7d1
205208
break;
206209

207210
case ObjectOperationAction.COUNTER_INC:
@@ -210,7 +213,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
210213
// leave an explicit return here, so that TS knows that update object is always set after the switch statement.
211214
return;
212215
} else {
213-
update = this._applyCounterInc(op.counterOp);
216+
update = this._applyCounterInc(op.counterOp); // RTLC7d2
214217
}
215218
break;
216219

@@ -219,6 +222,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
219222
break;
220223

221224
default:
225+
// RTLC7d3
222226
throw new this._client.ErrorInfo(
223227
`Invalid ${op.action} op for LiveCounter objectId=${this.getObjectId()}`,
224228
92000,
@@ -283,9 +287,8 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
283287
// override data for this object with data from the object state
284288
this._createOperationIsMerged = false; // RTLC6b
285289
this._dataRef = { data: objectState.counter?.count ?? 0 }; // RTLC6c
286-
// RTLC6d
287290
if (!this._client.Utils.isNil(objectState.createOp)) {
288-
this._mergeInitialDataFromCreateOperation(objectState.createOp);
291+
this._mergeInitialDataFromCreateOperation(objectState.createOp); // RTLC6d
289292
}
290293
}
291294

@@ -312,13 +315,14 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
312315
return { update: { amount: counterDiff } };
313316
}
314317

318+
/** @spec RTLC10 */
315319
protected _mergeInitialDataFromCreateOperation(objectOperation: ObjectOperation<ObjectData>): LiveCounterUpdate {
316320
// if a counter object is missing for the COUNTER_CREATE op, the initial value is implicitly 0 in this case.
317321
// note that it is intentional to SUM the incoming count from the create op.
318322
// if we got here, it means that current counter instance is missing the initial value in its data reference,
319323
// which we're going to add now.
320-
this._dataRef.data += objectOperation.counter?.count ?? 0; // RTLC6d1
321-
this._createOperationIsMerged = true; // RTLC6d2
324+
this._dataRef.data += objectOperation.counter?.count ?? 0; // RTLC10a
325+
this._createOperationIsMerged = true; // RTLC10b
322326

323327
return { update: { amount: objectOperation.counter?.count ?? 0 } };
324328
}
@@ -331,8 +335,10 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
331335
);
332336
}
333337

338+
/** @spec RTLC8 */
334339
private _applyCounterCreate(op: ObjectOperation<ObjectData>): LiveCounterUpdate | LiveObjectUpdateNoop {
335340
if (this._createOperationIsMerged) {
341+
// RTLC8b
336342
// There can't be two different create operation for the same object id, because the object id
337343
// fully encodes that operation. This means we can safely ignore any new incoming create operations
338344
// if we already merged it once.
@@ -345,11 +351,12 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
345351
return { noop: true };
346352
}
347353

348-
return this._mergeInitialDataFromCreateOperation(op);
354+
return this._mergeInitialDataFromCreateOperation(op); // RTLC8c
349355
}
350356

357+
/** @spec RTLC9 */
351358
private _applyCounterInc(op: ObjectsCounterOp): LiveCounterUpdate {
352-
this._dataRef.data += op.amount;
359+
this._dataRef.data += op.amount; // RTLC9b
353360
return { update: { amount: op.amount } };
354361
}
355362
}

src/plugins/objects/livemap.ts

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
374374

375375
/**
376376
* @internal
377+
* @spec RTLM15, RTLM15a
377378
*/
378379
applyOperation(op: ObjectOperation<ObjectData>, msg: ObjectMessage): void {
379380
if (op.objectId !== this.getObjectId()) {
@@ -387,6 +388,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
387388
const opSerial = msg.serial!;
388389
const opSiteCode = msg.siteCode!;
389390
if (!this._canApplyOperation(opSerial, opSiteCode)) {
391+
// RTLM15b
390392
this._client.Logger.logAction(
391393
this._client.logger,
392394
this._client.Logger.LOG_MICRO,
@@ -397,17 +399,18 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
397399
}
398400
// should update stored site serial immediately. doesn't matter if we successfully apply the op,
399401
// as it's important to mark that the op was processed by the object
400-
this._siteTimeserials[opSiteCode] = opSerial;
402+
this._siteTimeserials[opSiteCode] = opSerial; // RTLM15c
401403

402404
if (this.isTombstoned()) {
403405
// this object is tombstoned so the operation cannot be applied
404406
return;
405407
}
406408

407409
let update: LiveMapUpdate<T> | LiveObjectUpdateNoop;
410+
// RTLM15d
408411
switch (op.action) {
409412
case ObjectOperationAction.MAP_CREATE:
410-
update = this._applyMapCreate(op);
413+
update = this._applyMapCreate(op); // RTLM15d1
411414
break;
412415

413416
case ObjectOperationAction.MAP_SET:
@@ -416,7 +419,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
416419
// leave an explicit return here, so that TS knows that update object is always set after the switch statement.
417420
return;
418421
} else {
419-
update = this._applyMapSet(op.mapOp, opSerial);
422+
update = this._applyMapSet(op.mapOp, opSerial); // RTLM15d2
420423
}
421424
break;
422425

@@ -426,7 +429,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
426429
// leave an explicit return here, so that TS knows that update object is always set after the switch statement.
427430
return;
428431
} else {
429-
update = this._applyMapRemove(op.mapOp, opSerial, msg.serialTimestamp);
432+
update = this._applyMapRemove(op.mapOp, opSerial, msg.serialTimestamp); // RTLM15d3
430433
}
431434
break;
432435

@@ -435,6 +438,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
435438
break;
436439

437440
default:
441+
// RTLM15d4
438442
throw new this._client.ErrorInfo(
439443
`Invalid ${op.action} op for LiveMap objectId=${this.getObjectId()}`,
440444
92000,
@@ -515,9 +519,8 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
515519
// override data for this object with data from the object state
516520
this._createOperationIsMerged = false; // RTLM6b
517521
this._dataRef = this._liveMapDataFromMapEntries(objectState.map?.entries ?? {}); // RTLM6c
518-
// RTLM6d
519522
if (!this._client.Utils.isNil(objectState.createOp)) {
520-
this._mergeInitialDataFromCreateOperation(objectState.createOp);
523+
this._mergeInitialDataFromCreateOperation(objectState.createOp); // RTLM6d
521524
}
522525
}
523526

@@ -603,6 +606,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
603606
return update;
604607
}
605608

609+
/** @spec RTLM17 */
606610
protected _mergeInitialDataFromCreateOperation(objectOperation: ObjectOperation<ObjectData>): LiveMapUpdate<T> {
607611
if (this._client.Utils.isNil(objectOperation.map)) {
608612
// if a map object is missing for the MAP_CREATE op, the initial value is implicitly an empty map.
@@ -611,18 +615,18 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
611615
}
612616

613617
const aggregatedUpdate: LiveMapUpdate<T> = { update: {} };
614-
// RTLM6d1
618+
// RTLM17a
615619
// in order to apply MAP_CREATE op for an existing map, we should merge their underlying entries keys.
616620
// we can do this by iterating over entries from MAP_CREATE op and apply changes on per-key basis as if we had MAP_SET, MAP_REMOVE operations.
617621
Object.entries(objectOperation.map.entries ?? {}).forEach(([key, entry]) => {
618622
// for a MAP_CREATE operation we must use the serial value available on an entry, instead of a serial on a message
619623
const opSerial = entry.timeserial;
620624
let update: LiveMapUpdate<T> | LiveObjectUpdateNoop;
621625
if (entry.tombstone === true) {
622-
// RTLM6d1b - entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op
626+
// RTLM17a2 - entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op
623627
update = this._applyMapRemove({ key }, opSerial, entry.serialTimestamp);
624628
} else {
625-
// RTLM6d1a - entry in MAP_CREATE op is not removed, try to set it via MAP_SET op
629+
// RTLM17a1 - entry in MAP_CREATE op is not removed, try to set it via MAP_SET op
626630
update = this._applyMapSet({ key, data: entry.data }, opSerial);
627631
}
628632

@@ -635,7 +639,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
635639
Object.assign(aggregatedUpdate.update, update.update);
636640
});
637641

638-
this._createOperationIsMerged = true; // RTLM6d2
642+
this._createOperationIsMerged = true; // RTLM17b
639643

640644
return aggregatedUpdate;
641645
}
@@ -648,8 +652,10 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
648652
);
649653
}
650654

655+
/** @spec RTLM16 */
651656
private _applyMapCreate(op: ObjectOperation<ObjectData>): LiveMapUpdate<T> | LiveObjectUpdateNoop {
652657
if (this._createOperationIsMerged) {
658+
// RTLM16b
653659
// There can't be two different create operation for the same object id, because the object id
654660
// fully encodes that operation. This means we can safely ignore any new incoming create operations
655661
// if we already merged it once.
@@ -663,20 +669,21 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
663669
}
664670

665671
if (this._semantics !== op.map?.semantics) {
672+
// RTLM16c
666673
throw new this._client.ErrorInfo(
667674
`Cannot apply MAP_CREATE op on LiveMap objectId=${this.getObjectId()}; map's semantics=${this._semantics}, but op expected ${op.map?.semantics}`,
668675
92000,
669676
500,
670677
);
671678
}
672679

673-
return this._mergeInitialDataFromCreateOperation(op);
680+
return this._mergeInitialDataFromCreateOperation(op); // RTLM16d
674681
}
675682

676683
/** @spec RTLM7 */
677684
private _applyMapSet(
678-
op: ObjectsMapOp<ObjectData>,
679-
opSerial: string | undefined,
685+
op: ObjectsMapOp<ObjectData>, // RTLM7d1
686+
opSerial: string | undefined, // RTLM7d2
680687
): LiveMapUpdate<T> | LiveObjectUpdateNoop {
681688
const { ErrorInfo, Utils } = this._client;
682689

@@ -740,8 +747,8 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
740747

741748
/** @spec RTLM8 */
742749
private _applyMapRemove(
743-
op: ObjectsMapOp<ObjectData>,
744-
opSerial: string | undefined,
750+
op: ObjectsMapOp<ObjectData>, // RTLM8c1
751+
opSerial: string | undefined, // RTLM8c2
745752
opTimestamp: number | undefined,
746753
): LiveMapUpdate<T> | LiveObjectUpdateNoop {
747754
const existingEntry = this._dataRef.data.get(op.key);
@@ -881,17 +888,21 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
881888

882889
// RTLM5d2f - otherwise, it has an objectId reference, and we should get the actual object from the pool
883890
const objectId = (data as ObjectIdObjectData).objectId;
884-
const refObject: LiveObject | undefined = this._objects.getPool().get(objectId);
885-
if (!refObject) {
886-
return undefined; // RTLM5d2f1
887-
}
891+
if (objectId != null) {
892+
const refObject: LiveObject | undefined = this._objects.getPool().get(objectId);
893+
if (!refObject) {
894+
return undefined; // RTLM5d2f1
895+
}
896+
897+
if (refObject.isTombstoned()) {
898+
// tombstoned objects must not be surfaced to the end users
899+
return undefined;
900+
}
888901

889-
if (refObject.isTombstoned()) {
890-
// tombstoned objects must not be surfaced to the end users
891-
return undefined;
902+
return refObject; // RTLM5d2f2
892903
}
893904

894-
return refObject; // RTLM5d2f2
905+
return undefined; // RTLM5d2g
895906
}
896907

897908
/** @spec RTLM14 */

src/plugins/objects/liveobject.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,22 @@ export interface OnLiveObjectLifecycleEventResponse {
3535
off(): void;
3636
}
3737

38+
/** @spec RTLO1, RTLO2 */
3839
export abstract class LiveObject<
3940
TData extends LiveObjectData = LiveObjectData,
4041
TUpdate extends LiveObjectUpdate = LiveObjectUpdate,
4142
> {
4243
protected _client: BaseClient;
4344
protected _subscriptions: EventEmitter;
4445
protected _lifecycleEvents: EventEmitter;
45-
protected _objectId: string;
46+
protected _objectId: string; // RTLO3a
4647
/**
4748
* Represents an aggregated value for an object, which combines the initial value for an object from the create operation,
4849
* and all object operations applied to the object.
4950
*/
5051
protected _dataRef: TData;
51-
protected _siteTimeserials: Record<string, string>;
52-
protected _createOperationIsMerged: boolean;
52+
protected _siteTimeserials: Record<string, string>; // RTLO3b
53+
protected _createOperationIsMerged: boolean; // RTLO3c
5354
private _tombstone: boolean;
5455
private _tombstonedAt: number | undefined;
5556

@@ -60,11 +61,11 @@ export abstract class LiveObject<
6061
this._client = this._objects.getClient();
6162
this._subscriptions = new this._client.EventEmitter(this._client.logger);
6263
this._lifecycleEvents = new this._client.EventEmitter(this._client.logger);
63-
this._objectId = objectId;
64+
this._objectId = objectId; // RTLO3a1
6465
this._dataRef = this._getZeroValueData();
6566
// use empty map of serials by default, so any future operation can be applied to this object
66-
this._siteTimeserials = {};
67-
this._createOperationIsMerged = false;
67+
this._siteTimeserials = {}; // RTLO3b1
68+
this._createOperationIsMerged = false; // RTLO3c1
6869
this._tombstone = false;
6970
}
7071

@@ -198,18 +199,20 @@ export abstract class LiveObject<
198199
*
199200
* An operation should be applied if its serial is strictly greater than the serial in the `siteTimeserials` map for the same site.
200201
* If `siteTimeserials` map does not contain a serial for the same site, the operation should be applied.
202+
*
203+
* @spec RTLO4a
201204
*/
202205
protected _canApplyOperation(opSerial: string | undefined, opSiteCode: string | undefined): boolean {
203206
if (!opSerial) {
204-
throw new this._client.ErrorInfo(`Invalid serial: ${opSerial}`, 92000, 500);
207+
throw new this._client.ErrorInfo(`Invalid serial: ${opSerial}`, 92000, 500); // RTLO4a3
205208
}
206209

207210
if (!opSiteCode) {
208-
throw new this._client.ErrorInfo(`Invalid site code: ${opSiteCode}`, 92000, 500);
211+
throw new this._client.ErrorInfo(`Invalid site code: ${opSiteCode}`, 92000, 500); // RTLO4a3
209212
}
210213

211-
const siteSerial = this._siteTimeserials[opSiteCode];
212-
return !siteSerial || opSerial > siteSerial;
214+
const siteSerial = this._siteTimeserials[opSiteCode]; // RTLO4a4
215+
return !siteSerial || opSerial > siteSerial; // RTLO4a5, RTLO4a6
213216
}
214217

215218
protected _applyObjectDelete(objectMessage: ObjectMessage): TUpdate {

0 commit comments

Comments
 (0)