@ably/chat
Version:
Ably Chat is a set of purpose-built APIs for a host of chat features enabling you to create 1:1, 1:Many, Many:1 and Many:Many chat rooms for any scale. It is designed to meet a wide range of chat use cases, such as livestreams, in-game communication, cust
342 lines (300 loc) • 10.3 kB
text/typescript
import * as Ably from 'ably';
import cloneDeep from 'lodash.clonedeep';
import { ErrorCode } from './errors.js';
import {
ChatMessageAction,
ChatMessageEvent,
ChatMessageEventType,
MessageReactionSummaryEvent,
MessageReactionSummaryEventType,
} from './events.js';
import { Headers } from './headers.js';
import { Metadata } from './metadata.js';
import { OperationMetadata } from './operation-metadata.js';
/**
* {@link Headers} type for chat messages.
*/
export type MessageHeaders = Headers;
/**
* {@link Metadata} type for chat messages.
*/
export type MessageMetadata = Metadata;
/**
* {@link OperationMetadata} type for a chat message. Contains information about an update or deletion operation.
*/
export type MessageOperationMetadata = OperationMetadata;
/**
* Represents the detail of a message deletion or update.
*/
export interface MessageVersion {
/**
* A unique identifier for the latest version of this message.
*/
serial: string;
/**
* The timestamp at which this version was updated, deleted, or created.
*/
timestamp: Date;
/**
* The optional clientId of the user who performed an update or deletion.
*/
clientId?: string;
/**
* The optional description for an update or deletion.
*/
description?: string;
/**
* The optional metadata associated with an update or deletion.
*/
metadata?: MessageOperationMetadata;
}
/**
* Represents a single message in a chat room.
*/
export interface Message {
/**
* The unique identifier of the message.
*/
readonly serial: string;
/**
* The clientId of the user who created the message.
*/
readonly clientId: string;
/**
* The text of the message.
*/
readonly text: string;
/**
* The timestamp at which the message was created.
*/
readonly timestamp: Date;
/**
* The metadata of a chat message. Allows for attaching extra info to a message,
* which can be used for various features such as animations, effects, or simply
* to link it to other resources such as images, relative points in time, etc.
*
* Metadata is part of the Ably Pub/sub message content and is not read by Ably.
*
* This value is always set. If there is no metadata, this is an empty object.
*
* Do not use metadata for authoritative information. There is no server-side
* validation. When reading the metadata treat it like user input.
*/
readonly metadata: MessageMetadata;
/**
* The headers of a chat message. Headers enable attaching extra info to a message,
* which can be used for various features such as linking to a relative point in
* time of a livestream video or flagging this message as important or pinned.
*
* Headers are part of the Ably realtime message extras.headers and they can be used
* for Filtered Subscriptions and similar.
*
* This value is always set. If there are no headers, this is an empty object.
*
* Do not use the headers for authoritative information. There is no server-side
* validation. When reading the headers, treat them like user input.
*/
readonly headers: MessageHeaders;
/**
* The action type of the message. This can be used to determine if the message was created, updated, or deleted.
*/
readonly action: ChatMessageAction;
/**
* Information about the latest version of this message.
*/
readonly version: MessageVersion;
/**
* The reactions summary for this message.
*/
readonly reactions: MessageReactionSummary;
/**
* Creates a new message instance with the event applied.
*
* NOTE: This method will not replace the message reactions if the event is of type `Message`.
* @param event The event to be applied to the returned message.
* @throws {@link ErrorInfo} if the event is for a different message.
* @throws {@link ErrorInfo} if the event is a {@link ChatMessageEventType.Created}.
* @returns A new message instance with the event applied. If the event is a no-op, such
* as an event for an old version, the same message is returned (not a copy).
*/
with(event: Message | ChatMessageEvent | MessageReactionSummaryEvent): Message;
/**
* Creates a copy of the message with fields replaced per the parameters.
* @param params The parameters to replace in the message.
* @returns The message copy.
*/
copy(params?: MessageCopyParams): Message;
}
/**
* Parameters for copying a message.
*/
export interface MessageCopyParams {
/**
* The text of the copied message.
*/
text?: string;
/**
* The metadata of the copied message.
*/
metadata?: MessageMetadata;
/**
* The headers of the copied message.
*/
headers?: MessageHeaders;
}
/**
* Represents a summary of all reactions on a message.
*/
export interface MessageReactionSummary {
/**
* Map of reaction to the summary (total and clients) for reactions of type {@link MessageReactionType.Unique}.
*/
unique: Ably.SummaryUniqueValues;
/**
* Map of reaction to the summary (total and clients) for reactions of type {@link MessageReactionType.Distinct}.
*/
distinct: Ably.SummaryDistinctValues;
/**
* Map of reaction to the summary (total and clients) for reactions of type {@link MessageReactionType.Multiple}.
*/
multiple: Ably.SummaryMultipleValues;
}
/**
* Parameters for creating a new DefaultMessage instance.
*/
export interface DefaultMessageParams {
serial: string;
clientId: string;
text: string;
metadata: MessageMetadata;
headers: MessageHeaders;
action: ChatMessageAction;
version: MessageVersion;
timestamp: Date;
reactions: MessageReactionSummary;
}
/**
* An implementation of the Message interface for chat messages.
*
* Allows for comparison of messages based on their serials.
*/
export class DefaultMessage implements Message {
public readonly serial: string;
public readonly clientId: string;
public readonly text: string;
public readonly metadata: MessageMetadata;
public readonly headers: MessageHeaders;
public readonly action: ChatMessageAction;
public readonly version: MessageVersion;
public readonly timestamp: Date;
public readonly reactions: MessageReactionSummary;
constructor({
serial,
clientId,
text,
metadata,
headers,
action,
version,
timestamp,
reactions,
}: DefaultMessageParams) {
this.serial = serial;
this.clientId = clientId;
this.text = text;
this.metadata = metadata;
this.headers = headers;
this.action = action;
this.version = version;
this.timestamp = timestamp;
this.reactions = reactions;
// The object is frozen after constructing to enforce readonly at runtime too
Object.freeze(this.version);
Object.freeze(this.reactions);
Object.freeze(this.reactions.multiple);
Object.freeze(this.reactions.distinct);
Object.freeze(this.reactions.unique);
Object.freeze(this);
}
with(event: Message | ChatMessageEvent | MessageReactionSummaryEvent): Message {
// If event has the property "serial", then it's a message
if ('serial' in event) {
return this._getLatestMessageVersion(event);
}
// If the event is a created event, throw an error
if (event.type === ChatMessageEventType.Created) {
throw new Ably.ErrorInfo(
'unable to apply message event; unable to apply created event to existing message',
ErrorCode.InvalidArgument,
400,
);
}
// reaction summary
if (event.type === MessageReactionSummaryEventType.Summary) {
if (event.messageSerial !== this.serial) {
throw new Ably.ErrorInfo(
'unable to apply message event; event is for a different message',
ErrorCode.InvalidArgument,
400,
);
}
const newReactions: MessageReactionSummary = {
unique: cloneDeep(event.reactions.unique),
distinct: cloneDeep(event.reactions.distinct),
multiple: cloneDeep(event.reactions.multiple),
};
return DefaultMessage._clone(this, { reactions: newReactions });
}
// Message event (update or delete)
return this._getLatestMessageVersion(event.message);
}
/**
* Get the latest message version, based on the event.
* If "this" is the latest version, return "this", otherwise clone the message and apply the reactions.
* @param message The message to get the latest version of
* @returns The latest message version
*/
private _getLatestMessageVersion(message: Message): Message {
// message event (update or delete)
if (message.serial !== this.serial) {
throw new Ably.ErrorInfo(
'unable to apply message event; event is for a different message',
ErrorCode.InvalidArgument,
400,
);
}
// event is older, keep this instead
if (this.version.serial >= message.version.serial) {
return this;
}
// event is newer, copy reactions from this and make new message from event
// TODO: This ignores summaries being newer on the message passed in, and is something we need to address
return DefaultMessage._clone(message, { reactions: this.reactions });
}
// Clone a message, optionally replace the given fields
private static _clone(source: Message, replace?: Partial<Message>): DefaultMessage {
return new DefaultMessage({
serial: replace?.serial ?? source.serial,
clientId: replace?.clientId ?? source.clientId,
text: replace?.text ?? source.text,
metadata: replace?.metadata ?? cloneDeep(source.metadata),
headers: replace?.headers ?? cloneDeep(source.headers),
action: replace?.action ?? source.action,
version: replace?.version ?? cloneDeep(source.version),
timestamp: replace?.timestamp ?? source.timestamp,
reactions: replace?.reactions ?? cloneDeep(source.reactions),
});
}
copy(params: MessageCopyParams = {}): Message {
return DefaultMessage._clone(this, params);
}
}
/**
* Creates an empty MessageReactionSummary object with empty unique and distinct reaction collections.
* @returns An empty MessageReactionSummary object.
*/
export const emptyMessageReactions = (): MessageReactionSummary => ({
unique: {},
distinct: {},
multiple: {},
});