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
Copy link
Copy Markdown
Contributor

@VeskeR VeskeR commented May 15, 2026

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
Copy link
Copy Markdown
Collaborator

sacOO7 commented May 18, 2026

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:

const objectId = 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' in wire) return this._decodeWireRestLiveMap(wire, format);
if ('counter' in wire) return wire;
return decodeWireObjectData(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 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