0Z4C0
// navigation
// @ozaco/server

Client

A typed, standalone client that mirrors the server broker pipeline over `std:fetch`.

import from@ozaco/server/client

The client lets any environment — a browser, another service, a test — call a remote @ozaco/server app with the same ergonomics as an in-process call, and full end-to-end types. It reuses the server's broker pipeline verbatim (call span → per-action policy resolution → the policy onion) but replaces the transport leaf: instead of dispatching through a Transport protocol, the dispatch core talks to the remote server directly over std:fetch, routed by an emitted route Manifest.

connect is the one call you need. Given a baseUrl and a manifest (the route table emitted by @ozaco/devkit), it installs the ClientBroker, registers a stub service per remote service so the broker can resolve each call, starts the broker, and returns a nested, fully-typed call surface. No server runtime is pulled in.

Each endpoint on that surface returns a handle with three consumption modes — body() decodes the whole response once, stream() decodes one value per chunk, and raw() hands back the undecoded byte stream — all sharing one request shape. Because it is the server pipeline, resilience policies (retry, cache, timeout, …) are opt-in and apply to client calls the moment they are installed; the codec and tracer auto-install if absent.

// on this page
01

Client

Connect to a remote app and build its stub services.

fn

connect

Install the client broker, register stubs, start, and return the typed call surface.

signature
function* connect<TServices>(
  options: ClientDef.Options,
): Operation<ClientDef.Client<TServices>>

Installs ClientBroker with options, builds a stub service per entry in options.manifest (each a real defineService/defineAction registered with the broker), starts the broker, and layers the typed endpoint surface on top. Standalone — no server runtime. Install any policies before or after; they apply on the next call.

example
import { connect } from '@ozaco/server/client'
import { run } from '@ozaco/std/effect'
import { manifest } from './generated/manifest'
import type { Services } from './services'

run(function* () {
  const api = yield* connect<Services>({
    baseUrl: 'https://api.example.com',
    manifest,
  })

  // body(): decode the whole response once
  const user = yield* api.users.actions.get({ id: '42' }).body()

  // stream(): one decoded value per chunk
  for (const todo of yield* each(yield* api.todos.actions.list().stream())) {
    render(todo)
    yield* each.next()
  }
})
fn

defineClient

Build the installable stub services from a route `Manifest` (the block `connect` layers on).

signature
function defineClient(
  manifest: ClientDef.Manifest,
): Record<string, Service>

Turns a manifest into a service name -> stub Service map. Each stub is a real defineService/defineAction whose action bodies dispatch themselves back through the broker, so Broker.actions.call can resolve their { serviceName, actionKey }. Use it directly when you want to install and wire the stubs yourself; connect calls it for you.

const

ClientBroker

The transport-less broker: the server’s `DefaultBroker` with a direct `std:fetch` dispatch core.

signature
const ClientBroker: Plugin<ClientDef.Context, [options: ClientDef.Options], BrokerDef.Actions>

Its setup installs the codec (JSON if absent) and tracer (DefaultTracer if absent) but no Transport; call's dispatch core issues an HTTP request to the remote server, routed by the manifest, honouring the per-call mode (body/stream/raw). Everything between — call span, policy resolution, the policy onion — is the server pipeline unchanged. emit/broadcast fail (no transport). The tracer needs a platform std:io impl (WebIO in the browser, Node/Bun IO server-side) for span ids.

02

Types

The `ClientDef` namespace: options, manifest, and the inferred call surface.

ns

ClientDef

Options, route manifest, dispatch modes, and the type-level client surface.

signature
namespace ClientDef {
  interface Route { method: string; path: string }
  type Manifest = Record<string, Record<string, Route>>

  interface Options extends BrokerDef.Options {
    baseUrl: string
    manifest: Manifest
    headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
  }
  interface Context extends BrokerDef.Context {
    baseUrl: string
    manifest: Manifest
    headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
  }

  type DispatchMode = 'body' | 'stream' | 'raw'
  interface CallOptions extends BrokerDef.CallOptions { mode?: DispatchMode }
}

Options extends the broker options with baseUrl (the remote origin), manifest (the emitted route table), and optional static or lazily-resolved headers merged into every request. CallOptions adds the response consumption mode.

iface

Endpoint

The handle returned by calling an endpoint — pick one of three consumption modes.

signature
interface Endpoint<T> {
  // Codec-decoded full body.
  body(): Future<T>
  // Codec-decoded stream of response chunks (one decoded value per chunk).
  stream<U = StreamItem<T>>(): Future<Stream<U, true | Result.Failure<unknown>>>
  // Raw, undecoded stream of response bytes.
  raw(): Future<Stream<Uint8Array, true | Result.Failure<unknown>>>
}

Every endpoint supports all three modes; each re-enters the broker pipeline with its mode, so they share one request shape and differ only in how the response is read. Stream close is true on a clean end or a Result.Failure mid-stream.

type

Client

The nested, per-route call surface inferred from the app’s `services` object type.

signature
type Client<TServices> = {
  [S in keyof TServices]: TServices[S] extends { actions: infer TActions }
    ? {
        actions: {
          [A in keyof TActions as TActions[A] extends Action ? A : never]: EndpointFn<TActions[A]>
        }
      }
    : never
}

// each action becomes an endpoint factory mirroring its args:
//   (...args) => Endpoint<TReturn>

Passing the app’s Services type to connect<Services> reflects every service and action into a typed surface: api.users.actions.get({ id }) mirrors the backend action’s signature and yields an Endpoint.

type

Stub

A generated client service stub (a real `Service` whose action bodies dispatch remotely).

signature
type Stub<TService> = TService extends { actions: infer TActions }
  ? Service<unknown, [], TActions>
  : never