Core
The broker, its transport/policy/tracer/gateway protocols, and the service/action model.
@ozaco/server/corecore is the heart of @ozaco/server. It defines the message Broker and the four protocols the broker dispatches through — Transport (where a call is executed), Policy (the resilience onion around every dispatch), Tracer (spans), and Gateway (the single HTTP/WebSocket web surface) — together with the Service/Action model apps are written in.
A Service is a plugin bundling named Actions; defineService/defineAction build them. Broker.actions.call(action, params) resolves the owning service, opens a trace span, resolves the action's per-policy settings once, then runs the policy onion whose innermost layer is the transport dispatch. The bundled InternalTransport runs the action in-process; installing NatsTransport or WorkerTransport (from @ozaco/server/transport/*) routes the same call across a network or a worker with no change to the action.
Every layer is a std:plugin protocol, so implementations are swappable and coexisting: DefaultBroker, DefaultTracer and InternalTransport are the batteries-included impls, and resilience policies (retry, cache, timeout, …) install as Policy implementations sorted into an onion by PolicyPriority.
Actions read their ambient request/response/trace state through scoped Contexts and the use* accessors, and fail with tagged CoreErrors (mapped to HTTP status via CoreStatusMap) rather than throwing.
Service & Action
Author services and their actions.
defineService
Bundle named actions into a `Service` plugin with an optional `setup` context.
function defineService<TContext, TArgs extends unknown[] = [], TActions = unknown>(
options: {
name: string
version: string
description?: string
actions: TActions
isPrivate?: boolean
setup?: (...args: TArgs) => Operation<TContext>
},
): Service<TContext, TArgs, TActions>A Service is a std:plugin tagged with the SERVICE subtype. Its setup runs on install and its actions object holds defineAction handlers. Register a service on the broker (Broker.actions.register) to make its actions callable.
import { defineService, defineAction } from '@ozaco/server/core'
export const Todos = defineService({
name: 'todos',
version: '1.0.0',
actions: {
list: defineAction(function* () {
return yield* db.todos.all()
}),
},
})defineAction
Build a callable `Action` — bare handler, or a config with input/output schemas.
const defineAction: {
// schema-validated: input is parsed, output is validated
<TSchema extends StandardSchemaV1, TReturn>(
config: { input: TSchema } & Partial<Omit<Action.Meta<TSchema>, '_t' | 'input'>>,
handler: (body: StandardSchemaV1.InferOutput<TSchema>) => Operation<TReturn>,
): Action<[StandardSchemaV1.InferOutput<TSchema>], TReturn>
// config without an input schema
<TReturn>(
config: Partial<Omit<Action.Meta<unknown>, '_t' | 'input'>>,
handler: (body?: unknown) => Operation<TReturn>,
): Action<[body?: unknown], TReturn>
// bare handler
<Args extends unknown[], T>(fn: (...args: Args) => Operation<T>): Action<Args, T>
}The action is a function tagged with the ACTION symbol and carrying Action.Meta (title, description, input/output schemas, isPrivate, allow/deny, and lazy per-policy settings). When input is given the body is validated (Standard Schema) before the handler runs and output is validated after.
import { defineAction } from '@ozaco/server/core'
import { bypass } from '@ozaco/server/core'
const get = defineAction(
{
input: UserId,
description: 'Fetch a user by id',
settings: bypass(CachePolicy), // per-action policy tuning
},
function* (id) {
return yield* users.find(id)
},
)Broker
The message broker: register services, call actions, emit/broadcast events.
Broker
The broker protocol — the dispatch entry point every call and event flows through.
const Broker: Protocol<BrokerDef.Context, unknown[], BrokerDef.Actions>
// Broker.actions:
Broker.actions.start(): Future<BrokerDef.Context>
Broker.actions.register(service: Service, name?: string): Future<void>
Broker.actions.call<TArgs, TReturn>(
target: Action<TArgs, TReturn>,
params?: TArgs,
options?: BrokerDef.CallOptions,
): Future<TReturn>
Broker.actions.emit(name: string, payload?: unknown, groups?: ReadonlyArray<string | Service>): Future<void>
Broker.actions.broadcast(name: string, payload?: unknown, groups?: ReadonlyArray<string | Service>): Future<void>
Broker.actions.on(name, listener): Future<() => void>
// plus pause/resume/destroy, getService(s), listActionscall resolves the action's owning service, opens a trace span, resolves the action's policy settings once, and runs the policy onion around a transport dispatch. emit targets a group (round-robin per service group); broadcast fans out to every node. The default implementation is DefaultBroker; the client package ships a transport-less variant.
import { Broker } from '@ozaco/server/core'
import { install } from '@ozaco/std/plugin'
yield* install(Broker) // = DefaultBroker
yield* Broker.actions.register(Todos)
yield* Broker.actions.start()
const todos = yield* Broker.actions.call(Todos.actions.list)DefaultBroker
Batteries-included broker: auto-installs a tracer, codec, and the internal transport.
const DefaultBroker: Plugin<BrokerDef.Context, [options?: BrokerDef.Options], BrokerDef.Actions>On setup it installs DefaultTracer and JsonCodec if none are present, then installs InternalTransport as the in-process dispatch leaf. Options: name, nodeId, shortenCauses (default true), trace (per-policy-layer spans, default false), and initial services.
Transport
Where a resolved dispatch is actually executed — locally or across the wire.
Transport
Cloneable protocol routing `dispatch`/`emit`/`broadcast` across installed transports.
const Transport: Protocol<
TransportDef.Context,
unknown[],
TransportDef.Actions,
TransportDef.Handlers
>
// Transport.actions:
Transport.actions.dispatch(req: TransportDef.DispatchRequest): Future<unknown>
Transport.actions.emit(req: TransportDef.EventRequest): Future<void>
Transport.actions.broadcast(req: TransportDef.EventRequest): Future<void>cloneable: true, so several transports coexist, ordered by priority; a transport's next(failure) predicate decides whether a failure falls through to the next transport in the chain (the internal transport falls through on CoreErrors.NotFound). Handlers (dispatchRoot/emitRoot/broadcastRoot, register/unregister/getTransports) run once as the registry plumbing.
InternalTransport
In-process transport: runs the target action directly on the local broker.
const InternalTransport: Plugin<TransportDef.Context, [options?: TransportDef.Options], TransportDef.Actions>Priority 0 (outermost fall-through). Resolves the service + action on the local broker, runs it in a fresh scope under the call/stream/trace contexts (and a per-action logger child when a Logger is installed), and honours a gateway ResponseSink for streaming byte results. Fails with CoreErrors.NotFound when the service/action is not local, so a higher-priority remote transport can take over.
Policy
The resilience onion wrapped around every dispatch, plus helpers to author policies.
Policy
Cloneable protocol that layers installed policies into an onion around the dispatch core.
const Policy: Protocol<
PolicyDef.Context,
unknown[],
PolicyDef.Actions,
PolicyDef.Handlers
>
// each policy implements:
apply<T>(ctx: PolicyDef.DispatchContext, next: PolicyDef.Next<T>): Future<T>Policies are applied outermost-first, sorted ascending by priority (see PolicyPriority): the lowest number is the outermost layer, and timeout sits innermost on purpose so its thrown CoreErrors.Timeout is observable by the stateful policies (circuit-breaker, metrics, retry, fallback) around it. Each apply receives the resolved DispatchContext and calls next() to descend.
definePolicy
Build a resilience policy from a spec, centralising config/disable, settings and teardown.
function definePolicy<TOptions extends PolicyDef.Options, TContext extends PolicyDef.Context>(
spec: {
key: string
name: string
contextName: string
priority: number
version?: string
setup(options: TOptions | undefined, base: PolicyDef.Context): Operation<TContext>
apply(args: {
dispatch: PolicyDef.DispatchContext
ctx: TContext
override: Partial<TOptions> | undefined
next: PolicyDef.Next<unknown>
}): Operation<unknown>
teardown?(ctx: TContext): void
},
): Policy<TOptions>Handles the boilerplate every policy shares: it wires config/disable (returning per-action Settings), the per-action disable check + typed override lookup, the typed context access, register/unregister and teardown, and optional per-layer tracing. The built policy's install options and config/disable are precisely typed against TOptions.
describePolicyChain
Inspect the installed policies in resolved onion order (outermost first).
function describePolicyChain(): Future<PolicyDef.PolicyChainEntry[]>Read-only. Returns each policy { name, priority } in the order they wrap a dispatch — useful to confirm layering (e.g. that timeout sits innermost). Empty when no policies are installed.
bypass
Disable several policies for one action in a single `settings` entry.
function bypass(
...policies: ReadonlyArray<PolicyDef.DisableablePolicy>
): Future<PolicyDef.Setting<unknown>>[]Policies are independent — disabling the cache does not disable request coalescing — so a "fresh" read bypasses each explicitly. Pass the result as defineAction({ settings: bypass(CachePolicy, BucketPolicy) }, ...).
resolvePolicySettings
Resolve an action’s lazy per-policy settings once per dispatch into a `policy -> setting` map.
function resolvePolicySettings(
action: Action.Meta<unknown> | undefined,
): Operation<Map<string, PolicyDef.Setting>>Runs each lazy settings future once (O(settings), not O(policies × settings)); a setting whose target policy is not installed is skipped rather than failing unrelated policies.
findPolicySetting
O(1) lookup of a setting already resolved by `resolvePolicySettings`.
function findPolicySetting<T>(
dispatchCtx: PolicyDef.DispatchContext,
policyKey: string,
): PolicyDef.Setting<T> | undefinedmakePolicySetting
Construct a per-action policy `Setting` (a disable flag or a partial value).
function makePolicySetting<T>(
policyKey: string,
payload: { disabled: true } | { value: Partial<T> },
): PolicyDef.Setting<T>Tracer
OpenTelemetry-style spans around calls and dispatches.
Tracer
The tracer protocol — `startSpan` opens a span nested under the current trace context.
const Tracer: Protocol<TracerDef.Context, unknown[], TracerDef.Actions>
// Tracer.actions:
Tracer.actions.startSpan(name: string, options?: TracerDef.SpanOptions): Future<TracerDef.Span>A Span carries attributes, events, and status, and reports a SpanSnapshot to the tracer’s onSpanEnd callback when ended. Spans nest via the TraceContext scoped value.
DefaultTracer
In-memory tracer that generates trace/span ids and reports snapshots to `onSpanEnd`.
const DefaultTracer: Plugin<TracerDef.Context, [options?: TracerDef.Options], TracerDef.Actions>Auto-installed by the default broker when no tracer is present. Options: onSpanEnd(snapshot). Span ids come from the installed std:io implementation.
Gateway
The single web protocol: server lifecycle, routing, and REST/WS transformers.
Gateway
One protocol gathering server lifecycle, routing, REST/WS transformers and realtime push.
const Gateway: Protocol<
GatewayDef.Context,
[options?: GatewayDef.Options],
GatewayDef.Actions
>
// Gateway.actions (selection):
Gateway.actions.start(options): Future<{ port: number; host: string }>
Gateway.actions.mount(prefix: string, target: Service | Action): Future<void>
Gateway.actions.rest<T extends RestOptions>(options: T): Future<T & { transformer }>
Gateway.actions.ws<T extends WsOptions>(options: T): Future<T & { method; transformer }>
Gateway.actions.listen(path: string, handlers: GatewayDef.ListenHandlers): Future<void>
Gateway.actions.emit(socketId, message) / broadcast / join / leave / toRoom
// plus toInternal/fromInternal (the cors/plugin hook points), add/remove/find/optimizePlatform impls (BunGateway / NodeGateway, from @ozaco/server/gateway/*) own their server, router (rou3), REST and WS internals behind this one surface, so plugins hook exactly one protocol — e.g. cors does Gateway.before({ *fromInternal }) and docs calls Gateway.actions.mount. rest/ws author per-route settings on an action; listen registers a raw WebSocket endpoint whose handlers get an effect-native Socket.
import { Gateway } from '@ozaco/server/core'
yield* Gateway.actions.mount('/api', Todos)
yield* Gateway.actions.start({ port: 3000 })Context accessors
Read an action’s ambient request/response/trace state from inside its handler.
useRequest
The transport-agnostic `ActionRequest` envelope (method, url, headers, files).
function useRequest(): Operation<ActionRequest>import { useRequest } from '@ozaco/server/core'
const list = defineAction(function* () {
const req = yield* useRequest()
const page = req.url.searchParams.get('page')
// ...
})useResponse
The mutable `ActionResponse` envelope — set status, headers, or body.
function useResponse(): Operation<ActionResponse>useAction
The `Action` currently executing.
function useAction<T = Action>(): Operation<T>useService
The `Service` that owns the current action.
function useService<T = Service>(): Operation<T>useCall
The `BrokerDef.CallContext` for this dispatch (service/action names, raw req/res).
function useCall<T = BrokerDef.CallContext>(): Operation<T>useTrace
The current trace `SpanContext`.
function useTrace<T = TracerDef.Context>(): Operation<T>useStream
The input `Stream`s attached to this call (for streaming requests).
function useStream<T = unknown>(): Operation<Stream<T, void>[]>useRawRequest
The underlying platform request object (Bun/Node `Request`, NATS msg, …).
function useRawRequest<T = unknown>(): Operation<T>useRawResponse
The underlying platform response object, when the transport exposes one.
function useRawResponse<T = unknown>(): Operation<T>useActionSignal
An `AbortSignal` that fires when the action is halted or fails.
function useActionSignal(): Operation<AbortSignal>Contexts
The scoped `Context` slots the accessors above read from. Set them to inject state.
ActionRequestContext
Holds the `ActionRequest` envelope (`useRequest`).
const ActionRequestContext: Context<ActionRequest>ActionResponseContext
Holds the `ActionResponse` envelope (`useResponse`).
const ActionResponseContext: Context<ActionResponse>CallContext
Holds the `BrokerDef.CallContext` for the active dispatch (`useCall`).
const CallContext: Context<BrokerDef.CallContext>ActionContext
Holds the executing `Action` (`useAction`).
const ActionContext: Context<Action>ServiceContext
Holds the owning `Service` (`useService`).
const ServiceContext: Context<Service>TraceContext
Holds the current `SpanContext`; nesting `with` a new value parents child spans.
const TraceContext: Context<TracerDef.SpanContext>StreamContext
Holds the input streams for the call (`useStream`). Defaults to `[]`.
const StreamContext: Context<Stream<unknown, void>[]>ActionRawRequestContext
Holds the raw platform request (`useRawRequest`).
const ActionRawRequestContext: Context<unknown>ActionRawResponseContext
Holds the raw platform response (`useRawResponse`).
const ActionRawResponseContext: Context<unknown>ActionSignalContext
Holds the action `AbortSignal` (`useActionSignal`).
const ActionSignalContext: Context<AbortSignal>ResponseSinkContext
Optional sink a gateway sets so a transport streams a byte-`Stream` result straight to the client.
const ResponseSinkContext: Context<ResponseSink>When present and the action returns a byte Stream, the transport hands the stream to the sink (built through the normal fromInternal transformer) instead of buffering it as a value.
Guards
Narrow unknown values to core shapes.
isService
Narrow a value to a `Service` (a plugin tagged with the `SERVICE` subtype).
function isService(value: unknown): value is ServiceisAction
Narrow a value to an `Action` (a function tagged with the `ACTION` symbol).
function isAction(value: unknown): value is ActionisStreamResult
Whether an action result is a genuine effect `Stream` to pump, not a buffered body.
function isStreamResult<T>(value: unknown): value is Stream<T, unknown>Unlike std:effect’s isStream (which matches any [Symbol.iterator]), this excludes string, Array, and ArrayBuffer views so a buffered body passes through as a value. Every streaming transport must gate on this.
Utilities
Request/response envelope helpers and status mapping.
statusFor
Map an error tag to an HTTP status, consulting overrides then `CoreStatusMap`.
function statusFor(
code: unknown,
...overrides: Array<Record<string, number> | null | undefined>
): numberReturns DEFAULT_STATUS (500) for non-string codes or unmapped tags. Override maps are checked in order before the built-in CoreStatusMap.
createEmptyReq
A blank `internal` `ActionRequest` for dispatches that originate outside HTTP/WS.
function createEmptyReq(): ActionRequestcreateEmptyRes
A blank `ActionResponse` (null status, empty meta/files/body).
function createEmptyRes(): ActionResponsetoRequestEnvelope
Flatten an `ActionRequest` into the serializable envelope carried over a transport.
function toRequestEnvelope(req: ActionRequest): TransportDef.RequestEnvelopefromRequestEnvelope
Rebuild an `ActionRequest` on the far side of a transport.
function fromRequestEnvelope(envelope: TransportDef.RequestEnvelope): ActionRequestFile streams cannot cross the wire, so files comes back empty; url is re-parsed from its string form.
Types
The core contracts. Each `*Def` namespace also exposes Options/Context/Actions.
Action
A callable unit of work carrying validation and policy metadata.
interface Action<TArgs extends unknown[] = any[], TReturn = any> extends Action.Meta<unknown> {
(...args: TArgs): Operation<TReturn>
}
namespace Action {
interface Meta<TSchema> {
_t: typeof ACTION
title?: string
description?: string
input?: TSchema
output?: StandardSchemaV1
isPrivate: boolean
allow: unknown[]
deny: unknown[]
settings: Future<unknown>[]
}
}Service
A `std:plugin` bundling named actions, tagged with the `SERVICE` subtype.
interface Service<TContext = unknown, TArgs extends unknown[] = unknown[], TActions = unknown>
extends Plugin<TContext, TArgs, TActions> {
_st: typeof SERVICE
}BrokerDef
The broker plugin shape and its Options/Context/Actions/EventMap.
type BrokerDef = Plugin<BrokerDef.Context, unknown[], BrokerDef.Actions>The BrokerDef namespace also exposes:
- •
Options—name,nodeId,shortenCauses,trace, initialservices. - •
Context— resolved config plus theservicesmap and eventbus. - •
Actions— start/pause/resume/destroy, register/unregister,call,emit,broadcast,on, getService(s), listActions. - •
CallContext/CallOptions— per-dispatch context and options (streams,rawReq). - •
EventMap— broker/service/event lifecycle events foron.
TransportDef
The transport plugin shape and its request/event envelopes.
type TransportDef = Plugin<TransportDef.Context, unknown[], TransportDef.Actions>The TransportDef namespace also exposes:
- •
Options/Context—name,priority,next(failure)fall-through predicate. - •
DispatchRequest—serviceName,actionKey,params,streams,contexts. - •
DispatchContexts/RequestEnvelope— per-call context propagated across the wire. - •
EventRequest— name/payload/groups for emit/broadcast. - •
Actions(dispatch/emit/broadcast) andHandlers(the registry plumbing).
PolicyDef
The policy plugin shape, its `DispatchContext`, and per-action `Setting`.
type PolicyDef = Plugin<PolicyDef.Context, unknown[], PolicyDef.Actions>The PolicyDef namespace also exposes:
- •
Options/Context—name,priority. - •
Setting<T>— a per-action tuning entry (disabledor partialvalue). - •
DispatchContext— the resolved request, service/action, dispatchkey, callerprincipal,isStreaming, and resolvedsettings. - •
Next<T>— the inner dispatch continuation. - •
Actions(apply, optionalconfig/disable) andHandlers.
TracerDef
Span, span context, attributes, status, and the tracer Options/Actions.
namespace TracerDef {
interface Options { onSpanEnd?(snapshot: SpanSnapshot): void }
interface Span {
spanContext(): Future<SpanContext>
setAttribute(key, value): Future<Span>
setStatus(status: SpanStatus): Future<Span>
addEvent(name, attributes?, startTime?): Future<Span>
recordException(failure, time?): Future<void>
end(endTime?): Future<void>
// ...
}
interface Actions { startSpan(name: string, options?: SpanOptions): Future<Span> }
// SpanContext, SpanStatus, SpanOptions, SpanEvent, SpanSnapshot, SpanAttributes
}GatewayDef
Options/Context/Actions plus the REST/WS route and socket types.
namespace GatewayDef {
interface Options { port?; host?; statusMap?; maxBodyBytes?; reusePort?; simplify? }
interface RestOptions { method: string; path: string; files?; statusMap? }
interface WsOptions { path: string; onOpen?; onMessage?; onClose? }
interface Socket { id; data; rooms; send; join; leave; toRoom; broadcast; spawn }
interface ListenHandlers { open?; message?; close?; on? }
interface Actions { /* start, mount, rest, ws, listen, emit, broadcast, ... */ }
// Context, GatewaySocket, TransformerMeta, RegisteredRoute, ...
}ActionRequest
Transport-agnostic request envelope (`useRequest`).
interface ActionRequest {
type: 'http' | 'ws' | 'rpc' | 'internal'
method: string
url: URL
meta: Record<string, string>
files: Record<string, ActionFile[]>
}ActionResponse
Transport-agnostic, mutable response envelope (`useResponse`).
interface ActionResponse {
status: number | null
meta: Record<string, string>
files: Record<string, ActionFile[]>
body: unknown
}ActionFile
A streamed file extracted from a multipart/form-data body.
interface ActionFile {
name: string
type: string
size: number
lastModified?: number
stream: Stream<Uint8Array, any>
}ResponseSink
A streaming-response sink the gateway installs so a transport can stream bytes back.
interface ResponseSink {
respond(stream: Stream<Uint8Array, unknown>): Operation<void>
}Constants
Error tags, status mapping, policy priorities, subtype symbols, and OTel constants.
CoreErrors
The `server:core.*` error tags actions and the broker fail with.
const CoreErrors: {
Validation: 'server:core.validation'
Forbidden: 'server:core.forbidden'
NotFound: 'server:core.not-found'
Unauthorized: 'server:core.unauthorized'
Exists: 'server:core.exists'
BrokerInternal: 'server:core.broker-internal'
BrokerPaused: 'server:core.broker-paused'
PayloadTooLarge: 'server:core.payload-too-large'
TransportDispatch: 'server:core.transport-dispatch'
CircuitOpen: 'server:core.circuit-open'
Timeout: 'server:core.timeout'
// ...and more
}Built with std:shared createTags. Actions yield* fail(CoreErrors.NotFound, message) instead of throwing, so callers pattern-match on the tag.
CoreStatusMap
Default map from a `CoreErrors`/`CodecErrors` tag to an HTTP status code.
const CoreStatusMap: Record<string, number>
// Validation -> 400, Unauthorized -> 401, Forbidden -> 403, NotFound -> 404,
// Exists -> 409, PayloadTooLarge -> 413, CircuitOpen/BrokerPaused -> 503,
// Timeout -> 504, TransportDispatch -> 502, ...DEFAULT_STATUS
The fallback HTTP status (500) used when no tag matches.
const DEFAULT_STATUS = 500PolicyPriority
Default onion ordering of the built-in policies (lower = outermost).
const PolicyPriority = {
Cache: 0,
Fallback: 5,
Bucket: 10,
Bulk: 20,
Retry: 30,
CircuitBreaker: 40,
Metrics: 50,
Timeout: 60,
} as constTimeout is innermost (highest) on purpose: its thrown CoreErrors.Timeout must surface to the stateful policies (circuit-breaker, metrics, retry, fallback) that sit outside it.
Subtype symbols
Registered symbols tagging each core shape: `ACTION`, `SERVICE`, `BROKER`, `TRANSPORT`, `TRACER`, `POLICY`, `POLICY_SETTING`, `GATEWAY`.
const ACTION: unique symbol // Symbol.for('server:core:action')
const SERVICE: unique symbol // Symbol.for('server:core:service')
const BROKER: unique symbol // Symbol.for('server:core:broker')
const TRANSPORT: unique symbol // Symbol.for('broker:core:transport')
const TRACER: unique symbol // Symbol.for('server:core:tracer')
const POLICY: unique symbol // Symbol.for('server:core:policy')
const POLICY_SETTING: unique symbol // Symbol.for('server:core:policy-setting')
const GATEWAY: unique symbol // Symbol.for('server:core:gateway')OtelAttrs
OpenTelemetry attribute keys, span kinds, and status codes used by the tracer.
const OtelAttrs: {
RPC_SYSTEM: 'rpc.system'
RPC_SERVICE: 'rpc.service'
RPC_METHOD: 'rpc.method'
MESSAGING_SYSTEM: 'messaging.system'
BROKER_NAME: 'broker.name'
SERVICE_NAME: 'service.name'
EXCEPTION_MESSAGE: 'exception.message'
// ...
}
const OtelSpanKind: { INTERNAL: 1; SERVER: 2; CLIENT: 3; PRODUCER: 4; CONSUMER: 5 }
const OtelSpanStatusCode: { UNSET: 0; OK: 1; ERROR: 2 }
const OTEL_RPC_SYSTEM = 'ozaco-broker'
const OTEL_MESSAGING_SYSTEM = 'ozaco-broker'