Skip to content

Commit

Permalink
Rename fields
Browse files Browse the repository at this point in the history
  • Loading branch information
vladvelici committed Dec 3, 2024
1 parent e838058 commit 94bb83d
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 223 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 edits 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
2 changes: 1 addition & 1 deletion demo/src/containers/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const Chat = (props: { roomId: string; setRoomId: (roomId: string) => voi
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/core/action-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* The type for metadata contained in the latestActionDetails field of a chat message.
* The type for metadata contained in the operations field of a chat message.
* This is a key-value pair where the key is a string, and the value is a string, it represents the metadata supplied
* to a message update or deletion request.
*
Expand Down
40 changes: 17 additions & 23 deletions src/core/chat-api.ts
Original file line number Diff line number Diff line change
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 @@ -69,18 +76,6 @@ interface DeleteMessageParams {
metadata?: MessageActionMetadata;
}

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

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

/**
* Chat SDK Backend
*/
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 ? new Date(message.createdAt) : new Date(message.timestamp),

Check failure on line 112 in src/core/chat-api.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
new Date(message.timestamp),
undefined, // operation
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type { LogContext, Logger, LogHandler } from './logger.js';
export { LogLevel } from './logger.js';
export type {
Message,
MessageActionDetails,
Operation as MessageActionDetails,
MessageActionMetadata,
MessageHeaders,
MessageMetadata,
Expand Down
89 changes: 42 additions & 47 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';

Check failure on line 1 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / lint

Run autofix to sort these imports!

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

interface MessagePayload {
data?: {
Expand All @@ -15,28 +15,23 @@ interface MessagePayload {
};

serial: string;
createdAt: number;
createdAt?: number;
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;
export function parseMessage(roomId: string | undefined, inboundMessage: Ably.InboundMessage): Message {
try {
return parseMessageX(roomId, inboundMessage);
} catch (err : unknown) {

Check failure on line 27 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / lint

The catch parameter `err` should be named `error`
console.error(`failed to parse message:`, err);
throw err;
}
}

export function parseMessage(roomId: string | undefined, inboundMessage: Ably.InboundMessage): Message {

export function parseMessageX(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 @@ -58,52 +53,52 @@ export function parseMessage(roomId: string | undefined, inboundMessage: Ably.In
throw new Ably.ErrorInfo(`received incoming message without extras`, 50000, 500);
}

if (!message.serial) {
throw new Ably.ErrorInfo(`received incoming message without serial`, 50000, 500);
if (!message.serial && !(message as unknown as {version? : string}).version) {
throw new Ably.ErrorInfo(`received incoming message without serial or version`, 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,
};

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

// both .serial and .version will always be set by ably-js in the future, so we can simplify this later
// todo: simplify after ably-js is updated
let serial = message.serial;
let version = message.version;

Check failure on line 79 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / lint

'version' is never reassigned. Use 'const' instead
if (!serial) {
serial = version as string;

Check failure on line 81 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / lint

Use a ! assertion to more succinctly remove null and undefined from the type
}

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

Check failure on line 85 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / test

test/core/helpers.test.ts > helpers > fromEncodedChatMessage > should return a chat message

Error: received incoming message without version ❯ parseMessageX src/core/message-parser.ts:85:11 ❯ parseMessage src/core/message-parser.ts:26:12 ❯ chatMessageFromAblyMessage src/core/helpers.ts:129:10 ❯ chatMessageFromEncoded src/core/helpers.ts:66:10 ❯ test/core/helpers.test.ts:39:22 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 50000, statusCode: 500 }
}

let timestamp = message.timestamp;

Check failure on line 88 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / lint

'timestamp' is never reassigned. Use 'const' instead
let createdAt = message.createdAt ?? timestamp;

Check failure on line 89 in src/core/message-parser.ts

View workflow job for this annotation

GitHub Actions / lint

'createdAt' is never reassigned. Use 'const' instead

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,
serial,
message.clientId,
roomId,
message.data.text,
message.data.metadata ?? {},
message.extras.headers ?? {},
message.action as ChatMessageActions,
version,
new Date(createdAt),
new Date(timestamp),
message.operation as Operation,
);
}
Loading

0 comments on commit 94bb83d

Please sign in to comment.