From 78f56ad0f7620159c46333b931a100948bcd89d7 Mon Sep 17 00:00:00 2001 From: weishu Date: Fri, 23 Jan 2026 14:03:44 +0800 Subject: [PATCH] refactor: migrate socket types and payloads to shared protocol package Move socket-related type definitions and schemas from cli and server packages into a shared @hapi/protocol package for centralized type management. This includes migrating Update types, socket event interfaces (ClientToServerEvents, ServerToClientEvents), and terminal payload schemas. Add strongly-typed socket handlers that reference the protocol package instead of duplicating definitions. --- cli/src/api/apiMachine.ts | 3 +- cli/src/api/apiSession.ts | 16 +- cli/src/api/types.ts | 145 ------------- cli/src/terminal/TerminalManager.ts | 6 +- cli/src/terminal/types.ts | 67 ------ server/src/socket/handlers/cli/index.ts | 4 +- .../socket/handlers/cli/machineHandlers.ts | 12 +- server/src/socket/handlers/cli/rpcHandlers.ts | 4 +- .../socket/handlers/cli/sessionHandlers.ts | 20 +- .../socket/handlers/cli/terminalHandlers.ts | 40 ++-- server/src/socket/handlers/terminal.ts | 8 +- server/src/socket/server.ts | 4 +- server/src/socket/socketTypes.ts | 3 + shared/src/index.ts | 1 + shared/src/socket.ts | 202 ++++++++++++++++++ 15 files changed, 261 insertions(+), 274 deletions(-) create mode 100644 shared/src/socket.ts diff --git a/cli/src/api/apiMachine.ts b/cli/src/api/apiMachine.ts index 23991b89..a8c7b1a5 100644 --- a/cli/src/api/apiMachine.ts +++ b/cli/src/api/apiMachine.ts @@ -6,7 +6,8 @@ import { io, type Socket } from 'socket.io-client' import { stat } from 'node:fs/promises' import { logger } from '@/ui/logger' import { configuration } from '@/configuration' -import type { RunnerState, Machine, MachineMetadata, Update, UpdateMachineBody } from './types' +import type { Update, UpdateMachineBody } from '@hapi/protocol' +import type { RunnerState, Machine, MachineMetadata } from './types' import { RunnerStateSchema, MachineMetadataSchema } from './types' import { backoff } from '@/utils/time' import { RpcHandlerManager } from './rpc/RpcHandlerManager' diff --git a/cli/src/api/apiSession.ts b/cli/src/api/apiSession.ts index 7386c548..e2dde705 100644 --- a/cli/src/api/apiSession.ts +++ b/cli/src/api/apiSession.ts @@ -8,17 +8,21 @@ import { backoff } from '@/utils/time' import { AsyncLock } from '@/utils/lock' import type { RawJSONLines } from '@/claude/types' import { configuration } from '@/configuration' +import type { ClientToServerEvents, ServerToClientEvents, Update } from '@hapi/protocol' +import { + TerminalClosePayloadSchema, + TerminalOpenPayloadSchema, + TerminalResizePayloadSchema, + TerminalWritePayloadSchema +} from '@hapi/protocol' import type { AgentState, - ClientToServerEvents, MessageContent, MessageMeta, Metadata, - ServerToClientEvents, Session, SessionModelMode, SessionPermissionMode, - Update, UserMessage } from './types' import { AgentStateSchema, CliMessagesResponseSchema, MetadataSchema, UserMessageSchema } from './types' @@ -26,12 +30,6 @@ import { RpcHandlerManager } from './rpc/RpcHandlerManager' import { registerCommonHandlers } from '../modules/common/registerCommonHandlers' import { cleanupUploadDir } from '../modules/common/handlers/uploads' import { TerminalManager } from '@/terminal/TerminalManager' -import { - TerminalClosePayloadSchema, - TerminalOpenPayloadSchema, - TerminalResizePayloadSchema, - TerminalWritePayloadSchema -} from '@/terminal/types' import { applyVersionedAck } from './versionedUpdate' export class ApiSessionClient extends EventEmitter { diff --git a/cli/src/api/types.ts b/cli/src/api/types.ts index f635de1c..142a6397 100644 --- a/cli/src/api/types.ts +++ b/cli/src/api/types.ts @@ -2,16 +2,6 @@ import { AgentStateSchema, MetadataSchema, ModelModeSchema, PermissionModeSchema import type { ModelMode, PermissionMode } from '@hapi/protocol/types' import { z } from 'zod' import { UsageSchema } from '@/claude/types' -import type { - TerminalClosePayload, - TerminalExitPayload, - TerminalOpenPayload, - TerminalOutputPayload, - TerminalReadyPayload, - TerminalResizePayload, - TerminalWritePayload, - TerminalErrorPayload -} from '@/terminal/types' export type Usage = z.infer @@ -56,59 +46,6 @@ export type Machine = { runnerStateVersion: number } -export const UpdateNewMessageBodySchema = z.object({ - t: z.literal('new-message'), - sid: z.string(), - message: z.object({ - id: z.string(), - seq: z.number(), - createdAt: z.number(), - localId: z.string().nullable().optional(), - content: z.unknown() - }) -}) - -export type UpdateNewMessageBody = z.infer - -export const UpdateSessionBodySchema = z.object({ - t: z.literal('update-session'), - sid: z.string(), - metadata: z.object({ - version: z.number(), - value: z.unknown() - }).nullable(), - agentState: z.object({ - version: z.number(), - value: z.unknown().nullable() - }).nullable() -}) - -export type UpdateSessionBody = z.infer - -export const UpdateMachineBodySchema = z.object({ - t: z.literal('update-machine'), - machineId: z.string(), - metadata: z.object({ - version: z.number(), - value: z.unknown() - }).nullable(), - runnerState: z.object({ - version: z.number(), - value: z.unknown().nullable() - }).nullable() -}) - -export type UpdateMachineBody = z.infer - -export const UpdateSchema = z.object({ - id: z.string(), - seq: z.number(), - body: z.union([UpdateNewMessageBodySchema, UpdateSessionBodySchema, UpdateMachineBodySchema]), - createdAt: z.number() -}) - -export type Update = z.infer - export const CliMessagesResponseSchema = z.object({ messages: z.array(z.object({ id: z.string(), @@ -210,85 +147,3 @@ export type AgentMessage = z.infer export const MessageContentSchema = z.union([UserMessageSchema, AgentMessageSchema]) export type MessageContent = z.infer - -export type SocketErrorReason = 'namespace-missing' | 'access-denied' | 'not-found' - -export interface ServerToClientEvents { - update: (data: Update) => void - 'rpc-request': (data: { method: string; params: string }, callback: (response: string) => void) => void - 'terminal:open': (data: TerminalOpenPayload) => void - 'terminal:write': (data: TerminalWritePayload) => void - 'terminal:resize': (data: TerminalResizePayload) => void - 'terminal:close': (data: TerminalClosePayload) => void - error: (data: { message: string; code?: SocketErrorReason; scope?: 'session' | 'machine'; id?: string }) => void -} - -export interface ClientToServerEvents { - message: (data: { sid: string; message: unknown; localId?: string }) => void - 'session-alive': (data: { - sid: string - time: number - thinking: boolean - mode?: 'local' | 'remote' - permissionMode?: SessionPermissionMode - modelMode?: SessionModelMode - }) => void - 'session-end': (data: { sid: string; time: number }) => void - 'update-metadata': (data: { sid: string; expectedVersion: number; metadata: unknown }, cb: (answer: { - result: 'error' - reason?: SocketErrorReason - } | { - result: 'version-mismatch' - version: number - metadata: unknown | null - } | { - result: 'success' - version: number - metadata: unknown | null - }) => void) => void - 'update-state': (data: { sid: string; expectedVersion: number; agentState: unknown | null }, cb: (answer: { - result: 'error' - reason?: SocketErrorReason - } | { - result: 'version-mismatch' - version: number - agentState: unknown | null - } | { - result: 'success' - version: number - agentState: unknown | null - }) => void) => void - 'machine-alive': (data: { machineId: string; time: number }) => void - 'machine-update-metadata': (data: { machineId: string; expectedVersion: number; metadata: unknown }, cb: (answer: { - result: 'error' - reason?: SocketErrorReason - } | { - result: 'version-mismatch' - version: number - metadata: unknown | null - } | { - result: 'success' - version: number - metadata: unknown | null - }) => void) => void - 'machine-update-state': (data: { machineId: string; expectedVersion: number; runnerState: unknown | null }, cb: (answer: { - result: 'error' - reason?: SocketErrorReason - } | { - result: 'version-mismatch' - version: number - runnerState: unknown | null - } | { - result: 'success' - version: number - runnerState: unknown | null - }) => void) => void - 'rpc-register': (data: { method: string }) => void - 'rpc-unregister': (data: { method: string }) => void - 'terminal:ready': (data: TerminalReadyPayload) => void - 'terminal:output': (data: TerminalOutputPayload) => void - 'terminal:exit': (data: TerminalExitPayload) => void - 'terminal:error': (data: TerminalErrorPayload) => void - ping: (callback: () => void) => void - 'usage-report': (data: unknown) => void -} diff --git a/cli/src/terminal/TerminalManager.ts b/cli/src/terminal/TerminalManager.ts index d918c061..b7bfecca 100644 --- a/cli/src/terminal/TerminalManager.ts +++ b/cli/src/terminal/TerminalManager.ts @@ -3,9 +3,9 @@ import type { TerminalErrorPayload, TerminalExitPayload, TerminalOutputPayload, - TerminalReadyPayload, - TerminalSession -} from './types' + TerminalReadyPayload +} from '@hapi/protocol' +import type { TerminalSession } from './types' type TerminalRuntime = TerminalSession & { proc: Bun.Subprocess diff --git a/cli/src/terminal/types.ts b/cli/src/terminal/types.ts index 3d4dd585..83d098fc 100644 --- a/cli/src/terminal/types.ts +++ b/cli/src/terminal/types.ts @@ -1,72 +1,5 @@ -import { z } from 'zod' - export type TerminalSession = { terminalId: string cols: number rows: number } - -export const TerminalOpenPayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - cols: z.number().int().positive(), - rows: z.number().int().positive() -}) - -export type TerminalOpenPayload = z.infer - -export const TerminalWritePayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - data: z.string() -}) - -export type TerminalWritePayload = z.infer - -export const TerminalResizePayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - cols: z.number().int().positive(), - rows: z.number().int().positive() -}) - -export type TerminalResizePayload = z.infer - -export const TerminalClosePayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1) -}) - -export type TerminalClosePayload = z.infer - -export const TerminalReadyPayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1) -}) - -export type TerminalReadyPayload = z.infer - -export const TerminalOutputPayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - data: z.string() -}) - -export type TerminalOutputPayload = z.infer - -export const TerminalExitPayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - code: z.number().int().nullable(), - signal: z.string().nullable() -}) - -export type TerminalExitPayload = z.infer - -export const TerminalErrorPayloadSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - message: z.string() -}) - -export type TerminalErrorPayload = z.infer diff --git a/server/src/socket/handlers/cli/index.ts b/server/src/socket/handlers/cli/index.ts index ee35c314..f71dd30e 100644 --- a/server/src/socket/handlers/cli/index.ts +++ b/server/src/socket/handlers/cli/index.ts @@ -3,7 +3,7 @@ import type { Store, StoredMachine, StoredSession } from '../../../store' import type { RpcRegistry } from '../../rpcRegistry' import type { SyncEvent } from '../../../sync/syncEngine' import type { TerminalRegistry } from '../../terminalRegistry' -import type { SocketServer, SocketWithData } from '../../socketTypes' +import type { CliSocketWithData, SocketServer } from '../../socketTypes' import { registerMachineHandlers } from './machineHandlers' import { registerRpcHandlers } from './rpcHandlers' import { registerSessionHandlers } from './sessionHandlers' @@ -45,7 +45,7 @@ type AccessResult = | { ok: true; value: T } | { ok: false; reason: AccessErrorReason } -export function registerCliHandlers(socket: SocketWithData, deps: CliHandlersDeps): void { +export function registerCliHandlers(socket: CliSocketWithData, deps: CliHandlersDeps): void { const { io, store, rpcRegistry, terminalRegistry, onSessionAlive, onSessionEnd, onMachineAlive, onWebappEvent } = deps const terminalNamespace = io.of('/terminal') const namespace = typeof socket.data.namespace === 'string' ? socket.data.namespace : null diff --git a/server/src/socket/handlers/cli/machineHandlers.ts b/server/src/socket/handlers/cli/machineHandlers.ts index 049e26e2..2e87d23c 100644 --- a/server/src/socket/handlers/cli/machineHandlers.ts +++ b/server/src/socket/handlers/cli/machineHandlers.ts @@ -1,8 +1,9 @@ +import type { ClientToServerEvents } from '@hapi/protocol' import { z } from 'zod' import { randomUUID } from 'node:crypto' import type { Store, StoredMachine } from '../../../store' import type { SyncEvent } from '../../../sync/syncEngine' -import type { SocketWithData } from '../../socketTypes' +import type { CliSocketWithData } from '../../socketTypes' type MachineAlivePayload = { machineId: string @@ -19,6 +20,9 @@ type ResolveMachineAccess = (machineId: string) => AccessResult type EmitAccessError = (scope: 'session' | 'machine', id: string, reason: AccessErrorReason) => void +type MachineUpdateMetadataHandler = ClientToServerEvents['machine-update-metadata'] +type MachineUpdateStateHandler = ClientToServerEvents['machine-update-state'] + const machineUpdateMetadataSchema = z.object({ machineId: z.string(), expectedVersion: z.number().int(), @@ -39,7 +43,7 @@ export type MachineHandlersDeps = { onWebappEvent?: (event: SyncEvent) => void } -export function registerMachineHandlers(socket: SocketWithData, deps: MachineHandlersDeps): void { +export function registerMachineHandlers(socket: CliSocketWithData, deps: MachineHandlersDeps): void { const { store, resolveMachineAccess, emitAccessError, onMachineAlive, onWebappEvent } = deps socket.on('machine-alive', (data: MachineAlivePayload) => { @@ -54,7 +58,7 @@ export function registerMachineHandlers(socket: SocketWithData, deps: MachineHan onMachineAlive?.(data) }) - const handleMachineMetadataUpdate = (data: unknown, cb: (answer: unknown) => void) => { + const handleMachineMetadataUpdate: MachineUpdateMetadataHandler = (data, cb) => { const parsed = machineUpdateMetadataSchema.safeParse(data) if (!parsed.success) { cb({ result: 'error' }) @@ -94,7 +98,7 @@ export function registerMachineHandlers(socket: SocketWithData, deps: MachineHan } } - const handleMachineStateUpdate = (data: unknown, cb: (answer: unknown) => void) => { + const handleMachineStateUpdate: MachineUpdateStateHandler = (data, cb) => { const parsed = machineUpdateStateSchema.safeParse(data) if (!parsed.success) { cb({ result: 'error' }) diff --git a/server/src/socket/handlers/cli/rpcHandlers.ts b/server/src/socket/handlers/cli/rpcHandlers.ts index 79415153..e76faafb 100644 --- a/server/src/socket/handlers/cli/rpcHandlers.ts +++ b/server/src/socket/handlers/cli/rpcHandlers.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import type { RpcRegistry } from '../../rpcRegistry' -import type { SocketWithData } from '../../socketTypes' +import type { CliSocketWithData } from '../../socketTypes' const rpcRegisterSchema = z.object({ method: z.string().min(1) @@ -10,7 +10,7 @@ const rpcUnregisterSchema = z.object({ method: z.string().min(1) }) -export function registerRpcHandlers(socket: SocketWithData, rpcRegistry: RpcRegistry): void { +export function registerRpcHandlers(socket: CliSocketWithData, rpcRegistry: RpcRegistry): void { socket.on('rpc-register', (data: unknown) => { const parsed = rpcRegisterSchema.safeParse(data) if (!parsed.success) { diff --git a/server/src/socket/handlers/cli/sessionHandlers.ts b/server/src/socket/handlers/cli/sessionHandlers.ts index 1fce8db7..077ccec9 100644 --- a/server/src/socket/handlers/cli/sessionHandlers.ts +++ b/server/src/socket/handlers/cli/sessionHandlers.ts @@ -1,10 +1,11 @@ +import type { ClientToServerEvents } from '@hapi/protocol' import { z } from 'zod' import { randomUUID } from 'node:crypto' import type { ModelMode, PermissionMode } from '@hapi/protocol/types' import type { Store, StoredSession } from '../../../store' import type { SyncEvent } from '../../../sync/syncEngine' import { extractTodoWriteTodosFromMessageContent } from '../../../sync/todos' -import type { SocketWithData } from '../../socketTypes' +import type { CliSocketWithData } from '../../socketTypes' type SessionAlivePayload = { sid: string @@ -30,6 +31,9 @@ type ResolveSessionAccess = (sessionId: string) => AccessResult type EmitAccessError = (scope: 'session' | 'machine', id: string, reason: AccessErrorReason) => void +type UpdateMetadataHandler = ClientToServerEvents['update-metadata'] +type UpdateStateHandler = ClientToServerEvents['update-state'] + const messageSchema = z.object({ sid: z.string(), message: z.union([z.string(), z.unknown()]), @@ -57,7 +61,7 @@ export type SessionHandlersDeps = { onWebappEvent?: (event: SyncEvent) => void } -export function registerSessionHandlers(socket: SocketWithData, deps: SessionHandlersDeps): void { +export function registerSessionHandlers(socket: CliSocketWithData, deps: SessionHandlersDeps): void { const { store, resolveSessionAccess, emitAccessError, onSessionAlive, onSessionEnd, onWebappEvent } = deps socket.on('message', (data: unknown) => { @@ -127,7 +131,7 @@ export function registerSessionHandlers(socket: SocketWithData, deps: SessionHan }) }) - socket.on('update-metadata', (data: unknown, cb: (answer: unknown) => void) => { + const handleUpdateMetadata: UpdateMetadataHandler = (data, cb) => { const parsed = updateMetadataSchema.safeParse(data) if (!parsed.success) { cb({ result: 'error' }) @@ -170,9 +174,11 @@ export function registerSessionHandlers(socket: SocketWithData, deps: SessionHan socket.to(`session:${sid}`).emit('update', update) onWebappEvent?.({ type: 'session-updated', sessionId: sid, data: { sid } }) } - }) + } - socket.on('update-state', (data: unknown, cb: (answer: unknown) => void) => { + socket.on('update-metadata', handleUpdateMetadata) + + const handleUpdateState: UpdateStateHandler = (data, cb) => { const parsed = updateStateSchema.safeParse(data) if (!parsed.success) { cb({ result: 'error' }) @@ -215,7 +221,9 @@ export function registerSessionHandlers(socket: SocketWithData, deps: SessionHan socket.to(`session:${sid}`).emit('update', update) onWebappEvent?.({ type: 'session-updated', sessionId: sid, data: { sid } }) } - }) + } + + socket.on('update-state', handleUpdateState) socket.on('session-alive', (data: SessionAlivePayload) => { if (!data || typeof data.sid !== 'string' || typeof data.time !== 'number') { diff --git a/server/src/socket/handlers/cli/terminalHandlers.ts b/server/src/socket/handlers/cli/terminalHandlers.ts index 017fa437..9ed75fd3 100644 --- a/server/src/socket/handlers/cli/terminalHandlers.ts +++ b/server/src/socket/handlers/cli/terminalHandlers.ts @@ -1,7 +1,12 @@ -import { z } from 'zod' +import { + TerminalErrorPayloadSchema, + TerminalExitPayloadSchema, + TerminalOutputPayloadSchema, + TerminalReadyPayloadSchema +} from '@hapi/protocol' import type { StoredSession } from '../../../store' import type { TerminalRegistry } from '../../terminalRegistry' -import type { SocketServer, SocketWithData } from '../../socketTypes' +import type { CliSocketWithData, SocketServer } from '../../socketTypes' type AccessErrorReason = 'namespace-missing' | 'access-denied' | 'not-found' @@ -15,29 +20,10 @@ type EmitAccessError = (scope: 'session' | 'machine', id: string, reason: Access type SocketNamespace = ReturnType -const terminalReadySchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1) -}) - -const terminalOutputSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - data: z.string() -}) - -const terminalExitSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - code: z.number().int().nullable(), - signal: z.string().nullable() -}) - -const terminalErrorSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - message: z.string() -}) +const terminalReadySchema = TerminalReadyPayloadSchema +const terminalOutputSchema = TerminalOutputPayloadSchema +const terminalExitSchema = TerminalExitPayloadSchema +const terminalErrorSchema = TerminalErrorPayloadSchema export type TerminalHandlersDeps = { terminalRegistry: TerminalRegistry @@ -46,7 +32,7 @@ export type TerminalHandlersDeps = { emitAccessError: EmitAccessError } -export function registerTerminalHandlers(socket: SocketWithData, deps: TerminalHandlersDeps): void { +export function registerTerminalHandlers(socket: CliSocketWithData, deps: TerminalHandlersDeps): void { const { terminalRegistry, terminalNamespace, resolveSessionAccess, emitAccessError } = deps const forwardTerminalEvent = (event: string, payload: { sessionId: string; terminalId: string } & Record) => { @@ -116,7 +102,7 @@ export function registerTerminalHandlers(socket: SocketWithData, deps: TerminalH }) } -export function cleanupTerminalHandlers(socket: SocketWithData, deps: { terminalRegistry: TerminalRegistry; terminalNamespace: SocketNamespace }): void { +export function cleanupTerminalHandlers(socket: CliSocketWithData, deps: { terminalRegistry: TerminalRegistry; terminalNamespace: SocketNamespace }): void { const removed = deps.terminalRegistry.removeByCliSocket(socket.id) for (const entry of removed) { const terminalSocket = deps.terminalNamespace.sockets.get(entry.socketId) diff --git a/server/src/socket/handlers/terminal.ts b/server/src/socket/handlers/terminal.ts index 0f84a2f1..a3ba299a 100644 --- a/server/src/socket/handlers/terminal.ts +++ b/server/src/socket/handlers/terminal.ts @@ -1,13 +1,9 @@ +import { TerminalOpenPayloadSchema } from '@hapi/protocol' import { z } from 'zod' import type { TerminalRegistry, TerminalRegistryEntry } from '../terminalRegistry' import type { SocketServer, SocketWithData } from '../socketTypes' -const terminalCreateSchema = z.object({ - sessionId: z.string().min(1), - terminalId: z.string().min(1), - cols: z.number().int().positive(), - rows: z.number().int().positive() -}) +const terminalCreateSchema = TerminalOpenPayloadSchema const terminalWriteSchema = z.object({ terminalId: z.string().min(1), diff --git a/server/src/socket/server.ts b/server/src/socket/server.ts index ad7e6424..9420c2a4 100644 --- a/server/src/socket/server.ts +++ b/server/src/socket/server.ts @@ -11,7 +11,7 @@ import { registerTerminalHandlers } from './handlers/terminal' import { RpcRegistry } from './rpcRegistry' import type { SyncEvent } from '../sync/syncEngine' import { TerminalRegistry } from './terminalRegistry' -import type { SocketData, SocketServer } from './socketTypes' +import type { CliSocketWithData, SocketData, SocketServer } from './socketTypes' const jwtPayloadSchema = z.object({ uid: z.number(), @@ -105,7 +105,7 @@ export function createSocketServer(deps: SocketServerDeps): { socket.data.namespace = parsedToken.namespace next() }) - cliNs.on('connection', (socket) => registerCliHandlers(socket, { + cliNs.on('connection', (socket) => registerCliHandlers(socket as CliSocketWithData, { io, store: deps.store, rpcRegistry, diff --git a/server/src/socket/socketTypes.ts b/server/src/socket/socketTypes.ts index 2c3662f9..cec06bb0 100644 --- a/server/src/socket/socketTypes.ts +++ b/server/src/socket/socketTypes.ts @@ -1,3 +1,4 @@ +import type { ClientToServerEvents, ServerToClientEvents } from '@hapi/protocol' import type { DefaultEventsMap, Server, Socket } from 'socket.io' export type SocketData = { @@ -7,3 +8,5 @@ export type SocketData = { export type SocketServer = Server export type SocketWithData = Socket +export type CliSocketServer = Server +export type CliSocketWithData = Socket diff --git a/shared/src/index.ts b/shared/src/index.ts index 93984422..3ec6f183 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,5 +1,6 @@ export * from './messages' export * from './modes' +export * from './socket' export * from './sessionSummary' export * from './utils' export type * from './types' diff --git a/shared/src/socket.ts b/shared/src/socket.ts new file mode 100644 index 00000000..ae9e0efb --- /dev/null +++ b/shared/src/socket.ts @@ -0,0 +1,202 @@ +import { z } from 'zod' +import type { ModelMode, PermissionMode } from './modes' + +export type SocketErrorReason = 'namespace-missing' | 'access-denied' | 'not-found' + +export const TerminalOpenPayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1), + cols: z.number().int().positive(), + rows: z.number().int().positive() +}) + +export type TerminalOpenPayload = z.infer + +export const TerminalWritePayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1), + data: z.string() +}) + +export type TerminalWritePayload = z.infer + +export const TerminalResizePayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1), + cols: z.number().int().positive(), + rows: z.number().int().positive() +}) + +export type TerminalResizePayload = z.infer + +export const TerminalClosePayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1) +}) + +export type TerminalClosePayload = z.infer + +export const TerminalReadyPayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1) +}) + +export type TerminalReadyPayload = z.infer + +export const TerminalOutputPayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1), + data: z.string() +}) + +export type TerminalOutputPayload = z.infer + +export const TerminalExitPayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1), + code: z.number().int().nullable(), + signal: z.string().nullable() +}) + +export type TerminalExitPayload = z.infer + +export const TerminalErrorPayloadSchema = z.object({ + sessionId: z.string().min(1), + terminalId: z.string().min(1), + message: z.string() +}) + +export type TerminalErrorPayload = z.infer + +export const UpdateNewMessageBodySchema = z.object({ + t: z.literal('new-message'), + sid: z.string(), + message: z.object({ + id: z.string(), + seq: z.number(), + createdAt: z.number(), + localId: z.string().nullable().optional(), + content: z.unknown() + }) +}) + +export type UpdateNewMessageBody = z.infer + +export const UpdateSessionBodySchema = z.object({ + t: z.literal('update-session'), + sid: z.string(), + metadata: z.object({ + version: z.number(), + value: z.unknown() + }).nullable(), + agentState: z.object({ + version: z.number(), + value: z.unknown().nullable() + }).nullable() +}) + +export type UpdateSessionBody = z.infer + +export const UpdateMachineBodySchema = z.object({ + t: z.literal('update-machine'), + machineId: z.string(), + metadata: z.object({ + version: z.number(), + value: z.unknown() + }).nullable(), + runnerState: z.object({ + version: z.number(), + value: z.unknown().nullable() + }).nullable() +}) + +export type UpdateMachineBody = z.infer + +export const UpdateSchema = z.object({ + id: z.string(), + seq: z.number(), + body: z.union([UpdateNewMessageBodySchema, UpdateSessionBodySchema, UpdateMachineBodySchema]), + createdAt: z.number() +}) + +export type Update = z.infer + +export interface ServerToClientEvents { + update: (data: Update) => void + 'rpc-request': (data: { method: string; params: string }, callback: (response: string) => void) => void + 'terminal:open': (data: TerminalOpenPayload) => void + 'terminal:write': (data: TerminalWritePayload) => void + 'terminal:resize': (data: TerminalResizePayload) => void + 'terminal:close': (data: TerminalClosePayload) => void + error: (data: { message: string; code?: SocketErrorReason; scope?: 'session' | 'machine'; id?: string }) => void +} + +export interface ClientToServerEvents { + message: (data: { sid: string; message: unknown; localId?: string }) => void + 'session-alive': (data: { + sid: string + time: number + thinking: boolean + mode?: 'local' | 'remote' + permissionMode?: PermissionMode + modelMode?: ModelMode + }) => void + 'session-end': (data: { sid: string; time: number }) => void + 'update-metadata': (data: { sid: string; expectedVersion: number; metadata: unknown }, cb: (answer: { + result: 'error' + reason?: SocketErrorReason + } | { + result: 'version-mismatch' + version: number + metadata: unknown | null + } | { + result: 'success' + version: number + metadata: unknown | null + }) => void) => void + 'update-state': (data: { sid: string; expectedVersion: number; agentState: unknown | null }, cb: (answer: { + result: 'error' + reason?: SocketErrorReason + } | { + result: 'version-mismatch' + version: number + agentState: unknown | null + } | { + result: 'success' + version: number + agentState: unknown | null + }) => void) => void + 'machine-alive': (data: { machineId: string; time: number }) => void + 'machine-update-metadata': (data: { machineId: string; expectedVersion: number; metadata: unknown }, cb: (answer: { + result: 'error' + reason?: SocketErrorReason + } | { + result: 'version-mismatch' + version: number + metadata: unknown | null + } | { + result: 'success' + version: number + metadata: unknown | null + }) => void) => void + 'machine-update-state': (data: { machineId: string; expectedVersion: number; runnerState: unknown | null }, cb: (answer: { + result: 'error' + reason?: SocketErrorReason + } | { + result: 'version-mismatch' + version: number + runnerState: unknown | null + } | { + result: 'success' + version: number + runnerState: unknown | null + }) => void) => void + 'rpc-register': (data: { method: string }) => void + 'rpc-unregister': (data: { method: string }) => void + 'terminal:ready': (data: TerminalReadyPayload) => void + 'terminal:output': (data: TerminalOutputPayload) => void + 'terminal:exit': (data: TerminalExitPayload) => void + 'terminal:error': (data: TerminalErrorPayload) => void + ping: (callback: () => void) => void + 'usage-report': (data: unknown) => void +}