Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename fields #427

Merged
merged 3 commits into from
Dec 4, 2024
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
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,
vladvelici marked this conversation as resolved.
Show resolved Hide resolved
);
};

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(
splindsay-92 marked this conversation as resolved.
Show resolved Hide resolved
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),
splindsay-92 marked this conversation as resolved.
Show resolved Hide resolved
new Date(message.timestamp),
message.operation as Operation,
);
}
Loading
Loading