Skip to content

[AIT-468] Add specification for the LiveObjects REST API - #476

Open
VeskeR wants to merge 1 commit into
mainfrom
AIT-468/liveobjects-rest-spec
Open

[AIT-468] Add specification for the LiveObjects REST API#476
VeskeR wants to merge 1 commit into
mainfrom
AIT-468/liveobjects-rest-spec

Conversation

@VeskeR

@VeskeRVeskeR commented May 15, 2026

Copy link
Copy Markdown
Contributor

Cross-checked against Paddy's proposal at #475 (commit 13be81c), with addressed open questsions.

The REST counterpart of LiveObjects shipped in ably-js in [1]. This spec PR adds an RSO* section under objects-features so other SDKs have a contract to implement against.

Decisions worth recording:

  • Same Objects plugin, both transports. Rather than introducing a separate PluginType for REST, PC5 now covers both RealtimeObjects and RestObject. This matches the ably-js packaging and keeps a single plugin concept for users.

  • RestObjectOperation as a flat discriminated union. Common targeting fields plus exactly-one body field, mirroring the wire format and the existing "set exactly one field" patterns (OD2, OOP3, PublishObjectData). A class hierarchy was considered but rejected: consistency with the rest of the Objects spec and the 1:1 mapping to the wire format won, and languages without sum types (Go, Python, PHP) gain little from inheritance either.

  • Paths are opaque to the SDK. RSO6 explicitly forbids client-side parsing or normalization. Path syntax (wildcards, escaping) is server-defined and can evolve without coupling SDK releases to it.

  • Full-mode leaves reuse ObjectData. Rather than introducing a parallel RestObjectData type, RSO9c defers to OD5 with explicit guarantees about which fields are unset post-decode. Avoids type duplication.

  • Client-side object IDs via generateObjectId. Realtime always generates IDs locally (RTO14); REST normally delegates to the server but supports local generation for batches where one operation needs to reference another newly-created object by ID. RSO5j documents this asymmetry so implementers understand why both Create and CreateWithObjectId variants exist.

[1] ably/ably-js#2109

Resolves AIT-468

The REST counterpart of LiveObjects shipped in ably-js in [1].
This spec PR adds an RSO* section under objects-features so
other SDKs have a contract to implement against.
Decisions worth recording:
- Same Objects plugin, both transports. Rather than introducing a
separate PluginType for REST, PC5 now covers both RealtimeObjects
and RestObject. This matches the ably-js packaging and keeps a
single plugin concept for users.
- RestObjectOperation as a flat discriminated union. Common targeting
fields plus exactly-one body field, mirroring the wire format and
the existing "set exactly one field" patterns (OD2, OOP3,
PublishObjectData). A class hierarchy was considered but rejected:
consistency with the rest of the Objects spec and the 1:1 mapping
to the wire format won, and languages without sum types (Go,
Python, PHP) gain little from inheritance either.
- Paths are opaque to the SDK. RSO6 explicitly forbids client-side
parsing or normalization. Path syntax (wildcards, escaping) is
server-defined and can evolve without coupling SDK releases to it.
- Full-mode leaves reuse ObjectData. Rather than introducing a
parallel RestObjectData type, RSO9c defers to OD5 with explicit
guarantees about which fields are unset post-decode. Avoids type
duplication.
- Client-side object IDs via generateObjectId. Realtime always
generates IDs locally (RTO14); REST normally delegates to the
server but supports local generation for batches where one
operation needs to reference another newly-created object by ID.
RSO5j documents this asymmetry so implementers understand why both
Create and CreateWithObjectId variants exist.
[1] ably/ably-js#2109
@sacOO7

sacOO7 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review against ably-js implementation

I've cross-checked this PR against the ably-js REST implementation at src/plugins/liveobjects/restobject.ts and the public TypeScript surface in liveobjects.d.ts (commit 3deeee8e). The RSO* section is comprehensive and the core semantics align well with the impl. A few observations and minor gaps below.

🟠 Type-naming divergence between spec and ably-js d.ts

The PR introduces these names:

Spec (this PR)ably-js d.ts
RestObjectMapCreate (RSO5c)RestObjectOperationMapCreate
RestObjectMapSet (RSO5e)RestObjectOperationMapSet
RestObjectMapRemove (RSO5f)RestObjectOperationMapRemove
RestObjectCounterCreate (RSO5g)RestObjectOperationCounterCreate
RestObjectCounterInc (RSO5i)RestObjectOperationCounterInc
RestObjectCreateWithObjectId (RSO5d, RSO5h)RestObjectOperationMapCreateWithObjectId / RestObjectOperationCounterCreateWithObjectId (two separate types)
RestObjectGenerateIdBody (RSO12)RestObjectOperationMapCreateBody | RestObjectOperationCounterCreateBody
RestObjectMapEntry (RSO13)RestLiveObjectMapEntry / RestObjectDataMapEntry

