Skip to content

Commit 13306f1

Browse files
committed
Implement LiveObjects REST client
Adds REST support for Objects REST API endpoints: - GET /channels/{channel}/object/{id?}?compact={boolean}&path={string} - POST /channels/{channel}/object Resolves PUB-1197
1 parent 349a395 commit 13306f1

File tree

12 files changed

+1482
-101
lines changed

12 files changed

+1482
-101
lines changed

liveobjects.d.ts

Lines changed: 281 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import {
1212
EventCallback,
1313
RealtimeChannel,
1414
RealtimeClient,
15+
RestClient,
1516
StatusSubscription,
1617
Subscription,
1718
__livetype,
1819
} from './ably';
19-
import { BaseRealtime } from './modular';
20+
import { BaseRealtime, BaseRest, Rest } from './modular';
2021
/* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */
2122

2223
/**
@@ -67,7 +68,256 @@ export type ObjectsEventCallback = () => void;
6768
export type BatchFunction<T extends LiveObject> = (ctx: BatchContext<T>) => void;
6869

6970
/**
70-
* Enables the Objects to be read, modified and subscribed to for a channel.
71+
* Enables REST-based operations on Objects on a channel.
72+
*/
73+
export declare interface RestObject {
74+
/**
75+
* Reads object data from the channel in a compact format.
76+
* Uses the channel's root object as the entrypoint when no objectId is provided.
77+
*
78+
* @param params - Optional parameters to specify the object to fetch and the format of the returned data.
79+
* @returns A promise that resolves to the object data in compact format, or `undefined` if the specified objectId/path does not resolve to an object.
80+
*/
81+
get(params?: Omit<GetObjectParams, 'compact'> & { compact?: true }): Promise<RestCompactObjectData | undefined>;
82+
/**
83+
* Reads object data from the channel in expanded format.
84+
* Uses the channel's root object as the entrypoint when no objectId is provided.
85+
*
86+
* @param params - Parameters specifying the object to fetch with `compact: false`.
87+
* @returns A promise that resolves to the object data in expanded format, or `undefined` if the specified objectId/path does not resolve to an object.
88+
*/
89+
get(params: Omit<GetObjectParams, 'compact'> & { compact: false }): Promise<RestLiveObject | undefined>;
90+
/**
91+
* Reads object data from the channel.
92+
* Uses the channel's root object as the entrypoint when no objectId is provided.
93+
*
94+
* @param params - Optional parameters to specify the object to fetch and the format of the returned data.
95+
* @returns A promise that resolves to the object data in the requested format, or `undefined` if the specified objectId/path does not resolve to an object.
96+
*/
97+
get(params?: GetObjectParams): Promise<RestCompactObjectData | RestLiveObject | undefined>;
98+
99+
/**
100+
* Publishes one or more operations to modify objects on the channel.
101+
*
102+
* @param op - A single operation or array of operations to publish.
103+
* @returns A promise that resolves to a {@link RestObjectPublishResult} containing information about the published operations.
104+
*/
105+
publish(op: RestObjectOperation | RestObjectOperation[]): Promise<RestObjectPublishResult>;
106+
}
107+
108+
/**
109+
* Base interface for all REST object operations. Contains common fields shared across all operation types.
110+
*/
111+
export interface RestObjectOperationBase {
112+
/**
113+
* An ID associated with the message. Clients may set this field explicitly when publishing an operation to enable
114+
* idempotent publishing. If not set, this will be generated by the server.
115+
*/
116+
id?: string;
117+
/**
118+
* A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include `headers`.
119+
*/
120+
extras?: {
121+
/**
122+
* A set of key–value pair headers included with this object message.
123+
*/
124+
headers?: Record<string, string>;
125+
[key: string]: any;
126+
};
127+
}
128+
129+
// Enforce exactly one target at compile time using a union of the types below.
130+
/**
131+
* Targets an object by its object ID.
132+
*/
133+
type TargetByObjectId = { objectId: string; path?: never };
134+
135+
/**
136+
* Targets an object by its location using the path.
137+
* Paths are expressed relative to the structure of the object as defined by the compact view of the object tree.
138+
*/
139+
type TargetByPath = { path: string; objectId?: never };
140+
141+
/**
142+
* Base type for operations that can target objects either by object ID or by path.
143+
* Ensures that exactly one targeting method is specified.
144+
*/
145+
export type AnyTargetOperationBase = RestObjectOperationBase & (TargetByObjectId | TargetByPath);
146+
147+
/**
148+
* Operation to create a new map object at the specified path with initial entries.
149+
*/
150+
export type OperationMapCreate = RestObjectOperationBase &
151+
TargetByPath & {
152+
operation: ObjectOperationActions.MAP_CREATE;
153+
entries: Record<string, PrimitiveOrObjectReference>;
154+
};
155+
156+
/**
157+
* Operation to set a key to a specified value in an existing map object.
158+
* Can target the map by either object ID or path.
159+
*/
160+
export type OperationMapSet = AnyTargetOperationBase & {
161+
operation: ObjectOperationActions.MAP_SET;
162+
key: string;
163+
value: PrimitiveOrObjectReference;
164+
encoding?: string;
165+
};
166+
167+
/**
168+
* Operation to remove a key from an existing map object.
169+
* Can target the map by either object ID or path.
170+
*/
171+
export type OperationMapRemove = AnyTargetOperationBase & {
172+
operation: ObjectOperationActions.MAP_REMOVE;
173+
key: string;
174+
};
175+
176+
/**
177+
* Operation to create a new counter object at the specified path with an initial count value.
178+
*/
179+
export type OperationCounterCreate = RestObjectOperationBase &
180+
TargetByPath & {
181+
operation: ObjectOperationActions.COUNTER_CREATE;
182+
count: number;
183+
};
184+
185+
/**
186+
* Operation to increment (or decrement with negative values) an existing counter object.
187+
* Can target the counter by either object ID or path.
188+
*/
189+
export type OperationCounterInc = AnyTargetOperationBase & {
190+
operation: ObjectOperationActions.COUNTER_INC;
191+
amount: number;
192+
};
193+
194+
/**
195+
* Union type representing all possible REST object operations.
196+
*/
197+
export type RestObjectOperation =
198+
| OperationMapCreate
199+
| OperationMapSet
200+
| OperationMapRemove
201+
| OperationCounterCreate
202+
| OperationCounterInc;
203+
204+
/**
205+
* Result returned after successfully publishing object operations via REST.
206+
* Contains information about the published message and affected object IDs.
207+
*/
208+
export interface RestObjectPublishResult {
209+
/** The ID of the message containing the published operations. */
210+
messageId: string;
211+
/** The name of the channel the object message was published to. */
212+
channel: string;
213+
/**
214+
* Array of object IDs that were affected by the operations.
215+
* May include multiple IDs for wildcard paths and batch operations.
216+
*/
217+
objectIds: string[];
218+
}
219+
220+
/**
221+
* Compact representation of object data returned from {@link RestObject.get} when `compact=true` (default).
222+
* Provides a JSON-like view with primitive values, nested objects, and object references for handling cycles.
223+
*/
224+
export type RestCompactObjectData =
225+
| string
226+
| number
227+
| boolean
228+
| null
229+
| JsonArray
230+
| JsonObject
231+
| { objectId: string } // cyclic references handling
232+
| { [key: string]: RestCompactObjectData };
233+
234+
/**
235+
* Object representation returned from {@link RestObject.get} when `compact=false`.
236+
* Provides an expanded structural view with object metadata.
237+
*/
238+
export type RestLiveObject = RestLiveMap | RestLiveCounter | AnyRestLiveObject;
239+
240+
/**
241+
* Expanded representation of a map object with metadata.
242+
*/
243+
export interface RestLiveMap {
244+
/** The ID of the map object. */
245+
objectId: string;
246+
/** Describes the value of a map object. */
247+
map: {
248+
/** The conflict-resolution semantics used by the map object, one of the {@link ObjectsMapSemantics} enum values. */
249+
semantics: ObjectsMapSemantics;
250+
/** The map entries, indexed by key. */
251+
entries: Record<string, RestObjectMapEntry>;
252+
};
253+
}
254+
255+
/**
256+
* Describes a value at a specific key in a map object.
257+
*/
258+
export interface RestObjectMapEntry {
259+
/** The value associated with this map entry. */
260+
data: RestLiveObject | RestObjectData;
261+
}
262+
263+
/**
264+
* Represents a value in an object on a channel.
265+
* Either a primitive data or a reference to another object.
266+
*/
267+
export interface RestObjectData {
268+
/** Reference to another object by its ID. */
269+
objectId?: string;
270+
/** A numeric value. */
271+
number?: number;
272+
/** A boolean value. */
273+
boolean?: boolean;
274+
/** A string value. */
275+
string?: string;
276+
/** A binary value. Typed differently depending on platform (`Buffer` in Node.js, `ArrayBuffer` elsewhere). */
277+
bytes?: Buffer | ArrayBuffer;
278+
/** A JSON value (array or object). */
279+
json?: JsonArray | JsonObject;
280+
}
281+
282+
/**
283+
* Expanded representation of a counter object with metadata.
284+
*/
285+
export interface RestLiveCounter {
286+
/** The ID of the counter object. */
287+
objectId: string;
288+
/** Describes the value of a counter object. */
289+
counter: {
290+
/** Holds the value of the counter. */
291+
data: {
292+
/** The value of the counter. */
293+
number: number;
294+
};
295+
};
296+
}
297+
298+
/**
299+
* Fallback type for compatibility with future object types.
300+
*/
301+
export type AnyRestLiveObject = {
302+
/** The ID of the object, available for all object types. */
303+
objectId: string;
304+
};
305+
306+
/**
307+
* Request parameters for {@link RestObject.get}.
308+
* All parameters are optional since the default behavior is to fetch the channel's root object with `compact=true`.
309+
*/
310+
export interface GetObjectParams {
311+
/** The object ID to fetch. If omitted, the entrypoint is the channel's root object. */
312+
objectId?: string;
313+
/** Path evaluated relative to the entrypoint (root or specified objectId). */
314+
path?: string;
315+
/** Whether to return compact representation. Defaults to true. */
316+
compact?: boolean;
317+
}
318+
319+
/**
320+
* Enables the Objects to be read, modified and subscribed to for a realtime channel.
71321
*/
72322
export declare interface RealtimeObject {
73323
/**
@@ -124,6 +374,14 @@ export type Primitive =
124374
| JsonArray
125375
| JsonObject;
126376

377+
/**
378+
* Primitive types and object references that may appear in `data` fields
379+
* of response bodies when using the Objects REST API.
380+
*
381+
* References to other objects are represented as `{ objectId: string }`.
382+
*/
383+
export type PrimitiveOrObjectReference = Primitive | { objectId: string };
384+
127385
/**
128386
* Represents a JSON-encodable value.
129387
*/
@@ -1492,7 +1750,7 @@ declare namespace ObjectOperationActions {
14921750
}
14931751

14941752
/**
1495-
* The possible values of the `action` field of an {@link ObjectOperation}.
1753+
* Describes object operation types for {@link ObjectOperation} and {@link RestObjectOperation}.
14961754
*/
14971755
export type ObjectOperationAction =
14981756
| ObjectOperationActions.MAP_CREATE
@@ -1793,22 +2051,23 @@ export class LiveCounter {
17932051
}
17942052

17952053
/**
1796-
* The LiveObjects plugin that provides a {@link RealtimeClient} instance with the ability to use LiveObjects functionality.
2054+
* The LiveObjects plugin that provides a {@link RestClient} or {@link RealtimeClient} instance with the ability to use LiveObjects functionality.
17972055
*
1798-
* To create a client that includes this plugin, include it in the client options that you pass to the {@link RealtimeClient.constructor}:
2056+
* To create a client that includes this plugin, include it in the client options that you pass to the {@link RestClient.constructor} or {@link RealtimeClient.constructor}:
17992057
*
18002058
* ```javascript
18012059
* import { Realtime } from 'ably';
18022060
* import { LiveObjects } from 'ably/liveobjects';
18032061
* const realtime = new Realtime({ ...options, plugins: { LiveObjects } });
18042062
* ```
18052063
*
1806-
* The LiveObjects plugin can also be used with a {@link BaseRealtime} client:
2064+
* The LiveObjects plugin can also be used with a {@link BaseRest} or {@link BaseRealtime} client,
2065+
* with the additional requirement that you must also use the {@link Rest} plugin
18072066
*
18082067
* ```javascript
1809-
* import { BaseRealtime, WebSocketTransport, FetchRequest } from 'ably/modular';
2068+
* import { BaseRealtime, Rest, WebSocketTransport, FetchRequest } from 'ably/modular';
18102069
* import { LiveObjects } from 'ably/liveobjects';
1811-
* const realtime = new BaseRealtime({ ...options, plugins: { WebSocketTransport, FetchRequest, LiveObjects } });
2070+
* const realtime = new BaseRealtime({ ...options, plugins: { Rest, WebSocketTransport, FetchRequest, LiveObjects } });
18122071
* ```
18132072
*
18142073
* You can also import individual utilities alongside the plugin:
@@ -1832,3 +2091,17 @@ declare module 'ably' {
18322091
object: RealtimeObject;
18332092
}
18342093
}
2094+
2095+
/**
2096+
* Module augmentation to add the `object` property to `RestChannel` when
2097+
* importing from 'ably/liveobjects'. This ensures all LiveObjects types come from
2098+
* the same module (CJS or ESM), avoiding type incompatibility issues.
2099+
*/
2100+
declare module 'ably' {
2101+
interface RestChannel {
2102+
/**
2103+
* A {@link RestObject} object.
2104+
*/
2105+
object: RestObject;
2106+
}
2107+
}

scripts/moduleReport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ async function checkLiveObjectsPluginFiles() {
341341
'src/plugins/liveobjects/pathobject.ts',
342342
'src/plugins/liveobjects/pathobjectsubscriptionregister.ts',
343343
'src/plugins/liveobjects/realtimeobject.ts',
344+
'src/plugins/liveobjects/restobject.ts',
344345
'src/plugins/liveobjects/rootbatchcontext.ts',
345346
'src/plugins/liveobjects/syncobjectspool.ts',
346347
]);

