Skip to content

[PUB-1197] LiveObjects REST client#2109

Open
VeskeR wants to merge 3 commits intomainfrom
PUB-1197/liveobjects-rest-api
Open

[PUB-1197] LiveObjects REST client#2109
VeskeR wants to merge 3 commits intomainfrom
PUB-1197/liveobjects-rest-api

Conversation

@VeskeR
Copy link
Contributor

@VeskeR VeskeR commented Nov 14, 2025

Resolves PUB-1197, AIT-27

Summary by CodeRabbit

  • New Features

    • Added a REST Objects API allowing retrieval and publication of live object state via REST channels (get/publish behaviors and REST object surface).
  • Tests

    • Added extensive REST Objects integration tests covering retrieval (compact/live), publishing (map/counter), and end-to-end state verification.
    • Updated test helpers to support new object sync and readiness flows.
  • Chores

    • Adjusted plugin exports and client/runtime integration to surface the REST Objects capability.

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

@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2025

Walkthrough

Moves the LiveObjects plugin field from realtime-only to the common client, adds a new RestObject class implementing REST get/publish with wire-format encode/decode, updates channels to use the relocated plugin field, extends typings/exports for REST objects, and adds comprehensive tests and test helpers for REST Objects.

Changes

Cohort / File(s) Summary
Client plugin wiring
src/common/lib/client/baseclient.ts, src/common/lib/client/baserealtime.ts, src/common/lib/client/realtimechannel.ts, src/common/lib/client/restchannel.ts
Relocate _liveObjectsPlugin from BaseRealtime to BaseClient; update RealtimeChannel/RestChannel constructors to read client._liveObjectsPlugin; add RestChannel object getter and private _object wiring with missing-plugin errors.
REST Objects implementation
src/plugins/liveobjects/restobject.ts, src/plugins/liveobjects/index.ts
Add RestObject class with get() and publish() plus wire-format encoding/decoding, supporting binary/JSON; export RestObject from plugin index.
Type definitions
liveobjects.d.ts
Add REST-related public types/interfaces (RestObject, operations, wire types, RestLiveObject shapes, module augmentation for RestChannel.object, etc.).
Tests — REST coverage
test/rest/objects.test.js, test/rest/request.test.js
Add large REST Objects test suite (get/publish variants, primitives, buffers, batch ops) and adjust request test to await header assertion asynchronously.
Tests — helpers & refactor
test/common/modules/liveobjects_helper.js, test/common/modules/shared_helper.js, test/realtime/liveobjects.test.js
Add async synchronization helpers for LiveObjects; refactor shared test wrapper factory; update realtime tests to use exported helpers.
Build/reporting
scripts/moduleReport.ts, package.json
Allow src/plugins/liveobjects/restobject.ts in module report; package manifest touched.

Sequence Diagram

sequenceDiagram
    participant Client as RestChannel
    participant RestObj as RestObject
    participant Transport as HTTP Transport
    participant API as Objects API

    rect rgba(70, 130, 180, 0.5)
    Note over Client,API: GET / get() flow
    Client->>RestObj: get(params)
    RestObj->>Transport: HTTP GET /objects/{id or root}
    Transport->>API: HTTP request
    API-->>Transport: WireRestLiveObject / 404
    Transport-->>RestObj: response
    RestObj->>RestObj: _decodeWireLiveObject/_decodeWireObjectData
    RestObj-->>Client: RestLiveObject | RestCompactObjectData | undefined
    end

    rect rgba(34, 139, 34, 0.5)
    Note over Client,API: POST / publish() flow
    Client->>RestObj: publish(operations[])
    RestObj->>RestObj: _constructPublishBody() / _encodePrimitive()
    RestObj->>Transport: HTTP POST /objects/publish (binary/JSON)
    Transport->>API: HTTP request
    API-->>Transport: RestObjectPublishResult
    Transport-->>RestObj: response
    RestObj->>RestObj: decode response
    RestObj-->>Client: RestObjectPublishResult
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mschristensen

Poem

🐇 I hopped from realtime into REST,

Moved the plugin where clients know best.
RestObject fetches, publishes with vim,
Wire-format bytes snug and prim.
Two channels now share the same nest.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[PUB-1197] LiveObjects REST client' accurately describes the main change—implementing a LiveObjects REST client feature as evidenced by the RestObject class, REST operations, and integration with REST channels.
Linked Issues check ✅ Passed The PR implements a LiveObjects REST client with RestObject class, get/publish operations, and REST channel integration, directly addressing the feature requirement in AIT-27.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the LiveObjects REST client: moving LiveObjects plugin reference to BaseClient, adding RestObject implementation, integrating with RestChannel, and providing comprehensive tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.40.5)
test/realtime/liveobjects.test.js

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
@github-actions github-actions bot temporarily deployed to staging/pull/2109/bundle-report November 14, 2025 09:35 Inactive
@VeskeR VeskeR force-pushed the liveobjects/remove-non-path-based-api branch from e7d734e to 70540ff Compare November 14, 2025 09:37
@VeskeR VeskeR force-pushed the PUB-1197/liveobjects-rest-api branch 2 times, most recently from 15f2c4a to 5f7807f Compare November 14, 2025 09:46
@github-actions github-actions bot temporarily deployed to staging/pull/2109/bundle-report November 14, 2025 09:47 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 14, 2025 09:47 Inactive
@VeskeR VeskeR force-pushed the liveobjects/remove-non-path-based-api branch from 70540ff to 9c9d510 Compare November 19, 2025 10:19
Copy link
Contributor

@mschristensen mschristensen left a comment

Choose a reason for hiding this comment

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

LGTM, although I dont think these tests run with the useBinaryProtocol option set to true, given

this._rest = helper.AblyRest({ useBinaryProtocol: false });

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

{} as Record<string, any>,
),
};
case 'map.set':

Choose a reason for hiding this comment

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

Hello, These operation keys in their current format won't pass the rest validation. The current format can be found here in the docs https://ably.com/docs/api/liveobjects-rest#/paths/~1channels~1%7BchannelName%7D~1objects/post

For example map.set would have to be MAP_SET.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

const channel = client.channels.get('publish-map-set');

const operation = {
operation: 'map.set',

Choose a reason for hiding this comment

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

This request will fail because the map set operation requires a key. If the intention of this request is to set a key of testKey on the root then it needs to be:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in 173b03c, thanks

@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
@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
@VeskeR VeskeR force-pushed the PUB-1197/liveobjects-rest-api branch from 3ac5641 to 0fc6cd1 Compare December 4, 2025 08:55
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features December 4, 2025 08:56 Inactive
@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 +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
@VeskeR VeskeR force-pushed the PUB-1197/liveobjects-rest-api branch from 0fc6cd1 to 892ccc8 Compare January 30, 2026 11:37
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features January 30, 2026 11:38 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

🤖 Fix all issues with AI agents
In `@src/common/lib/client/baseclient.ts`:
- Around line 108-109: There is a stray nullish-coalescing operator causing a
syntax error in the assignment to this._liveObjectsPlugin; remove the extra "??
null" so the property assignment uses the intended expression
(options.plugins?.LiveObjects ?? null) or simply (options.plugins?.LiveObjects)
as appropriate; update the assignment in the constructor where
this._liveObjectsPlugin is set to reference the correct expression and ensure no
duplicated "?? null" remains.

In `@src/plugins/liveobjects/restobject.ts`:
- Around line 1-13: Update the type-only import in restobject.ts to use the
correct module path: replace the import source '../../../ably' with
'../../../liveobjects' for the listed types (GetObjectParams,
ObjectsMapSemantics, PrimitiveOrObjectReference, RestCompactObjectData,
RestLiveObject, RestObjectData, RestObjectMapEntry, RestObjectOperation,
RestObjectPublishResult) so the file imports the types from liveobjects.d.ts
like the rest of the plugin; keep the existing RestChannel and Utils imports
unchanged.

In `@test/common/modules/liveobjects_helper.js`:
- Around line 81-126: Both waitForObjectOperation and waitForObjectSync replace
transport.onProtocolMessage but don’t restore the original handler if the
wrapper throws; modify both wrappers (in waitForObjectOperation and
waitForObjectSync) to always restore transport.onProtocolMessage to
onProtocolMessageOriginal in a finally-style path: call the original handler
inside a try block, perform the action check and resolve if matched, and in a
finally block (or before every reject/resolve) reassign
transport.onProtocolMessage = onProtocolMessageOriginal so the original handler
is restored on success or error to avoid leaking the wrapper across tests.

In `@test/realtime/liveobjects.test.js`:
- Around line 461-462: Replace the undefined objectsFixturesChannel with the
existing liveobjectsFixturesChannel wherever used (examples: calls to
waitFixtureChannelIsReady and any other references at the noted spots) to avoid
ReferenceError; update calls like waitFixtureChannelIsReady(client,
objectsFixturesChannel) to use waitFixtureChannelIsReady(client,
liveobjectsFixturesChannel) and ensure any other uses of objectsFixturesChannel
in this test file are similarly switched to liveobjectsFixturesChannel.

Comment on lines +108 to 109
this._liveObjectsPlugin = options.plugins?.LiveObjects ?? null; ?? null;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/common/lib/client/baseclient.ts | sed -n '100,115p'

Repository: ably/ably-js

Length of output: 750


Remove the stray ?? null to fix the syntax error.
The extra ?? null; makes this file unparsable (TS1109).

🐛 Fix syntax error
-    this._liveObjectsPlugin = options.plugins?.LiveObjects ?? null; ?? null;
+    this._liveObjectsPlugin = options.plugins?.LiveObjects ?? null;
📝 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
this._liveObjectsPlugin = options.plugins?.LiveObjects ?? null; ?? null;
}
this._liveObjectsPlugin = options.plugins?.LiveObjects ?? null;
}
🧰 Tools
🪛 Biome (2.3.13)

