Skip to content

Commit

Permalink
Merge pull request #427 from ably/rename-fields-as-agreed
Browse files Browse the repository at this point in the history
Rename fields
  • Loading branch information
vladvelici authored Dec 4, 2024
2 parents e838058 + 2b90c7b commit 9de6a77
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 332 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,17 +304,17 @@ const updatedMessage = await room.messages.update(message,

`updatedMessage` is a Message object with all updates applied. As with sending and deleting, the promise may resolve after the updated message is received via the messages subscription.

A `Message` that was updated will have `updatedAt` and `updatedBy` fields set, and `isUpdated()` will return `true`.
A `Message` that was updated will have values for `updatedAt` and `updatedBy`, and `isUpdated()` will return `true`.

Note that if you delete an updated message, it is no longer considered _updated_. Only the last operation takes effect.

#### Handling updates in realtime

Updated messages received from realtime have the `latestAction` parameter set to `ChatMessageActions.MessageUpdate`, and the event received has the `type` set to `MessageEvents.Updated`. Updated messages are full copies of the message, meaning that all that is needed to keep a state or UI up to date is to replace the old message with the received one.
Updated messages received from realtime have the `action` parameter set to `ChatMessageActions.MessageUpdate`, and the event received has the `type` set to `MessageEvents.Updated`. Updated messages are full copies of the message, meaning that all that is needed to keep a state or UI up to date is to replace the old message with the received one.

In rare occasions updates might arrive over realtime out of order. To keep a correct state, the `Message` interface provides methods to compare two instances of the same base message to determine which one is newer: `actionBefore()`, `actionAfter()`, and `actionEqual()`.
In rare occasions updates might arrive over realtime out of order. To keep a correct state, compare the `version` lexicographically (string compare). Alternatively, the `Message` interface provides convenience methods to compare two instances of the same base message to determine which version is newer: `versionBefore()`, `versionAfter()`, and `versionEqual()`.

The same out-of-order situation can happen between updates received over realtime and HTTP responses. In the situation where two concurrent edits happen, both might be received via realtime before the HTTP response of the first one arrives. Always use `actionAfter()`, `actionBefore()`, or `actionEqual()` to determine which instance of a `Message` is newer.
The same out-of-order situation can happen between updates received over realtime and HTTP responses. In the situation where two concurrent updates happen, both might be received via realtime before the HTTP response of the first one arrives. Always compare the message `version` to determine which instance of a `Message` is newer.

Example for handling updates:
```typescript
Expand All @@ -325,7 +325,7 @@ room.messages.subscribe(event => {
case MessageEvents.Updated: {
const serial = event.message.serial;
const index = messages.findIndex((m) => m.serial === serial);
if (index !== -1 && messages[index].actionBefore(event.message)) {
if (index !== -1 && messages[index].version < event.message.version) {
messages[index] = event.message;
}
break;
Expand Down
4 changes: 2 additions & 2 deletions demo/src/containers/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export const Chat = (props: { roomId: string; setRoomId: (roomId: string) => voi
return prevMessages;
}

// skip update if the received action is not newer
if (!prevMessages[index].actionBefore(message)) {
// skip update if the received version is not newer
if (!prevMessages[index].versionBefore(message)) {
return prevMessages;
}

Expand Down
48 changes: 21 additions & 27 deletions src/core/chat-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Ably from 'ably';

import { Logger } from './logger.js';
import { DefaultMessage, Message, MessageActionMetadata, MessageHeaders, MessageMetadata } from './message.js';
import { DefaultMessage, Message, MessageHeaders, MessageMetadata, MessageOperationMetadata } from './message.js';
import { OccupancyEvent } from './occupancy.js';
import { PaginatedResult } from './query.js';

Expand Down Expand Up @@ -31,18 +31,25 @@ interface SendMessageParams {
headers?: MessageHeaders;
}

export interface DeleteMessageResponse {
/**
* Represents the response for deleting or updating a message.
*/
interface MessageOperationResponse {
/**
* The serial of the deletion action.
* The new message version.
*/
serial: string;
version: string;

/**
* The timestamp of the deletion action.
* The timestamp of the operation.
*/
deletedAt: number;
timestamp: number;
}

export type UpdateMessageResponse = MessageOperationResponse;

export type DeleteMessageResponse = MessageOperationResponse;

interface UpdateMessageParams {
/**
* Message data to update. All fields are updated and if omitted they are
Expand All @@ -58,27 +65,15 @@ interface UpdateMessageParams {
description?: string;

/** Metadata of the update action */
metadata?: MessageActionMetadata;
metadata?: MessageOperationMetadata;
}

interface DeleteMessageParams {
/** Description of the delete action */
description?: string;

/** Metadata of the delete action */
metadata?: MessageActionMetadata;
}

interface UpdateMessageResponse {
/**
* The serial of the update action.
*/
serial: string;

/**
* The timestamp of when the update occurred.
*/
updatedAt: number;
metadata?: MessageOperationMetadata;
}

/**
Expand Down Expand Up @@ -112,12 +107,11 @@ export class ChatApi {
message.text,
metadata ?? {},
headers ?? {},
new Date(message.createdAt),
message.latestAction,
message.latestActionSerial,
message.deletedAt ? new Date(message.deletedAt) : undefined,
message.updatedAt ? new Date(message.updatedAt) : undefined,
message.latestActionDetails,
message.action,
message.version,
(message.createdAt as Date | undefined) ? new Date(message.createdAt) : new Date(message.timestamp),
new Date(message.timestamp),
message.operation,
);
};

Expand All @@ -139,7 +133,7 @@ export class ChatApi {
}

async deleteMessage(roomId: string, serial: string, params?: DeleteMessageParams): Promise<DeleteMessageResponse> {
const body: { description?: string; metadata?: MessageActionMetadata } = {
const body: { description?: string; metadata?: MessageOperationMetadata } = {
description: params?.description,
metadata: params?.metadata,
};
Expand Down
12 changes: 3 additions & 9 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @module chat-js
*/

export type { ActionMetadata } from './action-metadata.js';
export { ChatClient } from './chat.js';
export type { ClientOptions } from './config.js';
export type {
Expand All @@ -27,26 +26,21 @@ export {
} from './helpers.js';
export type { LogContext, Logger, LogHandler } from './logger.js';
export { LogLevel } from './logger.js';
export type { Message, MessageHeaders, MessageMetadata, MessageOperationMetadata, Operation } from './message.js';
export type {
Message,
MessageActionDetails,
MessageActionMetadata,
MessageHeaders,
MessageMetadata,
} from './message.js';
export type {
ActionDetails,
DeleteMessageParams,
MessageEventPayload,
MessageListener,
Messages,
MessageSubscriptionResponse,
OperationDetails,
QueryOptions,
SendMessageParams,
UpdateMessageParams,
} from './messages.js';
export type { Metadata } from './metadata.js';
export type { Occupancy, OccupancyEvent, OccupancyListener, OccupancySubscriptionResponse } from './occupancy.js';
export type { OperationMetadata } from './operation-metadata.js';
export type {
Presence,
PresenceData,
Expand Down
77 changes: 28 additions & 49 deletions src/core/message-parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Ably from 'ably';

import { ChatMessageActions } from './events.js';
import { DefaultMessage, Message, MessageActionDetails, MessageHeaders, MessageMetadata } from './message.js';
import { DefaultMessage, Message, MessageHeaders, MessageMetadata, Operation } from './message.js';

interface MessagePayload {
data?: {
Expand All @@ -16,28 +16,15 @@ interface MessagePayload {

serial: string;
createdAt: number;
version?: string;
version: string;
action: Ably.MessageAction;
operation?: Ably.Operation;
}

interface ChatMessageFields {
serial: string;
clientId: string;
roomId: string;
text: string;
metadata: MessageMetadata;
headers: MessageHeaders;
createdAt: Date;
latestAction: ChatMessageActions;
latestActionSerial: string;
updatedAt?: Date;
deletedAt?: Date;
operation?: MessageActionDetails;
}

// Parse a realtime message to a chat message
export function parseMessage(roomId: string | undefined, inboundMessage: Ably.InboundMessage): Message {
const message = inboundMessage as MessagePayload;

if (!roomId) {
throw new Ably.ErrorInfo(`received incoming message without roomId`, 50000, 500);
}
Expand All @@ -62,48 +49,40 @@ export function parseMessage(roomId: string | undefined, inboundMessage: Ably.In
throw new Ably.ErrorInfo(`received incoming message without serial`, 50000, 500);
}

const newMessage: ChatMessageFields = {
serial: message.serial,
clientId: message.clientId,
roomId,
text: message.data.text,
metadata: message.data.metadata ?? {},
headers: message.extras.headers ?? {},
createdAt: new Date(message.createdAt),
latestAction: message.action as ChatMessageActions,
latestActionSerial: message.version ?? message.serial,
updatedAt: message.timestamp ? new Date(message.timestamp) : undefined,
deletedAt: message.timestamp ? new Date(message.timestamp) : undefined,
operation: message.operation as MessageActionDetails,
};
if (!message.version) {
throw new Ably.ErrorInfo(`received incoming message without version`, 50000, 500);
}

if (!message.createdAt) {
throw new Ably.ErrorInfo(`received incoming message without createdAt`, 50000, 500);
}

if (!message.timestamp) {
throw new Ably.ErrorInfo(`received incoming message without timestamp`, 50000, 500);
}

switch (message.action) {
case ChatMessageActions.MessageCreate: {
break;
}
case ChatMessageActions.MessageCreate:
case ChatMessageActions.MessageUpdate:
case ChatMessageActions.MessageDelete: {
if (!message.version) {
throw new Ably.ErrorInfo(`received incoming ${message.action} without version`, 50000, 500);
}
break;
}
default: {
throw new Ably.ErrorInfo(`received incoming message with unhandled action; ${message.action}`, 50000, 500);
}
}

return new DefaultMessage(
newMessage.serial,
newMessage.clientId,
newMessage.roomId,
newMessage.text,
newMessage.metadata,
newMessage.headers,
newMessage.createdAt,
newMessage.latestAction,
newMessage.latestActionSerial,
newMessage.latestAction === ChatMessageActions.MessageDelete ? newMessage.deletedAt : undefined,
newMessage.latestAction === ChatMessageActions.MessageUpdate ? newMessage.updatedAt : undefined,
newMessage.operation,
message.serial,
message.clientId,
roomId,
message.data.text,
message.data.metadata ?? {},
message.extras.headers ?? {},
message.action as ChatMessageActions,
message.version,
new Date(message.createdAt),
new Date(message.timestamp),
message.operation as Operation,
);
}
Loading

0 comments on commit 9de6a77

Please sign in to comment.