@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
374 lines (324 loc) • 11 kB
text/typescript
import * as Ably from 'ably';
import cloneDeep from 'lodash.clonedeep';
import { ChannelManager } from './channel-manager.js';
import { ChatApi } from './chat-api.js';
import { DiscontinuityListener } from './discontinuity.js';
import { Logger } from './logger.js';
import { DefaultMessages, Messages } from './messages.js';
import { DefaultMessageReactions } from './messages-reactions.js';
import { DefaultOccupancy, Occupancy } from './occupancy.js';
import { DefaultPresence, Presence } from './presence.js';
import { RoomLifecycleManager } from './room-lifecycle-manager.js';
import { InternalRoomOptions, RoomOptions, validateRoomOptions } from './room-options.js';
import { DefaultRoomReactions, RoomReactions } from './room-reactions.js';
import { DefaultRoomLifecycle, InternalRoomLifecycle, RoomStatus, RoomStatusListener } from './room-status.js';
import { StatusSubscription } from './subscription.js';
import { DefaultTyping, Typing } from './typing.js';
/**
* Represents a chat room.
*/
export interface Room {
/**
* The unique identifier of the room.
* @returns The room name.
*/
get name(): string;
/**
* Allows you to send, subscribe-to and query messages in the room.
* @returns The messages instance for the room.
*/
get messages(): Messages;
/**
* Allows you to subscribe to presence events in the room.
* @returns The presence instance for the room.
*/
get presence(): Presence;
/**
* Allows you to interact with room-level reactions.
* @returns The room reactions instance for the room.
*/
get reactions(): RoomReactions;
/**
* Allows you to interact with typing events in the room.
* @returns The typing instance for the room.
*/
get typing(): Typing;
/**
* Allows you to interact with occupancy metrics for the room.
* @returns The occupancy instance for the room.
*/
get occupancy(): Occupancy;
/**
* The current status of the room.
* @returns The current status.
*/
get status(): RoomStatus;
/**
* The current error, if any, that caused the room to enter the current status.
*/
get error(): Ably.ErrorInfo | undefined;
/**
* Registers a listener that will be called whenever the room status changes.
* @param listener The function to call when the status changes.
* @returns An object that can be used to unregister the listener.
*/
onStatusChange(listener: RoomStatusListener): StatusSubscription;
/**
* Attaches to the room to receive events in realtime.
*
* If a room fails to attach, it will enter either the {@link RoomStatus.Suspended} or {@link RoomStatus.Failed} state.
*
* If the room enters the failed state, then it will not automatically retry attaching and intervention is required.
*
* If the room enters the suspended state, then the call to attach will reject with the {@link ErrorInfo} that caused the suspension. However,
* the room will automatically retry attaching after a delay.
* @returns A promise that resolves when the room is attached.
*/
attach(): Promise<void>;
/**
* Detaches from the room to stop receiving events in realtime.
* @returns A promise that resolves when the room is detached.
*/
detach(): Promise<void>;
/**
* Returns the room options.
* @returns A copy of the options used to create the room.
*/
options(): RoomOptions;
/**
* Registers a handler that will be called whenever a discontinuity is detected in the room's connection.
* A discontinuity occurs when the room's connection is interrupted and cannot be resumed from its previous state.
* @param handler The function to call when a discontinuity is detected.
* @returns An object that can be used to unregister the handler.
*/
onDiscontinuity(handler: DiscontinuityListener): StatusSubscription;
/**
* Get the underlying Ably realtime channel used for the room.
* @returns The realtime channel.
*/
get channel(): Ably.RealtimeChannel;
}
export class DefaultRoom implements Room {
private readonly _name: string;
private readonly _options: RoomOptions;
private readonly _chatApi: ChatApi;
private readonly _messages: DefaultMessages;
private readonly _typing: DefaultTyping;
private readonly _presence: DefaultPresence;
private readonly _reactions: DefaultRoomReactions;
private readonly _occupancy: DefaultOccupancy;
private readonly _logger: Logger;
private readonly _lifecycle: DefaultRoomLifecycle;
private readonly _lifecycleManager: RoomLifecycleManager;
private readonly _finalizer: () => Promise<void>;
private readonly _channelManager: ChannelManager;
/**
* A random identifier for the room instance, useful in debugging and logging.
*/
private readonly _nonce: string;
/**
* Constructs a new Room instance.
* @param name The unique identifier of the room.
* @param nonce A random identifier for the room instance, useful in debugging and logging.
* @param options The options for the room.
* @param realtime An instance of the Ably Realtime client.
* @param chatApi An instance of the ChatApi.
* @param logger An instance of the Logger.
*/
constructor(
name: string,
nonce: string,
options: InternalRoomOptions,
realtime: Ably.Realtime,
chatApi: ChatApi,
logger: Logger,
) {
validateRoomOptions(options);
this._nonce = nonce;
// Create a logger with room context
this._logger = logger.withContext({ roomName: name, roomNonce: nonce });
this._logger.debug('Room();', { options });
this._name = name;
this._options = options;
this._chatApi = chatApi;
this._lifecycle = new DefaultRoomLifecycle(this._logger);
const channelManager = (this._channelManager = this._getChannelManager(options, realtime, this._logger));
const channel = channelManager.get();
// Setup features
this._messages = new DefaultMessages(
name,
options.messages,
channel,
this._chatApi,
realtime.auth.clientId,
this._logger,
);
this._presence = new DefaultPresence(channel, realtime.auth.clientId, this._logger, options);
this._typing = new DefaultTyping(
options.typing,
realtime.connection,
channel,
realtime.auth.clientId,
this._logger,
);
this._reactions = new DefaultRoomReactions(channel, realtime.connection, realtime.auth.clientId, this._logger);
this._occupancy = new DefaultOccupancy(name, channel, this._chatApi, this._logger, options);
// Set the lifecycle manager last, so it becomes the last thing to find out about channel state changes
// This is to allow Messages to reset subscription points before users get told of a discontinuity
this._lifecycleManager = new RoomLifecycleManager(channelManager, this._lifecycle, this._logger);
// Setup a finalization function to clean up resources
let finalized = false;
this._finalizer = async () => {
// Cycle the channels in the feature and release them from the realtime client
if (finalized) {
this._logger.debug('Room.finalizer(); already finalized');
return;
}
// Release via the lifecycle manager
await this._lifecycleManager.release();
// Dispose of the lifecycle manager, removing all user-registered listeners from emitters
// and any listeners that have been registered to the realtime instance
this._lifecycleManager.dispose();
// Dispose of all features, removing any listeners that have been subscribed to the realtime instance
// and also removing any user-level listeners from the emitters
this._messages.dispose();
this._presence.dispose();
this._reactions.dispose();
this._occupancy.dispose();
await this._typing.dispose();
// Dispose of the RoomStatus instance
this._lifecycle.dispose();
finalized = true;
};
}
/**
* Gets the channel manager for the room, which handles merging channel options together and creating channels.
* @param options The room options.
* @param realtime An instance of the Ably Realtime client.
* @param logger An instance of the Logger.
* @returns The channel manager instance.
*/
private _getChannelManager(options: InternalRoomOptions, realtime: Ably.Realtime, logger: Logger): ChannelManager {
const manager = new ChannelManager(this._name, realtime, logger, options.isReactClient);
manager.mergeOptions(DefaultOccupancy.channelOptionMerger(options));
manager.mergeOptions(DefaultPresence.channelOptionMerger(options));
manager.mergeOptions(DefaultMessageReactions.channelOptionMerger(options));
return manager;
}
/**
* @inheritdoc
*/
get name(): string {
return this._name;
}
/**
* @inheritDoc
*/
options(): RoomOptions {
return cloneDeep(this._options);
}
/**
* @inheritdoc
*/
get messages(): Messages {
return this._messages;
}
/**
* @inheritdoc
*/
get presence(): Presence {
return this._presence;
}
/**
* @inheritdoc
*/
get reactions(): RoomReactions {
return this._reactions;
}
/**
* @inheritdoc
*/
get typing(): Typing {
return this._typing;
}
/**
* @inheritdoc
*/
get occupancy(): Occupancy {
return this._occupancy;
}
/**
* @inheritdoc
*/
get status(): RoomStatus {
return this._lifecycle.status;
}
/**
* @inheritdoc
*/
get error(): Ably.ErrorInfo | undefined {
return this._lifecycle.error;
}
/**
* @inheritdoc
*/
onStatusChange(listener: RoomStatusListener): StatusSubscription {
return this._lifecycle.onChange(listener);
}
/**
* @inheritdoc
*/
async attach() {
this._logger.trace('Room.attach();');
return this._lifecycleManager.attach();
}
/**
* @inheritdoc
*/
async detach(): Promise<void> {
this._logger.trace('Room.detach();');
return this._lifecycleManager.detach();
}
/**
* Releases resources associated with the room.
* @returns A promise that resolves when the room is released.
*/
release(): Promise<void> {
this._logger.trace('Room.release();');
return this._finalizer();
}
/**
* A random identifier for the room instance, useful in debugging and logging.
* @returns The nonce.
*/
get nonce(): string {
return this._nonce;
}
/**
* @internal
* @returns The internal room lifecycle.
*/
get lifecycle(): InternalRoomLifecycle {
return this._lifecycle;
}
/**
* @internal
* @returns The room lifecycle manager.
*/
get lifecycleManager(): RoomLifecycleManager {
return this._lifecycleManager;
}
/**
* @inheritdoc
*/
onDiscontinuity(handler: DiscontinuityListener): StatusSubscription {
this._logger.trace('Room.onDiscontinuity();');
return this._lifecycleManager.onDiscontinuity(handler);
}
/**
* @inheritdoc
*/
get channel(): Ably.RealtimeChannel {
return this._channelManager.get();
}
}