[error] 108-109: Expected a statement but instead found '?? null'.

Expected a statement here.

(parse)

🪛 GitHub Actions: API Reference

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Bundle size report

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Lint

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Spec coverage report

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Test (react hooks)

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Test browser

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Test NodeJS

[error] 108-108: TS1109: Expression expected.

🪛 GitHub Actions: Test NPM package

[error] 108-110: TS1109: Expression expected.

🤖 Prompt for AI Agents
In `@src/common/lib/client/baseclient.ts` around lines 108 - 109, There is a stray
nullish-coalescing operator causing a syntax error in the assignment to
this._liveObjectsPlugin; remove the extra "?? null" so the property assignment
uses the intended expression (options.plugins?.LiveObjects ?? null) or simply
(options.plugins?.LiveObjects) as appropriate; update the assignment in the
constructor where this._liveObjectsPlugin is set to reference the correct
expression and ensure no duplicated "?? null" remains.

Comment on lines +81 to +126
static async waitForObjectOperation(helper, client, waitForAction) {
return new Promise((resolve, reject) => {
helper.recordPrivateApi('call.connectionManager.activeProtocol.getTransport');
const transport = client.connection.connectionManager.activeProtocol.getTransport();
const onProtocolMessageOriginal = transport.onProtocolMessage;

helper.recordPrivateApi('replace.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);
}
};
});
}

