|
| 1 | +------------------------------------------------------------------------ |
| 2 | + |
| 3 | +title: Objects Features\ |
| 4 | +section: client-lib-development-guide\ |
| 5 | +index: 65\ |
| 6 | +jump_to:\ |
| 7 | +Help with:\ |
| 8 | +- Objects Features Overview#overview\ |
| 9 | +---- |
| 10 | + |
| 11 | +## Overview |
| 12 | + |
| 13 | +This document outlines the feature specification for the Objects feature |
| 14 | +of the Realtime system. It is currently under development and stored |
| 15 | +separately from the main specification to simplify the initial |
| 16 | +implementation of the feature in other SDKs. Once completed, it will be |
| 17 | +moved to the main [features](../features) spec. |
| 18 | + |
| 19 | +Objects feature enables clients to store shared data as “objects” on a |
| 20 | +channel. When an object is updated, changes are automatically propagated |
| 21 | +to all subscribed clients in realtime, ensuring each client always sees |
| 22 | +the latest state. |
| 23 | + |
| 24 | +### RealtimeObjects |
| 25 | + |
| 26 | +- `(RTO1)` `Objects#getRoot` function: |
| 27 | + - `(RTO1a)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted |
| 28 | + per [RTO2](#RTO2) |
| 29 | + - `(RTO1b)` If the channel is in the `DETACHED` or `FAILED` state, the |
| 30 | + library should indicate an error with code 90001 |
| 31 | + - `(RTO1c)` Waits for the objects sync sequence to complete and for |
| 32 | + [RTO5c](#RTO5c) to finish |
| 33 | + - `(RTO1d)` Returns the object with id `root` from the internal |
| 34 | + `ObjectsPool` as a `LiveMap` |
| 35 | +- `(RTO2)` Various object operations may require a specific channel mode |
| 36 | + to be set on a channel in order to be performed. If a specific channel |
| 37 | + mode is required by an operation, then: |
| 38 | + - `(RTO2a)` If the channel is in the `ATTACHED` state, the presence of |
| 39 | + the required channel mode is checked against the set of channel |
| 40 | + modes granted by the server per [RTL4m](../features#RTL4m) : |
| 41 | + - `(RTO2a1)` If the channel mode is in the set, the operation is |
| 42 | + allowed |
| 43 | + - `(RTO2a2)` If the channel mode is missing, unless otherwise |
| 44 | + specified by the operation, the library should indicate an error |
| 45 | + with code 40024 stating that the operation cannot be performed |
| 46 | + without the required channel mode |
| 47 | + - `(RTO2b)` Otherwise, a best-effort attempt is made, and the channel |
| 48 | + mode is checked against the set of channel modes requested by the |
| 49 | + user per [TB2d](../features#TB2d) : |
| 50 | + - `(RTO2b1)` If the channel mode is in the set, the operation is |
| 51 | + allowed |
| 52 | + - `(RTO2b2)` If the channel mode is missing, unless otherwise |
| 53 | + specified by the operation, the library should indicate an error |
| 54 | + with code 40024 stating that the operation cannot be performed |
| 55 | + without the required channel mode |
| 56 | +- `(RTO3)` An internal `ObjectsPool` should be used to maintain the list |
| 57 | + of objects present on a channel |
| 58 | + - `(RTO3a)` `ObjectsPool` is a `Dict<String, LiveObject>` - a map of |
| 59 | + `LiveObject`s keyed by [`objectId`](../features#OST2a) string |
| 60 | + - `(RTO3b)` It must always contain a `LiveMap` object with id `root` |
| 61 | +- `(RTO4)` When a channel `ATTACHED` `ProtocolMessage` is received, the |
| 62 | + `ProtocolMessage` may contain a `HAS_OBJECTS` bit flag indicating that |
| 63 | + it will perform an objects sync, see [TR3](../features#TR3) . Note |
| 64 | + that this does not imply that objects are definitely present on the |
| 65 | + channel, only that there may be; the `OBJECT_SYNC` message may be |
| 66 | + empty |
| 67 | + - `(RTO4a)` If the `HAS_OBJECTS` flag is 1, the server will shortly |
| 68 | + perform an `OBJECT_SYNC` sequence as described in [RTO5](#RTO5) |
| 69 | + - `(RTO4b)` If the `HAS_OBJECTS` flag is 0 or there is no `flags` |
| 70 | + field, the sync sequence must be considered complete immediately, |
| 71 | + and the client library must perform the following actions in order: |
| 72 | + - `(RTO4b1)` All objects except the one with id `root` must be |
| 73 | + removed from the internal `ObjectsPool` |
| 74 | + - `(RTO4b2)` The data for the `LiveMap` with id `root` must be |
| 75 | + cleared by setting it to a zero-value per [RTLM4](#RTLM4) |
| 76 | + - `(RTO4b3)` The `SyncObjectsPool` must be cleared |
| 77 | + - `(RTO4b4)` Perform the actions for objects sync completion as |
| 78 | + described in [RTO5c](#RTO5c) |
| 79 | +- `(RTO5)` The realtime system reserves the right to initiate an objects |
| 80 | + sync of the objects on a channel at any point once a channel is |
| 81 | + attached. A server initiated objects sync provides Ably with a means |
| 82 | + to send a complete list of objects present on the channel at any point |
| 83 | + - `(RTO5a)` When an `OBJECT_SYNC` `ProtocolMessage` is received with a |
| 84 | + `channel` attribute matching the channel name, the client library |
| 85 | + must parse the `channelSerial` attribute: |
| 86 | + - `(RTO5a1)` The `channelSerial` is used as the sync cursor and is a |
| 87 | + two-part identifier: `<sequence id>:<cursor value>` |
| 88 | + - `(RTO5a2)` If a new sequence id is sent from Ably, the client |
| 89 | + library must treat it as the start of a new objects sync sequence, |
| 90 | + and any previous in-flight sync must be discarded: |
| 91 | + - `(RTO5a2a)` The current `SyncObjectsPool` list must be cleared |
| 92 | + - `(RTO5a3)` If the sequence id matches the previously received |
| 93 | + sequence id, the client library should continue the sync process |
| 94 | + - `(RTO5a4)` The objects sync sequence for that sequence identifier |
| 95 | + is considered complete once the cursor is empty; that is when the |
| 96 | + `channelSerial` looks like `<sequence id>:` |
| 97 | + - `(RTO5a5)` An `OBJECT_SYNC` may also be sent with no |
| 98 | + `channelSerial` attribute. In this case, the sync data is entirely |
| 99 | + contained within the `ProtocolMessage` |
| 100 | + - `(RTO5b)` During the sync sequence, the `ObjectMessage.object` |
| 101 | + values from incoming `OBJECT_SYNC` `ProtocolMessage`s must be |
| 102 | + temporarily stored in the internal `SyncObjectsPool` list |
| 103 | + - `(RTO5c)` When the objects sync has completed, the client library |
| 104 | + must perform the following actions in order: |
| 105 | + - `(RTO5c1)` For each `ObjectState` member in the `SyncObjectsPool` |
| 106 | + list: |
| 107 | + - `(RTO5c1a)` If an object with `ObjectState.objectId` exists in |
| 108 | + the internal `ObjectsPool`: |
| 109 | + - `(RTO5c1a1)` Override the internal data for the object as per |
| 110 | + [RTLC6](#RTLC6), [RTLM6](#RTLM6) |
| 111 | + - `(RTO5c1b)` If an object with `ObjectState.objectId` does not |
| 112 | + exist in the internal `ObjectsPool`: |
| 113 | + - `(RTO5c1b1)` Create a new `LiveObject` using the data from |
| 114 | + `ObjectState` and add it to the internal `ObjectsPool`: |
| 115 | + - `(RTO5c1b1a)` If `ObjectState.counter` is present, create a |
| 116 | + zero-value `LiveCounter` (per [RTLC4](#RTLC4)), set its |
| 117 | + private `objectId` equal to `ObjectState.objectId` and |
| 118 | + override its internal data using the current `ObjectState` |
| 119 | + per [RTLC6](#RTLC6) |
| 120 | + - `(RTO5c1b1b)` If `ObjectState.map` is present, create a |
| 121 | + zero-value `LiveMap` (per [RTLM4](#RTLM4)), set its private |
| 122 | + `objectId` equal to `ObjectState.objectId`, set its private |
| 123 | + `semantics` equal to `ObjectState.map.semantics` and |
| 124 | + override its internal data using the current `ObjectState` |
| 125 | + per [RTLM6](#RTLM6) |
| 126 | + - `(RTO5c1b1c)` Otherwise, log a warning that an unsupported |
| 127 | + object state message has been received, and discard the |
| 128 | + current `ObjectState` without taking any action |
| 129 | + - `(RTO5c2)` Remove any objects from the internal `ObjectsPool` for |
| 130 | + which `objectId`s were not received during the sync sequence |
| 131 | + - `(RTO5c2a)` The object with ID `root` must not be removed from |
| 132 | + `ObjectsPool`, as per [RTO3b](#RTO3b) |
| 133 | + - `(RTO5c3)` Clear any stored sync sequence identifiers and cursor |
| 134 | + values |
| 135 | + - `(RTO5c4)` The `SyncObjectsPool` must be cleared |
| 136 | +- `(RTO6)` When needed, a zero-value object can be created if it does |
| 137 | + not exist in the internal `ObjectsPool` for an `objectId`, in the |
| 138 | + following way: |
| 139 | + - `(RTO6a)` If an object with `objectId` exists in `ObjectsPool`, do |
| 140 | + not create a new object |
| 141 | + - `(RTO6b)` The expected type of the object can be inferred from the |
| 142 | + provided `objectId`: |
| 143 | + - `(RTO6b1)` Split the `objectId` (formatted as |
| 144 | + `type:hash@timestamp`) on the separator `:` and parse the |
| 145 | + first part as the type string |
| 146 | + - `(RTO6b2)` If the parsed type is `map`, create a zero-value |
| 147 | + `LiveMap` per [RTLM4](#RTLM4) in the `ObjectsPool` |
| 148 | + - `(RTO6b3)` If the parsed type is `counter`, create a zero-value |
| 149 | + `LiveCounter` per [RTLC4](#RTLC4) in the `ObjectsPool` |
| 150 | + |
| 151 | +### LiveCounter |
| 152 | + |
| 153 | +- `(RTLC1)` The `LiveCounter` extends `LiveObject` |
| 154 | +- `(RTLC2)` Represents the counter object type for Object IDs of type |
| 155 | + `counter` |
| 156 | +- `(RTLC3)` Holds a 64-bit floating-point number as a private `data` |
| 157 | +- `(RTLC4)` The zero-value `LiveCounter` is a `LiveCounter` with `data` |
| 158 | + set to 0 |
| 159 | +- `(RTLC5)` `LiveCounter#value` function: |
| 160 | + - `(RTLC5a)` Requires the `OBJECT_SUBSCRIBE` channel mode to be |
| 161 | + granted per [RTO2](#RTO2) |
| 162 | + - `(RTLC5b)` If the channel is in the `DETACHED` or `FAILED` state, |
| 163 | + the library should indicate an error with code 90001 |
| 164 | + - `(RTLC5c)` Returns the current `data` value |
| 165 | +- `(RTLC6)` `LiveCounter`’s internal `data` can be overridden with the |
| 166 | + provided `ObjectState` in the following way: |
| 167 | + - `(RTLC6a)` Replace the private `siteTimeserials` of the |
| 168 | + `LiveCounter` with the value from `ObjectState.siteTimeserials` |
| 169 | + - `(RTLC6b)` Set the private flag `createOperationIsMerged` to `false` |
| 170 | + - `(RTLC6c)` Set `data` to the value of `ObjectState.counter.count`, |
| 171 | + or to 0 if it does not exist |
| 172 | + - `(RTLC6d)` If `ObjectState.createOp` is present: |
| 173 | + - `(RTLC6d1)` Add `ObjectState.createOp.counter.count` to `data`, if |
| 174 | + it exists |
| 175 | + - `(RTLC6d2)` Set the private flag `createOperationIsMerged` to |
| 176 | + `true` |
| 177 | + |
| 178 | +### LiveMap |
| 179 | + |
| 180 | +- `(RTLM1)` The `LiveMap` extends `LiveObject` |
| 181 | +- `(RTLM2)` Represents the map object type for Object IDs of type `map` |
| 182 | +- `(RTLM3)` Holds a `Dict<String, ObjectsMapEntry>` as a private `data` |
| 183 | + map |
| 184 | +- `(RTLM4)` The zero-value `LiveMap` is a `LiveMap` with `data` set to |
| 185 | + an empty map |
| 186 | +- `(RTLM5)` `LiveMap#get` function: |
| 187 | + - `(RTLM5a)` Accepts a key of type String |
| 188 | + - `(RTLM5b)` Requires the `OBJECT_SUBSCRIBE` channel mode to be |
| 189 | + granted per [RTO2](#RTO2) |
| 190 | + - `(RTLM5c)` If the channel is in the `DETACHED` or `FAILED` state, |
| 191 | + the library should indicate an error with code 90001 |
| 192 | + - `(RTLM5d)` Returns the value from the current `data` at the |
| 193 | + specified key, as follows: |
| 194 | + - `(RTLM5d1)` If no `ObjectsMapEntry` exists at the key, return |
| 195 | + undefined/null |
| 196 | + - `(RTLM5d2)` If an `ObjectsMapEntry` exists at the key: |
| 197 | + - `(RTLM5d2a)` If `ObjectsMapEntry.tombstone` is `true`, return |
| 198 | + undefined/null |
| 199 | + - `(RTLM5d2b)` If `ObjectsMapEntry.data.boolean` exists, return it |
| 200 | + - `(RTLM5d2c)` If `ObjectsMapEntry.data.bytes` exists, return it |
| 201 | + - `(RTLM5d2d)` If `ObjectsMapEntry.data.number` exists, return it |
| 202 | + - `(RTLM5d2e)` If `ObjectsMapEntry.data.string` exists, return it |
| 203 | + - `(RTLM5d2f)` If `ObjectsMapEntry.data.objectId` exists, get the |
| 204 | + object stored at that `objectId` from the internal |
| 205 | + `ObjectsPool`: |
| 206 | + - `(RTLM5d2f1)` If an object with id `objectId` does not exist, |
| 207 | + return undefined/null |
| 208 | + - `(RTLM5d2f2)` If an object with id `objectId` exists, return |
| 209 | + it |
| 210 | + - `(RTLM5d2g)` Otherwise, return undefined/null |
| 211 | +- `(RTLM6)` `LiveMap` internal `data` can be overridden with the |
| 212 | + provided `ObjectState` in the following way: |
| 213 | + - `(RTLM6a)` Replace the private `siteTimeserials` of the `LiveMap` |
| 214 | + with the value from `ObjectState.siteTimeserials` |
| 215 | + - `(RTLM6b)` Set the private flag `createOperationIsMerged` to `false` |
| 216 | + - `(RTLM6c)` Set `data` to `ObjectState.map.entries`, or to an empty |
| 217 | + map if it does not exist |
| 218 | + - `(RTLM6d)` If `ObjectState.createOp` is present: |
| 219 | + - `(RTLM6d1)` For each key–@ObjectsMapEntry@ pair in |
| 220 | + `ObjectState.createOp.map.entries`: |
| 221 | + - `(RTLM6d1a)` If `ObjectsMapEntry.tombstone` is `false`, apply |
| 222 | + the `MAP_SET` operation to the specified key using |
| 223 | + `ObjectsMapEntry.timeserial` and `ObjectsMapEntry.data` per |
| 224 | + [RTLM7](#RTLM7) |
| 225 | + - `(RTLM6d1b)` If `ObjectsMapEntry.tombstone` is `true`, apply the |
| 226 | + `MAP_REMOVE` operation to the specified key using |
| 227 | + `ObjectsMapEntry.timeserial` per [RTLM8](#RTLM8) |
| 228 | + - `(RTLM6d2)` Set the private flag `createOperationIsMerged` to |
| 229 | + `true` |
| 230 | +- `(RTLM7)` `MAP_SET` operation for a key can be applied to a `LiveMap` |
| 231 | + in the following way: |
| 232 | + - `(RTLM7a)` If an entry exists in the private `data` for the |
| 233 | + specified key: |
| 234 | + - `(RTLM7a1)` If the operation cannot be applied as per |
| 235 | + [RTLM9](#RTLM9), discard the operation without taking any action |
| 236 | + - `(RTLM7a2)` Otherwise, apply the operation: |
| 237 | + - `(RTLM7a2a)` Set `ObjectsMapEntry.data` to the `ObjectData` from |
| 238 | + the operation |
| 239 | + - `(RTLM7a2b)` Set `ObjectsMapEntry.timeserial` to the operation’s |
| 240 | + serial |
| 241 | + - `(RTLM7a2c)` Set `ObjectsMapEntry.tombstone` to `false` |
| 242 | + - `(RTLM7b)` If an entry does not exist in the private `data` for the |
| 243 | + specified key: |
| 244 | + - `(RTLM7b1)` Create a new entry in `data` for the specified key |
| 245 | + with the provided `ObjectData` and the operation’s serial |
| 246 | + - `(RTLM7b2)` Set `ObjectsMapEntry.tombstone` for the new entry to |
| 247 | + `false` |
| 248 | + - `(RTLM7c)` If the operation has a non-empty `ObjectData.objectId` |
| 249 | + attribute: |
| 250 | + - `(RTLM7c1)` Create a zero-value `LiveObject` in the internal |
| 251 | + `ObjectsPool` per [RTO6](#RTO6) |
| 252 | +- `(RTLM8)` `MAP_REMOVE` operation for a key can be applied to a |
| 253 | + `LiveMap` in the following way: |
| 254 | + - `(RTLM8a)` If an entry exists in the private `data` for the |
| 255 | + specified key: |
| 256 | + - `(RTLM8a1)` If the operation cannot be applied as per |
| 257 | + [RTLM9](#RTLM9), discard the operation without taking any action |
| 258 | + - `(RTLM8a2)` Otherwise, apply the operation: |
| 259 | + - `(RTLM8a2a)` Set `ObjectsMapEntry.data` to undefined/null |
| 260 | + - `(RTLM8a2b)` Set `ObjectsMapEntry.timeserial` to the operation’s |
| 261 | + serial |
| 262 | + - `(RTLM8a2c)` Set `ObjectsMapEntry.tombstone` to `true` |
| 263 | + - `(RTLM8b)` If an entry does not exist in the private `data` for the |
| 264 | + specified key: |
| 265 | + - `(RTLM8b1)` Create a new entry in `data` for the specified key, |
| 266 | + with `ObjectsMapEntry.data` set to undefined/null and the |
| 267 | + operation’s serial |
| 268 | + - `(RTLM8b2)` Set `ObjectsMapEntry.tombstone` for the new entry to |
| 269 | + `true` |
| 270 | +- `(RTLM9)` Whether a map operation can be applied to a map entry is |
| 271 | + determined as follows: |
| 272 | + - `(RTLM9a)` For a `LiveMap` using `LWW` (Last-Write-Wins) CRDT |
| 273 | + semantics, the operation must only be applied if its serial is |
| 274 | + strictly greater (”after”) than the entry’s serial when compared |
| 275 | + lexicographically |
| 276 | + - `(RTLM9b)` If both the entry serial and the operation serial are |
| 277 | + null or empty strings, they are treated as the “earliest possible” |
| 278 | + serials and considered “equal”, so the operation must not be applied |
| 279 | + - `(RTLM9c)` If only the entry serial exists, the missing operation |
| 280 | + serial is considered lower than the existing entry serial, so the |
| 281 | + operation must not be applied |
| 282 | + - `(RTLM9d)` If only the operation serial exists, it is considered |
| 283 | + greater than the missing entry serial, so the operation can be |
| 284 | + applied |
| 285 | + - `(RTLM9e)` If both serials exist, compare them lexicographically and |
| 286 | + allow operation to be applied only if the operation’s serial is |
| 287 | + greater than the entry’s serial |
0 commit comments