src/common/lib/client/baseclient.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { FilteredSubscriptions } from './filteredsubscriptions';
2020
import type { LocalDevice } from 'plugins/push/pushactivation';
2121
import EventEmitter from '../util/eventemitter';
2222
import { MessageEncoding } from '../types/basemessage';
23+
import type * as LiveObjectsPlugin from 'plugins/liveobjects';
2324

2425
type BatchResult<T> = API.BatchResult<T>;
2526
type BatchPublishSpec = API.BatchPublishSpec;
@@ -50,6 +51,7 @@ class BaseClient {
5051
readonly _additionalHTTPRequestImplementations: HTTPRequestImplementations | null;
5152
private readonly __FilteredSubscriptions: typeof FilteredSubscriptions | null;
5253
readonly _Annotations: AnnotationsPlugin | null;
54+
readonly _liveObjectsPlugin: typeof LiveObjectsPlugin | null;
5355
readonly logger: Logger;
5456
_device?: LocalDevice;
5557

@@ -103,6 +105,7 @@ class BaseClient {
103105
this._Crypto = options.plugins?.Crypto ?? null;
104106
this.__FilteredSubscriptions = options.plugins?.MessageInteractions ?? null;
105107
this._Annotations = options.plugins?.Annotations ?? null;
108+
this._liveObjectsPlugin = options.plugins?.LiveObjects ?? null;
106109
}
107110

108111
get rest(): Rest {

src/common/lib/client/baserealtime.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ import { ModularPlugins, RealtimePresencePlugin } from './modularplugins';
1313
import { TransportNames } from 'common/constants/TransportName';
1414
import { TransportImplementations } from 'common/platform';
1515
import Defaults from '../util/defaults';
16-
import type * as LiveObjectsPlugin from 'plugins/liveobjects';
1716

1817
/**
1918
`BaseRealtime` is an export of the tree-shakable version of the SDK, and acts as the base class for the `DefaultRealtime` class exported by the non tree-shakable version.
2019
*/
2120
class BaseRealtime extends BaseClient {
2221
readonly _RealtimePresence: RealtimePresencePlugin | null;
23-
readonly _liveObjectsPlugin: typeof LiveObjectsPlugin | null;
2422
// Extra transport implementations available to this client, in addition to those in Platform.Transports.bundledImplementations
2523
readonly _additionalTransportImplementations: TransportImplementations;
2624
_channels: any;
@@ -60,7 +58,6 @@ class BaseRealtime extends BaseClient {
6058

6159
this._additionalTransportImplementations = BaseRealtime.transportImplementationsFromPlugins(this.options.plugins);
6260
this._RealtimePresence = this.options.plugins?.RealtimePresence ?? null;
63-
this._liveObjectsPlugin = this.options.plugins?.LiveObjects ?? null;
6461
this.connection = new Connection(this, this.options);
6562
this._channels = new Channels(this);
6663
if (this.options.autoConnect !== false) this.connect();

0 commit comments

Comments
 (0)