Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,19 +645,19 @@ The authentication token must include corresponding capabilities for the client

#### Getting the Root Object

The root object represents the top-level entry point for Objects within a channel. It gives access to all other nested Live Objects.
The root object represents the top-level entry point for objects within a channel. It gives access to all other nested objects.

```typescript
const root = await objects.getRoot();
```

The root object is a `LiveMap` instance and serves as the starting point for storing and organizing Objects on a channel.

#### Live Object Types
#### Object Types

LiveObjects currently supports two primary data structures; `LiveMap` and `LiveCounter`.

`LiveMap` - A key/value map data structure, similar to a JavaScript `Map`, where all changes are synchronized across clients in realtime. It allows you to store primitive values and other Live Objects, enabling composability.
`LiveMap` - A key/value map data structure, similar to a JavaScript `Map`, where all changes are synchronized across clients in realtime. It enables you to store primitive values and other objects, enabling composability.

You can use `LiveMap` as follows:

Expand Down Expand Up @@ -689,7 +689,7 @@ await root.set('foo', 'Alice');
await root.set('bar', 1);
await root.set('baz', true);
await root.set('qux', new Uint8Array([21, 31]));
// as well as other live objects
// as well as other objects
const counter = await objects.createCounter();
await root.set('quux', counter);

Expand All @@ -714,11 +714,11 @@ await counter.decrement(2);

#### Subscribing to Updates

Subscribing to updates on Live Objects allows you to receive changes made by other clients in realtime. Since multiple clients may modify the same Live Objects, subscribing ensures that your application reacts to external updates as soon as they are received.
Subscribing to updates on objects enables you to receive changes made by other clients in realtime. Since multiple clients may modify the same objects, subscribing ensures that your application reacts to external updates as soon as they are received.

Additionally, mutation methods such as `LiveMap.set`, `LiveCounter.increment`, and `LiveCounter.decrement` do not directly edit the current state of the object locally. Instead, they send the intended operation to the Ably system, and the change is applied to the local object only when the corresponding realtime operation is echoed back to the client. This means that the state you retrieve immediately after a mutation may not reflect the latest updates yet.

You can subscribe to updates on all Live Objects using subscription listeners as follows:
You can subscribe to updates on all objects using subscription listeners as follows:

```typescript
const root = await objects.getRoot();
Expand Down Expand Up @@ -772,14 +772,14 @@ root.unsubscribeAll();

#### Creating New Objects

New `LiveMap` and `LiveCounter` instances can be created as follows:
New `LiveMap` and `LiveCounter` objects can be created as follows:

```typescript
const counter = await objects.createCounter(123); // with optional initial counter value
const map = await objects.createMap({ key: 'value' }); // with optional initial map entries
```

To persist them within the Objects state, they must be assigned to a parent `LiveMap` that is connected to the root object through the object hierarchy:
To persist them on a channel and share them between clients, they must be assigned to a parent `LiveMap` that is connected to the root object through the object hierarchy:

```typescript
const root = await objects.getRoot();
Expand All @@ -799,9 +799,9 @@ await root.set('outerMap', outerMap);

#### Batch Operations

Batching allows multiple operations to be grouped into a single message that is sent to the Ably service. This allows batched operations to be applied atomically together.
Batching enables multiple operations to be grouped into a single channel message that is sent to the Ably service. This guarantees that all changes are applied atomically.

Within a batch callback, the `BatchContext` instance provides wrapper objects around regular Live Objects with a synchronous API for storing changes in the batch context.
Within a batch callback, the `BatchContext` instance provides wrapper objects around regular `LiveMap` and `LiveCounter` objects with a synchronous API for storing changes in the batch context.

