nandi/oripublic Fork 0
ca7e020cbb6442f047e0002eb9831ba29f8a17af
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

ws.ts · 85 lines · 2.4 KBTypeScript Blame HistoryRaw
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g yesterday1/**
2 * WebSocket client for the ori backend: parses server messages into store
3 * events and reconnects automatically with a capped backoff. The server
4 * replays the session history on every connection, and the reducer resets on
5 * "hello", so a reconnection rebuilds the exact same thread.
6 */
7
8import type { ClientMessage, ServerMessage } from "./protocol";
9import type { ChatEvent } from "./reducer";
10
11/** What the UI needs from the connection. */
12export interface OriSocket {
13 send(message: ClientMessage): void;
14 close(): void;
15}
16
17interface ConnectOptions {
18 url: string;
19 dispatch: (event: ChatEvent) => void;
20 /** Injectable WebSocket constructor, for tests. Defaults to the browser's. */
21 webSocketImpl?: typeof WebSocket;
22 /** First reconnection delay in ms; doubles up to 16x. Defaults to 500. */
23 initialRetryMs?: number;
24}
25
26/**
27 * connectSocket opens the connection and keeps it alive until close().
28 *
29 * @example
30 * const socket = connectSocket({ url: wsUrl(), dispatch });
31 * socket.send({ type: "prompt", text: "hello" });
32 */
33export function connectSocket(options: ConnectOptions): OriSocket {
34 const Impl = options.webSocketImpl ?? WebSocket;
35 const initialRetryMs = options.initialRetryMs ?? 500;
36
37 let socket: WebSocket | null = null;
38 let retryMs = initialRetryMs;
39 let closedByUser = false;
40
41 const open = () => {
42 socket = new Impl(options.url);
43 options.dispatch({ type: "connection", status: "connecting" });
44
45 socket.onopen = () => {
46 retryMs = initialRetryMs;
47 };
48 socket.onmessage = (event: MessageEvent) => {
49 let message: ServerMessage;
50 try {
51 message = JSON.parse(String(event.data)) as ServerMessage;
52 } catch {
53 return; // a malformed frame must not kill the connection handling
54 }
55 options.dispatch({ type: "server", message });
56 };
57 socket.onclose = () => {
58 if (closedByUser) {
59 return;
60 }
61 options.dispatch({ type: "connection", status: "offline" });
62 setTimeout(open, retryMs);
63 retryMs = Math.min(retryMs * 2, initialRetryMs * 16);
64 };
65 };
66 open();
67
68 return {
69 send(message: ClientMessage) {
70 if (socket && socket.readyState === Impl.OPEN) {
71 socket.send(JSON.stringify(message));
72 }
73 },
74 close() {
75 closedByUser = true;
76 socket?.close();
77 },
78 };
79}
80
81/** wsUrl derives the backend WebSocket URL from the page's own origin. */
82export function wsUrl(): string {
83 const scheme = window.location.protocol === "https:" ? "wss" : "ws";
84 return `${scheme}://${window.location.host}/ws`;
85}