Skip to content

Conversation

@VeskeR
Copy link
Contributor

@VeskeR VeskeR commented Nov 14, 2025

Resolves PUB-1197, AIT-27

Summary by CodeRabbit

  • New Features
    • REST-based object API: channel.object supports get (compact/expanded) and publish for map/counter operations, including batch publishes; get returns undefined for missing objects.
  • Documentation
    • Docs updated to show RestClient / Rest usage with the Objects plugin and plugin integration notes.
  • Tests
    • Comprehensive REST and realtime tests added; shared object test helpers introduced and reused.
  • Chores
    • RestClient, BaseRest and Rest surfaced in modular exports for plugin use.

✏️ Tip: You can customize this high-level summary in your review settings.

VeskeR added 26 commits October 9, 2025 07:42
… with the naming convention for other realtime features in the SDK

This updates ably-js implementation to match the spec [1]

[1] ably/specification#332
Change the name for the `Objects` class to `RealtimeObjects`
[PUB-2059] Change LiveObjects entry point to `channel.object.get()`
Update Objects docstrings to not use "root" terminology
Adds new path-based types to ably.d.ts.

The previous LiveObject types are temporarily marked as *Deprecated in
ably.d.ts until they are fully converted to the new type system in the
following PRs.
The `RealtimeObject` entrypoint temporarily adds a `getPathObject` method
to get a PathObject instance for a root and preserves the existing
`.get()` method which returns a LiveMap instance directly so that
corresponding tests do not fail yet.

Resolves PUB-2057
[PUB-2057] Implement base PathObject class with access and mutation methods
This makes LiveObjects Instance methods to be in line with PathObject
methods handling of wrong underlying type, as discussed in [1].

Type/behavior overview:
- id(): may return `undefined` (when Instance is from .get() call that
  returned a primitive instance)
- compact(): is defined for all Instance types so can be non-nullable.
  See method implementation in [2]
- get(): may return `undefined` when not a map
- entries()/keys()/values()/size(): may return `undefined` when not a map
- value(): may return `undefined` when not a counter or a primitive

[1] #2091 (comment)
[2] #2098
Exposes LiveMap.create() and LiveCounter.create() static methods at
ably/objects export.
Updates all objects tests to use these new methods to create LiveMap and
LiveCounter instead of realtimeObject.createMap()/.createCounter() calls

Resolves PUB-2060
They are replaced by object creation via value types from previous
commit
[PUB-2063] Implement Instance API for LiveObjects PathObject
[PUB-2060] Implement LiveMap and LiveCounter creation via value types
Implements:
- LiveObject parent tracking
- Path-based LiveObject event emission
- PathObject subscriptions path matching with deep subscriptions
- Path event emission for LiveMap key updates
- Full ObjectMessage (user-facing type) argument for subscription callbacks

Resolves PUB-2061
Stricter typing removes `ArrayBufferView` from the union type to align
with the publicly exposed API, which only uses a union of `Buffer` and
`ArrayBuffer`.
This change enables the internal `ObjectMessage.operation` object to be
exposed as the public `ObjectOperation` type in the following commit
without buffer type mismatches for underlying values.
…iveObject subscriptions

LiveObject subscriptions (via PathObject and Instance APIs) now expose
the full operation object that caused the change in the
`event.message.operation` field.
This was replaced by PathObject/Instance subscription API from previous
commits
[PUB-2061] Add path based subscriptions support for LiveObjects
[PUB-2064] Implement `.compact()` method for PathObject and Instance types for LiveObjects
[PUB-2062] Implement async iterator subscriptions API for LiveObjects
@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2025

Walkthrough

Adds REST-based Objects support: new TypeScript types, a RestObject class with get/publish on RestChannel, BaseClient plugin wiring, RestChannel object getter and annotations getter, plugin export updates, a RestObject implementation, and new/updated tests and helpers for REST and realtime object operations.

Changes

Cohort / File(s) Summary
Type definitions
ably.d.ts
Added REST object operation and data types (RestObjectOperationBase, TargetByObjectId/Path, AnyTargetOperationBase, OperationMapCreate/Set/Remove, OperationCounterCreate/Inc, RestObjectOperation union, RestObjectPublishResult, RestCompactObjectData, RestLiveObject, RestLiveMap, RestObjectMapEntry, RestObjectData, RestLiveCounter, AnyRestLiveObject, GetObjectParams, RestObject interface, PrimitiveOrObjectReference); updated ObjectOperationAction docs.
Docs / usage exports
objects.d.ts
Export/import updates to reference RestClient, BaseRest, and Rest in examples/docs and modular exports.
Base client plugin wiring
src/common/lib/client/baseclient.ts
Added `readonly _objectsPlugin: typeof ObjectsPlugin
Realtime client cleanup
src/common/lib/client/baserealtime.ts
Removed the _objectsPlugin field and its initialization from BaseRealtime.
Realtime channel plugin sourcing
src/common/lib/client/realtimechannel.ts
RealtimeChannel now uses client._objectsPlugin to initialize RealtimeObject when present.
REST channel object API
src/common/lib/client/restchannel.ts
Added private _object?: RestObject, public get object(): RestObject (lazy-init, throws if plugin missing), reintroduced get annotations(): RestAnnotations, added RestObject type import.
Plugin exports
src/plugins/objects/index.ts
Exported RestObject in named exports and included it in the default export bundle.
RestObject implementation
src/plugins/objects/restobject.ts
New RestObject class with overloaded get() (compact/expanded) and publish() (single/batch). Implements path construction, request format negotiation (msgpack/json), encoding/decoding helpers, publish body construction, response decoding, and 404->undefined behavior. Exposes several wire types.
Test helpers
test/common/modules/objects_helper.js
Added static helpers: waitFixtureChannelIsReady, waitForMapKeyUpdate, waitForCounterUpdate, waitForObjectOperation, waitForObjectSync.
Realtime tests
test/realtime/objects.test.js
Replaced in-file wait helpers with ObjectsHelper references, removed local implementations, adjusted call signatures to pass channel name.
REST tests
test/rest/objects.test.js
Added REST Objects test suite covering channel.object behavior, get() variations (compact/expanded, id/path, primitives/buffers), publish() operations (MAP_SET/CREATE/REMOVE, COUNTER_CREATE/INC, batching) and state verification.
Test harness
test/common/modules/shared_helper.js
Consolidated protocol test wiring with a createTest wrapper to unify JSON/msgpack invocation.
REST request test
test/rest/request.test.js
Converted a header assertion to async coordination (manual promise) and updated the test callback to async.
Module report
scripts/moduleReport.ts
Added src/plugins/objects/restobject.ts to allowedFiles for bundle size checks.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RestChannel
    participant RestObject
    participant AblyAPI

    Note over Client,RestObject: RestObject.get() flow
    Client->>RestChannel: access channel.object
    RestChannel->>RestObject: lazy-init (new RestObject(channel))
    Client->>RestObject: get(params)
    RestObject->>RestObject: _basePath(params)
    RestObject->>AblyAPI: HTTP GET /objects{path|id}
    AblyAPI-->>RestObject: 200/404 + payload
    RestObject->>RestObject: decode (msgpack/json)
    RestObject-->>Client: RestCompactObjectData | RestLiveObject | undefined

    Note over Client,RestObject: RestObject.publish() flow
    Client->>RestObject: publish(op or [ops])
    RestObject->>RestObject: _constructPublishBody(ops)
    RestObject->>RestObject: _encodePrimitive(...) for values
    RestObject->>AblyAPI: HTTP POST /objects{path|id} with encoded body
    AblyAPI-->>RestObject: 200 + publish result
    RestObject-->>Client: RestObjectPublishResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay particular attention to:
    • src/plugins/objects/restobject.ts — encoding/decoding, wire-format construction, binary vs JSON handling, header composition, error mapping (404 -> undefined).
    • src/common/lib/client/* — plugin lifecycle, initialization differences between BaseClient and BaseRealtime, and channel getters.
    • test/rest/objects.test.js — many scenarios and encoding expectations; ensure fixtures and readiness helpers align with implementation.
    • ably.d.ts — overloads/unions and exported type compatibility.

Suggested reviewers

  • lawrence-forooghian

Poem

🐇 I hopped to a RESTful glade today,
where get() and publish dance and play,
compact crumbs or live-map stew,
counters nudge and maps hop through,
a rabbit cheers: "Objects — hip hooray!"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive The linked issue (AIT-27) title is incomplete/missing in the provided context, making it impossible to validate whether all coding requirements are met. Provide the complete description and acceptance criteria from AIT-27 and PUB-1197 to verify all coding requirements are addressed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[PUB-1197] LiveObjects REST client' clearly summarizes the main objective: implementing REST client support for LiveObjects functionality.
Out of Scope Changes check ✅ Passed All changes appear scoped to REST client LiveObjects support: type definitions, RestObject implementation, test helpers, test suites, and necessary infrastructure updates align with the PR objective.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch PUB-1197/liveobjects-rest-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 14, 2025 09:35 Inactive
@kaschula
Copy link

Something I noticed when testing this branch for Msgpack requests. The SDK currently isn't setting the correct content type header of application/msgpack it was being set to JSON still

Base automatically changed from liveobjects/remove-non-path-based-api to integration/objects-breaking-api November 26, 2025 09:21
[PUB-3439] Remove AblyObjectsTypes and expect the types for the object structure to be provided explicitly
@VeskeR VeskeR force-pushed the PUB-1197/liveobjects-rest-api branch from 5f7807f to 568d0a0 Compare December 4, 2025 04:20
@github-actions github-actions bot temporarily deployed to staging/pull/2109/bundle-report December 4, 2025 04:21 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features December 4, 2025 04:21 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
test/rest/objects.test.js (2)

214-362: MAP_SET test operations still missing required key field

In several publish tests, the map.set operations are constructed without a key:

const operation = {
  operation: 'map.set',
  path: 'testKey',
  value: 'testValue',
};

and in the batch test:

{
  operation: 'map.set',
  path: 'key1',
  value: 'value1',
},
{
  operation: 'map.set',
  path: 'key2',
  value: 42,
},

Given the earlier review comment that the REST API requires a key for map set and, for setting a key on the root, expects path: '' and key: 'testKey', these operations are still malformed and likely to fail REST validation. The current _constructPublishBody also expects op.key to be present when building data.key.

Recommend updating all root-level map.set operations in these tests along the lines of:

-const operation = {
-  operation: 'map.set',
-  path: 'testKey',
-  value: 'testValue',
-};
+const operation = {
+  operation: 'map.set',
+  path: '',
+  key: 'testKey',
+  value: 'testValue',
+};

and similarly for the batch:

-{
-  operation: 'map.set',
-  path: 'key1',
-  value: 'value1',
-},
+{
+  operation: 'map.set',
+  path: '',
+  key: 'key1',
+  value: 'value1',
+},
-{
-  operation: 'map.set',
-  path: 'key2',
-  value: 42,
-},
+{
+  operation: 'map.set',
+  path: '',
+  key: 'key2',
+  value: 42,
+},

366-412: Fix undefined result and align MAP_SET operation shape with API

In the “publish multiple operations and verify state changes” test:

const operation = {
  operation: 'map.set',
  path: 'integrationKey',
  value: 'integrationValue',
};

await channel.object.publish(operation);

// ...

expect(result).to.exist;
expect(result.integrationKey).to.equal('integrationValue');

Issues:

  1. result is never defined in this scope; this will throw a ReferenceError. You probably meant to assert against rootResult (retrieved via channel.object.get()).
  2. The map.set operation again omits the key field and uses path as if it were the key, conflicting with the REST API shape and the earlier review comment.

Suggested fix:

-        const operation = {
-          operation: 'map.set',
-          path: 'integrationKey',
-          value: 'integrationValue',
-        };
+        const operation = {
+          operation: 'map.set',
+          path: '',
+          key: 'integrationKey',
+          value: 'integrationValue',
+        };
@@
-        // Get root object to verify counter reference
+        // Get root object to verify counter reference and map value
         const rootResult = await channel.object.get();
         expect(rootResult).to.exist;
         expect(rootResult.testCounter).to.exist;
         expect(rootResult.testCounter.objectId).to.equal(counterObjectId);
 
-        expect(result).to.exist;
-        expect(result.integrationKey).to.equal('integrationValue');
+        expect(rootResult.integrationKey).to.equal('integrationValue');
src/plugins/objects/restobject.ts (1)

100-163: Map/counter publish body construction: fix operation names and rely on wire-level enums

Two issues here:

  1. Operation name casing / format

The switch uses operation values like 'map.create', 'map.set', 'map.remove', 'counter.create', 'counter.inc', and then forwards op.operation directly to the wire:

const operation = op.operation;
switch (operation) {
  case 'map.create':
    return {
      operation: op.operation,
      // ...
    };
  // ...
}

Per the earlier review comment and REST docs, the wire format expects MAP_CREATE, MAP_SET, MAP_REMOVE, COUNTER_CREATE, COUNTER_INC, etc. If you send the dotted lowercase strings, REST validation will fail.

A robust approach is to map the SDK-level operations to the wire-level enums:

-  private _constructPublishBody(op: RestObjectOperation, format: Utils.Format): any {
-    const operation = op.operation;
+  private _constructPublishBody(op: RestObjectOperation, format: Utils.Format): any {
+    const wireOperationMap: Record<string, string> = {
+      'map.create': 'MAP_CREATE',
+      'map.set': 'MAP_SET',
+      'map.remove': 'MAP_REMOVE',
+      'counter.create': 'COUNTER_CREATE',
+      'counter.inc': 'COUNTER_INC',
+    };
+    const operation = wireOperationMap[op.operation];
@@
-      case 'map.create':
+      case 'MAP_CREATE':
         return {
-          operation: op.operation,
+          operation,
@@
-      case 'map.set':
+      case 'MAP_SET':
         return {
-          operation: op.operation,
+          operation,
@@
-      case 'map.remove':
+      case 'MAP_REMOVE':
@@
-      case 'counter.create':
+      case 'COUNTER_CREATE':
@@
-      case 'counter.inc':
+      case 'COUNTER_INC':
@@
-      default:
+      default:
         throw new this.channel.client.ErrorInfo('Unsupported publish operation action: ' + operation, 40003, 400);
  1. Assumptions about map.set shape

The map.set case builds:

data: {
  key: op.key,
  value: {
    ...this._encodePrimitive(op.value, format),
    ...(op.encoding ? { encoding: op.encoding } : {}),
  },
},

This is consistent with the test suite’s more complex usages (e.g. objectId + key), but several tests still construct operations without key and try to overload path. Those tests should be fixed (see comments in test/rest/objects.test.js), rather than loosening this method.

🧹 Nitpick comments (2)
ably.d.ts (1)

2291-2538: REST Objects typings look consistent with runtime, but add property JSDoc to fix Typedoc warnings

The new REST object operation and data types (RestObjectOperation*, RestObject*Data, GetObjectParams, PrimitiveOrObjectReference, and the updated ObjectOperationAction) are coherent and line up with the RestObject implementation (ids, extras, map/counter payloads, and operation action strings).

The remaining issue is just documentation noise from Typedoc for the inline TargetByObjectId/TargetByPath property types. You can keep the current aliases but document the properties explicitly so the API Reference job stops warning:

-/**
- * Targets an object by its object ID.
- */
-type TargetByObjectId = { objectId: string; path?: never };
+/**
+ * Targets an object by its object ID.
+ */
+type TargetByObjectId = {
+  /** The ID of the object to target. Mutually exclusive with `path`. */
+  objectId: string;
+  /** Must not be specified when targeting by `objectId`. */
+  path?: never;
+};
@@
-/**
- * Targets an object by its location using the path.
- * Paths are expressed relative to the structure of the object as defined by the compact view of the object tree.
- */
-type TargetByPath = { path: string; objectId?: never };
+/**
+ * Targets an object by its location using the path.
+ * Paths are expressed relative to the structure of the object as defined by the compact view of the object tree.
+ */
+type TargetByPath = {
+  /** Path to the target, evaluated relative to the entrypoint object. Mutually exclusive with `objectId`. */
+  path: string;
+  /** Must not be specified when targeting by `path`. */
+  objectId?: never;
+};

This should resolve the API Reference warnings without changing the public surface.

Also applies to: 2626-2632, 3952-3960

test/common/modules/objects_helper.js (1)

42-127: Harden transport interception helpers to always restore original handler and add light guards

The new helpers are handy, but the onProtocolMessage interception is a bit fragile:

  • In both waitForObjectOperation and waitForObjectSync, if onProtocolMessageOriginal throws, the catch rejects the promise but leaves transport.onProtocolMessage patched, which can cascade into later tests.
  • Both helpers assume client.connection.connectionManager.activeProtocol and getTransport() are always available; if a test uses them before the connection is fully established, this will throw in a slightly opaque way.
  • waitFixtureChannelIsReady assumes entryPathObject.instance() is always non‑undefined; if object.get() ever returns a PathObject that doesn’t yet resolve to an instance, entryInstance.get(key) will throw.

Consider a small defensive refactor that keeps behavior but makes these helpers safer in failing tests, for example:

-        const transport = client.connection.connectionManager.activeProtocol.getTransport();
-        const onProtocolMessageOriginal = transport.onProtocolMessage;
+        const activeProtocol = client.connection.connectionManager.activeProtocol;
+        if (!activeProtocol) {
+          return reject(new Error('No active protocol when waiting for object operation'));
+        }
+        const transport = activeProtocol.getTransport();
+        const onProtocolMessageOriginal = transport.onProtocolMessage;
@@
-        transport.onProtocolMessage = function (message) {
-          try {
-            helper.recordPrivateApi('call.transport.onProtocolMessage');
-            onProtocolMessageOriginal.call(transport, message);
-
-            if (message.action === 19 && message.state[0]?.operation?.action === waitForAction) {
-              helper.recordPrivateApi('replace.transport.onProtocolMessage');
-              transport.onProtocolMessage = onProtocolMessageOriginal;
-              resolve();
-            }
-          } catch (err) {
-            reject(err);
-          }
-        };
+        transport.onProtocolMessage = function (message) {
+          try {
+            helper.recordPrivateApi('call.transport.onProtocolMessage');
+            onProtocolMessageOriginal.call(transport, message);
+
+            if (message.action === 19 && message.state[0]?.operation?.action === waitForAction) {
+              resolve();
+            }
+          } catch (err) {
+            reject(err);
+          } finally {
+            // Always restore the original handler once this helper completes
+            helper.recordPrivateApi('replace.transport.onProtocolMessage');
+            transport.onProtocolMessage = onProtocolMessageOriginal;
+          }
+        };

and similarly for waitForObjectSync. Optionally, you could also guard entryInstance in waitFixtureChannelIsReady and fail fast with a descriptive error if it can’t be resolved.

These changes would make the tests more robust without changing their intended behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 464d6b4 and 568d0a0.

📒 Files selected for processing (11)
  • ably.d.ts (3 hunks)
  • objects.d.ts (2 hunks)
  • src/common/lib/client/baseclient.ts (3 hunks)
  • src/common/lib/client/baserealtime.ts (0 hunks)
  • src/common/lib/client/realtimechannel.ts (1 hunks)
  • src/common/lib/client/restchannel.ts (4 hunks)
  • src/plugins/objects/index.ts (2 hunks)
  • src/plugins/objects/restobject.ts (1 hunks)
  • test/common/modules/objects_helper.js (1 hunks)
  • test/realtime/objects.test.js (1 hunks)
  • test/rest/objects.test.js (1 hunks)
💤 Files with no reviewable changes (1)
  • src/common/lib/client/baserealtime.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/baseclient.ts
  • src/common/lib/client/restchannel.ts
🧰 Additional context used
🧬 Code graph analysis (4)
test/realtime/objects.test.js (1)
test/rest/objects.test.js (1)
  • waitFixtureChannelIsReady (13-13)
test/rest/objects.test.js (2)
src/common/lib/client/realtimechannel.ts (1)
  • object (153-158)
src/common/lib/client/restchannel.ts (1)
  • object (71-76)
src/plugins/objects/restobject.ts (1)
ably.d.ts (6)
  • GetObjectParams (2493-2500)
  • RestCompactObjectData (2407-2415)
  • RestLiveObject (2421-2421)
  • RestObjectOperation (2380-2385)
  • RestObjectPublishResult (2391-2401)
  • PrimitiveOrObjectReference (2632-2632)
ably.d.ts (1)
src/plugins/objects/restobject.ts (1)
  • RestObject (12-184)
🪛 GitHub Actions: API Reference
ably.d.ts

[warning] 1-1: Typedoc: ably.TargetByObjectId.__type.objectId does not have any documentation.


[warning] 1-1: Typedoc: ably.TargetByObjectId.__type.path does not have any documentation.


[warning] 1-1: Typedoc: ably.TargetByPath.__type.path does not have any documentation.


[warning] 1-1: Typedoc: ably.TargetByPath.__type.objectId does not have any documentation.

🪛 GitHub Actions: Bundle size report
src/plugins/objects/restobject.ts

[error] 1-1: Unexpected file src/plugins/objects/restobject.ts, contributes 2332B to bundle, more than allowed 100B

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test-browser (chromium)
  • GitHub Check: test-browser (webkit)
  • GitHub Check: test-browser (firefox)
  • GitHub Check: test-node (20.x)
  • GitHub Check: test-node (16.x)
  • GitHub Check: test-node (18.x)
🔇 Additional comments (8)
objects.d.ts (1)

3-12: Objects plugin docs align with new REST support

The updated imports and JSDoc correctly describe usage with both Realtime/Rest and modular BaseRealtime/BaseRest (including the Rest plugin requirement). No issues from a typings or behavior perspective.

Also applies to: 46-63

src/plugins/objects/index.ts (1)

5-22: RestObject export wiring looks correct

Importing RestObject from ./restobject and exposing it in both the named and default exports matches how the other Objects types are surfaced; nothing further needed here.

test/realtime/objects.test.js (1)

20-24: Reusing shared wait helpers from ObjectsHelper looks good

Aliasing these helpers from ObjectsHelper keeps this test file DRY and consistent with the REST tests. As long as the underlying helpers don’t depend on a this context (which appears true given their other usages), this change is sound and should be behavior‑preserving.

test/rest/objects.test.js (3)

47-55: Channel .object error test looks good

The “Rest without Objects plugin” test correctly asserts that accessing channel.object without the plugin throws with the expected message, matching the Utils.throwMissingPluginError('Objects') behavior from restchannel.ts.


66-210: RestObject.get() test coverage is comprehensive and consistent

The RestObject.get() suite (non-existent IDs/paths, root default, compact true/false behaviors, objectId and path variants, primitive type handling, and binary checks via BufferUtils) is thorough and matches the intended API semantics.

These tests should give good confidence that RestObject.get and the server responses behave correctly once the fixtures are in place.


415-477: Complex workflow test is solid, but depends on correct object-reference encoding

This test does a good job of exercising a realistic workflow (map.create, map.set, counter.create, then wiring a counter reference into the map) and verifying both compact and expanded views.

Note that the expectation:

expect(mapContent.map.entries.counter.data.objectId).to.equal(counterObjectId);

assumes that value: { objectId: counterObjectId } in the map.set operation is preserved as an object reference on the wire, not JSON-stringified. This needs RestObject._encodePrimitive to special‑case { objectId: string } so that the server sees it as a reference rather than generic JSON.

src/plugins/objects/restobject.ts (2)

19-56: get() flow looks reasonable; ensure headers / format match existing REST patterns

The get implementation follows the existing client patterns (choosing format from useBinaryProtocol, using envelope based on supportsLinkHeaders, and decoding via Utils.decodeBody). The 404 handling via isErrorInfoOrPartialErrorInfo and error.code === 40400 is a good way to surface “not found” as undefined.

Given the msgpack-related concerns on this branch, it’s worth double‑checking that Defaults.defaultGetHeaders(client.options) sets an appropriate Accept header (and any other necessary flags) when useBinaryProtocol is true, in the same way as other REST GETs in the SDK, so that the server returns msgpack rather than JSON.


1-184: Address bundlesize failure for restobject.ts

The bundlesize check reports:

Unexpected file src/plugins/objects/restobject.ts, contributes 2332B to bundle, more than allowed 100B

This new file is clearly larger than the default allowance. You’ll need to either:

  • Update the bundlesize configuration to include this file with an appropriate budget, or
  • Restructure / split code (e.g. moving shared helpers or types) so that the per‑file contribution is within the configured threshold.

Given this is core new functionality, adjusting the bundlesize config to account for it is likely the pragmatic fix.

@VeskeR VeskeR force-pushed the PUB-1197/liveobjects-rest-api branch from 568d0a0 to 879347f Compare December 4, 2025 08:44
@github-actions github-actions bot temporarily deployed to staging/pull/2109/bundle-report December 4, 2025 08:45 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features December 4, 2025 08:45 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
objects.d.ts (1)

67-67: Fix TypeScript export incompatibility.

TypeScript does not allow export = (CommonJS-style export assignment) to coexist with named exports like the LiveMap and LiveCounter classes defined earlier in this file. This is causing the pipeline failure.

Consider one of these solutions:

Solution 1 (recommended): Use default export

-declare const Objects: any;
-
-export = Objects;
+export default Objects;
+declare const Objects: any;

Solution 2: Remove named exports and export everything through the default
Move LiveMap and LiveCounter class declarations to be properties of the Objects object instead of separate named exports.

src/common/lib/client/realtimechannel.ts (1)

136-142: Consider caching the Push plugin as an internal field for consistency.

The Objects plugin is now cached as client._objectsPlugin, following the pattern established for other core plugins (Crypto, Annotations). However, Push continues to be accessed directly via client.options.plugins?.Push throughout the codebase. For consistency and potential performance benefits, consider caching Push similarly—e.g., as client._pushPlugin in BaseClient's constructor, similar to line 108.

♻️ Duplicate comments (3)
test/rest/objects.test.js (1)

15-21: Add default parameter for options to prevent runtime errors.

Both helper functions spread options directly, which will throw a TypeError if called without the second argument. Several call sites (e.g., lines 108, 142, 208, 257) pass only the helper argument via RealtimeWithObjects(helper, options) where options comes from the test callback - this works, but if ever called without options it would fail.

-function RealtimeWithObjects(helper, options) {
-  return helper.AblyRealtime({ ...options, plugins: { Objects: ObjectsPlugin } });
-}
-
-function RestWithObjects(helper, options) {
-  return helper.AblyRest({ ...options, plugins: { Objects: ObjectsPlugin } });
-}
+function RealtimeWithObjects(helper, options = {}) {
+  return helper.AblyRealtime({ ...options, plugins: { Objects: ObjectsPlugin } });
+}
+
+function RestWithObjects(helper, options = {}) {
+  return helper.AblyRest({ ...options, plugins: { Objects: ObjectsPlugin } });
+}
src/plugins/objects/restobject.ts (2)

125-125: Pass format parameter to defaultPostHeaders() to fix Content-Type for MsgPack.

The publish() method calls defaultPostHeaders(client.options) without passing the format parameter. This causes the Content-Type header to default to JSON regardless of the useBinaryProtocol setting, while the body is encoded as msgpack on line 133. This is the root cause of the MsgPack Content-Type issue reported in the PR comments.

-    const headers = client.Defaults.defaultPostHeaders(client.options);
+    const headers = client.Defaults.defaultPostHeaders(client.options, { format });

152-158: Change path segment from /object to /objects.

The REST API endpoint for LiveObjects is /channels/{channelName}/objects (plural), but _basePath constructs paths with /object (singular). This mismatch will cause all REST API calls to fail with 404 errors.

     return (
       this._channel.client.rest.channelMixin.basePath(this._channel) +
-      '/object' +
+      '/objects' +
       (objectId ? '/' + encodeURIComponent(objectId) : '')
     );
🧹 Nitpick comments (8)
test/rest/request.test.js (1)

35-57: Consider adding a comment to explain the promise coordination pattern.

The manual promise creation (lines 35-40) and the non-settling promise return (line 50) form a coordination pattern that ensures assertions complete before the test finishes. While this works correctly, a brief comment would help future maintainers understand why this approach is necessary.

Consider adding a comment like:

       let savedResolve;
       let savedReject;
+      // Manual promise coordination: testRequestHandler returns a non-settling promise
+      // to keep the request "in progress" while we control test completion via savedResolve
       let promise = new Promise((res, rej) => {
test/realtime/objects.test.js (1)

20-24: Centralising wait helpers via ObjectsHelper looks good

Pulling waitFixtureChannelIsReady / waitFor* from ObjectsHelper keeps waiter behaviour consistent across test suites and removes local duplication. The alias pattern is fine here as long as those helpers don’t rely on this; if they ever do, consider binding them (ObjectsHelper.waitForMapKeyUpdate.bind(ObjectsHelper), etc.) as a small hardening step.

test/rest/objects.test.js (2)

108-108: Close Realtime client after use to prevent resource leaks.

The RealtimeWithObjects client created here is used only to wait for the fixture channel to be ready but is never closed. This pattern repeats at lines 142, 208, and 257. Consider storing the client and closing it after the wait completes.

-          await waitFixtureChannelIsReady(RealtimeWithObjects(helper, options), objectsFixturesChannel);
+          const realtimeClient = RealtimeWithObjects(helper, options);
+          await waitFixtureChannelIsReady(realtimeClient, objectsFixturesChannel);
+          realtimeClient.close();

279-292: Consider moving primitiveKeyData definition before its first usage.

The primitiveKeyData array is used at line 262 inside the RestObject.get() tests but defined here after those tests. While this works due to hoisting, moving the definition before its first usage (before line 250) would improve readability.

test/common/modules/objects_helper.js (2)

93-93: Replace magic numbers with named constants for clarity.

The action values 19 and 20 are protocol message action codes. Consider defining constants for these (e.g., OBJECT_ACTION = 19, OBJECT_SYNC_ACTION = 20) to improve readability and maintainability.

+  static OBJECT_ACTION = 19;
+  static OBJECT_SYNC_ACTION = 20;
+
   static async waitForObjectOperation(helper, client, waitForAction) {
     // ...
-          if (message.action === 19 && message.state[0]?.operation?.action === waitForAction) {
+          if (message.action === ObjectsHelper.OBJECT_ACTION && message.state[0]?.operation?.action === waitForAction) {

Also applies to: 117-117


81-127: Consider extracting shared transport interception logic.

Both waitForObjectOperation and waitForObjectSync follow the same pattern: get transport, replace onProtocolMessage, restore on match. The only difference is the condition. A shared helper could reduce duplication, though this is minor for test utilities.

src/plugins/objects/restobject.ts (2)

86-91: Simplify redundant condition check.

The format variable is always truthy (either 'msgpack' or 'json' from line 70), so the if (format) condition at line 87 will always be true. The else branch is unreachable.

-      let decodedBody: RestCompactObjectData | WireRestLiveObject | undefined;
-      if (format) {
-        decodedBody = client.Utils.decodeBody(response.body, client._MsgPack, format);
-      } else {
-        decodedBody = response.body;
-      }
+      const decodedBody = client.Utils.decodeBody(response.body, client._MsgPack, format);

145-149: Simplify redundant condition check in publish().

Similar to the get() method, format is always truthy here, making the condition unnecessary.

-    if (format) {
-      return client.Utils.decodeBody(response.body, client._MsgPack, format);
-    }
-
-    return response.body!;
+    return client.Utils.decodeBody(response.body, client._MsgPack, format);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 568d0a0 and 879347f.

📒 Files selected for processing (13)
  • ably.d.ts (3 hunks)
  • objects.d.ts (2 hunks)
  • src/common/lib/client/baseclient.ts (3 hunks)
  • src/common/lib/client/baserealtime.ts (0 hunks)
  • src/common/lib/client/realtimechannel.ts (1 hunks)
  • src/common/lib/client/restchannel.ts (4 hunks)
  • src/plugins/objects/index.ts (2 hunks)
  • src/plugins/objects/restobject.ts (1 hunks)
  • test/common/modules/objects_helper.js (1 hunks)
  • test/common/modules/shared_helper.js (1 hunks)
  • test/realtime/objects.test.js (4 hunks)
  • test/rest/objects.test.js (1 hunks)
  • test/rest/request.test.js (2 hunks)
💤 Files with no reviewable changes (1)
  • src/common/lib/client/baserealtime.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/common/lib/client/baseclient.ts
🧰 Additional context used
🧬 Code graph analysis (5)
test/rest/request.test.js (2)
test/rest/http.test.js (3)
  • helper (11-11)
  • helper (27-27)
  • helper (72-72)
test/rest/status.test.js (2)
  • helper (11-11)
  • rest (38-38)
src/plugins/objects/restobject.ts (1)
ably.d.ts (8)
  • ObjectsMapSemantics (3975-3975)
  • GetObjectParams (2493-2500)
  • RestLiveObject (2421-2421)
  • RestObjectOperation (2380-2385)
  • RestObjectPublishResult (2391-2401)
  • PrimitiveOrObjectReference (2632-2632)
  • RestObjectMapEntry (2441-2444)
  • RestObjectData (2450-2463)
test/rest/objects.test.js (2)
src/common/lib/client/realtimechannel.ts (1)
  • object (153-158)
src/common/lib/client/restchannel.ts (1)
  • object (71-76)
src/common/lib/client/restchannel.ts (2)
src/plugins/objects/restobject.ts (1)
  • RestObject (59-354)
src/plugins/objects/index.ts (1)
  • RestObject (12-12)
ably.d.ts (1)
src/plugins/objects/restobject.ts (1)
  • RestObject (59-354)
🪛 GitHub Actions: API Reference
ably.d.ts

[warning] 1-1: Typedoc warning: ably.TargetByObjectId.__type.objectId does not have any documentation.


[warning] 1-1: Typedoc warning: ably.TargetByObjectId.__type.path does not have any documentation.


[warning] 1-1: Typedoc warning: ably.TargetByPath.__type.path does not have any documentation.


[warning] 1-1: Typedoc warning: ably.TargetByPath.__type.objectId does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapCreate.__type.operation does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapCreate.__type.entries does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapSet.__type.operation does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapSet.__type.key does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapSet.__type.value does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapSet.__type.encoding does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapRemove.__type.operation does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationMapRemove.__type.key does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationCounterCreate.__type.operation does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationCounterCreate.__type.count does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationCounterInc.__type.operation does not have any documentation.


[warning] 1-1: Typedoc warning: ably.OperationCounterInc.__type.amount does not have any documentation.


[warning] 1-1: Typedoc warning: ably.RestCompactObjectData.__type.objectId does not have any documentation.


[warning] 1-1: Typedoc warning: ably.RestObject.get.params.__type.compact does not have any documentation.


[warning] 1-1: Typedoc warning: ably.PrimitiveOrObjectReference.__type.objectId does not have any documentation.

🪛 GitHub Actions: Bundle size report
src/plugins/objects/restobject.ts

[error] 1-1: Unexpected file src/plugins/objects/restobject.ts, contributes 4040B to bundle, more than allowed 100B. Command failed during 'npm run modulereport' (exit code 1).

🪛 GitHub Actions: Test NPM package
objects.d.ts

[error] 67-67: TS2309: An export assignment cannot be used in a module with other exported elements.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test-browser (chromium)
  • GitHub Check: test-browser (webkit)
  • GitHub Check: test-browser (firefox)
  • GitHub Check: test-node (16.x)
  • GitHub Check: test-node (20.x)
  • GitHub Check: test-node (18.x)
🔇 Additional comments (6)
objects.d.ts (1)

8-8: Documentation updates look good.

The additions of RestClient, BaseRest, and Rest to the imports and documentation accurately reflect the expanded REST support for Objects functionality.

Also applies to: 11-11, 47-63

test/common/modules/shared_helper.js (1)

385-393: LGTM! Clean refactoring for async test support.

The createTest helper unifies test invocation and enables passing the helper as a third argument to test functions. The async-only approach is appropriate for testOnJsonMsgpack, which differs from testOnAllTransportsAndProtocols that supports both callback and promise styles.

src/plugins/objects/index.ts (1)

5-5: LGTM! RestObject export added correctly.

The RestObject import and export follow the same pattern as existing exports in this file.

Also applies to: 12-12, 21-21

test/realtime/objects.test.js (1)

433-433: Reusing waitFixtureChannelIsReady from ObjectsHelper is an appropriate refactor

Switching these scenarios to await waitFixtureChannelIsReady(client, objectsFixturesChannel); keeps fixture-channel readiness logic in one place and preserves the existing call shape, so the tests remain easy to follow.

Also applies to: 605-605, 631-631

src/common/lib/client/restchannel.ts (1)

21-21: LGTM!

The RestObject integration follows the established plugin patterns used for Push and Annotations. The lazy initialization in the constructor and the defensive getter that throws when the plugin is missing are consistent with the existing codebase conventions.

Also applies to: 38-38, 52-54, 71-76

src/plugins/objects/restobject.ts (1)

250-256: LGTM on object reference handling.

The _encodePrimitive method now correctly handles object references by checking for the objectId property before falling back to JSON stringification. This addresses the previously flagged issue where object references were incorrectly being JSON-encoded.

@VeskeR VeskeR force-pushed the PUB-1197/liveobjects-rest-api branch from 879347f to 3ac5641 Compare December 4, 2025 08:54
@github-actions github-actions bot temporarily deployed to staging/pull/2109/bundle-report December 4, 2025 08:54 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features December 4, 2025 08:54 Inactive
Adds REST support for Objects REST API endpoints:
- GET /channel/{channel}/object/{id?}?compact={boolean}&path={string}
- POST /channels/{channel}/object

Resolves PUB-1197
@VeskeR
Copy link
Contributor Author

VeskeR commented Dec 4, 2025

Something I noticed when testing this branch for Msgpack requests. The SDK currently isn't setting the correct content type header of application/msgpack it was being set to JSON still

Fixed in https://github.com/ably/ably-js/compare/879347fb8c1ce499a871d718619a6f84987d6c62..3ac564136007a5f3b9d53bf7f30136d425a9cdba, now sends correct headers based on client format.

@VeskeR
Copy link
Contributor Author

VeskeR commented Dec 4, 2025

Can we try running these tests with msgpack to see if it works? I did find this ticket which may indicate an issue with msgpack over rest currently: https://ably.atlassian.net/browse/PUB-1835

Updated all Objects REST tests to run with msgpack and json, see 173b03c

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
test/rest/objects.test.js (1)

15-21: Guard against options being undefined.

Both helpers spread options directly, but call sites may pass only helper. This was flagged in a previous review and should be addressed.

-function RealtimeWithObjects(helper, options) {
+function RealtimeWithObjects(helper, options = {}) {
   return helper.AblyRealtime({ ...options, plugins: { Objects: ObjectsPlugin } });
 }

-function RestWithObjects(helper, options) {
+function RestWithObjects(helper, options = {}) {
   return helper.AblyRest({ ...options, plugins: { Objects: ObjectsPlugin } });
 }
src/plugins/objects/restobject.ts (1)

152-158: REST path segment must be /objects, not /object

_basePath() currently builds URLs using /object:

this._channel.client.rest.channelMixin.basePath(this._channel) +
  '/object' + ...

The LiveObjects REST API expects /channels/{channelName}/objects (plural). Using /object will cause all REST object requests (get/publish) to hit a non‑existent endpoint.

Recommend:

-      '/object' +
+      '/objects' +

This was already flagged previously and is still outstanding here.

🧹 Nitpick comments (5)
test/common/modules/objects_helper.js (2)

61-79: Consider adding timeouts to prevent indefinite hangs.

Both waitForMapKeyUpdate and waitForCounterUpdate create promises that never reject. If the expected update never arrives (e.g., due to a bug or network issue), tests will hang until the global timeout.

Consider adding optional timeout parameters:

-static async waitForMapKeyUpdate(mapInstance, key) {
+static async waitForMapKeyUpdate(mapInstance, key, timeoutMs = 30000) {
   return new Promise((resolve, reject) => {
+    const timer = setTimeout(() => {
+      unsubscribe();
+      reject(new Error(`Timeout waiting for map key update: ${key}`));
+    }, timeoutMs);
     const { unsubscribe } = mapInstance.subscribe(({ message }) => {
       if (message?.operation?.mapOp?.key === key) {
+        clearTimeout(timer);
         unsubscribe();
         resolve();
       }
     });
   });
 }

81-127: Magic numbers for protocol message actions reduce readability.

The action values 19 (OBJECT) and 20 (OBJECT_SYNC) are used as magic numbers. Consider defining these as constants at the top of the file alongside the existing ACTIONS object for better maintainability.

+const PROTOCOL_MESSAGE_ACTIONS = {
+  OBJECT: 19,
+  OBJECT_SYNC: 20,
+};
+
 // In waitForObjectOperation:
-if (message.action === 19 && message.state[0]?.operation?.action === waitForAction) {
+if (message.action === PROTOCOL_MESSAGE_ACTIONS.OBJECT && message.state[0]?.operation?.action === waitForAction) {

 // In waitForObjectSync:
-if (message.action === 20) {
+if (message.action === PROTOCOL_MESSAGE_ACTIONS.OBJECT_SYNC) {
test/rest/objects.test.js (1)

279-292: Consider moving primitiveKeyData definition before its first usage.

primitiveKeyData is defined at line 279 but first referenced at line 262. While JavaScript hoisting makes this work, placing the constant before its first use improves readability.

src/plugins/objects/restobject.ts (2)

238-259: Object reference vs JSON encoding handled appropriately in _encodePrimitive

Buffers go through MessageEncoding.encodeDataForWire, primitives are wrapped in the corresponding discriminator field, and { objectId: string } references are preserved instead of being JSON‑stringified, which is what the REST API expects.

One minor thing to double‑check: top‑level null values and any unsupported shapes currently fall through to return value; and will be sent as raw JSON. If the REST API ever requires them to go via the json wrapper instead, you may want to normalize those through the { json: JSON.stringify(value) } branch as well.


271-293: Live map decode logic is sound; consider reflecting 'unknown' semantics in the type

Decoding live maps by transforming WireObjectsMapSemantics to ObjectsMapSemantics and recursively decoding entries.data into either RestObjectData or nested RestLiveObject matches the runtime model. Since _decodeWireRestLiveMap can emit 'unknown' when it encounters an unsupported semantics value, you might want to extend ObjectsMapSemantics in ably.d.ts (e.g. 'lww' | 'unknown') so the type system reflects that possible runtime value.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 879347f and 0fc6cd1.

📒 Files selected for processing (14)
  • ably.d.ts (3 hunks)
  • objects.d.ts (2 hunks)
  • scripts/moduleReport.ts (1 hunks)
  • src/common/lib/client/baseclient.ts (3 hunks)
  • src/common/lib/client/baserealtime.ts (0 hunks)
  • src/common/lib/client/realtimechannel.ts (1 hunks)
  • src/common/lib/client/restchannel.ts (4 hunks)
  • src/plugins/objects/index.ts (2 hunks)
  • src/plugins/objects/restobject.ts (1 hunks)
  • test/common/modules/objects_helper.js (1 hunks)
  • test/common/modules/shared_helper.js (1 hunks)
  • test/realtime/objects.test.js (4 hunks)
  • test/rest/objects.test.js (1 hunks)
  • test/rest/request.test.js (2 hunks)
💤 Files with no reviewable changes (1)
  • src/common/lib/client/baserealtime.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/common/lib/client/baseclient.ts
  • src/common/lib/client/restchannel.ts
  • src/common/lib/client/realtimechannel.ts
  • objects.d.ts
🧰 Additional context used
🧬 Code graph analysis (6)
test/rest/request.test.js (2)
test/rest/http.test.js (3)
  • helper (11-11)
  • helper (27-27)
  • helper (72-72)
test/rest/status.test.js (2)
  • helper (11-11)
  • rest (38-38)
test/common/modules/shared_helper.js (1)
test/realtime/objects.test.js (1)
  • itFn (59-59)
test/realtime/objects.test.js (1)
test/rest/objects.test.js (2)
  • waitFixtureChannelIsReady (13-13)
  • objectsFixturesChannel (12-12)
test/rest/objects.test.js (2)
src/common/lib/client/realtimechannel.ts (1)
  • object (153-158)
src/common/lib/client/restchannel.ts (1)
  • object (71-76)
src/plugins/objects/restobject.ts (1)
ably.d.ts (9)
  • ObjectsMapSemantics (3975-3975)
  • GetObjectParams (2493-2500)
  • RestCompactObjectData (2407-2415)
  • RestLiveObject (2421-2421)
  • RestObjectOperation (2380-2385)
  • RestObjectPublishResult (2391-2401)
  • PrimitiveOrObjectReference (2632-2632)
  • RestObjectMapEntry (2441-2444)
  • RestObjectData (2450-2463)
ably.d.ts (1)
src/plugins/objects/restobject.ts (1)
  • RestObject (59-354)
🪛 GitHub Actions: API Reference
ably.d.ts

[warning] 1-1: Typedoc warning: definitions referenced in ably.d.ts do not have any documentation (e.g., ably.TargetByObjectId.__type.objectId).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test-node (16.x)
  • GitHub Check: test-node (20.x)
  • GitHub Check: test-browser (webkit)
  • GitHub Check: test-node (18.x)
  • GitHub Check: test-browser (firefox)
  • GitHub Check: test-browser (chromium)
🔇 Additional comments (14)
test/common/modules/objects_helper.js (1)

42-59: LGTM - Clear helper for fixture readiness.

The implementation correctly waits for all expected fixture keys by checking if they already exist before subscribing for updates. Good use of Promise.all for parallel waiting.

test/common/modules/shared_helper.js (1)

385-393: Clean refactor of test wrapper.

The new createTest helper factory simplifies async test handling by returning the Promise directly, allowing Mocha to handle it natively. The addition of helper as a third parameter to testFn improves ergonomics for tests that need helper access.

test/rest/request.test.js (1)

31-57: LGTM - Proper async coordination for header verification.

The manual Promise pattern correctly coordinates the asynchronous assertion in the mocked request handler. The test now properly awaits completion before the test function returns.

scripts/moduleReport.ts (1)

344-344: LGTM - RestObject added to allowed files for bundle analysis.

Correctly includes the new restobject.ts file in the Objects plugin bundle size allowlist.

src/plugins/objects/index.ts (1)

5-23: LGTM - RestObject properly exported.

The RestObject is correctly added to both named and default exports, following the existing pattern for other plugin exports.

test/rest/objects.test.js (2)

228-246: Good handling of binary vs text protocol differences for bytes.

The test correctly distinguishes between text protocol (base64 strings) and binary protocol (Buffer objects) when verifying byte data. This aligns with the PR objective comment about Content-Type header behavior with MsgPack.


632-729: Comprehensive workflow test validates complex object operations.

The test effectively validates the full lifecycle of creating maps, counters, setting references, and verifying both compact and expanded representations. Good coverage of the REST Objects API.

test/realtime/objects.test.js (2)

20-24: Centralising wait helpers via ObjectsHelper is a good refactor

Referencing the shared static helpers through local aliases removes in-file duplication and keeps the tests aligned with objects_helper. No issues spotted with these bindings.


433-433: Updated waitFixtureChannelIsReady calls look correct and consistent

Passing objectsFixturesChannel explicitly into waitFixtureChannelIsReady(client, objectsFixturesChannel) matches the new helper signature and mirrors the pattern in the REST objects tests. This should keep the fixture channel setup consistent across scenarios.

Also applies to: 605-605, 631-631

src/plugins/objects/restobject.ts (4)

23-57: Wire/live object wire types and semantics mapping look consistent with public types

The WireRest* wire shapes and mapSemantics mapping (LWW → 'lww') align with the public RestLive* and ObjectsMapSemantics types, and the fallback handling via WireAnyRestLiveObject gives you a reasonable forward‑compatibility story.


66-116: get() encoding/decoding and 404 handling look correct

Using format to drive decodeBody, falling back to primitives / JSON objects when not a live map, and treating 40400 “object not found” as a soft undefined all match the intended API surface for RestObject.get.


121-150: MsgPack Content-Type header now matches encoded body in publish()

Passing { format } into client.Defaults.defaultPostHeaders and then encoding the body with encodeBody(wireOps, client._MsgPack, format) ensures the Content-Type matches the actual wire format (JSON vs msgpack), which addresses the earlier bug reported for msgpack publishes.


160-236: Publish body construction matches REST operation contract

The mapping from RestObjectOperation actions (map.create, map.set, map.remove, counter.create, counter.inc) to the uppercase wire operation values (MAP_CREATE, etc.), along with the runtime validation for required path on *.create, is consistent with the REST API and past review feedback.

ably.d.ts (1)

2403-2538: REST object typings and operation/action wiring align with the implementation

  • RestObjectPublishResult, RestCompactObjectData, RestLiveObject/RestLiveMap/RestLiveCounter/AnyRestLiveObject, and GetObjectParams are consistent with how RestObject is implemented in src/plugins/objects/restobject.ts.
  • The overloaded RestObject.get and publish signatures mirror the runtime method overloads, including the compact vs live shapes.
  • PrimitiveOrObjectReference matches the shapes handled in _encodePrimitive (primitive values plus { objectId: string } references).
  • Updating ObjectOperationAction’s docstring to cover both ObjectOperation and RestObjectOperation keeps the action value set in sync across realtime and REST.

These declarations look correct and coherent with the rest of the Objects API surface.

Also applies to: 2626-2632, 3952-3960

Comment on lines 2291 to 2376
/**
* Base interface for all REST object operations. Contains common fields shared across all operation types.
*/
export interface RestObjectOperationBase {
/**
* An ID associated with the message. Clients may set this field explicitly when publishing an operation to enable
* idempotent publishing. If not set, this will be generated by the server.
*/
id?: string;
/**
* A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include `headers`.
*/
extras?: {
/**
* A set of key–value pair headers included with this object message.
*/
headers?: Record<string, string>;
[key: string]: any;
};
}

// Enforce exactly one target at compile time using a union of the types below.
/**
* Targets an object by its object ID.
*/
type TargetByObjectId = { objectId: string; path?: never };

/**
* Targets an object by its location using the path.
* Paths are expressed relative to the structure of the object as defined by the compact view of the object tree.
*/
type TargetByPath = { path: string; objectId?: never };

/**
* Base type for operations that can target objects either by object ID or by path.
* Ensures that exactly one targeting method is specified.
*/
export type AnyTargetOperationBase = RestObjectOperationBase & (TargetByObjectId | TargetByPath);

/**
* Operation to create a new map object at the specified path with initial entries.
*/
export type OperationMapCreate = RestObjectOperationBase &
TargetByPath & {
operation: ObjectOperationActions.MAP_CREATE;
entries: Record<string, PrimitiveOrObjectReference>;
};

/**
* Operation to set a key to a specified value in an existing map object.
* Can target the map by either object ID or path.
*/
export type OperationMapSet = AnyTargetOperationBase & {
operation: ObjectOperationActions.MAP_SET;
key: string;
value: PrimitiveOrObjectReference;
encoding?: string;
};

/**
* Operation to remove a key from an existing map object.
* Can target the map by either object ID or path.
*/
export type OperationMapRemove = AnyTargetOperationBase & {
operation: ObjectOperationActions.MAP_REMOVE;
key: string;
};

/**
* Operation to create a new counter object at the specified path with an initial count value.
*/
export type OperationCounterCreate = RestObjectOperationBase &
TargetByPath & {
operation: ObjectOperationActions.COUNTER_CREATE;
count: number;
};

/**
* Operation to increment (or decrement with negative values) an existing counter object.
* Can target the counter by either object ID or path.
*/
export type OperationCounterInc = AnyTargetOperationBase & {
operation: ObjectOperationActions.COUNTER_INC;
amount: number;
};

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ably.d.ts" -type f

Repository: ably/ably-js

Length of output: 93


🏁 Script executed:

wc -l ./ably.d.ts

Repository: ably/ably-js

Length of output: 72


🏁 Script executed:

sed -n '2291,2376p' ./ably.d.ts

Repository: ably/ably-js

Length of output: 2815


🏁 Script executed:

sed -n '2376,2450p' ./ably.d.ts

Repository: ably/ably-js

Length of output: 2323


🏁 Script executed:

rg "TargetByObjectId\.__type" . 2>/dev/null | head -20

Repository: ably/ably-js

Length of output: 38


🏁 Script executed:

rg "type.*=\s*\{.*:.*;" ./ably.d.ts | head -30

Repository: ably/ably-js

Length of output: 289


🏁 Script executed:

sed -n '2320,2340p' ./ably.d.ts | cat -A

Repository: ably/ably-js

Length of output: 862


Add property-level JSDoc to TargetByObjectId, TargetByPath, and Operation types to document inline object properties*

The API Reference job warns about undocumented fields like ably.TargetByObjectId.__type.objectId. This occurs because these inline object type aliases lack JSDoc on individual properties:

type TargetByObjectId = { objectId: string; path?: never };
type TargetByPath = { path: string; objectId?: never };

The same issue affects OperationMapCreate, OperationMapSet, OperationMapRemove, OperationCounterCreate, and OperationCounterInc.

Document each property to satisfy TypeDoc and improve clarity:

-/**
- * Targets an object by its object ID.
- */
-type TargetByObjectId = { objectId: string; path?: never };
+/**
+ * Targets an object by its object ID.
+ */
+type TargetByObjectId = {
+  /** The ID of the object to target. */
+  objectId: string;
+  /**
+   * Must not be set when targeting by object ID. Present only to enforce
+   * compile‑time exclusivity with {@link TargetByPath}.
+   */
+  path?: never;
+};
@@
-/**
- * Targets an object by its location using the path.
- * Paths are expressed relative to the structure of the object as defined by the compact view of the object tree.
- */
-type TargetByPath = { path: string; objectId?: never };
+/**
+ * Targets an object by its location using the path.
+ * Paths are expressed relative to the structure of the object as defined by the compact view of the object tree.
+ */
+type TargetByPath = {
+  /**
+   * Path to the object relative to the channel's root object, expressed using
+   * the compact‑view object tree syntax.
+   */
+  path: string;
+  /**
+   * Must not be set when targeting by path. Present only to enforce
+   * compile‑time exclusivity with {@link TargetByObjectId}.
+   */
+  objectId?: never;
+};
@@
-export type OperationMapCreate = RestObjectOperationBase &
-  TargetByPath & {
-    operation: ObjectOperationActions.MAP_CREATE;
-    entries: Record<string, PrimitiveOrObjectReference>;
-  };
+export type OperationMapCreate = RestObjectOperationBase &
+  TargetByPath & {
+    /** The operation action, must be {@link ObjectOperationActions.MAP_CREATE}. */
+    operation: ObjectOperationActions.MAP_CREATE;
+    /** Initial map entries to create at the target path. */
+    entries: Record<string, PrimitiveOrObjectReference>;
+  };
@@
-export type OperationMapSet = AnyTargetOperationBase & {
-  operation: ObjectOperationActions.MAP_SET;
-  key: string;
-  value: PrimitiveOrObjectReference;
-  encoding?: string;
-};
+export type OperationMapSet = AnyTargetOperationBase & {
+  /** The operation action, must be {@link ObjectOperationActions.MAP_SET}. */
+  operation: ObjectOperationActions.MAP_SET;
+  /** The key within the map to set. */
+  key: string;
+  /** The value to assign at the given key. */
+  value: PrimitiveOrObjectReference;
+  /**
+   * Optional encoding for the value (for example a custom content‑type or
+   * transformation hint).
+   */
+  encoding?: string;
+};
@@
-export type OperationMapRemove = AnyTargetOperationBase & {
-  operation: ObjectOperationActions.MAP_REMOVE;
-  key: string;
-};
+export type OperationMapRemove = AnyTargetOperationBase & {
+  /** The operation action, must be {@link ObjectOperationActions.MAP_REMOVE}. */
+  operation: ObjectOperationActions.MAP_REMOVE;
+  /** The key to remove from the map. */
+  key: string;
+};
@@
-export type OperationCounterCreate = RestObjectOperationBase &
-  TargetByPath & {
-    operation: ObjectOperationActions.COUNTER_CREATE;
-    count: number;
-  };
+export type OperationCounterCreate = RestObjectOperationBase &
+  TargetByPath & {
+    /** The operation action, must be {@link ObjectOperationActions.COUNTER_CREATE}. */
+    operation: ObjectOperationActions.COUNTER_CREATE;
+    /** Initial value to assign to the new counter. */
+    count: number;
+  };
@@
-export type OperationCounterInc = AnyTargetOperationBase & {
-  operation: ObjectOperationActions.COUNTER_INC;
-  amount: number;
-};
+export type OperationCounterInc = AnyTargetOperationBase & {
+  /** The operation action, must be {@link ObjectOperationActions.COUNTER_INC}. */
+  operation: ObjectOperationActions.COUNTER_INC;
+  /**
+   * Amount by which to change the counter. Negative values decrement the
+   * counter.
+   */
+  amount: number;
+};

After applying these changes, re-run the API Reference workflow to confirm the TypeDoc warnings are resolved.

Comment on lines +104 to +113
Helper.testOnJsonMsgpack('should get root object by default', async function (options, channelName, helper) {
const client = RestWithObjects(helper, options);
const channel = client.channels.get(objectsFixturesChannel);

await waitFixtureChannelIsReady(RealtimeWithObjects(helper, options), objectsFixturesChannel);

const root = await channel.object.get();
expect(root).to.exist;
expect(root.initialValueCounter).to.equal(10);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Realtime client created for fixture readiness may leak resources.

RealtimeWithObjects creates a Realtime client at line 108 to wait for fixture readiness, but it's never closed. This pattern repeats at lines 142, 208, and 257. Consider closing these clients after use to avoid resource leaks:

-await waitFixtureChannelIsReady(RealtimeWithObjects(helper, options), objectsFixturesChannel);
+const realtimeClient = RealtimeWithObjects(helper, options);
+await waitFixtureChannelIsReady(realtimeClient, objectsFixturesChannel);
+realtimeClient.close();

Alternatively, create a shared Realtime client in a before hook and close it in after.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Helper.testOnJsonMsgpack('should get root object by default', async function (options, channelName, helper) {
const client = RestWithObjects(helper, options);
const channel = client.channels.get(objectsFixturesChannel);
await waitFixtureChannelIsReady(RealtimeWithObjects(helper, options), objectsFixturesChannel);
const root = await channel.object.get();
expect(root).to.exist;
expect(root.initialValueCounter).to.equal(10);
});
Helper.testOnJsonMsgpack('should get root object by default', async function (options, channelName, helper) {
const client = RestWithObjects(helper, options);
const channel = client.channels.get(objectsFixturesChannel);
const realtimeClient = RealtimeWithObjects(helper, options);
await waitFixtureChannelIsReady(realtimeClient, objectsFixturesChannel);
realtimeClient.close();
const root = await channel.object.get();
expect(root).to.exist;
expect(root.initialValueCounter).to.equal(10);
});
🤖 Prompt for AI Agents
In test/rest/objects.test.js around lines 104 to 113, the Realtime client
created via RealtimeWithObjects to wait for fixture readiness is never closed,
leaking resources; either call close()/disconnect() (or the appropriate cleanup
method) on the client returned by RealtimeWithObjects immediately after
waitFixtureChannelIsReady completes (ensure await completion before closing), or
refactor tests to create a single shared Realtime client in a before hook and
close it in an after hook so all waits reuse that client and no per-test clients
are leaked.

@VeskeR VeskeR force-pushed the integration/objects-breaking-api branch from 6f46f9d to f9c7d38 Compare December 18, 2025 18:13
Base automatically changed from integration/objects-breaking-api to main December 19, 2025 13:50
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.

4 participants