From deedceabffcc7d905aeda6553c32b1ba316b5ecd Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Thu, 17 Oct 2024 11:58:23 +0100 Subject: [PATCH] materialisation: Add new message attributes and actions handling - Added new message attributes, including `action`, `serial`, `refSerial`, `refType`, `updatedAt`, `deletedAt`, and `operation`. Additionally, create functions to map message actions between string and number representations. This update also changes the `fromValues` function to handle action transformations. --- ably.d.ts | 89 +++++++++++++++++++++++++- src/common/lib/types/defaultmessage.ts | 11 ++-- src/common/lib/types/message.ts | 60 +++++++++++++++-- 3 files changed, 150 insertions(+), 10 deletions(-) diff --git a/ably.d.ts b/ably.d.ts index b8e85c6a4..cb9901731 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -2335,12 +2335,99 @@ export interface Message { * Timestamp of when the message was received by Ably, as milliseconds since the Unix epoch. */ timestamp?: number; + /** + * The action type of the message, one of the {@link MessageAction} enum values. + */ + action?: MessageAction; + /** + * This message's unique serial. + */ + serial?: string; + /** + * The serial of the message that this message is a reference to. + */ + refSerial?: string; + /** + * The type of reference this message is, in relation to the message it references. + */ + refType?: string; + /** + * If an `update` operation was applied to this message, this will be the timestamp the update occurred. + */ + updatedAt?: number; + /** + * If a `deletion` operation was applied to this message, this will be the timestamp the deletion occurred. + */ + deletedAt?: number; + /** + * If this message resulted from an operation, this will contain the operation details. + */ + operation?: Operation; } +/** + * Contains the details of an operation, such as update of deletion, supplied by the actioning client. + */ +export interface Operation { + /** + * The client ID of the client that initiated the operation. + */ + clientId?: string; + /** + * The description provided by the client that initiated the operation. + */ + description?: string; + /** + * A JSON object of string key-value pairs that may contain metadata associated with the operation. + */ + metadata?: Record; +} + +/** + * The namespace containing the different types of message actions. + */ +declare namespace MessageActions { + /** + * Message action has not been set. + */ + type MESSAGE_UNSET = 'message_unset'; + /** + * Message action for a newly created message. + */ + type MESSAGE_CREATE = 'message_create'; + /** + * Message action for an updated message. + */ + type MESSAGE_UPDATE = 'message_update'; + /** + * Message action for a deleted message. + */ + type MESSAGE_DELETE = 'message_delete'; + /** + * Message action for a newly created annotation. + */ + type MESSAGE_ANNOTATION_CREATE = 'message_annotation_create'; + /** + * Message action for a deleted annotation. + */ + type MESSAGE_ANNOTATION_DELETE = 'message_annotation_delete'; +} + +/** + * Describes the possible action types used on an {@link Message}. + */ +export type MessageAction = + | MessageActions.MESSAGE_UNSET + | MessageActions.MESSAGE_CREATE + | MessageActions.MESSAGE_UPDATE + | MessageActions.MESSAGE_DELETE + | MessageActions.MESSAGE_ANNOTATION_CREATE + | MessageActions.MESSAGE_ANNOTATION_DELETE; + /** * A message received from Ably. */ -export type InboundMessage = Message & Required>; +export type InboundMessage = Message & Required>; /** * Static utilities related to messages. diff --git a/src/common/lib/types/defaultmessage.ts b/src/common/lib/types/defaultmessage.ts index dfc4a02b1..33246b30e 100644 --- a/src/common/lib/types/defaultmessage.ts +++ b/src/common/lib/types/defaultmessage.ts @@ -1,10 +1,11 @@ import Message, { CipherOptions, - fromEncoded, - fromEncodedArray, - encode, decode, + encode, EncodingDecodingContext, + fromEncoded, + fromEncodedArray, + fromValues, } from './message'; import * as API from '../../../../ably'; import Platform from 'common/platform'; @@ -25,8 +26,8 @@ export class DefaultMessage extends Message { } // Used by tests - static fromValues(values: unknown): Message { - return Object.assign(new Message(), values); + static fromValues(values: Message | Record, stringifyAction?: boolean): Message { + return fromValues(values, stringifyAction); } // Used by tests diff --git a/src/common/lib/types/message.ts b/src/common/lib/types/message.ts index 7cc8b80ac..923d9b0a6 100644 --- a/src/common/lib/types/message.ts +++ b/src/common/lib/types/message.ts @@ -9,6 +9,32 @@ import * as API from '../../../../ably'; import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; import { MsgPack } from 'common/types/msgpack'; +const MessageActionArray: API.MessageAction[] = [ + 'message_unset', + 'message_create', + 'message_update', + 'message_delete', + 'message_annotation_create', + 'message_annotation_delete', +]; + +function toMessageActionString(actionNumber: number): API.MessageAction { + if (actionNumber in MessageActionArray) { + return MessageActionArray[actionNumber]; + } else { + throw new ErrorInfo(`Unsupported action number: ${actionNumber}`, 40000, 400); + } +} + +function toMessageActionNumber(messageAction: API.MessageAction): number { + for (const [index, value] of MessageActionArray.entries()) { + if (value === messageAction) { + return index; + } + } + throw new ErrorInfo(`Unsupported action string: ${messageAction}`, 40000, 400); +} + export type CipherOptions = { channelCipher: { encrypt: Function; @@ -82,7 +108,7 @@ export async function fromEncoded( encoded: unknown, inputOptions?: API.ChannelOptions, ): Promise { - const msg = fromValues(encoded); + const msg = fromValues(encoded as Message | Record, true); const options = normalizeCipherOptions(Crypto, logger, inputOptions ?? null); /* if decoding fails at any point, catch and return the message decoded to * the fullest extent possible */ @@ -260,7 +286,7 @@ export async function fromResponseBody( } for (let i = 0; i < body.length; i++) { - const msg = (body[i] = fromValues(body[i])); + const msg = (body[i] = fromValues(body[i], true)); try { await decode(msg, options); } catch (e) { @@ -270,14 +296,17 @@ export async function fromResponseBody( return body; } -export function fromValues(values: unknown): Message { +export function fromValues(values: Message | Record, stringifyAction?: boolean): Message { + if (stringifyAction) { + values.action = toMessageActionString(values.action as number); + } return Object.assign(new Message(), values); } export function fromValuesArray(values: unknown[]): Message[] { const count = values.length, result = new Array(count); - for (let i = 0; i < count; i++) result[i] = fromValues(values[i]); + for (let i = 0; i < count; i++) result[i] = fromValues(values[i] as Record); return result; } @@ -304,6 +333,13 @@ class Message { encoding?: string | null; extras?: any; size?: number; + action?: API.MessageAction | number; + serial?: string; + refSerial?: string; + refType?: string; + updatedAt?: number; + deletedAt?: number; + operation?: API.Operation; /** * Overload toJSON() to intercept JSON.stringify() @@ -334,6 +370,14 @@ class Message { connectionId: this.connectionId, connectionKey: this.connectionKey, extras: this.extras, + serial: this.serial, + // If `action` has not been set, it will be set once received by realtime + action: this.action ? toMessageActionNumber(this.action as API.MessageAction) : 0, + refSerial: this.refSerial, + refType: this.refType, + updatedAt: this.updatedAt, + deletedAt: this.deletedAt, + operation: this.operation, encoding, data, }; @@ -355,6 +399,14 @@ class Message { else result += '; data (json)=' + JSON.stringify(this.data); } if (this.extras) result += '; extras=' + JSON.stringify(this.extras); + + if (this.action) result += '; action=' + this.action; + if (this.serial) result += '; serial=' + this.serial; + if (this.refSerial) result += '; refSerial=' + this.refSerial; + if (this.refType) result += '; refType=' + this.refType; + if (this.updatedAt) result += '; updatedAt=' + this.updatedAt; + if (this.deletedAt) result += '; deletedAt=' + this.deletedAt; + if (this.operation) result += '; operation=' + JSON.stringify(this.operation); result += ']'; return result; }