Skip to content

[AIT-313] Add spec for new fields for ObjectOperation in protocol v6+#426

Merged
VeskeR merged 1 commit intomainfrom
AIT-313/protocol-v6-state-message
Feb 27, 2026
Merged

[AIT-313] Add spec for new fields for ObjectOperation in protocol v6+#426
VeskeR merged 1 commit intomainfrom
AIT-313/protocol-v6-state-message

Conversation

@VeskeR
Copy link
Contributor

@VeskeR VeskeR commented Feb 20, 2026

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

@VeskeR VeskeR changed the title Add spec for new fields for ObjectOperation in protocol v6+ [AIT-313] Add spec for new fields for ObjectOperation in protocol v6+ Feb 20, 2026
@VeskeR VeskeR force-pushed the AIT-313/protocol-v6-state-message branch from 63bcf84 to 4008703 Compare February 20, 2026 14:02
@VeskeR VeskeR changed the base branch from main to feature/AIT-236 February 20, 2026 14:03
@VeskeR VeskeR force-pushed the feature/AIT-236 branch 3 times, most recently from 7774f24 to 1f22417 Compare February 23, 2026 16:23
Base automatically changed from feature/AIT-236 to main February 23, 2026 16:31
** @(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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this needs to include mapCreateWithObjectId and counterCreateWithObjectId given that these are the ones that the SDK actually sends?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specified in 4d1dabc as a result of #426 (comment) conversation

@lawrence-forooghian
Copy link
Collaborator

From Slack conversation:

Would you be able to actually change the order of the spec branches? i.e. first of all do the ObjectState changes and then base the sync on top of it

Reason is that I need to implement things in that order in Swift (well, in any SDK really) because both need protocol v6 but the SDK will completely break if I update to v6 to do partial sync without making the ObjectState changes

this also then matches the stacking of the ably-js branches

@VeskeR
Copy link
Contributor Author

VeskeR commented Feb 23, 2026

Would you be able to actually change the order of the spec branches? i.e. first of all do the ObjectState changes and then base the sync on top of it
Reason is that I need to implement things in that order in Swift (well, in any SDK really) because both need protocol v6 but the SDK will completely break if I update to v6 to do partial sync without making the ObjectState changes
this also then matches the stacking of the ably-js branches

This and partial sync spec PR have no intersecting spec points

lawrence-forooghian added a commit to ably/ably-liveobjects-swift-plugin that referenced this pull request Feb 25, 2026
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>
lawrence-forooghian added a commit to ably/ably-liveobjects-swift-plugin that referenced this pull request Feb 25, 2026
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>
lawrence-forooghian added a commit to ably/ably-liveobjects-swift-plugin that referenced this pull request Feb 25, 2026
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>
VeskeR added a commit to ably/ably-js that referenced this pull request Feb 26, 2026
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)
VeskeR added a commit to ably/ably-js that referenced this pull request Feb 26, 2026
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)
@VeskeR VeskeR force-pushed the AIT-313/protocol-v6-state-message branch from 694d291 to 4d1dabc Compare February 26, 2026 16:27
VeskeR added a commit to ably/ably-js that referenced this pull request Feb 26, 2026
@VeskeR VeskeR force-pushed the AIT-313/protocol-v6-state-message branch from 4d1dabc to 49f0364 Compare February 26, 2026 21:55
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>
@VeskeR VeskeR force-pushed the AIT-313/protocol-v6-state-message branch from 49f0364 to 47a9d51 Compare February 27, 2026 15:52
@VeskeR VeskeR merged commit 5d9877c into main Feb 27, 2026
2 checks passed
@VeskeR VeskeR deleted the AIT-313/protocol-v6-state-message branch February 27, 2026 15:53
VeskeR added a commit to ably/ably-js that referenced this pull request Feb 27, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants