diff --git a/README.md b/README.md index e8c2c213..83c5af71 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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; diff --git a/demo/src/containers/Chat/Chat.tsx b/demo/src/containers/Chat/Chat.tsx index 689dd6ea..512751f5 100644 --- a/demo/src/containers/Chat/Chat.tsx +++ b/demo/src/containers/Chat/Chat.tsx @@ -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; } diff --git a/src/core/action-metadata.ts b/src/core/action-metadata.ts index a5306a33..7a6d8c45 100644 --- a/src/core/action-metadata.ts +++ b/src/core/action-metadata.ts @@ -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. * diff --git a/src/core/chat-api.ts b/src/core/chat-api.ts index ec340b49..ca68e0b4 100644 --- a/src/core/chat-api.ts +++ b/src/core/chat-api.ts @@ -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 @@ -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 */ @@ -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), + new Date(message.timestamp), + undefined, // operation ); }; diff --git a/src/core/index.ts b/src/core/index.ts index 48167fa9..3f8e7465 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -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, diff --git a/src/core/message-parser.ts b/src/core/message-parser.ts index 0b695399..6b0bb2f7 100644 --- a/src/core/message-parser.ts +++ b/src/core/message-parser.ts @@ -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, Operation, MessageHeaders, MessageMetadata } from './message.js'; interface MessagePayload { data?: { @@ -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) { + 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); @@ -58,33 +53,18 @@ 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; } @@ -92,18 +72,33 @@ export function parseMessage(roomId: string | undefined, inboundMessage: Ably.In 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; + if (!serial) { + serial = version as string; + } + + if (!version) { + throw new Ably.ErrorInfo(`received incoming message without version`, 50000, 500); + } + + let timestamp = message.timestamp; + let createdAt = message.createdAt ?? timestamp; + 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, ); } diff --git a/src/core/message.ts b/src/core/message.ts index 4ef31abc..679d5d81 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -16,14 +16,14 @@ export type MessageHeaders = Headers; export type MessageMetadata = Metadata; /** - * {@link ActionMetadata} type for a chat messages {@link MessageActionDetails}. + * {@link ActionMetadata} type for a chat messages {@link Operation}. */ export type MessageActionMetadata = ActionMetadata; /** * Represents the detail of a message deletion or update. */ -export interface MessageActionDetails { +export interface Operation { /** * The optional clientId of the user who performed the update or deletion. */ @@ -99,71 +99,77 @@ export interface Message { /** * The latest action of the message. This can be used to determine if the message was created, updated, or deleted. */ - readonly latestAction: ChatMessageActions; + readonly action: ChatMessageActions; /** * A unique identifier for the latest action that updated the message. This is only set for update and deletes. */ - readonly latestActionSerial: string; + readonly version: string; + + /** + * The timestamp at which this version was created. + */ + readonly timestamp : Date; /** * The details of the latest action that updated the message. This is only set for update and delete actions. */ - readonly latestActionDetails?: MessageActionDetails; + readonly operation?: Operation; /** * Indicates if the message has been updated. */ - readonly isUpdated: boolean; + get isUpdated(): boolean; /** * Indicates if the message has been deleted. */ - readonly isDeleted: boolean; + get isDeleted(): boolean; /** * The clientId of the user who deleted the message. */ - readonly deletedBy?: string; + get deletedBy(): string | undefined; /** * The clientId of the user who updated the message. */ - readonly updatedBy?: string; + get updatedBy(): string | undefined; /** * The timestamp at which the message was deleted. */ - readonly deletedAt?: Date; + get deletedAt(): Date | undefined; /** * The timestamp at which the message was updated. */ - readonly updatedAt?: Date; + get updatedAt(): Date | undefined; + /** - * Determines if the action of this message is before the action of the given message. + * Determines if the version of this message is older the version of the given message. * @param message The message to compare against. - * @returns true if the action of this message is before the given message. - * @throws {@link ErrorInfo} if both message serials do not match, or if {@link latestActionSerial} of either is invalid. + * @returns true if the version of this message is before the given message. + * @throws {@link ErrorInfo} if both message serials do not match. */ - actionBefore(message: Message): boolean; + versionBefore(message: Message): boolean; /** - * Determines if the action of this message is after the action of the given message. + * Determines if the version of this message is newer the version of the given message. * @param message The message to compare against. - * @returns true if the action of this message is after the given message. - * @throws {@link ErrorInfo} if both message serials do not match, or if {@link latestActionSerial} of either is invalid. + * @returns true if the version of this message is after the given message. + * @throws {@link ErrorInfo} if both message serials do not match. */ - actionAfter(message: Message): boolean; + versionAfter(message: Message): boolean; /** - * Determines if the action of this message is equal to the action of the given message. + * Determines if the version of this message is the same as to the version of the given message. * @param message The message to compare against. - * @returns true if the action of this message is equal to the given message. - * @throws {@link ErrorInfo} if both message serials do not match, or if {@link latestActionSerial} of either is invalid. + * @returns true if the version of this message is equal to the given message. + * @throws {@link ErrorInfo} if both message serials do not match. */ - actionEqual(message: Message): boolean; + versionEqual(message: Message): boolean; /** * Determines if this message was created before the given message. This comparison is based on @@ -207,62 +213,66 @@ export class DefaultMessage implements Message { public readonly text: string, public readonly metadata: MessageMetadata, public readonly headers: MessageHeaders, + public readonly action: ChatMessageActions, + public readonly version: string, public readonly createdAt: Date, - public readonly latestAction: ChatMessageActions, - - // the `latestActionSerial` will be set to the current message `serial` for new messages, - // else it will be set to the `updateSerial` corresponding to whatever action - // (update/delete) that was just performed. - public readonly latestActionSerial: string, - public readonly deletedAt?: Date, - public readonly updatedAt?: Date, - public readonly latestActionDetails?: MessageActionDetails, + public readonly timestamp: Date, + public readonly operation?: Operation, ) { + // The object is frozen after constructing to enforce readonly at runtime too Object.freeze(this); } get isUpdated(): boolean { - return this.updatedAt !== undefined; + return this.action === ChatMessageActions.MessageUpdate; } get isDeleted(): boolean { - return this.deletedAt !== undefined; + return this.action === ChatMessageActions.MessageDelete; } get updatedBy(): string | undefined { - return this.latestAction === ChatMessageActions.MessageUpdate ? this.latestActionDetails?.clientId : undefined; + return this.isUpdated ? this.operation?.clientId : undefined; } get deletedBy(): string | undefined { - return this.latestAction === ChatMessageActions.MessageDelete ? this.latestActionDetails?.clientId : undefined; + return this.isDeleted ? this.operation?.clientId : undefined; + } + + get updatedAt(): Date | undefined { + return this.isUpdated ? this.timestamp : undefined; + } + + get deletedAt(): Date | undefined { + return this.isDeleted ? this.timestamp : undefined; } - actionBefore(message: Message): boolean { + versionBefore(message: Message): boolean { // Check to ensure the messages are the same before comparing operation order if (!this.equal(message)) { throw new ErrorInfo('actionBefore(): Cannot compare actions, message serials must be equal', 50000, 500); } - return this.latestActionSerial < message.latestActionSerial; + return this.version < message.version; } - actionAfter(message: Message): boolean { + versionAfter(message: Message): boolean { // Check to ensure the messages are the same before comparing operation order if (!this.equal(message)) { throw new ErrorInfo('actionAfter(): Cannot compare actions, message serials must be equal', 50000, 500); } - return this.latestActionSerial > message.latestActionSerial; + return this.version > message.version; } - actionEqual(message: Message): boolean { + versionEqual(message: Message): boolean { // Check to ensure the messages are the same before comparing operation order if (!this.equal(message)) { throw new ErrorInfo('actionEqual(): Cannot compare actions, message serials must be equal', 50000, 500); } - return this.latestActionSerial === message.latestActionSerial; + return this.version === message.version; } before(message: Message): boolean { diff --git a/src/core/messages.ts b/src/core/messages.ts index 7632560e..9535c93e 100644 --- a/src/core/messages.ts +++ b/src/core/messages.ts @@ -496,9 +496,10 @@ export class DefaultMessages text, metadata ?? {}, headers ?? {}, - new Date(response.createdAt), ChatMessageActions.MessageCreate, response.serial, + new Date(response.createdAt), + new Date(response.createdAt), ); } @@ -510,24 +511,26 @@ export class DefaultMessages message: update, }); - return new DefaultMessage( + const updatedMessage = new DefaultMessage( message.serial, message.clientId, this._roomId, update.text, update.metadata ?? {}, update.headers ?? {}, - message.createdAt, ChatMessageActions.MessageUpdate, - response.serial, - undefined, - response.updatedAt ? new Date(response.updatedAt) : undefined, + response.version, + new Date(message.createdAt), + new Date(response.timestamp), { clientId: this._clientId, description: details?.description, metadata: details?.metadata, }, ); + + this._logger.debug('Messages.update(); message update successfully', { updatedMessage }); + return updatedMessage; } /** @@ -535,25 +538,29 @@ export class DefaultMessages */ async delete(message: Message, params?: DeleteMessageParams): Promise { this._logger.trace('Messages.delete();', { params }); + const response = await this._chatApi.deleteMessage(this._roomId, message.serial, params); + + console.log("delete message http response", response); + const deletedMessage: Message = new DefaultMessage( message.serial, message.clientId, - message.roomId, + this._roomId, message.text, - message.metadata, - message.headers, - message.createdAt, + message.metadata ?? {}, + message.headers ?? {}, ChatMessageActions.MessageDelete, - response.serial, - response.deletedAt ? new Date(response.deletedAt) : undefined, - message.updatedAt, + response.version, + new Date(message.createdAt), + new Date(response.timestamp), { clientId: this._clientId, description: params?.description, metadata: params?.metadata, }, ); + this._logger.debug('Messages.delete(); message deleted successfully', { deletedMessage }); return deletedMessage; } @@ -622,6 +629,7 @@ export class DefaultMessages */ private _parseNewMessage(channelEventMessage: Ably.InboundMessage): Message | undefined { try { + console.log("about to parse message", this._roomId, "raw msg=", channelEventMessage); return parseMessage(this._roomId, channelEventMessage); } catch (error: unknown) { this._logger.error(`failed to parse incoming message;`, { channelEventMessage, error: error as Ably.ErrorInfo }); diff --git a/test/core/helpers.test.ts b/test/core/helpers.test.ts index 647132ff..e45e40d0 100644 --- a/test/core/helpers.test.ts +++ b/test/core/helpers.test.ts @@ -45,9 +45,10 @@ describe('helpers', () => { 'I have the high ground now', {}, {}, - new Date(1719948956834), ChatMessageActions.MessageCreate, '01719948956834-000@108TeGZDQBderu97202638', + new Date(1719948956834), + new Date(1719948956834), ), ); }); diff --git a/test/core/message-parser.test.ts b/test/core/message-parser.test.ts index 7780e4f9..35dcf51c 100644 --- a/test/core/message-parser.test.ts +++ b/test/core/message-parser.test.ts @@ -167,8 +167,8 @@ describe('parseMessage', () => { expect(result.updatedAt).toBeUndefined(); expect(result.updatedBy).toBeUndefined(); - expect(result.latestAction).toEqual(ChatMessageActions.MessageCreate); - expect(result.latestActionDetails).toBeUndefined(); + expect(result.action).toEqual(ChatMessageActions.MessageCreate); + expect(result.operation).toBeUndefined(); }); it('should return a DefaultMessage instance for a valid updated message', () => { @@ -199,9 +199,9 @@ describe('parseMessage', () => { expect(result.headers).toEqual({ headerKey: 'headerValue' }); expect(result.updatedAt).toEqual(new Date(1728402074206)); expect(result.updatedBy).toBe('client2'); - expect(result.latestAction).toEqual(ChatMessageActions.MessageUpdate); - expect(result.latestActionSerial).toEqual('01728402074206-000@cbfkKvEYgBhDaZ38195418:0'); - expect(result.latestActionDetails).toEqual({ + expect(result.action).toEqual(ChatMessageActions.MessageUpdate); + expect(result.version).toEqual('01728402074206-000@cbfkKvEYgBhDaZ38195418:0'); + expect(result.operation).toEqual({ clientId: 'client2', description: 'update message', metadata: { 'custom-update': 'some flag' }, @@ -244,8 +244,8 @@ describe('parseMessage', () => { expect(result.headers).toEqual({ headerKey: 'headerValue' }); expect(result.deletedAt).toEqual(new Date(1728402074206)); expect(result.deletedBy).toBe('client2'); - expect(result.latestActionSerial).toEqual('01728402074206-000@cbfkKvEYgBhDaZ38195418:0'); - expect(result.latestActionDetails).toEqual({ + expect(result.version).toEqual('01728402074206-000@cbfkKvEYgBhDaZ38195418:0'); + expect(result.operation).toEqual({ clientId: 'client2', description: 'delete message', metadata: { 'custom-warning': 'this is a warning' }, diff --git a/test/core/message.test.ts b/test/core/message.test.ts index c33951a0..0497c548 100644 --- a/test/core/message.test.ts +++ b/test/core/message.test.ts @@ -5,24 +5,6 @@ import { ChatMessageActions } from '../../src/core/events.ts'; import { DefaultMessage } from '../../src/core/message.ts'; describe('ChatMessage', () => { - it('should correctly parse createdAt from serial', () => { - const serial = '01672531200000-123@abcdefghij'; - - const message = new DefaultMessage( - serial, - 'clientId', - 'roomId', - 'hello there', - {}, - {}, - new Date(1672531200000), - ChatMessageActions.MessageCreate, - serial, - ); - - expect(message.createdAt).toEqual(new Date(1672531200000)); - }); - it('is the same as another message', () => { const firstSerial = '01672531200000-123@abcdefghij'; const secondSerial = '01672531200000-123@abcdefghij'; @@ -34,9 +16,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, firstSerial, + new Date(1672531200000), + new Date(1672531200000), ); const secondMessage = new DefaultMessage( secondSerial, @@ -45,9 +28,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, secondSerial, + new Date(1672531200000), + new Date(1672531200000), ); expect(firstMessage.equal(secondMessage)).toBe(true); @@ -64,9 +48,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, firstSerial, + new Date(1672531200000), + new Date(1672531200000), ); const secondMessage = new DefaultMessage( secondSerial, @@ -75,9 +60,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, secondSerial, + new Date(1672531200000), + new Date(1672531200000), ); expect(firstMessage.equal(secondMessage)).toBe(false); @@ -94,9 +80,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, firstSerial, + new Date(1672531200000), + new Date(1672531200000), ); const secondMessage = new DefaultMessage( secondSerial, @@ -105,11 +92,13 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, secondSerial, + new Date(1672531200000), + new Date(1672531200000), ); + expect(firstMessage.before(secondMessage)).toBe(true); }); it('is after another message', () => { @@ -123,9 +112,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, firstSerial, + new Date(1672531200000), + new Date(1672531200000), ); const secondMessage = new DefaultMessage( secondSerial, @@ -134,15 +124,16 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageCreate, secondSerial, + new Date(1672531200000), + new Date(1672531200000), ); expect(firstMessage.after(secondMessage)).toBe(true); }); - describe('message actions', () => { + describe('message versions', () => { it('is deleted', () => { const firstSerial = '01672531200000-124@abcdefghij:0'; const firstMessage = new DefaultMessage( @@ -152,11 +143,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageDelete, - '01672531200000-123@abcdefghij:0', + '01672531300000-123@abcdefghij:0', + new Date(1672531200000), new Date(1672531300000), - undefined, { clientId: 'clientId2', }, @@ -174,10 +164,9 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageUpdate, '01672531200000-123@abcdefghij:0', - undefined, + new Date(1672531200000), new Date(1672531300000), { clientId: 'clientId2' }, ); @@ -185,12 +174,12 @@ describe('ChatMessage', () => { expect(firstMessage.updatedBy).toBe('clientId2'); }); - it(`throws an error when trying to compare actions belonging to different origin messages`, () => { + it(`throws an error when trying to compare versions belonging to different origin messages`, () => { const firstSerial = '01672531200000-124@abcdefghij'; const secondSerial = '01672531200000-123@abcdefghij'; - const firstActionSerial = '01672531200000-123@abcdefghij:0'; - const secondActionSerial = '01672531200000-123@abcdefghij:0'; + const firstVersion = '01672531200000-123@abcdefghij:0'; + const secondVersion = '01672531200000-123@abcdefghij:0'; const firstMessage = new DefaultMessage( firstSerial, @@ -199,9 +188,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageUpdate, - firstActionSerial, + firstVersion, + new Date(1672531200000), + new Date(1672531200000) ); const secondMessage = new DefaultMessage( secondSerial, @@ -210,63 +200,64 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageUpdate, - secondActionSerial, + secondVersion, + new Date(1672531200000), + new Date(1672531200000), ); - expect(() => firstMessage.actionEqual(secondMessage)).toThrowErrorInfo({ + expect(() => firstMessage.versionEqual(secondMessage)).toThrowErrorInfo({ code: 50000, - message: 'actionEqual(): Cannot compare actions, message serials must be equal', + message: 'versionEqual(): Cannot compare versions, message serials must be equal', }); }); describe.each([ [ - 'returns true when this message action is the same as another', + 'returns true when this message version is the same as another', { - firstActionSerial: '01672531200000-123@abcdefghij:0', - secondActionSerial: '01672531200000-123@abcdefghij:0', - action: 'actionEqual', + firstVersion: '01672531200000-123@abcdefghij:0', + secondVersion: '01672531200000-123@abcdefghij:0', + action: 'versionEqual', expected: (firstMessage: Message, secondMessage: Message) => { - expect(firstMessage.actionEqual(secondMessage)).toBe(true); + expect(firstMessage.versionEqual(secondMessage)).toBe(true); }, }, ], [ - 'returns false when this message action is not same as another message action', + 'returns false when this message version is not same as another message version', { - firstActionSerial: '01672531200000-123@abcdefghij:0', - secondActionSerial: '01672531200000-124@abcdefghij:0', - action: 'actionEqual', + firstVersion: '01672531200000-123@abcdefghij:0', + secondVersion: '01672531200000-124@abcdefghij:0', + action: 'versionEqual', expected: (firstMessage: Message, secondMessage: Message) => { - expect(firstMessage.actionEqual(secondMessage)).toBe(false); + expect(firstMessage.versionEqual(secondMessage)).toBe(false); }, }, ], [ - 'returns true when this message action is before another message action', + 'returns true when this message version is before another message version', { - firstActionSerial: '01672531200000-123@abcdefghij:0', - secondActionSerial: '01672531200000-124@abcdefghij:0', - action: 'actionBefore', + firstVersion: '01672531200000-123@abcdefghij:0', + secondVersion: '01672531200000-124@abcdefghij:0', + action: 'versionBefore', expected: (firstMessage: Message, secondMessage: Message) => { - expect(firstMessage.actionBefore(secondMessage)).toBe(true); + expect(firstMessage.versionBefore(secondMessage)).toBe(true); }, }, ], [ - 'returns true when this message action is after another message action', + 'returns true when this message version is after another message version', { - firstActionSerial: '01672531200000-124@abcdefghij:0', - secondActionSerial: '01672531200000-123@abcdefghij:0', - action: 'actionAfter', + firstVersion: '01672531200000-124@abcdefghij:0', + secondVersion: '01672531200000-123@abcdefghij:0', + action: 'versionAfter', expected: (firstMessage: Message, secondMessage: Message) => { - expect(firstMessage.actionAfter(secondMessage)).toBe(true); + expect(firstMessage.versionAfter(secondMessage)).toBe(true); }, }, ], - ])('compare message action serials', (name, { firstActionSerial, secondActionSerial, expected }) => { + ])('compare message versions', (name, { firstVersion, secondVersion, expected }) => { it(name, () => { const messageSerial = '01672531200000-123@abcdefghij'; const firstMessage = new DefaultMessage( @@ -276,9 +267,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageUpdate, - firstActionSerial, + firstVersion, + new Date(1672531200000), + new Date(1672531200001) ); const secondMessage = new DefaultMessage( messageSerial, @@ -287,9 +279,10 @@ describe('ChatMessage', () => { 'hello there', {}, {}, - new Date(1672531200000), ChatMessageActions.MessageUpdate, - secondActionSerial, + secondVersion, + new Date(1672531200000), + new Date(1672531200001), ); expected(firstMessage, secondMessage); }); diff --git a/test/core/messages.integration.test.ts b/test/core/messages.integration.test.ts index 4a786ba6..e40039f4 100644 --- a/test/core/messages.integration.test.ts +++ b/test/core/messages.integration.test.ts @@ -116,6 +116,18 @@ describe('messages integration', () => { metadata: { key: 'value' }, }); + console.log("received response from deleteMessage:", deletedMessage1); + + // deleted message should look like a deleted message and convenience getters should work + expect(deletedMessage1.version).not.toEqual(deletedMessage1.serial); + expect(deletedMessage1.version).not.toEqual(message1.version); + expect(deletedMessage1.action).toEqual(ChatMessageActions.MessageDelete); + expect(deletedMessage1.deletedAt).toBeDefined(); + expect(deletedMessage1.deletedAt).toEqual(deletedMessage1.timestamp); + expect(deletedMessage1.deletedBy).toBeDefined(); + expect(deletedMessage1.operation?.clientId).toBeDefined(); + expect(deletedMessage1.deletedBy).toEqual(deletedMessage1.operation?.clientId); + // Wait up to 5 seconds for the promises to resolve await waitForMessages(messages, 1); await waitForMessages(deletions, 1); @@ -134,10 +146,10 @@ describe('messages integration', () => { text: 'Hello there!', clientId: chat.clientId, serial: deletedMessage1.serial, - deletedAt: deletedMessage1.deletedAt, - deletedBy: chat.clientId, - latestAction: ChatMessageActions.MessageDelete, - latestActionSerial: deletedMessage1.latestActionSerial, + timestamp: deletedMessage1.deletedAt, + operation: expect.objectContaining({clientId: chat.clientId}), + action: ChatMessageActions.MessageDelete, + version: deletedMessage1.version, }), ]); }); @@ -200,8 +212,8 @@ describe('messages integration', () => { serial: message1.serial, updatedAt: updated1.updatedAt, updatedBy: chat.clientId, - latestAction: ChatMessageActions.MessageUpdate, - latestActionSerial: updated1.latestActionSerial, + action: ChatMessageActions.MessageUpdate, + version: updated1.version, createdAt: message1.createdAt, }), ]); diff --git a/test/core/messages.test.ts b/test/core/messages.test.ts index e28e7073..fdb70a20 100644 --- a/test/core/messages.test.ts +++ b/test/core/messages.test.ts @@ -89,7 +89,7 @@ describe('Messages', () => { it('should be able to delete a message and get it back from response', async (context) => { const { chatApi } = context; const sendTimestamp = Date.now(); - const sendSerial = 'abcdefghij@' + String(sendTimestamp) + '-123'; + const sendSerial = '01672531200001-123@abcdefghij:0'; vi.spyOn(chatApi, 'sendMessage').mockResolvedValue({ serial: sendSerial, createdAt: sendTimestamp, @@ -97,8 +97,8 @@ describe('Messages', () => { const deleteTimestamp = Date.now(); vi.spyOn(chatApi, 'deleteMessage').mockResolvedValue({ - serial: '01672531200000-123@abcdefghij:0', - deletedAt: deleteTimestamp, + version: '01672531200001-123@abcdefghij:0', + timestamp: deleteTimestamp, }); const message1 = await context.room.messages.send({ text: 'hello there' }); @@ -109,8 +109,8 @@ describe('Messages', () => { serial: sendSerial, text: 'hello there', clientId: 'clientId', - deletedAt: new Date(deleteTimestamp), - deletedBy: 'clientId', + timestamp: new Date(deleteTimestamp), + operation: expect.objectContaining({clientId: 'clientId'}), createdAt: new Date(sendTimestamp), roomId: context.room.roomId, }), @@ -183,6 +183,7 @@ describe('Messages', () => { extras: {}, timestamp: publishTimestamp, createdAt: publishTimestamp, + operation: { clientId: 'yoda' }, }); context.emulateBackendPublish({ clientId: 'yoda', @@ -196,6 +197,7 @@ describe('Messages', () => { extras: {}, timestamp: publishTimestamp, createdAt: publishTimestamp, + operation: { clientId: 'yoda' }, }); context.emulateBackendPublish({ clientId: 'yoda', @@ -203,6 +205,7 @@ describe('Messages', () => { data: { text: 'may the fourth be with you', }, + version: '01672531200000-123@abcdefghij', serial: '01672531200000-123@abcdefghij', action: ChatMessageActions.MessageCreate, extras: {}, diff --git a/test/react/hooks/use-messages.integration.test.tsx b/test/react/hooks/use-messages.integration.test.tsx index e4fe5d37..73f61a94 100644 --- a/test/react/hooks/use-messages.integration.test.tsx +++ b/test/react/hooks/use-messages.integration.test.tsx @@ -199,9 +199,9 @@ describe('useMessages', () => { expect(update?.updatedBy).toBe(chatClientOne.clientId); expect(update?.text).toBe('hello universe'); expect(update?.metadata).toEqual({ icon: 'universe' }); - expect(update?.latestAction).toBe(ChatMessageActions.MessageUpdate); - expect(update?.latestActionDetails?.description).toBe('make it better'); - expect(update?.latestActionDetails?.metadata).toEqual({ something: 'else' }); + expect(update?.action).toBe(ChatMessageActions.MessageUpdate); + expect(update?.operation?.description).toBe('make it better'); + expect(update?.operation?.metadata).toEqual({ something: 'else' }); }, 10000); it('should receive messages on a subscribed listener', async () => { diff --git a/test/react/hooks/use-messages.test.tsx b/test/react/hooks/use-messages.test.tsx index 78b4e2dc..6f6319b4 100644 --- a/test/react/hooks/use-messages.test.tsx +++ b/test/react/hooks/use-messages.test.tsx @@ -116,8 +116,8 @@ describe('useMessages', () => { clientId: '123', roomId: '123', createdAt: new Date(), - latestAction: ChatMessageActions.MessageCreate, - latestActionSerial: '122', + action: ChatMessageActions.MessageCreate, + version: '122', isUpdated: false, isDeleted: false, actionBefore: vi.fn(),