Skip to content

Commit b7d046b

Browse files
authored
Merge pull request #343 from ably/objects-operation-application
[PUB-1825, PUB-1826] Add spec for applying incoming OBJECT messages
2 parents bc0fbed + f4d4a05 commit b7d046b

1 file changed

Lines changed: 225 additions & 24 deletions

File tree

src/objects-features.md

Lines changed: 225 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ the latest state.
8282
that the client SDK must not create a new `LiveMap` instance with
8383
id `root`; it must only clear the internal data of the existing
8484
`LiveMap` with id `root`
85-
- `(RTO4b3)` The `SyncObjectsPool` must be cleared
85+
- `(RTO4b3)` The `SyncObjectsPool` list must be cleared
86+
- `(RTO4b5)` The `BufferedObjectOperations` list must be cleared
8687
- `(RTO4b4)` Perform the actions for objects sync completion as
8788
described in [RTO5c](#RTO5c)
8889
- `(RTO5)` The realtime system reserves the right to initiate an objects
@@ -100,7 +101,8 @@ the latest state.
100101
- `(RTO5a2)` If a new sequence id is sent from Ably, the client
101102
library must treat it as the start of a new objects sync sequence,
102103
and any previous in-flight sync must be discarded:
103-
- `(RTO5a2a)` The current `SyncObjectsPool` list must be cleared
104+
- `(RTO5a2a)` The `SyncObjectsPool` list must be cleared
105+
- `(RTO5a2b)` The `BufferedObjectOperations` list must be cleared
104106
- `(RTO5a3)` If the sequence id matches the previously received
105107
sequence id, the client library should continue the sync process
106108
- `(RTO5a4)` The objects sync sequence for that sequence identifier
@@ -111,7 +113,7 @@ the latest state.
111113
contained within the `ProtocolMessage`
112114
- `(RTO5b)` During the sync sequence, the
113115
[`ObjectMessage.object`](../features#TR4r) values from incoming
114-
`OBJECT_SYNC` `ProtocolMessage`s must be temporarily stored in the
116+
`OBJECT_SYNC` `ProtocolMessages` must be temporarily stored in the
115117
internal `SyncObjectsPool` list
116118
- `(RTO5c)` When the objects sync has completed, the client library
117119
must perform the following actions in order:
@@ -143,9 +145,13 @@ the latest state.
143145
which `objectId`s were not received during the sync sequence
144146
- `(RTO5c2a)` The object with ID `root` must not be removed from
145147
`ObjectsPool`, as per [RTO3b](#RTO3b)
148+
- `(RTO5c6)` `ObjectMessages` stored in the
149+
`BufferedObjectOperations` list are applied as described in
150+
[RTO9](#RTO9)
146151
- `(RTO5c3)` Clear any stored sync sequence identifiers and cursor
147152
values
148153
- `(RTO5c4)` The `SyncObjectsPool` must be cleared
154+
- `(RTO5c5)` The `BufferedObjectOperations` list must be cleared
149155
- `(RTO6)` Certain object operations may require creating a zero-value
150156
object if one does not already exist in the internal `ObjectsPool` for
151157
the given `objectId`. This can be done as follows:
@@ -160,11 +166,100 @@ the latest state.
160166
`LiveMap` per [RTLM4](#RTLM4) in the `ObjectsPool`
161167
- `(RTO6b3)` If the parsed type is `counter`, create a zero-value
162168
`LiveCounter` per [RTLC4](#RTLC4) in the `ObjectsPool`
169+
- `(RTO7)` The client library may receive `OBJECT` `ProtocolMessages` in
170+
realtime over the channel concurrently with `OBJECT_SYNC`
171+
`ProtocolMessages` during the object sync sequence ([RTO5](#RTO5)).
172+
Some of the incoming `OBJECT` messages may have already been applied
173+
to the objects described in the sync sequence, while others may not.
174+
Therefore, the client must buffer `OBJECT` messages during the sync
175+
sequence so that it can determine which of them should be applied to
176+
the objects once the sync is complete. See [RTO8](#RTO8)
177+
- `(RTO7a)` An internal `BufferedObjectOperations` should be used to
178+
store the buffered `ObjectMessages`, as described in
179+
[RTO8a](#RTO8a). `BufferedObjectOperations` is an array of
180+
`ObjectMessage` instances
181+
- `(RTO7a1)` This array is empty upon `RealtimeObjects`
182+
initialization
183+
- `(RTO8)` When the library receives a `ProtocolMessage` with an action
184+
of `OBJECT`, each member of the `ProtocolMessage.state` array (decoded
185+
into `ObjectMessage` objects) is passed to the `RealtimeObjects`
186+
instance per [RTL1](../features#RTL1). Each `ObjectMessage` from
187+
`OBJECT` `ProtocolMessage` (also referred to as an `OBJECT` message)
188+
describes an operation to be applied to an object on a channel and
189+
must be handled as follows:
190+
- `(RTO8a)` If an object sync sequence is currently in progress, add
191+
the `ObjectMessages` to the internal `BufferedObjectOperations`
192+
array
193+
- `(RTO8b)` Otherwise, apply the `ObjectMessages` as described in
194+
[RTO9](#RTO9)
195+
- `(RTO9)` `OBJECT` messages can be applied to `RealtimeObjects` in the
196+
following way:
197+
- `(RTO9a)` For each `ObjectMessage` in the provided list:
198+
- `(RTO9a1)` If `ObjectMessage.operation` is null or omitted, log a
199+
warning indicating that an unsupported object operation message
200+
has been received, and discard the current `ObjectMessage` without
201+
taking any action
202+
- `(RTO9a2)` The `ObjectMessage.operation.action` field (see
203+
[`ObjectOperationAction`](../features#OOP2)) determines the type
204+
of operation to apply:
205+
- `(RTO9a2a)` If `ObjectMessage.operation.action` is one of the
206+
following: `MAP_CREATE`, `MAP_SET`, `MAP_REMOVE`,
207+
`COUNTER_CREATE`, `COUNTER_INC`, or `OBJECT_DELETE`, then:
208+
- `(RTO9a2a1)` If it does not already exist, create a zero-value
209+
`LiveObject` in the internal `ObjectsPool` per [RTO6](#RTO6)
210+
using the `objectId` from `ObjectMessage.operation.objectId`
211+
- `(RTO9a2a2)` Get the `LiveObject` instance from the internal
212+
`ObjectsPool` using the `objectId` from
213+
`ObjectMessage.operation.objectId`
214+
- `(RTO9a2a3)` Apply the `ObjectMessage.operation` to the
215+
`LiveObject`; see [RTLC7](#RTLC7), [RTLM15](#RTLM15)
216+
- `(RTO9a2b)` Otherwise, log a warning that an object operation
217+
message with an unsupported action has been received, and
218+
discard the current `ObjectMessage` without taking any action
163219

164220
### LiveObject
165221

166222
- `(RTLO1)` The `LiveObject` represents the common interface and
167223
includes shared functionality for concrete object types
224+
- `(RTLO2)` The client library may choose to implement `LiveObject` as
225+
an abstract class
226+
- `(RTLO3)` `LiveObject` properties:
227+
- `(RTLO3a)` protected `objectId` string - an Object ID for this
228+
object
229+
- `(RTLO3a1)` Must be provided and set in the constructor
230+
- `(RTLO3b)` protected `siteTimeserials` `Dict<String, String>` - a
231+
map of [serials](../features#OM2h) keyed by
232+
[siteCode](../features#OM2i), representing the last operations
233+
applied to this object
234+
- `(RTLO3b1)` Set to an empty map when the `LiveObject` is
235+
initialized, so that any future operation can be applied to this
236+
object
237+
- `(RTLO3c)` protected `createOperationIsMerged` boolean - a flag
238+
indicating whether the corresponding `MAP_CREATE` or
239+
`COUNTER_CREATE` operation has been applied to this `LiveObject`
240+
instance
241+
- `(RTLO3c1)` Set to `false` when the `LiveObject` is initialized
242+
- `(RTLO4)` `LiveObject` methods:
243+
- `(RTLO4a)` protected `canApplyOperation` - a convenience method used
244+
to determine whether the `ObjectMessage.operation` should be applied
245+
to this object based on a serial value:
246+
- `(RTLO4a1)` Expects the following arguments:
247+
- `(RTLO4a1a)` `ObjectMessage`
248+
- `(RTLO4a2)` Returns a boolean indicating whether the operation
249+
should be applied to this object
250+
- `(RTLO4a3)` Both `ObjectMessage.serial` and
251+
`ObjectMessage.siteCode` must be non-empty strings. Otherwise, log
252+
a warning that the object operation message has invalid serial
253+
values. The client library must not apply this operation to the
254+
object
255+
- `(RTLO4a4)` Get the `siteSerial` value stored for this
256+
`LiveObject` in the `siteTimeserials` map using the key
257+
`ObjectMessage.siteCode`
258+
- `(RTLO4a5)` If the `siteSerial` for this `LiveObject` is null or
259+
an empty string, return true
260+
- `(RTLO4a6)` If the `siteSerial` for this `LiveObject` is not an
261+
empty string, return true if `ObjectMessage.serial` is greater
262+
than `siteSerial` when compared lexicographically
168263

169264
### LiveCounter
170265

@@ -188,11 +283,60 @@ the latest state.
188283
- `(RTLC6b)` Set the private flag `createOperationIsMerged` to `false`
189284
- `(RTLC6c)` Set `data` to the value of `ObjectState.counter.count`,
190285
or to 0 if it does not exist
191-
- `(RTLC6d)` If `ObjectState.createOp` is present:
192-
- `(RTLC6d1)` Add `ObjectState.createOp.counter.count` to `data`, if
193-
it exists
194-
- `(RTLC6d2)` Set the private flag `createOperationIsMerged` to
195-
`true`
286+
- `(RTLC6d)` If `ObjectState.createOp` is present, merge the initial
287+
value into the `LiveCounter` as described in [RTLC10](#RTLC10),
288+
passing in the `ObjectState.createOp` instance
289+
- `(RTLC6d1)` This clause has been replaced by [RTLC10a](#RTLC10a)
290+
- `(RTLC6d2)` This clause has been replaced by [RTLC10b](#RTLC10b)
291+
- `(RTLC7)` An `ObjectOperation` from `ObjectMessage.operation` can be
292+
applied to a `LiveCounter` in the following way:
293+
- `(RTLC7a)` A client library may choose to implement this logic as a
294+
convenience method named `applyOperation`, which accepts an
295+
`ObjectMessage` instance with an existing `ObjectMessage.operation`
296+
object, with `ObjectMessage.operation.objectId` matching the Object
297+
ID of this `LiveCounter`. This `ObjectMessage` represents the
298+
operation to be applied to this `LiveCounter`
299+
- `(RTLC7b)` If `ObjectMessage.operation` cannot be applied based on
300+
the result of [`LiveObject.canApplyOperation`](#RTLO4a), log a debug
301+
or trace message indicating that the operation cannot be applied
302+
because its serial value is not newer than the object’s, and discard
303+
the `ObjectMessage` without taking any action
304+
- `(RTLC7c)` Set the entry in the private `siteTimeserials` map at the
305+
key `ObjectMessage.siteCode` to equal `ObjectMessage.serial`
306+
- `(RTLC7d)` The `ObjectMessage.operation.action` field (see
307+
[`ObjectOperationAction`](../features#OOP2)) determines the type of
308+
operation to apply:
309+
- `(RTLC7d1)` If `ObjectMessage.operation.action` is set to
310+
`COUNTER_CREATE`, apply the operation as described in
311+
[RTLC8](#RTLC8), passing in `ObjectMessage.operation`
312+
- `(RTLC7d2)` If `ObjectMessage.operation.action` is set to
313+
`COUNTER_INC`, apply the operation as described in
314+
[RTLC9](#RTLC9), passing in `ObjectMessage.operation.counterOp`
315+
- `(RTLC7d3)` Otherwise, log a warning that an object operation
316+
message with an unsupported action has been received, and discard
317+
the current `ObjectMessage` without taking any action
318+
- `(RTLC8)` A `COUNTER_CREATE` operation can be applied to a
319+
`LiveCounter` in the following way:
320+
- `(RTLC8a)` Expects the following arguments:
321+
- `(RTLC8a1)` `ObjectOperation`
322+
- `(RTLC8b)` If the private flag `createOperationIsMerged` is `true`,
323+
log a debug or trace message indicating that the operation will not
324+
be applied because a `COUNTER_CREATE` operation has already been
325+
applied to this `LiveCounter`, and discard the operation without
326+
taking any further action
327+
- `(RTLC8c)` Otherwise merge the initial value into the `LiveCounter`
328+
as described in [RTLC10](#RTLC10), passing in the `ObjectOperation`
329+
instance
330+
- `(RTLC9)` A `COUNTER_INC` operation can be applied to a `LiveCounter`
331+
in the following way:
332+
- `(RTLC9a)` Expects the following arguments:
333+
- `(RTLC9a1)` `ObjectsCounterOp`
334+
- `(RTLC9b)` Add `ObjectsCounterOp.amount` to `data`, if it exists
335+
- `(RTLC10)` The initial value from `ObjectOperation.counter` can be
336+
merged into this `LiveCounter` in the following way:
337+
- `(RTLC10a)` Add `ObjectOperation.counter.count` to `data`, if it
338+
exists
339+
- `(RTLC10b)` Set the private flag `createOperationIsMerged` to `true`
196340

197341
### LiveMap
198342

@@ -289,22 +433,65 @@ the latest state.
289433
- `(RTLM6b)` Set the private flag `createOperationIsMerged` to `false`
290434
- `(RTLM6c)` Set `data` to `ObjectState.map.entries`, or to an empty
291435
map if it does not exist
292-
- `(RTLM6d)` If `ObjectState.createOp` is present:
293-
- `(RTLM6d1)` For each key–@ObjectsMapEntry@ pair in
294-
`ObjectState.createOp.map.entries`:
295-
- `(RTLM6d1a)` If `ObjectsMapEntry.tombstone` is `false` or
296-
omitted, apply the `MAP_SET` operation to the current key as
297-
described in [RTLM7](#RTLM7), passing in `ObjectsMapEntry.data`
298-
and the current key as `ObjectsMapOp`, and
299-
`ObjectsMapEntry.timeserial` as `serial`
300-
- `(RTLM6d1b)` If `ObjectsMapEntry.tombstone` is `true`, apply the
301-
`MAP_REMOVE` operation to the current key as described in
302-
[RTLM8](#RTLM8), passing in the current key as `ObjectsMapOp`,
303-
and `ObjectsMapEntry.timeserial` as `serial`
304-
- `(RTLM6d2)` Set the private flag `createOperationIsMerged` to
305-
`true`
306-
- `(RTLM7)` `MAP_SET` operation for a key can be applied to a `LiveMap`
307-
in the following way:
436+
- `(RTLM6d)` If `ObjectState.createOp` is present, merge the initial
437+
value into the `LiveMap` as described in [RTLM17](#RTLM17), passing
438+
in the `ObjectState.createOp` instance
439+
- `(RTLM6d1)` This clause has been replaced by [RTLM17a](#RTLM17a)
440+
- `(RTLM6d1a)` This clause has been replaced by
441+
[RTLM17a1](#RTLM17a1)
442+
- `(RTLM6d1b)` This clause has been replaced by
443+
[RTLM17a2](#RTLM17a2)
444+
- `(RTLM6d2)` This clause has been replaced by [RTLM17b](#RTLM17b)
445+
- `(RTLM15)` An `ObjectOperation` from `ObjectMessage.operation` can be
446+
applied to a `LiveMap` in the following way:
447+
- `(RTLM15a)` A client library may choose to implement this logic as a
448+
convenience method named `applyOperation`, which accepts an
449+
`ObjectMessage` instance with an existing `ObjectMessage.operation`
450+
object, with `ObjectMessage.operation.objectId` matching the Object
451+
ID of this `LiveMap`. This `ObjectMessage` represents the operation
452+
to be applied to this `LiveMap`
453+
- `(RTLM15b)` If `ObjectMessage.operation` cannot be applied based on
454+
the result of [`LiveObject.canApplyOperation`](#RTLO4a), log a debug
455+
or trace message indicating that the operation cannot be applied
456+
because its serial value is not newer than the object’s, and discard
457+
the `ObjectMessage` without taking any action
458+
- `(RTLM15c)` Set the entry in the private `siteTimeserials` map at
459+
the key `ObjectMessage.siteCode` to equal `ObjectMessage.serial`
460+
- `(RTLM15d)` The `ObjectMessage.operation.action` field (see
461+
[`ObjectOperationAction`](../features#OOP2)) determines the type of
462+
operation to apply:
463+
- `(RTLM15d1)` If `ObjectMessage.operation.action` is set to
464+
`MAP_CREATE`, apply the operation as described in
465+
[RTLM16](#RTLM16), passing in `ObjectMessage.operation`
466+
- `(RTLM15d2)` If `ObjectMessage.operation.action` is set to
467+
`MAP_SET`, apply the operation as described in [RTLM7](#RTLM7),
468+
passing in `ObjectMessage.operation.mapOp` and
469+
`ObjectMessage.serial`
470+
- `(RTLM15d3)` If `ObjectMessage.operation.action` is set to
471+
`MAP_REMOVE`, apply the operation as described in [RTLM8](#RTLM8),
472+
passing in `ObjectMessage.operation.mapOp` and
473+
`ObjectMessage.serial`
474+
- `(RTLM15d4)` Otherwise, log a warning that an object operation
475+
message with an unsupported action has been received, and discard
476+
the current `ObjectMessage` without taking any action
477+
- `(RTLM16)` A `MAP_CREATE` operation can be applied to a `LiveMap` in
478+
the following way:
479+
- `(RTLM16a)` Expects the following argument:
480+
- `(RTLM16a1)` `ObjectOperation`
481+
- `(RTLM16b)` If the private flag `createOperationIsMerged` is `true`,
482+
log a debug or trace message indicating that the operation will not
483+
be applied because a `MAP_CREATE` operation has already been applied
484+
to this `LiveMap`, and discard the operation without taking any
485+
further action
486+
- `(RTLM16c)` If the private `semantics` field does not match
487+
`ObjectOperation.map.semantics`, log a warning that the operation
488+
cannot be applied due to mismatched semantics, and discard the
489+
operation without taking any further action
490+
- `(RTLM16d)` Otherwise merge the initial value into the `LiveMap` as
491+
described in [RTLM17](#RTLM17), passing in the `ObjectOperation`
492+
instance
493+
- `(RTLM7)` A `MAP_SET` operation for a key can be applied to a
494+
`LiveMap` in the following way:
308495
- `(RTLM7d)` Expects the following arguments:
309496
- `(RTLM7d1)` `ObjectsMapOp`
310497
- `(RTLM7d2)` `serial` string - operation’s serial value
@@ -371,3 +558,17 @@ the latest state.
371558
- `(RTLM9e)` If both serials exist and are not empty strings, compare
372559
them lexicographically and allow operation to be applied only if the
373560
operation’s serial is greater than the entry’s serial
561+
- `(RTLM17)` The initial value from `ObjectOperation.map` can be merged
562+
into this `LiveMap` in the following way:
563+
- `(RTLM17a)` For each key–@ObjectsMapEntry@ pair in
564+
`ObjectOperation.map.entries`:
565+
- `(RTLM17a1)` If `ObjectsMapEntry.tombstone` is `false` or omitted,
566+
apply the `MAP_SET` operation to the current key as described in
567+
[RTLM7](#RTLM7), passing in `ObjectsMapEntry.data` and the current
568+
key as `ObjectsMapOp`, and `ObjectsMapEntry.timeserial` as
569+
`serial`
570+
- `(RTLM17a2)` If `ObjectsMapEntry.tombstone` is `true`, apply the
571+
`MAP_REMOVE` operation to the current key as described in
572+
[RTLM8](#RTLM8), passing in the current key as `ObjectsMapOp`, and
573+
`ObjectsMapEntry.timeserial` as `serial`
574+
- `(RTLM17b)` Set the private flag `createOperationIsMerged` to `true`

0 commit comments

Comments
 (0)