Skip to content

Commit bf2aa76

Browse files
committed
Add spec tags for OBJECT_SYNC sequence
Based on the spec added in [1] [1] ably/specification#333
1 parent 0ecbdd6 commit bf2aa76

6 files changed

Lines changed: 111 additions & 67 deletions

File tree

src/common/lib/client/realtimechannel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ class RealtimeChannel extends EventEmitter {
614614
break;
615615
}
616616

617+
// RTL1
617618
// OBJECT and OBJECT_SYNC message processing share most of the logic, so group them together
618619
case actions.OBJECT:
619620
case actions.OBJECT_SYNC: {

src/plugins/objects/livecounter.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import { ObjectMessage, ObjectOperation, ObjectOperationAction, ObjectsCounterOp
44
import { Objects } from './objects';
55

66
export interface LiveCounterData extends LiveObjectData {
7-
data: number;
7+
data: number; // RTLC3
88
}
99

1010
export interface LiveCounterUpdate extends LiveObjectUpdate {
1111
update: { amount: number };
1212
}
1313

14+
/** @spec RTLC1, RTLC2 */
1415
export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate> {
1516
/**
1617
* Returns a {@link LiveCounter} instance with a 0 value.
1718
*
1819
* @internal
20+
* @spec RTLC4
1921
*/
2022
static zeroValue(objects: Objects, objectId: string): LiveCounter {
2123
return new LiveCounter(objects, objectId);
@@ -122,9 +124,10 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
122124
};
123125
}
124126

127+
/** @spec RTLC5 */
125128
value(): number {
126-
this._objects.throwIfInvalidAccessApiConfiguration();
127-
return this._dataRef.data;
129+
this._objects.throwIfInvalidAccessApiConfiguration(); // RTLC5a, RTLC5b
130+
return this._dataRef.data; // RTLC5c
128131
}
129132

130133
/**
@@ -221,6 +224,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
221224

222225
/**
223226
* @internal
227+
* @spec RTLC6
224228
*/
225229
overrideWithObjectState(objectState: ObjectState): LiveCounterUpdate | LiveObjectUpdateNoop {
226230
if (objectState.objectId !== this.getObjectId()) {
@@ -252,7 +256,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
252256

253257
// object's site serials are still updated even if it is tombstoned, so always use the site serials received from the operation.
254258
// should default to empty map if site serials do not exist on the object state, so that any future operation may be applied to this object.
255-
this._siteTimeserials = objectState.siteTimeserials ?? {};
259+
this._siteTimeserials = objectState.siteTimeserials ?? {}; // RTLC6a
256260

257261
if (this.isTombstoned()) {
258262
// this object is tombstoned. this is a terminal state which can't be overridden. skip the rest of object state message processing
@@ -265,8 +269,9 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
265269
this.tombstone();
266270
} else {
267271
// override data for this object with data from the object state
268-
this._createOperationIsMerged = false;
269-
this._dataRef = { data: objectState.counter?.count ?? 0 };
272+
this._createOperationIsMerged = false; // RTLC6b
273+
this._dataRef = { data: objectState.counter?.count ?? 0 }; // RTLC6c
274+
// RTLC6d
270275
if (!this._client.Utils.isNil(objectState.createOp)) {
271276
this._mergeInitialDataFromCreateOperation(objectState.createOp);
272277
}
@@ -285,6 +290,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
285290
return;
286291
}
287292

293+
/** @spec RTLC4 */
288294
protected _getZeroValueData(): LiveCounterData {
289295
return { data: 0 };
290296
}
@@ -299,8 +305,8 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
299305
// note that it is intentional to SUM the incoming count from the create op.
300306
// if we got here, it means that current counter instance is missing the initial value in its data reference,
301307
// which we're going to add now.
302-
this._dataRef.data += objectOperation.counter?.count ?? 0;
303-
this._createOperationIsMerged = true;
308+
this._dataRef.data += objectOperation.counter?.count ?? 0; // RTLC6d1
309+
this._createOperationIsMerged = true; // RTLC6d2
304310

305311
return { update: { amount: objectOperation.counter?.count ?? 0 } };
306312
}

src/plugins/objects/livemap.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@ export interface LiveMapEntry {
4949
}
5050

5151
export interface LiveMapData extends LiveObjectData {
52-
data: Map<string, LiveMapEntry>;
52+
data: Map<string, LiveMapEntry>; // RTLM3
5353
}
5454

5555
export interface LiveMapUpdate<T extends API.LiveMapType> extends LiveObjectUpdate {
5656
update: { [keyName in keyof T & string]?: 'updated' | 'removed' };
5757
}
5858

59+
/** @spec RTLM1, RTLM2 */
5960
export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, LiveMapUpdate<T>> {
6061
constructor(
6162
objects: Objects,
@@ -69,6 +70,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
6970
* Returns a {@link LiveMap} instance with an empty map data.
7071
*
7172
* @internal
73+
* @spec RTLM4
7274
*/
7375
static zeroValue<T extends API.LiveMapType>(objects: Objects, objectId: string): LiveMap<T> {
7476
return new LiveMap<T>(objects, ObjectsMapSemantics.LWW, objectId);
@@ -294,21 +296,25 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
294296
* - If the value associated with the provided key is an objectId string of another LiveObject, a reference to that LiveObject
295297
* is returned, provided it exists in the local pool and is not tombstoned. Otherwise, `undefined` is returned.
296298
* - If the value is not an objectId, then that value is returned.
299+
*
300+
* @spec RTLM5, RTLM5a
297301
*/
298302
// force the key to be of type string as we only allow strings as key in a map
299303
get<TKey extends keyof T & string>(key: TKey): T[TKey] | undefined {
300-
this._objects.throwIfInvalidAccessApiConfiguration();
304+
this._objects.throwIfInvalidAccessApiConfiguration(); // RTLM5b, RTLM5c
301305

302306
if (this.isTombstoned()) {
303307
return undefined as T[TKey];
304308
}
305309

306310
const element = this._dataRef.data.get(key);
307311

312+
// RTLM5d1
308313
if (element === undefined) {
309314
return undefined as T[TKey];
310315
}
311316

317+
// RTLM5d2a
312318
if (element.tombstone === true) {
313319
return undefined as T[TKey];
314320
}
@@ -465,6 +471,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
465471

466472
/**
467473
* @internal
474+
* @spec RTLM6
468475
*/
469476
overrideWithObjectState(objectState: ObjectState): LiveMapUpdate<T> | LiveObjectUpdateNoop {
470477
if (objectState.objectId !== this.getObjectId()) {
@@ -512,7 +519,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
512519

513520
// object's site serials are still updated even if it is tombstoned, so always use the site serials received from the op.
514521
// should default to empty map if site serials do not exist on the object state, so that any future operation may be applied to this object.
515-
this._siteTimeserials = objectState.siteTimeserials ?? {};
522+
this._siteTimeserials = objectState.siteTimeserials ?? {}; // RTLM6a
516523

517524
if (this.isTombstoned()) {
518525
// this object is tombstoned. this is a terminal state which can't be overridden. skip the rest of object state message processing
@@ -525,8 +532,9 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
525532
this.tombstone();
526533
} else {
527534
// override data for this object with data from the object state
528-
this._createOperationIsMerged = false;
529-
this._dataRef = this._liveMapDataFromMapEntries(objectState.map?.entries ?? {});
535+
this._createOperationIsMerged = false; // RTLM6b
536+
this._dataRef = this._liveMapDataFromMapEntries(objectState.map?.entries ?? {}); // RTLM6c
537+
// RTLM6d
530538
if (!this._client.Utils.isNil(objectState.createOp)) {
531539
this._mergeInitialDataFromCreateOperation(objectState.createOp);
532540
}
@@ -553,6 +561,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
553561
keysToDelete.forEach((x) => this._dataRef.data.delete(x));
554562
}
555563

564+
/** @spec RTLM4 */
556565
protected _getZeroValueData(): LiveMapData {
557566
return { data: new Map<string, LiveMapEntry>() };
558567
}
@@ -621,17 +630,18 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
621630
}
622631

623632
const aggregatedUpdate: LiveMapUpdate<T> = { update: {} };
633+
// RTLM6d1
624634
// in order to apply MAP_CREATE op for an existing map, we should merge their underlying entries keys.
625635
// 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.
626636
Object.entries(objectOperation.map.entries ?? {}).forEach(([key, entry]) => {
627637
// for a MAP_CREATE operation we must use the serial value available on an entry, instead of a serial on a message
628638
const opSerial = entry.timeserial;
629639
let update: LiveMapUpdate<T> | LiveObjectUpdateNoop;
630640
if (entry.tombstone === true) {
631-
// entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op
641+
// RTLM6d1b - entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op
632642
update = this._applyMapRemove({ key }, opSerial);
633643
} else {
634-
// entry in MAP_CREATE op is not removed, try to set it via MAP_SET op
644+
// RTLM6d1a - entry in MAP_CREATE op is not removed, try to set it via MAP_SET op
635645
update = this._applyMapSet({ key, data: entry.data }, opSerial);
636646
}
637647

@@ -644,7 +654,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
644654
Object.assign(aggregatedUpdate.update, update.update);
645655
});
646656

647-
this._createOperationIsMerged = true;
657+
this._createOperationIsMerged = true; // RTLM6d2
648658

649659
return aggregatedUpdate;
650660
}
@@ -682,12 +692,14 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
682692
return this._mergeInitialDataFromCreateOperation(op);
683693
}
684694

695+
/** @spec RTLM7 */
685696
private _applyMapSet(op: ObjectsMapOp, opSerial: string | undefined): LiveMapUpdate<T> | LiveObjectUpdateNoop {
686697
const { ErrorInfo, Utils } = this._client;
687698

688699
const existingEntry = this._dataRef.data.get(op.key);
700+
// RTLM7a
689701
if (existingEntry && !this._canApplyMapOperation(existingEntry.timeserial, opSerial)) {
690-
// the operation's serial <= the entry's serial, ignore the operation.
702+
// RTLM7a1 - the operation's serial <= the entry's serial, ignore the operation.
691703
this._client.Logger.logAction(
692704
this._client.logger,
693705
this._client.Logger.LOG_MICRO,
@@ -713,13 +725,14 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
713725
}
714726

715727
let liveData: ObjectData;
728+
// RTLM7c
716729
if (!Utils.isNil(op.data.objectId)) {
717730
liveData = { objectId: op.data.objectId } as ObjectIdObjectData;
718731
// this MAP_SET op is setting a key to point to another object via its object id,
719732
// but it is possible that we don't have the corresponding object in the pool yet (for example, we haven't seen the *_CREATE op for it).
720733
// we don't want to return undefined from this map's .get() method even if we don't have the object,
721734
// so instead we create a zero-value object for that object id if it not exists.
722-
this._objects.getPool().createZeroValueObjectIfNotExists(op.data.objectId);
735+
this._objects.getPool().createZeroValueObjectIfNotExists(op.data.objectId); // RTLM7c1
723736
} else {
724737
liveData = {
725738
encoding: op.data.encoding,
@@ -731,13 +744,15 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
731744
}
732745

733746
if (existingEntry) {
734-
existingEntry.tombstone = false;
747+
// RTLM7a2
748+
existingEntry.tombstone = false; // RTLM7a2c
735749
existingEntry.tombstonedAt = undefined;
736-
existingEntry.timeserial = opSerial;
737-
existingEntry.data = liveData;
750+
existingEntry.timeserial = opSerial; // RTLM7a2b
751+
existingEntry.data = liveData; // RTLM7a2a
738752
} else {
753+
// RTLM7b, RTLM7b1
739754
const newEntry: LiveMapEntry = {
740-
tombstone: false,
755+
tombstone: false, // RTLM7b2
741756
tombstonedAt: undefined,
742757
timeserial: opSerial,
743758
data: liveData,
@@ -752,10 +767,12 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
752767
return update;
753768
}
754769

770+
/** @spec RTLM8 */
755771
private _applyMapRemove(op: ObjectsMapOp, opSerial: string | undefined): LiveMapUpdate<T> | LiveObjectUpdateNoop {
756772
const existingEntry = this._dataRef.data.get(op.key);
773+
// RTLM8a
757774
if (existingEntry && !this._canApplyMapOperation(existingEntry.timeserial, opSerial)) {
758-
// the operation's serial <= the entry's serial, ignore the operation.
775+
// RTLM8a1 - the operation's serial <= the entry's serial, ignore the operation.
759776
this._client.Logger.logAction(
760777
this._client.logger,
761778
this._client.Logger.LOG_MICRO,
@@ -766,13 +783,15 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
766783
}
767784

768785
if (existingEntry) {
769-
existingEntry.tombstone = true;
786+
// RTLM8a2
787+
existingEntry.tombstone = true; // RTLM8a2c
770788
existingEntry.tombstonedAt = Date.now();
771-
existingEntry.timeserial = opSerial;
772-
existingEntry.data = undefined;
789+
existingEntry.timeserial = opSerial; // RTLM8a2b
790+
existingEntry.data = undefined; // RTLM8a2a
773791
} else {
792+
// RTLM8b, RTLM8b1
774793
const newEntry: LiveMapEntry = {
775-
tombstone: true,
794+
tombstone: true, // RTLM8b2
776795
tombstonedAt: Date.now(),
777796
timeserial: opSerial,
778797
data: undefined,
@@ -790,28 +809,29 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
790809
/**
791810
* Returns true if the serials of the given operation and entry indicate that
792811
* the operation should be applied to the entry, following the CRDT semantics of this LiveMap.
812+
* @spec RTLM9
793813
*/
794814
private _canApplyMapOperation(mapEntrySerial: string | undefined, opSerial: string | undefined): boolean {
795815
// for LWW CRDT semantics (the only supported LiveMap semantic) an operation
796816
// should only be applied if its serial is strictly greater ("after") than an entry's serial.
797817

798818
if (!mapEntrySerial && !opSerial) {
799-
// if both serials are nullish or empty strings, we treat them as the "earliest possible" serials,
819+
// RTLM9b - if both serials are nullish or empty strings, we treat them as the "earliest possible" serials,
800820
// in which case they are "equal", so the operation should not be applied
801821
return false;
802822
}
803823

804824
if (!mapEntrySerial) {
805-
// any operation serial is greater than non-existing entry serial
825+
// RTLM9d - any operation serial is greater than non-existing entry serial
806826
return true;
807827
}
808828

809829
if (!opSerial) {
810-
// non-existing operation serial is lower than any entry serial
830+
// RTLM9c - non-existing operation serial is lower than any entry serial
811831
return false;
812832
}
813833

814-
// if both serials exist, compare them lexicographically
834+
// RTLM9e - if both serials exist, compare them lexicographically
815835
return opSerial > mapEntrySerial;
816836
}
817837

@@ -859,31 +879,31 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
859879
// if object data stores one of the primitive values, just return it as is.
860880
const asValueObject = data as ValueObjectData;
861881
if (asValueObject.boolean !== undefined) {
862-
return asValueObject.boolean;
882+
return asValueObject.boolean; // RTLM5d2b
863883
}
864884
if (asValueObject.bytes !== undefined) {
865-
return asValueObject.bytes;
885+
return asValueObject.bytes; // RTLM5d2c
866886
}
867887
if (asValueObject.number !== undefined) {
868-
return asValueObject.number;
888+
return asValueObject.number; // RTLM5d2d
869889
}
870890
if (asValueObject.string !== undefined) {
871-
return asValueObject.string;
891+
return asValueObject.string; // RTLM5d2e
872892
}
873893

874-
// otherwise, it has an objectId reference, and we should get the actual object from the pool
894+
// RTLM5d2f - otherwise, it has an objectId reference, and we should get the actual object from the pool
875895
const objectId = (data as ObjectIdObjectData).objectId;
876896
const refObject: LiveObject | undefined = this._objects.getPool().get(objectId);
877897
if (!refObject) {
878-
return undefined;
898+
return undefined; // RTLM5d2f1
879899
}
880900

881901
if (refObject.isTombstoned()) {
882902
// tombstoned objects must not be surfaced to the end users
883903
return undefined;
884904
}
885905

886-
return refObject;
906+
return refObject; // RTLM5d2f2
887907
}
888908

889909
private _isMapEntryTombstoned(entry: LiveMapEntry): boolean {

src/plugins/objects/objectid.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class ObjectId {
4242
throw new client.ErrorInfo('Invalid object id string', 92000, 500);
4343
}
4444

45+
// RTO6b1
4546
const [type, rest] = objectId.split(':');
4647
if (!type || !rest) {
4748
throw new client.ErrorInfo('Invalid object id string', 92000, 500);

0 commit comments

Comments
 (0)