Files
zeekay 507a7da852 fix(realtime): swap Phoenix WebSocket transport for backend SSE
The FE connected realtime via Phoenix Channels (wss .../socket/v2), but the
Go backend serves no Phoenix endpoint — its realtime is SSE at
/v1/base/realtime (envelope {event,chain,data}). Result: repeating ws 404 ->
'Live updates temporarily delayed'.

lib/socket/sse.ts reimplements the slice of the Phoenix Socket/Channel API the
27 realtime consumers use, backed by one EventSource, behind the existing
SocketProvider/useSocketChannel/useSocketMessage seam — no consumer changes.
Envelope routes via an optional topic field or a hub-channel translation table
(blocks -> blocks:new_block/new_block today). CT harness becomes a tiny SSE
server with the same signatures. 12 vitest cases cover it.

Frontend-only; chain/indexer untouched. Data freshness remains gated on the
separate C-Chain-rollback/indexer-reorg owner items.
2026-06-28 09:01:10 -07:00

72 lines
1.9 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import type { Channel, SocketOptions } from './sse';
import { Socket } from './sse';
type ChannelRegistry = Record<string, { channel: Channel; subscribers: number }>;
const socketContexts = new Map<string, React.Context<{
socket: Socket | null;
channelRegistry: React.MutableRefObject<ChannelRegistry>;
} | null>>();
function getSocketContext(name: string) {
if (!socketContexts.has(name)) {
socketContexts.set(name, React.createContext<{
socket: Socket | null;
channelRegistry: React.MutableRefObject<ChannelRegistry>;
} | null>(null));
}
return socketContexts.get(name)!;
}
interface SocketProviderProps {
children: React.ReactNode;
url?: string;
options?: SocketOptions;
name?: string;
}
export function SocketProvider({ children, options, url, name = 'default' }: SocketProviderProps) {
const [ socket, setSocket ] = useState<Socket | null>(null);
const channelRegistry = React.useRef<ChannelRegistry>({});
useEffect(() => {
if (!url) {
return;
}
const socketInstance = new Socket(url, options);
socketInstance.connect();
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
setSocket(null);
};
}, [ options, url ]);
const value = React.useMemo(() => ({
socket,
channelRegistry,
}), [ socket, channelRegistry ]);
const SocketContext = getSocketContext(name);
return (
<SocketContext.Provider value={ value }>
{ children }
</SocketContext.Provider>
);
}
// Hook to use a specific named socket
export function useSocket(name: string = 'default') {
const SocketContext = getSocketContext(name);
const context = React.useContext(SocketContext);
if (context === undefined) {
throw new Error(`useSocket must be used within a SocketProvider with name "${ name }"`);
}
return context;
}