UNPKG

@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

218 lines (189 loc) 7.04 kB
import * as Ably from 'ably'; import { ChannelOptionsMerger } from './channel-manager.js'; import { ChatApi } from './chat-api.js'; import { OccupancyEvent, OccupancyEventType, RealtimeMetaEventType } from './events.js'; import { Logger } from './logger.js'; import { OccupancyData, parseOccupancyMessage } from './occupancy-parser.js'; import { subscribe } from './realtime-subscriptions.js'; import { InternalRoomOptions } from './room-options.js'; import { Subscription } from './subscription.js'; import EventEmitter, { emitterHasListeners, wrap } from './utils/event-emitter.js'; /** * This interface is used to interact with occupancy in a chat room: subscribing to occupancy updates and * fetching the current room occupancy metrics. * * Get an instance via {@link Room.occupancy}. */ export interface Occupancy { /** * Subscribe a given listener to occupancy updates of the chat room. * * Note: This requires occupancy events to be enabled via the `enableEvents` option in * the {@link OccupancyOptions} options provided to the room. If this is not enabled, an error will be thrown. * @param listener A listener to be called when the occupancy of the room changes. * @returns A subscription object that can be used to unsubscribe the listener. * @throws {Ably.ErrorInfo} If occupancy events are not enabled for this room. */ subscribe(listener: OccupancyListener): Subscription; /** * Get the current occupancy of the chat room. * @returns A promise that resolves to the current occupancy of the chat room. */ get(): Promise<OccupancyData>; /** * Get the latest occupancy data received from realtime events. * @returns The latest occupancy data, or undefined if no realtime events have been received yet. * @throws {Ably.ErrorInfo} If occupancy events are not enabled for this room. */ current(): OccupancyData | undefined; } /** * A listener that is called when the occupancy of a chat room changes. * @param event The occupancy event. */ export type OccupancyListener = (event: OccupancyEvent) => void; interface OccupancyEventsMap { [OccupancyEventType.Updated]: OccupancyEvent; } /** * @inheritDoc */ export class DefaultOccupancy implements Occupancy { private readonly _roomName: string; private readonly _channel: Ably.RealtimeChannel; private readonly _chatApi: ChatApi; private readonly _logger: Logger; private readonly _emitter = new EventEmitter<OccupancyEventsMap>(); private readonly _roomOptions: InternalRoomOptions; private _latestOccupancyData?: OccupancyData; private readonly _unsubscribeOccupancyEvents: () => void; /** * Constructs a new `DefaultOccupancy` instance. * @param roomName The unique identifier of the room. * @param channel An instance of the Realtime channel. * @param chatApi An instance of the ChatApi. * @param logger An instance of the Logger. * @param roomOptions The room options. */ constructor( roomName: string, channel: Ably.RealtimeChannel, chatApi: ChatApi, logger: Logger, roomOptions: InternalRoomOptions, ) { this._roomName = roomName; this._channel = channel; this._chatApi = chatApi; this._logger = logger; this._roomOptions = roomOptions; // Create bound listener const occupancyEventsListener = this._internalOccupancyListener.bind(this); // Use subscription helper to create cleanup function if (this._roomOptions.occupancy.enableEvents) { this._logger.debug('DefaultOccupancy(); subscribing to occupancy events'); this._unsubscribeOccupancyEvents = subscribe( this._channel, [RealtimeMetaEventType.Occupancy], occupancyEventsListener, ); } else { this._unsubscribeOccupancyEvents = () => { // No-op function when events are not enabled }; } } /** * @inheritdoc */ subscribe(listener: OccupancyListener): Subscription { this._logger.trace('Occupancy.subscribe();'); if (!this._roomOptions.occupancy.enableEvents) { throw new Ably.ErrorInfo( 'cannot subscribe to occupancy; occupancy events are not enabled in room options', 40000, 400, ) as unknown as Error; } const wrapped = wrap(listener); this._emitter.on(wrapped); return { unsubscribe: () => { this._logger.trace('Occupancy.unsubscribe();'); this._emitter.off(wrapped); }, }; } /** * @inheritdoc */ async get(): Promise<OccupancyData> { this._logger.trace('Occupancy.get();'); return this._chatApi.getOccupancy(this._roomName); } /** * @inheritdoc */ current(): OccupancyData | undefined { this._logger.trace('Occupancy.current();'); // CHA-O7c if (!this._roomOptions.occupancy.enableEvents) { throw new Ably.ErrorInfo( 'cannot get current occupancy; occupancy events are not enabled in room options', 40000, 400, ) as unknown as Error; } // CHA-07a // CHA-07b return this._latestOccupancyData; } /** * An internal listener that listens for occupancy events from the underlying channel and translates them into * occupancy events for the public API. * @param message The inbound message containing occupancy data. */ private _internalOccupancyListener(message: Ably.InboundMessage): void { this._logger.trace('Occupancy._internalOccupancyListener();', message); this._latestOccupancyData = parseOccupancyMessage(message); this._emitter.emit(OccupancyEventType.Updated, { type: OccupancyEventType.Updated, occupancy: this._latestOccupancyData, }); } /** * Merges the channel options for the room with the ones required for occupancy. * @param roomOptions The internal room options. * @returns A function that merges the channel options for the room with the ones required for occupancy. */ static channelOptionMerger(roomOptions: InternalRoomOptions): ChannelOptionsMerger { return (options) => { // Occupancy not required, so we can skip this. if (!roomOptions.occupancy.enableEvents) { return options; } return { ...options, params: { ...options.params, occupancy: 'metrics' } }; }; } /** * Disposes of the occupancy instance, removing all listeners and subscriptions. * This method should be called when the room is being released to ensure proper cleanup. * @internal */ dispose(): void { this._logger.trace('DefaultOccupancy.dispose();'); // Remove occupancy event subscriptions using stored unsubscribe function this._unsubscribeOccupancyEvents(); // Remove user-level listeners this._emitter.off(); this._logger.debug('DefaultOccupancy.dispose(); disposed successfully'); } /** * Checks if there are any listeners registered by users. * @internal * @returns true if there are listeners, false otherwise. */ hasListeners(): boolean { return emitterHasListeners(this._emitter); } }