A few notes on the divergence:

  1. RestObjectCreateWithObjectId (shared between map and counter) — ably-js currently uses two separate types (d.ts:306-323 and 375-391). They have identical shapes (initialValue + nonce), so consolidating into one type per the spec is fine, but the ably-js d.ts will need an update to match.
  2. RestObjectGenerateIdBody — ably-js uses a union of two body types. The spec's "exactly one of mapCreate/counterCreate must be set" (RSO12) constraint is identical in effect.
  3. *Operation* prefix — ably-js consistently uses RestObjectOperation* to signal these are operation payloads. The PR drops the Operation infix. Consistency with the existing ObjectOperation* types (e.g. ObjectsMapEntry, MapCreate, etc., which are also prefix-less) is reasonable.

Suggest documenting which naming wins (so wrapper SDKs in other languages and ably-js can converge), and updating the ably-js d.ts in a follow-up.

🟡 RSO5j description of client vs server ID generation could reference RTO14

(RSO5j) explains the asymmetry:

For plain mapCreate (RSO5c) and counterCreate (RSO5g) operations, the server generates the objectId and returns it in RestObjectPublishResult.objectIds.

ably-js's restobject.ts:194-200 shows the local-generation path matching RTO14:

constobjectId=ObjectId.fromInitialValue(client.Platform,objectType,initialValueJSONString,nonce,msTimestamp,).toString();

The cross-reference to RTO14 in RSO4h is correct; RSO5j could also mention RTO14 for symmetry — "...where the client library generates the objectId locally per RTO14" — currently it says "locally per RTO14" — ✓ checked, already correct.

🟡 RSO3f should mention idempotency more precisely

(RSO3f) says:

The library must not auto-generate RestObjectOperation.id values. The idempotentRestPublishing option (RSL1k1) does not apply to RestObject#publish.

That's correct but the impl-side reasoning is worth noting: ably-js restobject.ts:265-284 preserves id verbatim if supplied (if (id != null) result.id = id). Users wanting idempotency must explicitly set id. Worth a one-liner: "Callers wanting idempotent re-publishing MUST supply id explicitly on each operation."

🟡 RSO2g3 references OD5 decoding — verify field-mask alignment

(RSO2g3) and (RSO9c) both reference OD5 decoding for typed leaf values. The impl uses decodeWireObjectData which calls into the protocol-wide decoder. RSO9c1 correctly notes that objectId and encoding fields are unset post-decode (because OD5 decodes encoding away and the leaf path can't carry objectId).

One question for clarity: does the impl preserve the original encoding field as a debug aid, or strip it? Looking at the code, it appears to strip it. Worth confirming the spec wording is "MUST be unset" and not "SHOULD be unset" for cross-SDK consistency.

🟡 RSO5j consider mentioning ably-js commit

For implementer reference (per CLAUDE.md scope rules, this would be a non-normative comment, not spec text — but worth mentioning in the PR description or commit message): the REST API ships in ably-js as of #2109. Cross-implementing SDKs can reference that PR for a working baseline.

🟡 RSO9d forwards-compatibility fallback

(RSO9d) says unrecognized objects return at minimum { objectId: String }. The impl in restobject.ts:225-241 passes the wire body through with its objectId preserved:

if('map'inwire)returnthis._decodeWireRestLiveMap(wire,format);if('counter'inwire)returnwire;returndecodeWireObjectData(wire,this._channel.client,format);

If the wire body has neither map nor counter but DOES have objectId (i.e. an unrecognized live-object type), the fall-through is decodeWireObjectData(wire, …). This decodes the inner fields but doesn't strip objectId. So in practice the returned object will have objectId preserved + any other fields. The spec wording matches this if we assume RestObjectData is Omit<ObjectData, 'value'> (as the d.ts defines).

Worth a non-normative sentence: "the unrecognized-object fallback is reached when the wire body has an objectId but neither a map nor a counter field. The library MUST preserve objectId and any additional unknown fields verbatim."

⚪ Minor / editorial

  • RSO2g4 and RSF1 cross-reference is good; consider also mentioning RSF1 in RSO5b1 (validation rejection of unknown body fields) to confirm forwards-compatibility doesn't apply to operation construction (the library can be strict on outgoing requests even though it's lenient on incoming responses).
  • IDL block: RestObjectGetCompactResult and RestObjectGetFullResult are correctly noted as "opaque to the library and not modelled here". Consider linking to RSO8 / RSO9 for completeness so a reader scrolling only the IDL knows where to find the definitions.
  • (RSO6) "paths are opaque to the SDK" — strong, clear rule. The ably-js impl simply passes paths through (restobject.ts:280-283). ✓
  • (RSO2c) percent-encoding requirement matches impl (restobject.ts:213 uses encodeURIComponent). ✓
  • PC5 update to cover both Realtime and REST: aligns with ably-js packaging where a single LiveObjects plugin exposes both. ✓

Summary

PR 476 closes the spec gap that's tracked as B-61 in local LIVEOBJECTS_SPEC_REVIEW.md.

Sign up for freeto 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

@VeskeR@sacOO7