Skip to content

Commit 27a198c

Browse files
committed
Add separate page for Objects spec, RealtimeChannel.objects and OBJECT_SYNC spec
1 parent 64dbd47 commit 27a198c

2 files changed

Lines changed: 305 additions & 13 deletions

File tree

src/features.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,11 +1265,12 @@ as it is useful as a reference and also needs to be kept up to date*
12651265
string it should be encoded to binary using UTF-8 before being
12661266
passed as base argument of the `VCDiffDecoder.decode` method.
12671267
- `(PC5)` A plugin provided with the `PluginType` enum key value of
1268-
`Objects` should provide the [RealtimeObjects](#RTO1) feature
1269-
functionality for realtime channels ([RTL27](#RTL27)). The plugin
1270-
object itself is not expected to provide a public API. The type of the
1271-
plugin object, and how it enables the Objects feature for a realtime
1272-
channel, are left for individual implementations to decide.
1268+
`Objects` should provide the
1269+
[RealtimeObjects](../objects-features#RTO1) feature functionality for
1270+
realtime channels ([RTL27](#RTL27)). The plugin object itself is not
1271+
expected to provide a public API. The type of the plugin object, and
1272+
how it enables the Objects feature for a realtime channel, are left
1273+
for individual implementations to decide.
12731274
- `(PC4)` A client library is allowed to accept plugins other than those
12741275
specified in this specification, through the use of additional
12751276
`ClientOptions.plugins` keys defined by that library. The library is
@@ -2217,11 +2218,14 @@ makes extensive use of goroutines and channels.
22172218
- `(RTL23)` `RealtimeChannel#name` attribute is a string containing the
22182219
channel’s name
22192220
- `(RTL1)` As soon as a `RealtimeChannel` becomes attached, all incoming
2220-
messages and presence messages (where ‘incoming’ is defined as
2221-
‘received from Ably over the realtime transport’) are processed and
2222-
emitted where applicable. `PRESENCE` and `SYNC` messages are passed to
2223-
the `RealtimePresence` object ensuring it maintains a map of current
2224-
members on a channel in realtime
2221+
messages, presence messages and object messages (where ‘incoming’ is
2222+
defined as ‘received from Ably over the realtime transport’) are
2223+
processed and emitted where applicable. `PRESENCE` and `SYNC` messages
2224+
are passed to the `RealtimePresence` object ensuring it maintains a
2225+
map of current members on a channel in realtime. `OBJECT` and
2226+
`OBJECT_SYNC` messages are passed to the `RealtimeObjects` object
2227+
ensuring it maintains an up-to-date representation of objects on a
2228+
channel in realtime
22252229
- `(RTL2)` The `RealtimeChannel` implements `EventEmitter` and emits
22262230
`ChannelEvent` events, where a `ChannelEvent` is either a
22272231
`ChannelState` or `UPDATE`, and a `ChannelState` is either
@@ -2614,7 +2618,7 @@ makes extensive use of goroutines and channels.
26142618
- `(RTL9a)` Returns the `RealtimePresence` object for this channel
26152619
- `(RTL27)` `RealtimeChannel#objects` attribute:
26162620
- `(RTL27a)` Returns the `RealtimeObjects` object for this channel
2617-
[RTO1](#RTO1)
2621+
[RTO1](../objects-features#RTO1)
26182622
- `(RTL27b)` It is a programmer error to access this property without
26192623
first providing the `Objects` plugin ([PC5](#PC5)) in the client
26202624
options. This programmer error should be handled in an idiomatic
@@ -3190,8 +3194,9 @@ makes extensive use of goroutines and channels.
31903194

31913195
### RealtimeObjects
31923196

3193-
Reserved for `RealtimeObjects` feature specification. Reserved spec
3194-
points: `RTO`, `RTLO`, `RTLC`, `RTLM`
3197+
Reserved for `RealtimeObjects` feature specification, see
3198+
[objects-features](../objects-features). Reserved spec points: `RTO`,
3199+
`RTLO`, `RTLC`, `RTLM`
31953200

31963201
### RealtimeAnnotations
31973202

src/objects-features.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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&#64;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

Comments
 (0)