static async waitForObjectSync(helper, client) {
return new Promise((resolve, reject) => {
helper.recordPrivateApi('call.connectionManager.activeProtocol.getTransport');
const transport = client.connection.connectionManager.activeProtocol.getTransport();
const onProtocolMessageOriginal = transport.onProtocolMessage;

helper.recordPrivateApi('replace.transport.onProtocolMessage');
transport.onProtocolMessage = function (message) {
try {
helper.recordPrivateApi('call.transport.onProtocolMessage');
onProtocolMessageOriginal.call(transport, message);

if (message.action === 20) {
helper.recordPrivateApi('replace.transport.onProtocolMessage');
transport.onProtocolMessage = onProtocolMessageOriginal;
resolve();
}
} catch (err) {
reject(err);
}
};
});
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

Restore transport.onProtocolMessage on error to avoid leaking the wrapper.
If the wrapper throws, the original handler is never restored, which can bleed into later tests. Consider restoring in a finally‑style path.

Proposed fix
     static async waitForObjectOperation(helper, client, waitForAction) {
       return new Promise((resolve, reject) => {
         helper.recordPrivateApi('call.connectionManager.activeProtocol.getTransport');
         const transport = client.connection.connectionManager.activeProtocol.getTransport();
         const onProtocolMessageOriginal = transport.onProtocolMessage;
+        const restore = () => {
+          helper.recordPrivateApi('replace.transport.onProtocolMessage');
+          transport.onProtocolMessage = onProtocolMessageOriginal;
+        };

         helper.recordPrivateApi('replace.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;
+              restore();
               resolve();
             }
           } catch (err) {
+            restore();
             reject(err);
           }
         };
       });
     }

     static async waitForObjectSync(helper, client) {
       return new Promise((resolve, reject) => {
         helper.recordPrivateApi('call.connectionManager.activeProtocol.getTransport');
         const transport = client.connection.connectionManager.activeProtocol.getTransport();
         const onProtocolMessageOriginal = transport.onProtocolMessage;
+        const restore = () => {
+          helper.recordPrivateApi('replace.transport.onProtocolMessage');
+          transport.onProtocolMessage = onProtocolMessageOriginal;
+        };

         helper.recordPrivateApi('replace.transport.onProtocolMessage');
         transport.onProtocolMessage = function (message) {
           try {
             helper.recordPrivateApi('call.transport.onProtocolMessage');
             onProtocolMessageOriginal.call(transport, message);

             if (message.action === 20) {
-              helper.recordPrivateApi('replace.transport.onProtocolMessage');
-              transport.onProtocolMessage = onProtocolMessageOriginal;
+              restore();
               resolve();
             }
           } catch (err) {
+            restore();
             reject(err);
           }
         };
       });
     }
🤖 Prompt for AI Agents
In `@test/common/modules/liveobjects_helper.js` around lines 81 - 126, Both
waitForObjectOperation and waitForObjectSync replace transport.onProtocolMessage
but don’t restore the original handler if the wrapper throws; modify both
wrappers (in waitForObjectOperation and waitForObjectSync) to always restore
transport.onProtocolMessage to onProtocolMessageOriginal in a finally-style
path: call the original handler inside a try block, perform the action check and
resolve if matched, and in a finally block (or before every reject/resolve)
reassign transport.onProtocolMessage = onProtocolMessageOriginal so the original
handler is restored on success or error to avoid leaking the wrapper across
tests.

Comment on lines +461 to 462
await waitFixtureChannelIsReady(client, objectsFixturesChannel);

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

Fix undefined objectsFixturesChannel to avoid ReferenceError.
objectsFixturesChannel isn’t defined in this file, so these calls will fail at runtime. Either define it or use the existing liveobjectsFixturesChannel (which is already used to initialize fixtures).

🔧 Suggested fix (use existing fixture channel)
-            await waitFixtureChannelIsReady(client, objectsFixturesChannel);
+            await waitFixtureChannelIsReady(client, liveobjectsFixturesChannel);
-            await waitFixtureChannelIsReady(client, objectsFixturesChannel);
+            await waitFixtureChannelIsReady(client, liveobjectsFixturesChannel);
-            await waitFixtureChannelIsReady(client, objectsFixturesChannel);
+            await waitFixtureChannelIsReady(client, liveobjectsFixturesChannel);

Also applies to: 633-634, 659-660

🤖 Prompt for AI Agents
In `@test/realtime/liveobjects.test.js` around lines 461 - 462, Replace the
undefined objectsFixturesChannel with the existing liveobjectsFixturesChannel
wherever used (examples: calls to waitFixtureChannelIsReady and any other
references at the noted spots) to avoid ReferenceError; update calls like
waitFixtureChannelIsReady(client, objectsFixturesChannel) to use
waitFixtureChannelIsReady(client, liveobjectsFixturesChannel) and ensure any
other uses of objectsFixturesChannel in this test file are similarly switched to
liveobjectsFixturesChannel.

Adds REST support for Objects REST API endpoints:
- GET /channel/{channel}/object/{id?}?compact={boolean}&path={string}
- POST /channels/{channel}/object

Resolves PUB-1197
Test actually didn't work properly before this fix.
Helper.testOnJsonMsgpack expects an async function test, but the test
was callback based on tried to use done() callback - which wasn't even
provided.
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.

3 participants