```typescript
await objects.batch((ctx) => {
Expand All @@ -819,9 +819,9 @@ await objects.batch((ctx) => {

#### Lifecycle Events

Live Objects emit lifecycle events to signal critical state changes, such as synchronization progress and object deletions.
LiveObjects emit events that allow you to monitor objects' lifecycle changes, such as synchronization progress and object deletions.

**Synchronization Events** - the `syncing` and `synced` events notify when the Objects state is being synchronized with the Ably service. These events can be useful for displaying loading indicators, preventing user edits during synchronization, or triggering application logic when the data was loaded for the first time.
**Synchronization Events** - the `syncing` and `synced` events notify when the local Objects state on a client is being synchronized with the Ably service. These events can be useful for displaying loading indicators, preventing user edits during synchronization, or triggering application logic when the data was loaded for the first time.

```typescript
objects.on('syncing', () => {
Expand All @@ -835,14 +835,14 @@ objects.on('synced', () => {
});
```

**Object Deletion Events** - objects that have been orphaned for a long period (i.e., not connected to the state tree graph by being set as a key in a map accessible from the root map object) will eventually be deleted. Once a Live Object is deleted, it can no longer be interacted with. You should avoid accessing its data or trying to update its value and you should remove all references to the deleted object in your application.
**Object Deletion Events** - objects that have been orphaned for a long period (i.e., not connected to the object tree by being set as a key in a map accessible from the root map object) will eventually be deleted. Once an object is deleted, it can no longer be interacted with. You should avoid accessing its data or trying to update its value and you should remove all references to the deleted object in your application.

```typescript
const root = await objects.getRoot();
const counter = root.get('counter');

counter.on('deleted', () => {
console.log('Live Object has been deleted.');
console.log('Object has been deleted.');
// Example: Remove references to the object from the application
});
```
Expand Down
39 changes: 20 additions & 19 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,11 +872,11 @@ declare namespace ChannelModes {
*/
type PRESENCE_SUBSCRIBE = 'PRESENCE_SUBSCRIBE';
/**
* The client can publish Objects messages.
* The client can publish object messages.
*/
type OBJECT_PUBLISH = 'OBJECT_PUBLISH';
/**
* The client can receive Objects messages.
* The client can receive object messages.
*/
type OBJECT_SUBSCRIBE = 'OBJECT_SUBSCRIBE';
/**
Expand Down Expand Up @@ -1560,9 +1560,9 @@ export type DeregisterCallback = (device: DeviceDetails, callback: StandardCallb
export type ErrorCallback = (error: ErrorInfo | null) => void;

/**
* A callback used in {@link LiveObject} to listen for updates to the Live Object.
* A callback used in {@link LiveObject} to listen for updates to the object.
*
* @param update - The update object describing the changes made to the Live Object.
* @param update - The update object describing the changes made to the object.
*/
export type LiveObjectUpdateCallback<T> = (update: T) => void;

Expand Down Expand Up @@ -2181,9 +2181,9 @@ declare global {

/**
* Represents the type of data stored in a {@link LiveMap}.
* It maps string keys to scalar values ({@link StateValue}), or other Live Objects.
* It maps string keys to scalar values ({@link ObjectValue}), or other {@link LiveObject | LiveObjects}.
*/
export type LiveMapType = { [key: string]: StateValue | LiveMap<LiveMapType> | LiveCounter | undefined };
export type LiveMapType = { [key: string]: ObjectValue | LiveMap<LiveMapType> | LiveCounter | undefined };

/**
* The default type for the `root` object for Objects on a channel, based on the globally defined {@link ObjectsTypes} interface.
Expand Down Expand Up @@ -2236,7 +2236,7 @@ export declare interface BatchContextLiveMap<T extends LiveMapType> {
get<TKey extends keyof T & string>(key: TKey): T[TKey] | undefined;

/**
* Returns the number of key/value pairs in the map.
* Returns the number of key-value pairs in the map.
*/
size(): number;

Expand Down Expand Up @@ -2293,10 +2293,11 @@ export declare interface BatchContextLiveCounter {
}

/**
* The `LiveMap` class represents a key/value map data structure, similar to a JavaScript Map, where all changes are synchronized across clients in realtime.
* Conflict-free resolution for updates follows Last Write Wins (LWW) semantics, meaning that if two clients update the same key in the map, the update with the most recent timestamp wins.
* The `LiveMap` class represents a key-value map data structure, similar to a JavaScript Map, where all changes are synchronized across clients in realtime.
* Conflicts in a LiveMap are automatically resolved with last-write-wins (LWW) semantics,
* meaning that if two clients update the same key in the map, the update with the most recent timestamp wins.
*
* Keys must be strings. Values can be another Live Object, or a primitive type, such as a string, number, boolean, or binary data (see {@link StateValue}).
* Keys must be strings. Values can be another {@link LiveObject}, or a primitive type, such as a string, number, boolean, or binary data (see {@link ObjectValue}).
*/
export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveMapUpdate> {
/**
Expand All @@ -2310,12 +2311,12 @@ export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveM
get<TKey extends keyof T & string>(key: TKey): T[TKey] | undefined;

/**
* Returns the number of key/value pairs in the map.
* Returns the number of key-value pairs in the map.
*/
size(): number;

/**
* Returns an iterable of key/value pairs for every entry in the map.
* Returns an iterable of key-value pairs for every entry in the map.
*/
entries<TKey extends keyof T & string>(): IterableIterator<[TKey, T[TKey]]>;

Expand Down Expand Up @@ -2372,7 +2373,7 @@ export declare interface LiveMapUpdate extends LiveObjectUpdate {
*
* For binary data, the resulting type depends on the platform (`Buffer` in Node.js, `ArrayBuffer` elsewhere).
*/
export type StateValue = string | number | boolean | Buffer | ArrayBuffer;
export type ObjectValue = string | number | boolean | Buffer | ArrayBuffer;

/**
* The `LiveCounter` class represents a counter that can be incremented or decremented and is synchronized across clients in realtime.
Expand Down Expand Up @@ -2424,22 +2425,22 @@ export declare interface LiveCounterUpdate extends LiveObjectUpdate {
*/
export declare interface LiveObject<TUpdate extends LiveObjectUpdate = LiveObjectUpdate> {
/**
* Registers a listener that is called each time this Live Object is updated.
* Registers a listener that is called each time this LiveObject is updated.
*
* @param listener - An event listener function that is called with an update object whenever this Live Object is updated.
* @param listener - An event listener function that is called with an update object whenever this LiveObject is updated.
* @returns A {@link SubscribeResponse} object that allows the provided listener to be deregistered from future updates.
*/
subscribe(listener: LiveObjectUpdateCallback<TUpdate>): SubscribeResponse;

/**
* Deregisters the given listener from updates for this Live Object.
* Deregisters the given listener from updates for this LiveObject.
*
* @param listener - An event listener function.
*/
unsubscribe(listener: LiveObjectUpdateCallback<TUpdate>): void;

/**
* Deregisters all listeners from updates for this Live Object.
* Deregisters all listeners from updates for this LiveObject.
*/
unsubscribeAll(): void;

Expand Down Expand Up @@ -2467,7 +2468,7 @@ export declare interface LiveObject<TUpdate extends LiveObjectUpdate = LiveObjec
}

/**
* Represents a generic update object describing the changes that occurred on a Live Object.
* Represents a generic update object describing the changes that occurred on a LiveObject.
*/
export declare interface LiveObjectUpdate {
/**
Expand Down Expand Up @@ -2626,7 +2627,7 @@ export declare interface RealtimeChannel extends EventEmitter<channelEventCallba
*/
presence: RealtimePresence;
/**
* A {@link Objects} object.
* An {@link Objects} object.
*/
objects: Objects;
/**
Expand Down
2 changes: 1 addition & 1 deletion scripts/moduleReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,9 @@ async function checkObjectsPluginFiles() {
'src/plugins/objects/livemap.ts',
'src/plugins/objects/liveobject.ts',
'src/plugins/objects/objectid.ts',
'src/plugins/objects/objectmessage.ts',
'src/plugins/objects/objects.ts',
'src/plugins/objects/objectspool.ts',
'src/plugins/objects/statemessage.ts',
'src/plugins/objects/syncobjectsdatapool.ts',
]);

Expand Down
42 changes: 21 additions & 21 deletions src/common/lib/client/realtimechannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { ChannelOptions } from '../../types/channel';
import { normaliseChannelOptions } from '../util/defaults';
import { PaginatedResult } from './paginatedresource';
import type { PushChannel } from 'plugins/push';
import type { Objects, StateMessage } from 'plugins/objects';
import type { Objects, ObjectMessage } from 'plugins/objects';

interface RealtimeHistoryParams {
start?: number;
Expand Down Expand Up @@ -507,12 +507,12 @@ class RealtimeChannel extends EventEmitter {
this.sendMessage(msg, callback);
}

sendState(state: StateMessage[]): Promise<void> {
sendState(objectMessages: ObjectMessage[]): Promise<void> {
return new Promise((resolve, reject) => {
const msg = protocolMessageFromValues({
action: actions.STATE,
action: actions.OBJECT,
channel: this.name,
state,
state: objectMessages,
});
this.sendMessage(msg, (err) => (err ? reject(err) : resolve()));
});
Expand All @@ -524,7 +524,7 @@ class RealtimeChannel extends EventEmitter {
message.action === actions.ATTACHED ||
message.action === actions.MESSAGE ||
message.action === actions.PRESENCE ||
message.action === actions.STATE
message.action === actions.OBJECT
) {
// RTL15b
this.setChannelSerial(message.channelSerial);
Expand All @@ -542,17 +542,17 @@ class RealtimeChannel extends EventEmitter {
const resumed = message.hasFlag('RESUMED');
const hasPresence = message.hasFlag('HAS_PRESENCE');
const hasBacklog = message.hasFlag('HAS_BACKLOG');
const hasState = message.hasFlag('HAS_STATE');
const hasObjects = message.hasFlag('HAS_OBJECTS');
if (this.state === 'attached') {
if (!resumed) {
// we have lost continuity.
// the presence set needs to be re-synced
if (this._presence) {
this._presence.onAttached(hasPresence);
}
// the Objects state needs to be re-synced
// the Objects tree needs to be re-synced
if (this._objects) {
this._objects.onAttached(hasState);
this._objects.onAttached(hasObjects);
}
}
const change = new ChannelStateChange(this.state, this.state, resumed, hasBacklog, message.error);
Expand All @@ -564,7 +564,7 @@ class RealtimeChannel extends EventEmitter {
/* RTL5i: re-send DETACH and remain in the 'detaching' state */
this.checkPendingState();
} else {
this.notifyState('attached', message.error, resumed, hasPresence, hasBacklog, hasState);
this.notifyState('attached', message.error, resumed, hasPresence, hasBacklog, hasObjects);
}
break;
}
Expand Down Expand Up @@ -612,25 +612,25 @@ class RealtimeChannel extends EventEmitter {
break;
}

// STATE and STATE_SYNC message processing share most of the logic, so group them together
case actions.STATE:
case actions.STATE_SYNC: {
// OBJECT and OBJECT_SYNC message processing share most of the logic, so group them together
case actions.OBJECT:
case actions.OBJECT_SYNC: {
if (!this._objects) {
return;
}

const stateMessages = message.state ?? [];
const objectMessages = message.state ?? [];
const options = this.channelOptions;
await this._decodeAndPrepareMessages(message, stateMessages, (msg) =>
await this._decodeAndPrepareMessages(message, objectMessages, (msg) =>
this.client._objectsPlugin
? this.client._objectsPlugin.StateMessage.decode(msg, options, MessageEncoding)
? this.client._objectsPlugin.ObjectMessage.decode(msg, options, MessageEncoding)
: Utils.throwMissingPluginError('Objects'),
);

if (message.action === actions.STATE) {
this._objects.handleStateMessages(stateMessages);
if (message.action === actions.OBJECT) {
this._objects.handleObjectMessages(objectMessages);
} else {
this._objects.handleStateSyncMessages(stateMessages, message.channelSerial);
this._objects.handleObjectSyncMessages(objectMessages, message.channelSerial);
}

break;
Expand Down Expand Up @@ -740,7 +740,7 @@ class RealtimeChannel extends EventEmitter {
* @returns `unrecoverableError` flag. If `true` indicates that unrecoverable error was encountered during message decoding
* and any further message processing should be stopped. Always equals to `false` if `decodeErrorRecoveryHandler` was not provided
*/
private async _decodeAndPrepareMessages<T extends Message | PresenceMessage | StateMessage>(
private async _decodeAndPrepareMessages<T extends Message | PresenceMessage | ObjectMessage>(
protocolMessage: ProtocolMessage,
messages: T[],
decodeFn: (msg: T) => Promise<void>,
Expand Down Expand Up @@ -809,7 +809,7 @@ class RealtimeChannel extends EventEmitter {
resumed?: boolean,
hasPresence?: boolean,
hasBacklog?: boolean,
hasState?: boolean,
hasObjects?: boolean,
): void {
Logger.logAction(
this.logger,
Expand All @@ -831,7 +831,7 @@ class RealtimeChannel extends EventEmitter {
this._presence.actOnChannelState(state, hasPresence, reason);
}
if (this._objects) {
this._objects.actOnChannelState(state, hasState);
this._objects.actOnChannelState(state, hasObjects);
}
if (state === 'suspended' && this.connectionManager.state.sendEvents) {
this.startRetryTimer();
Expand Down
2 changes: 1 addition & 1 deletion src/common/lib/transport/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class PendingMessage {
const action = message.action;
this.sendAttempted = false;
this.ackRequired =
typeof action === 'number' && [actions.MESSAGE, actions.PRESENCE, actions.STATE].includes(action);
typeof action === 'number' && [actions.MESSAGE, actions.PRESENCE, actions.OBJECT].includes(action);
}
}

Expand Down
Loading
Loading