UNPKG

stream-chat

Version:

JS SDK for the Stream Chat API

122 lines (121 loc) 6.18 kB
import type { ReadResponse, UserResponse } from '../types'; type MessageId = string; export type MsgRef = { timestampMs: number; msgId: MessageId; }; export type OwnMessageReceiptsTrackerMessageLocator = (timestampMs: number) => MsgRef | null; export type UserProgress = { user: UserResponse; lastReadRef: MsgRef; lastDeliveredRef: MsgRef; }; export type OwnMessageReceiptsTrackerOptions = { locateMessage: OwnMessageReceiptsTrackerMessageLocator; }; /** * MessageReceiptsTracker * -------------------------------- * Tracks **other participants’** delivery/read progress toward **own (outgoing) messages** * within a **single timeline** (one channel/thread). * * How it works * ------------ * - Each user has a compact progress record: * - `lastReadRef`: latest message they have **read** * - `lastDeliveredRef`: latest message they have **received** (always `>= lastReadRef`) * - Internally keeps two arrays sorted **ascending by timestamp**: * - `readSorted` (by `lastReadRef`) * - `deliveredSorted` (by `lastDeliveredRef`) * - Queries like “who read message M?” become a **binary search + suffix slice**. * * Construction * ------------ * `new MessageReceiptsTracker({locateMessage})` * - `locateMessage(timestamp) => MsgRef | null` must resolve a message ref representation - `{ timestamp, msgId }`. * - If `locateMessage` returns `null`, the event is ignored (message unknown locally). * * Event ingestion * --------------- * - `ingestInitial(rows: ReadResponse[])`: Builds initial state from server snapshot. * If a user’s `last_read` is ahead of `last_delivered_at`, the tracker enforces * the invariant `lastDeliveredRef >= lastReadRef`. * - `onMessageRead(user, readAtISO)`: * Advances the user’s read; also bumps delivered to match if needed. * - `onMessageDelivered(user, deliveredAtISO)`: * Advances the user’s delivered to `max(currentRead, deliveredAt)`. * * Queries * ------- * - `readersForMessage(msgRef) : UserResponse[]` → users with `lastReadRef >= msgRef` * - `deliveredForMessage(msgRef) : UserResponse[]` → users with `lastDeliveredRef >= msgRef` * - `deliveredNotReadForMessage(msgRef): UserResponse[]` → delivered but `lastReadRef < msgRef` * - `usersWhoseLastReadIs : UserResponse[]` → users for whom `msgRef` is their *last read* (exact match) * - `usersWhoseLastDeliveredIs : UserResponse[]` → users for whom `msgRef` is their *last delivered* (exact match) * - `groupUsersByLastReadMessage : Record<MsgId, UserResponse[]> → mapping of messages to their readers * - `groupUsersByLastDeliveredMessage : Record<MsgId, UserResponse[]> → mapping of messages to their receivers * - `hasUserRead(msgRef, userId) : boolean` * - `hasUserDelivered(msgRef, userId) : boolean` * * Complexity * ---------- * - Update on read/delivered: **O(log U)** (binary search + one splice) per event, where U is count of users stored by tracker. * - Query lists: **O(log U + K)** where `K` is the number of returned users (suffix length). * - Memory: **O(U)** - tracker’s memory grows linearly with the number of users in the channel/thread and does not depend on the number of messages. * * Scope & notes * ------------- * - One tracker instance is **scoped to a single timeline**. Instantiate per channel/thread. * - Ordering is by **ascending timestamp**; ties are kept stable by inserting at the end of the * equal-timestamp plateau (upper-bound insertion), preserving intuitive arrival order. * - This tracker models **others’ progress toward own messages**; */ export declare class MessageReceiptsTracker { private byUser; private readSorted; private deliveredSorted; private locateMessage; constructor({ locateMessage }: OwnMessageReceiptsTrackerOptions); /** Build initial state from server snapshots (single pass + sort). */ ingestInitial(responses: ReadResponse[]): void; /** message.delivered — user device confirmed delivery up to and including messageId. */ onMessageDelivered({ user, deliveredAt, lastDeliveredMessageId, }: { user: UserResponse; deliveredAt: string; lastDeliveredMessageId?: string; }): void; /** message.read — user read up to and including messageId. */ onMessageRead({ user, readAt, lastReadMessageId, }: { user: UserResponse; readAt: string; lastReadMessageId?: string; }): void; /** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`. * Sets lastReadRef to the event’s last_read_* values. Delivery never moves backward. * The event is sent only to the user that triggered the action (own user), so we will never adjust read ref * for other users - we will not see changes in the UI for other users. However, this implementation does not * take into consideration this fact and is ready to handle the mark-unread event for any user. */ onNotificationMarkUnread({ user, lastReadAt, lastReadMessageId, }: { user: UserResponse; lastReadAt?: string; lastReadMessageId?: string; }): void; /** All users who READ this message. */ readersForMessage(msgRef: MsgRef): UserResponse[]; /** All users who have it DELIVERED (includes readers). */ deliveredForMessage(msgRef: MsgRef): UserResponse[]; /** Users who delivered but have NOT read. */ deliveredNotReadForMessage(msgRef: MsgRef): UserResponse[]; /** Users for whom `msgRef` is their *last read* (exact match). */ usersWhoseLastReadIs(msgRef: MsgRef): UserResponse[]; /** Users for whom `msgRef` is their *last delivered* (exact match). */ usersWhoseLastDeliveredIs(msgRef: MsgRef): UserResponse[]; hasUserRead(msgRef: MsgRef, userId: string): boolean; hasUserDelivered(msgRef: MsgRef, userId: string): boolean; getUserProgress(userId: string): UserProgress | null; groupUsersByLastReadMessage(): Record<MessageId, UserResponse[]>; groupUsersByLastDeliveredMessage(): Record<MessageId, UserResponse[]>; private ensureUser; } export {};