Live updates
ChatNotificationsClient opens the realtime stream so your app shows new messages without polling. This guide covers connecting, the events, the connection state and what to do after a reconnect.
Append new messages to the open thread, update the inbox, and recover cleanly when the connection drops.
Prerequisites
Section titled “Prerequisites”- A
getAccessTokenthat returns the user’s Chat token. See Get the user’s Chat token. - A browser with
EventSource. Realtime works for signed-in users only; a machine token is refused.
1. Connect
Section titled “1. Connect”Create one client per tenant application and call connect(). It reads the token through getAccessToken on every (re)connect.
import { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';import { context } from './fe-guide-context';
declare function appendToThread(conversationId: string, message: unknown): void;declare function dropConversation(conversationId: string): void;declare function reloadOpenThreadAndInbox(): Promise<void>;
const live = new ChatNotificationsClient(context);
live.on('message.created', ({ conversation_id, message }) => appendToThread(conversation_id, message));live.on('conversation.access_revoked', ({ conversation_id }) => dropConversation(conversation_id));
// Events published while the stream was reopening are not replayed: reload after every reconnect.let connectedBefore = false;live.on('connectionStateChange', (state) => { if (state !== 'connected') return; if (connectedBefore) void reloadOpenThreadAndInbox(); connectedBefore = true;});
live.connect();
/** A PUBLIC channel you can read but are not a member of: its events do not arrive on their own. */export function viewPublicChannel(conversationId: string): () => void { live.subscribeToConversation(conversationId); return () => live.unsubscribeFromConversation(conversationId);}
export function signOut(): void { live.disconnect();}import { DestroyRef, inject, Injectable } from '@angular/core';import { createChatNotifications } from '@central-code-station/ccs-chat-notifications-client/angular';import { getChatAccessToken } from './fe-guide-chat-token';
@Injectable({ providedIn: 'root' })export class ChatLiveUpdates { private readonly notifications = createChatNotifications( { chatApiUrl: 'https://chat.centralcodestation.com/api/v1', tenantId: 'fitness', applicationId: 'fitness-app', getAccessToken: getChatAccessToken, }, inject(DestroyRef), // disconnects when this service's injector is destroyed );
readonly connectionState = this.notifications.connectionState; // Signal<ConnectionState> readonly messageCreated$ = this.notifications.on$('message.created'); readonly typing$ = this.notifications.on$('typing');
connect(): void { this.notifications.connect(); }
viewPublicChannel(conversationId: string): void { this.notifications.subscribeToConversation(conversationId); }}import { createContext, useContext, type ReactNode } from 'react';import { useChatNotifications } from '@central-code-station/ccs-chat-notifications-client/react';import type { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';import { context } from './fe-guide-context';
const LiveContext = createContext<ChatNotificationsClient | null>(null);
/** Connect once, near the root, and pass the client down. */export function LiveUpdatesProvider({ signedIn, children }: { signedIn: boolean; children: ReactNode }) { const { client, connectionState } = useChatNotifications({ ...context, enabled: signedIn }); return ( <LiveContext.Provider value={client}> {connectionState === 'error' && <p>Live updates are paused.</p>} {children} </LiveContext.Provider> );}
export function useLiveClient(): ChatNotificationsClient { const client = useContext(LiveContext); if (!client) throw new Error('Wrap your app in <LiveUpdatesProvider>'); return client;}One connection carries the user’s personal events and the channels they are a member of (up to 100 channels). The server issues a stream credential that lasts at most 10 minutes. The client reopens the stream with a fresh one 30 seconds early by default (tokenRefreshBufferSeconds).
2. Handle events
Section titled “2. Handle events”message.created carries the full message, in the same shape as messages.list, so you render it without another request. Other events cover edits, deletes, reactions, members, conversations, typing and presence. See Realtime events for every payload. Use conversation.access_revoked to remove a conversation the user lost.
3. Watch the connection state
Section titled “3. Watch the connection state”connectionStateChange reports disconnected, connecting, connected or error. Show a quiet “reconnecting” hint on connecting and a clear one on error.
4. View a public channel you have not joined
Section titled “4. View a public channel you have not joined”Events for a PUBLIC channel arrive only if the user is a member. To watch one without joining, call subscribeToConversation(id) while it is open and unsubscribeFromConversation(id) when it closes. The client holds one such channel at a time; a new call replaces the previous one. Joining or creating a channel needs no call: the client reopens the stream by itself when the user gains a channel.
5. Reconnect and recover
Section titled “5. Reconnect and recover”A short network drop is retried by the browser. If the stream fails outright, for example the token was rejected, the client retries up to six times, 1 s, 2 s, 4 s, 8 s, 16 s and 30 s apart. Then it stays in error.
Events published while the stream was reopening are not replayed. Reload the open thread and the inbox each time the state becomes connected again. The client also reopens the stream on its own before the token expires, so keep that reload cheap, for example one page of 50 messages.
To start again after the client gave up, call disconnect() first. A bare connect() does nothing, because the client still counts as started.
import { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';import { context } from './fe-guide-context';
const live = new ChatNotificationsClient(context);
// After the library gives up (state 'error'), connect() alone is ignored: disconnect first.export function restartLiveUpdates(): void { live.disconnect(); live.connect();}Pass debug: true while you develop. Stream errors are otherwise swallowed on purpose, so a flaky connection never throws into your app.
Verify
Section titled “Verify”Open the app as coach-ana and send a message to client-john from another browser. The message appears in John’s open thread without a reload and connectionState is connected.
Troubleshooting
Section titled “Troubleshooting”| You see | Cause | Fix |
|---|---|---|
State stays error |
Expired or missing token, wrong context, or realtime is not available. | Turn on debug, then check the realtime-token request. See Troubleshooting. |
403 client_context_mismatch on the token request |
tenantId or applicationId is not your application’s. |
Use your application’s ids. |
| Messages missing after a drop | Events during a reconnect are not replayed. | Reload after connected. |
| No events for a PUBLIC channel | The user is not a member. | Call subscribeToConversation. |