Requests
A request is a client→server call that awaits one typed reply — super-line's request/response primitive. Declare it under clientToServer with an input and output schema:
roles: {
user: {
clientToServer: {
send: { input: z.object({ room: z.string(), text: z.string() }), output: z.object({ id: z.string() }) },
},
},
}Server: handle it
Handlers live in implement, keyed by role (and shared). The handler receives the validated input, the connection's ctx, and the conn:
srv.implement({
user: {
send: async ({ room, text }, ctx, conn) => {
// input is already validated against the schema
return { id: crypto.randomUUID() } // typed to the output schema
},
},
})The server always validates inbound input before your handler runs — bad input rejects with a VALIDATION error and the handler never sees it.
Client: call it
The client is a typed proxy; call requests as methods:
const out = await client.send({ room: 'lobby', text: 'hi' })
// ^? { id: string }Timeouts and cancellation
Each call accepts per-call options:
await client.send(input, { timeoutMs: 5000 }) // override the default 30s
await client.send(input, { signal: controller.signal }) // cancel via AbortControllerA timed-out call rejects with TIMEOUT; an aborted call rejects with BAD_REQUEST. Set timeoutMs: 0 to disable the timeout for a call.
Errors
Throw a typed SocketError from a handler and the client's promise rejects with the same code:
import { SocketError } from '@super-line/core'
send: async ({ room }, ctx) => {
if (!ctx.canPost(room)) throw new SocketError('FORBIDDEN', 'not a member')
// ...
}Unknown throws become INTERNAL (your internals aren't leaked to the client).
Shared vs role requests
Put a request in shared.clientToServer to make it callable by every role; put it in a role block to scope it. A request a connection's role can't see is rejected with NOT_FOUND. See Roles & auth.
Next: Events & rooms.