feat(web): add WebSocket client and context infrastructure
- WsClient class: connect/disconnect/send/on/off/isConnected/clearCallbacks
- connect() derives wss:// from https:// automatically, returns Promise<void>
- on/off typed with Extract<WsServerMessage, { type: T }> for precise
callback narrowing, callbacks stored as Map<string, Set<fn>>
- ws-context.ts: WsContextValue type + WsContext definition
- ws-provider.tsx: WsProvider with module-level wsClient singleton,
owns connection lifecycle (connect/disconnect/isConnected state)
- ws-hooks.ts: useWsClient, useWsConnected, useWsConnect, useWsDisconnect
This commit is contained in:
parent
d60b0da9df
commit
9affe339c6
4 changed files with 99 additions and 0 deletions
|
|
@ -12,7 +12,16 @@ export class WsClient {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private callbacks = new Map<string, Set<(msg: WsServerMessage) => void>>();
|
private callbacks = new Map<string, Set<(msg: WsServerMessage) => void>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the WebSocket connection closes.
|
||||||
|
* Set by WsProvider — do not set directly in components.
|
||||||
|
*/
|
||||||
public onError: ((event: Event) => void) | null = null;
|
public onError: ((event: Event) => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the WebSocket connection encounters an error.
|
||||||
|
* Set by WsProvider — do not set directly in components.
|
||||||
|
*/
|
||||||
public onClose: ((event: CloseEvent) => void) | null = null;
|
public onClose: ((event: CloseEvent) => void) | null = null;
|
||||||
|
|
||||||
connect(apiUrl: string): Promise<void> {
|
connect(apiUrl: string): Promise<void> {
|
||||||
|
|
|
||||||
11
apps/web/src/lib/ws-context.ts
Normal file
11
apps/web/src/lib/ws-context.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createContext } from "react";
|
||||||
|
import type { WsClient } from "./ws-client.js";
|
||||||
|
|
||||||
|
export type WsContextValue = {
|
||||||
|
client: WsClient;
|
||||||
|
isConnected: boolean;
|
||||||
|
connect: (url: string) => Promise<void>;
|
||||||
|
disconnect: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WsContext = createContext<WsContextValue | null>(null);
|
||||||
35
apps/web/src/lib/ws-hooks.ts
Normal file
35
apps/web/src/lib/ws-hooks.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { WsContext } from "./ws-context.js";
|
||||||
|
import type { WsClient } from "./ws-client.js";
|
||||||
|
|
||||||
|
export const useWsClient = (): WsClient => {
|
||||||
|
const ctx = useContext(WsContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useWsClient must be used within a WsProvider");
|
||||||
|
}
|
||||||
|
return ctx.client;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWsConnected = (): boolean => {
|
||||||
|
const ctx = useContext(WsContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useWsConnected must be used within a WsProvider");
|
||||||
|
}
|
||||||
|
return ctx.isConnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWsConnect = (): ((url: string) => Promise<void>) => {
|
||||||
|
const ctx = useContext(WsContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useWsConnect must be used within a WsProvider");
|
||||||
|
}
|
||||||
|
return ctx.connect;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWsDisconnect = (): (() => void) => {
|
||||||
|
const ctx = useContext(WsContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useWsDisconnect must be used within a WsProvider");
|
||||||
|
}
|
||||||
|
return ctx.disconnect;
|
||||||
|
};
|
||||||
44
apps/web/src/lib/ws-provider.tsx
Normal file
44
apps/web/src/lib/ws-provider.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { WsClient } from "./ws-client.js";
|
||||||
|
import { WsContext } from "./ws-context.js";
|
||||||
|
|
||||||
|
const wsClient = new WsClient();
|
||||||
|
|
||||||
|
export const WsProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
const connect = useCallback(async (url: string): Promise<void> => {
|
||||||
|
wsClient.onClose = () => setIsConnected(false);
|
||||||
|
wsClient.onError = () => setIsConnected(false);
|
||||||
|
try {
|
||||||
|
await wsClient.connect(url);
|
||||||
|
setIsConnected(true);
|
||||||
|
} catch (err) {
|
||||||
|
setIsConnected(false);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const disconnect = useCallback((): void => {
|
||||||
|
wsClient.disconnect();
|
||||||
|
setIsConnected(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
wsClient.disconnect();
|
||||||
|
wsClient.clearCallbacks();
|
||||||
|
wsClient.onClose = null;
|
||||||
|
wsClient.onError = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WsContext.Provider
|
||||||
|
value={{ client: wsClient, isConnected, connect, disconnect }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</WsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue