Skip to content

Requests

Declare, implement, and call a request — super-line's client→server call that awaits one typed reply. A request declaration carries both an input and an output schema; the server validates the input, runs your handler, and the client gets back the typed output. For the model behind requests and the other interaction flavors, see The contract.

Declare it on the contract

Put the request under a role's clientToServer (or shared.clientToServer — see below), with an input and output schema:

ts
roles: {
  user: {
    clientToServer: {
      send: { input: z.object({ room: z.string(), text: z.string() }), output: z.object({ id: z.string() }) },
    },
  },
}

Implement the handler

Handlers live in implement, keyed by role (and shared). The handler receives the validated input, the connection's ctx, and the conn:

ts
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.

Call it from the client

The client is a typed proxy; call requests as methods:

ts
const out = await client.send({ room: 'lobby', text: 'hi' })
//    ^? { id: string }

Set a timeout or cancel

Each call accepts per-call options:

ts
await client.send(input, { timeoutMs: 5000 })          // override the default 30s
await client.send(input, { signal: controller.signal }) // cancel via AbortController

A timed-out call rejects with TIMEOUT; an aborted call rejects with BAD_REQUEST. Set timeoutMs: 0 to disable the timeout for a call.

Reject with a typed error

Throw a typed SuperLineError from a handler and the client's promise rejects with the same code:

ts
import { SuperLineError } from '@super-line/core'

send: async ({ room }, ctx) => {
  if (!ctx.canPost(room)) throw new SuperLineError('FORBIDDEN', 'not a member')
  // ...
}

Unknown throws become INTERNAL (your internals aren't leaked to the client).

Scope it: shared vs role

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.

Released under the MIT License.