Client
A typed, standalone client that mirrors the server broker pipeline over `std:fetch`.
@ozaco/server/clientThe 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.
Client
Connect to a remote app and build its stub services.
connect
Install the client broker, register stubs, start, and return the typed call surface.
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.
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()
}
})defineClient
Build the installable stub services from a route `Manifest` (the block `connect` layers on).
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.
ClientBroker
The transport-less broker: the server’s `DefaultBroker` with a direct `std:fetch` dispatch core.
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.
Types
The `ClientDef` namespace: options, manifest, and the inferred call surface.
ClientDef
Options, route manifest, dispatch modes, and the type-level client surface.
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.
Endpoint
The handle returned by calling an endpoint — pick one of three consumption modes.
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.
Client
The nested, per-route call surface inferred from the app’s `services` object type.
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.
Stub
A generated client service stub (a real `Service` whose action bodies dispatch remotely).
type Stub<TService> = TService extends { actions: infer TActions }
? Service<unknown, [], TActions>
: never