stream-chat
Version:
JS SDK for the Stream Chat API
122 lines (121 loc) • 6.18 kB
TypeScript
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 {};