export function mergeChatMessages< T extends { id: string; sequence: string; revision?: number }, >(current: T[], incoming: T[]): T[] { const byId = new Map(current.map((message) => [message.id, message])); for (const message of incoming) { const previous = byId.get(message.id); if (!previous || (message.revision ?? 1) >= (previous.revision ?? 1)) byId.set(message.id, message); } return [...byId.values()].sort((a, b) => { const left = BigInt(a.sequence); const right = BigInt(b.sequence); return left < right ? -1 : left > right ? 1 : 0; }); } // Each async continuation retains the generation it started in. A return to the // same channel is still a new generation, so old completions cannot re-enter it. export function createChatScope(workspace: string, channelId?: string) { let key = JSON.stringify([workspace, channelId ?? ""]); let generation = 0; let revoked = false; return { select(nextWorkspace: string, nextChannelId?: string) { const next = JSON.stringify([nextWorkspace, nextChannelId ?? ""]); if (next === key) return; key = next; generation++; revoked = false; }, revoke() { if (!revoked) { revoked = true; generation++; } }, dispose() { revoked = true; generation++; }, capture() { const captured = generation; return () => !revoked && captured === generation; }, captureCleanup() { const captured = generation; return () => revoked && captured === generation; }, isRevoked() { return revoked; }, }; } export type CatchUpWindow = { after: string; through?: string }; // Keep the first response's high-water fixed until every page of that interval is read. export function advanceCatchUpWindow( window: CatchUpWindow, page: { next: string; through: string; hasMore: boolean }, ): CatchUpWindow { if (page.hasMore) { if (BigInt(page.next) <= BigInt(window.after)) return window; return { after: page.next, through: window.through ?? page.through }; } return { after: page.through }; } export function rootReadIsProven( sequence: string, scanned: string | null, unseenReply: string | null, readBoundary: string, ): boolean { return ( scanned !== null && BigInt(scanned) >= BigInt(sequence) && (unseenReply === null || BigInt(unseenReply) > BigInt(sequence) || BigInt(unseenReply) <= BigInt(readBoundary)) ); } export function chatBadge(summary: { unreadCount: number; mentionCount: number; directUnreadCount: number; }) { return { dot: summary.unreadCount > 0, count: summary.mentionCount + summary.directUnreadCount, }; } export function messageError(code?: string): string { if (code === "NOT_FOUND" || code === "FORBIDDEN") return "You no longer have access to this channel."; if (code === "CONFLICT") return "This request was already used for a different message. Please compose a new message."; if (code === "BAD_REQUEST") return "This message could not be sent. Check the text and retry."; return "Could not send message. Please retry."; }