WebSocket
Real-time communication uses Centrifugo as a WebSocket server, integrated via @djangocfg/centrifugo.
For Django-side Channels setup see Django Runtime — Channels & ASGI. For the generated API layer see Data Layer.
Architecture
Django backend → Centrifugo server → @djangocfg/centrifugo → React appTwo generated artifacts per app (in _ws/generated/):
| File | Purpose |
|---|---|
client.ts | Typed RPC client |
types.ts | Message type definitions |
subscriptions.ts | Channel definitions |
Never edit _ws/generated/ manually. Regenerate with make centrifuge.
Provider Setup
import { CentrifugoProvider } from '@djangocfg/centrifugo';
<CentrifugoProvider
url="ws://localhost:8000/ws"
getToken={async () => {
const response = await api.cfg_centrifugo.getCentrifugoAuthToken();
return response.token;
}}
>
<App />
</CentrifugoProvider>The Centrifugo JWT token comes from the Django API (/cfg/centrifugo/auth-token/), not the API auth token.
Subscription Pattern
import { useSubscription } from '@djangocfg/centrifugo';
useSubscription({
channel: `project:${projectId}`,
enabled: isConnected && !!projectId, // Only subscribe when ready
onPublication: (data) => {
switch (data.type) {
case 'task.progress':
setTaskProgress(data.payload);
break;
case 'asset.ready':
mutateAssets(); // SWR revalidation
break;
}
},
});The enabled flag must guard against subscribing when workspace or session is not ready.
Channel Naming Convention
const channels = {
project: `project:${projectId}`,
terminal: `terminal:session:${sessionId}`,
transfer: `transfer:${transferId}`,
workspace: `workspace:${workspaceId}`,
aiCommand: `ai_command:${sessionId}`,
};Pattern: <resource>:<id> or <resource>:<scope>:<id>.
RPC Client
Generated typed RPC calls for request-response over WebSocket:
import { getAPIClientInstance } from '@/_ws';
const wsClient = getAPIClientInstance();
// Typed RPC calls
const messages = await wsClient.aiChatGetMessages({ session_id: sessionId });
const status = await wsClient.generationTaskStatus({ task_id: taskId });
await wsClient.generationTaskCancel({ task_id: taskId });
// Fire-and-forget variant
wsClient.terminalInputNoWait({ session_id: sessionId, data: 'ls -la\n' });Channel Authorization
Some channels require explicit authorization before subscribing:
const result = await apiClient.channelAuthorize({
channel: `terminal:session:${sessionId}`
});
// Only subscribe after authorization verified
useSubscription({
channel: `terminal:session:${sessionId}`,
enabled: isConnected && isActive && isAuthorized,
onPublication: handlePublication,
});Discriminated Union Messages
All WebSocket messages use a type discriminant for type-safe handling:
type ProjectEvent =
| { type: 'task.started'; payload: { taskId: string; taskType: string } }
| { type: 'task.progress'; payload: { taskId: string; progress: number } }
| { type: 'task.completed'; payload: { taskId: string; result: unknown } }
| { type: 'asset.ready'; payload: { assetId: string } }
| { type: 'render.progress'; payload: { jobId: string; percent: number } };
function handleEvent(event: ProjectEvent) {
switch (event.type) {
case 'task.progress':
setProgress(event.payload.progress);
break;
case 'asset.ready':
mutateAssets();
break;
}
}Stale Closure Prevention
Store handlers in useRef to avoid stale closures in subscription callbacks:
const handlersRef = useRef({ onTaskComplete, onAssetReady });
useEffect(() => {
handlersRef.current = { onTaskComplete, onAssetReady };
}, [onTaskComplete, onAssetReady]);
useSubscription({
channel: `project:${projectId}`,
enabled: true,
onPublication: (data) => {
// Always calls latest handler version
handlersRef.current.onTaskComplete?.(data);
},
});Workspace Isolation
Clear all WebSocket-related state when workspace changes:
useEffect(() => {
// Reset message history on workspace change
setMessages([]);
setTaskProgress({});
}, [currentWorkspace?.id]);Subscription auto-disables via enabled=false when workspace ID is null.
Rules
- Never edit
_ws/generated/— regenerate withmake centrifuge - Always guard subscriptions with
enabled— never subscribe before workspace, session, or auth is ready - Use
useReffor callback handlers inonPublicationto prevent stale closures - Channel names follow
<resource>:<id>or<resource>:<scope>:<id>pattern - Handle WebSocket messages with discriminated unions and
switchontype - Reset all WebSocket-related state (messages, progress, etc.) when workspace changes