[AIT-313] Add spec for new fields for ObjectOperation in protocol v6+#426
[AIT-313] Add spec for new fields for ObjectOperation in protocol v6+#426
Conversation
63bcf84 to
4008703
Compare
7774f24 to
1f22417
Compare
textile/features.textile
Outdated
| ** @(OOP4d)@ The size of the @map@ property is calculated per "OMP4":#OMP4 | ||
| ** @(OOP4e)@ The size of the @counter@ property is calculated per "OCN3":#OCN3 | ||
| ** @(OOP4f)@ The size of a @null@ or omitted property is zero | ||
| ** @(OOP4a)@ The size is the sum of the sizes of the @mapCreate@, @mapSet@, @mapRemove@, @counterCreate@, and @counterInc@ properties |
There was a problem hiding this comment.
I think that this needs to include mapCreateWithObjectId and counterCreateWithObjectId given that these are the ones that the SDK actually sends?
There was a problem hiding this comment.
specified in 4d1dabc as a result of #426 (comment) conversation
|
From Slack conversation:
|
4008703 to
694d291
Compare
This and partial sync spec PR have no intersecting spec points |
ObjectCreationHelpers was using a single ObjectOperation for both the wire message (which needs *CreateWithObjectId) and local merge (which needs *Create via mergeInitialValue). Split these into two separate operations: sendOperation for the wire and applyOperation for local merge. The helpers already construct CounterCreate / MapCreate as intermediates before stringifying them for initialValue, so the apply operation reuses these directly — no new work needed. This behaviour (applying a different operation to the one you send) is not yet specified; see discussion on the spec PR: ably/specification#426 (comment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ObjectCreationHelpers was using a single ObjectOperation for both the wire message (which needs *CreateWithObjectId) and local merge (which needs *Create via mergeInitialValue). Split these into two separate operations: sendOperation for the wire and applyOperation for local merge. The helpers already construct CounterCreate / MapCreate as intermediates before stringifying them for initialValue, so the apply operation reuses these directly — no new work needed. This behaviour (applying a different operation to the one you send) is not yet specified; see discussion on the spec PR: ably/specification#426 (comment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ObjectCreationHelpers was using a single ObjectOperation for both the wire message (which needs *CreateWithObjectId) and local merge (which needs *Create via mergeInitialValue). Split these into two separate operations: sendOperation for the wire and applyOperation for local merge. The helpers already construct CounterCreate / MapCreate as intermediates before stringifying them for initialValue, so the apply operation reuses these directly — no new work needed. This behaviour (applying a different operation to the one you send) is not yet specified; see discussion on the spec PR: ably/specification#426 (comment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This fixes message size calculation and apply-on-ACK for client-generated create operations. Protocol v6 introduces *CreateWithObjectId fields (mapCreateWithObjectId, counterCreateWithObjectId) for create operations where the client generates the object ID. The client needs to know object IDs upfront to support apply-on-ACK and batched operations. These fields contain a nonce and an initialValue JSON string representing the encoded mapCreate/counterCreate, and are the only create fields sent to realtime. Since only *CreateWithObjectId is sent over the wire, outgoing create operations would technically only need these fields. However, the client also needs the source *Create (mapCreate/counterCreate) locally for: - Message size calculation: payload size is computed from the decoded fields to enforce maxMessageSize before sending. - Apply-on-ACK: the decoded fields are applied locally after server acknowledgement to update client state. To solve this, *CreateWithObjectId carries a _derivedFrom reference to the source *Create from which it was derived. This is purely local - stripped before wire transmission. The size of a *CreateWithObjectId operation is the size of its _derivedFrom *Create; to apply it locally, apply the _derivedFrom *Create. This follows the approach specified in [1] Considered alternatives: - Carrying mapCreate/counterCreate as sibling properties on the ObjectOperation and stripping them before wire serialisation. This works but muddies the semantics: mapCreate is a legitimate wire property for server-originated creates, so overloading it for a local-only purpose on client-originated operations requires special-case stripping logic in the encoding path. - Only keeping *CreateWithObjectId and deserializing the initialValue JSON back into mapCreate/counterCreate when needed. This adds an unnecessary encode-then-decode round-trip for every create operation. [1] ably/specification#426 (comment)
This fixes message size calculation and apply-on-ACK for client-generated create operations. Protocol v6 introduces *CreateWithObjectId fields (mapCreateWithObjectId, counterCreateWithObjectId) for create operations where the client generates the object ID. The client needs to know object IDs upfront to support apply-on-ACK and batched operations. These fields contain a nonce and an initialValue JSON string representing the encoded mapCreate/counterCreate, and are the only create fields sent to realtime. Since only *CreateWithObjectId is sent over the wire, outgoing create operations would technically only need these fields. However, the client also needs the source *Create (mapCreate/counterCreate) locally for: - Message size calculation: payload size is computed from the decoded fields to enforce maxMessageSize before sending. - Apply-on-ACK: the decoded fields are applied locally after server acknowledgement to update client state. To solve this, *CreateWithObjectId carries a _derivedFrom reference to the source *Create from which it was derived. This is purely local - stripped before wire transmission. The size of a *CreateWithObjectId operation is the size of its _derivedFrom *Create; to apply it locally, apply the _derivedFrom *Create. This follows the approach specified in [1] Considered alternatives: - Carrying mapCreate/counterCreate as sibling properties on the ObjectOperation and stripping them before wire serialisation. This works but muddies the semantics: mapCreate is a legitimate wire property for server-originated creates, so overloading it for a local-only purpose on client-originated operations requires special-case stripping logic in the encoding path. - Only keeping *CreateWithObjectId and deserializing the initialValue JSON back into mapCreate/counterCreate when needed. This adds an unnecessary encode-then-decode round-trip for every create operation. [1] ably/specification#426 (comment)
694d291 to
4d1dabc
Compare
4d1dabc to
49f0364
Compare
Protocol v6 restructures ObjectOperation to improve type safety and developer ergonomics: each operation action now has a typed, same-named property instead of an untyped polymorphic data field, aligning the realtime protocol structure with the REST API. See realtime implementation [1], and DR [2]. Most new fields are straightforward, but *CreateWithObjectId poses a problem: it only carries an encoded initialValue, yet the client needs the original *Create data for message size calculation and apply-on-ACK - decoding the initialValue back would be an unnecessary round-trip for something the client itself created. Several approaches were considered (see [3]), including a send/apply operation pair for publishAndApply and carrying both *Create and *CreateWithObjectId as sibling properties on the ObjectOperation. Ultimately, the spec describes the relationship abstractly - "the *Create from which the *CreateWithObjectId was derived" - allowing each SDK to choose its own internal representation without misusing wire properties or requiring special-case encoding. [1] ably/realtime#8025 [2] https://ably.atlassian.net/wiki/x/AQAPEgE [3] #426 (comment) Resolves AIT-313 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
49f0364 to
47a9d51
Compare
Based on realtime implementation in [1], and DR [2]. Two aspects of this change were not covered by the DR and required additional decisions: 1. Public ObjectOperation and ObjectData interfaces The DR covered the internal wire protocol changes but not the ably-js public API surface which has ObjectOperation interface exposed in LO subscription callbacks. The public ObjectOperation interface is updated to use the protocol v6 field names (mapCreate, mapSet, mapRemove, counterCreate, counterInc). The previous fields (mapOp, counterOp, map, counter) are preserved as deprecated aliases. The public ObjectData interface previously exposed a combined `value` field, which was an incorrect internal representation leaking through the public API (introduced in 54f8ae2). The LODR-042 [3] DR proposed that subscription events should be equivalent to the REST API publish endpoint syntax. With protocol v6 aligning the realtime protocol and REST API, ObjectData now exposes the same typed fields available on the wire: boolean, bytes, number, string, json - with decoded values (bytes as Buffer/ArrayBuffer, json as parsed objects). The combined `value` field is preserved as a deprecated alias. Both new and deprecated fields are populated for backwards compatibility; deprecated fields will be removed in a future major version. 2. Retaining source *Create for *CreateWithObjectId operations Protocol v6 introduces *CreateWithObjectId fields for create operations where the client generates the object ID. These contain a nonce and an initialValue JSON string, and are the only create fields sent to realtime. However, the client also needs the source *Create (mapCreate/counterCreate) locally for: - Message size calculation: payload size is computed from the encoded fields to enforce maxMessageSize before sending. - Apply-on-ACK: the decoded fields are applied locally after server acknowledgement to update client state. To solve this, *CreateWithObjectId carries a _derivedFrom reference to the source *Create from which it was derived. This is purely local - stripped before wire transmission. The size of a *CreateWithObjectId operation is the size of its _derivedFrom *Create; to apply it locally, apply the _derivedFrom *Create. This follows the approach specified in [4]. Considered alternatives: - Carrying mapCreate/counterCreate as sibling properties on the ObjectOperation and stripping them before wire serialisation. This works but muddies the semantics: mapCreate is a legitimate wire property for server-originated creates, so overloading it for a local-only purpose on client-originated operations requires special-case stripping logic in the encoding path. - Only keeping *CreateWithObjectId and deserializing the initialValue JSON back into mapCreate/counterCreate when needed. This adds an unnecessary encode-then-decode round-trip for every create operation. Resolves AIT-315 [1] ably/realtime#8025 [2] https://ably.atlassian.net/wiki/x/AQAPEgE [3] https://ably.atlassian.net/wiki/spaces/LOB/pages/4235722804/LODR-042+LiveObjects+Realtime+Client+API+Improvements#Subscriptions [4] ably/specification#426 (comment)
This PR is based on #413, please review that one first.
See realtime implementation [1], and DR [2].
[1] https://github.com/ably/realtime/pull/8025
[2] https://ably.atlassian.net/wiki/x/AQAPEgE
Resolves AIT-313