zokor

Zokor documentation

API reference

Browse public Zokor types, functions, and methods, with signatures and descriptions generated from the source package.

Markdown · llms.txt · api.json

The public package surface contains 589 entries across 38 source files. This page is generated from docs/api.txt, which is generated from zokor/.

auth.sl#

Authentication helpers: bearer JWTs, Basic credentials and API keys.

gc struct JwtAuth { keys: [bytes], audience: str, issuer: str, leeway: int }
jwt_auth(secrets: [str]) -> result[JwtAuth, str]

Secrets of at least 32 bytes each, newest first.

jwt_auth_from(cfg: Config) -> JwtAuth

JWT_SECRETS (comma-separated, newest first), JWT_AUDIENCE, JWT_ISSUER and JWT_LEEWAY (seconds, default 30).

sign_token[T](claims: T, a: JwtAuth) -> result[str, str]

A token for claims, signed with the newest key.

token_problem(token: str, a: JwtAuth, now: int) -> str

Why token is not acceptable at now, or "" when it is.

bearer_claims[S, T](c: Ctx[S], a: JwtAuth) -> result[T, http.Response]

The token's claims as your own type, once the token has passed every check; otherwise the 401 to send.

gc struct BasicCredentials { user: str, password: str }
basic_credentials[S](c: Ctx[S]) -> opt[BasicCredentials]

The credentials of an Authorization: Basic header, or none when there is no such header or it does not decode to...

basic_matches(got: BasicCredentials, user: str, password: str) -> bool

Whether got is user / password.

basic_denied[S](c: Ctx[S], realm: str) -> http.Response

The 401 that asks a browser for Basic credentials for realm.

gc struct ApiKeys { names: [str], digests: [bytes] }
api_keys(entries: [str]) -> result[ApiKeys, str]

name:key entries; each key at least 24 bytes.

api_keys_from(cfg: Config) -> ApiKeys

API_KEYS: comma-separated name:key entries.

api_key_client[S](c: Ctx[S], keys: ApiKeys) -> opt[str]

The name of the client whose key is in the X-Api-Key header, or none.

client.sl#

The client's address: who is actually on the other end of a request.

gc struct TrustedProxies { nets: [int], header: str }
no_trusted_proxies() -> TrustedProxies
parse_ipv4(s: str) -> int

"a.b.c.d" as an unsigned 32-bit value, or -1.

parse_trusted_proxies(list: str) -> result[TrustedProxies, str]

TRUSTED_PROXIES's value: comma-separated IPv4 addresses and CIDRs ("10.0.0.0/8, 192.168.1.10").

ipv4_str(v: int) -> str
TrustedProxies.trusts(ip: int) -> bool
TrustedProxies.is_empty() -> bool
peer_ip_of(rendered: str) -> str

The address part of inspect(link.peer()), "a.b.c.d:port" -> "a.b.c.d".

client_ip_of(peer_ip: str, headers: bytes, tp: TrustedProxies) -> str

Who the request came from.

compression.sl#

Response compression: gzip through slang's compress (zlib), for clients that accept it, on bodies worth it.

gc struct Compression { min_size: int, level: int, types: [str] }
compression() -> Compression
accepts_gzip(header: str) -> bool

Whether an Accept-Encoding value allows gzip.

conditional.sl#

Conditional requests (RFC 9110 section 13): answer "not modified" when the client already has the current version, and refuse...

etag_of(data: bytes) -> str

A strong entity tag for data: its length and 64-bit FNV-1a hash, quoted.

with_validators(resp: http.Response, etag: str, last_modified: opt[timez.Time]) -> http.Response

resp with ETag (when etag is not "") and Last-Modified (when known, to the second: an HTTP date has no fraction).

not_modified(etag: str, last_modified: opt[timez.Time]) -> http.Response

The 304 for a read whose validators matched: no body, and the validators the 200 would have carried.

preconditions[S](c: Ctx[S], etag: str, last_modified: opt[timez.Time]) -> opt[http.Response]

What a conditional request's headers decide, before the handler does its work: some(304) or some(412) to send as is, none to...

config.sl#

Configuration: read once, validated once, then held as a value.

gc struct Problem { key: str, reason: str, shown: str }
gc struct Config { values: map[str]str, problems: [Problem], source: str, profile: str, sources: [str] }
gc struct ConfigLoad { profile: str, directory: str, max_file_bytes: int }

Profile selection and file limits, chosen before startup loading.

config_load() -> ConfigLoad
config_from_values(values: map[str]str) -> Config

Own a copy of supplied values, with no process-state reads: useful for fakes.

default_env_path() -> str { return ".env"; } // Base .env, then .env.<profile>, then one environment snapshot. pub fn load_config() -> Config { return load_config_with(config_load()); } // Explicit dev/test/prod overrides ZOKOR_PROFILE for file selection. pub fn load_config_profile(profile: str) -> Config
load_config_with(options: ConfigLoad) -> Config
load_config_from(path: str) -> Config { return load_config_file(path, config_load().max_file_bytes); } pub fn load_config_file(path: str, max_file_bytes: int) -> Config

Load one explicit file, then apply the captured environment; no profile overlay.

get(c: Config, key: str) -> opt[str]
has_key(c: Config, key: str) -> bool
str_or(c: Config, key: str, fallback: str) -> str
int_or(c: Config, key: str, fallback: int) -> int
bool_or(c: Config, key: str, fallback: bool) -> bool
duration_or(c: Config, key: str, fallback_ns: int) -> int

A duration in nanoseconds, the unit every slang timeout takes.

require(c: Config, key: str) -> str
require_int(c: Config, key: str) -> int
require_range(c: Config, key: str, lo: int, hi: int) -> int
require_port(c: Config, key: str) -> int
require_bool(c: Config, key: str) -> bool
require_float(c: Config, key: str) -> float
require_email(c: Config, key: str) -> str
require_url(c: Config, key: str) -> str
require_dsn(c: Config, key: str) -> str
require_host(c: Config, key: str) -> str
require_ipv4(c: Config, key: str) -> str
require_uuid(c: Config, key: str) -> str
require_one_of(c: Config, key: str, allowed: [str]) -> str
require_duration(c: Config, key: str) -> int
require_min_len(c: Config, key: str, n: int) -> str
require_max_len(c: Config, key: str, n: int) -> str
optional_one_of(c: Config, key: str, allowed: [str], fallback: str) -> str

An optional value that still has to be valid WHEN it is set: the common case of a feature that is off until configured.

optional_url(c: Config, key: str, fallback: str) -> str
optional_dsn(c: Config, key: str, fallback: str) -> str

Same shape as optional_url, for a connection string rather than an HTTP one: is_dsn accepts any scheme with a host...

problems(c: Config) -> [Problem]
is_valid(c: Config) -> bool
check(c: Config) -> result[bool, str]

Every problem, one per line, named and explained.

cookies.sl#

Cookies.

enum SameSite { LAX, STRICT, NONE }
gc struct Cookie { name: str, value: str, path: str, domain: str, max_age: int, expires: opt[timez.Time], secure: bool, http_only: bool, same_site: SameSite, partitioned: bool }
parse_cookies(header: str) -> map[str]str

Every well-formed name=value pair in a Cookie header, in order; a pair without =, with a bad name or with bytes a cookie...

gc struct CookieKeys { keys: [bytes] }
constant_time_equal(a: bytes, b: bytes) -> bool

Compares in time that depends only on the lengths, so a forged signature cannot be found a byte at a time by timing the answer.

signed_value[S](c: Ctx[S], name: str, keys: CookieKeys) -> opt[str]

The value of signed cookie name when its signature matches any key, none when it is missing, unsigned or forged.

verify_signed(name: str, raw: str, keys: CookieKeys) -> opt[str]

cors.sl#

CORS.

gc struct CorsPolicy { origins: [str], methods: [str], headers: [str], expose: [str], credentials: bool, max_age: int }
no_cors() -> CorsPolicy

Off: no origins.

cors_policy(origins: [str]) -> CorsPolicy

The defaults, for these origins.

cors_problem(p: CorsPolicy) -> str

What is wrong with p, or "" when it is usable.

cors_from(cfg: Config) -> CorsPolicy

CORS_ORIGINS (comma-separated), CORS_METHODS, CORS_HEADERS, CORS_EXPOSE (comma-separated, replacing the defaults),...

gc struct Cors { on: bool, any: bool, credentials: bool, origins: [str], allow_methods: str, allow_headers: str, allow_headers_lower: [str], expose: str, max_age: str }

What the router keeps: the policy, and the header values built once.

cors_off() -> Cors
Cors.allows(origin: str) -> bool

Whether origin (an Origin header's value) is allowed.

Cors.apply(resp: http.Response, origin: str) -> http.Response

The headers for an ordinary response to a request with this Origin ("" when it had none).

Cors.preflight(origin: str) -> http.Response

The answer to a preflight from origin.

is_preflight(method: str, raw_headers: bytes) -> bool

Whether a request is a CORS preflight: OPTIONS, with an Origin and the method the real request will use.

errors.sl#

Failure, as one shape.

gc struct Code { name: str, status: i32, message: str }
gc struct FieldError { field: str, reason: str }
gc struct ErrorView { code: str, message: str, status: i32, request_id: str, fields: [FieldError], trace_id: str }

Everything the renderer is given.

gc struct Registry { codes: map[str]Code, render: fn(ErrorView) -> http.Response, trace_id: str }
new_registry() -> Registry

Every HTTP failure status, under the name a handler will reach for.

set_renderer(r: Registry, f: fn(ErrorView) -> http.Response) -> int

Replace the shape every failure is rendered in.

register(r: Registry, name: str, status: i32, message: str) -> int
lookup(r: Registry, name: str) -> opt[Code]
status_of(r: Registry, name: str) -> i32

A code nobody registered is a bug in the service, not a client error: it answers 500 and says which name was missing, so the...

message_of(r: Registry, name: str) -> str
code_names(r: Registry) -> [str]
respond(r: Registry, code: str, request_id: str) -> http.Response

The registered wording.

respond_with(r: Registry, code: str, detail: str, request_id: str) -> http.Response

The registered code, with wording for this one occurrence ("no such organisation 'acme'").

respond_fields(r: Registry, code: str, fields: [FieldError], request_id: str) -> http.Response
field(name: str, reason: str) -> FieldError
default_render(v: ErrorView) -> http.Response

zokor's own shape, and the one a service gets until it says otherwise.

quote(s: str) -> str

A JSON string, escaped.

quote_into(sb: builder.Str, s: str) -> builder.Str

The same, appended to a builder.

status_text(status: i32) -> str

files.sl#

Static files: a directory served under a path prefix, safely.

gc struct FileMount { prefix: str, root: str, index: str, max_age: int, immutable: bool, max_size: int }
files_at(prefix: str, dir: str) -> result[FileMount, str]

A mount of dir under prefix.

content_type_for(name: str) -> str
parse_range(header: str, size: int) -> [int]

One byte range of a file of size: [first, last] inclusive; [-1, -1] when the header is not one usable range (several,...

health.sl#

Health and readiness.

gc struct ReadyCheck[S] { name: str, check: fn(S) -> result[int, str], lock: mutex, started: int, finished: int, passed: bool, reported: str }
gc struct Readiness[S] { live_path: str, ready_path: str, checks: [ReadyCheck[S]], timeout: int, logger: Logger }
default_ready_timeout() -> int

0.9s: inside Kubernetes' default 1s probe timeout, so a slow dependency is answered as a 503 naming it rather than as a probe...

check_name_ok(name: str) -> bool

A check name is 1-64 of [A-Za-z0-9_.-]: it is written into the JSON body and to stderr as is.

idempotency.sl#

Idempotency keys (the IETF Idempotency-Key header): a client retrying a POST or PATCH after a timeout gets the first answer...

gc struct Idempotency { ttl: int, lock_ttl: int, max_body: int, scope_local: str, required: bool, shards: [IdemShard], remote: opt[redis.Pool] }
idempotency_in_memory() -> Idempotency

Answers kept in this process: right for one instance.

idempotency_redis(p: redis.Pool) -> Idempotency

Answers kept in Redis, shared by every instance behind the balancer.

ids.sl#

Identifiers.

new_request_id() -> str

128 bits of randomness, hex, prefixed so it is recognisable in a log that carries other people's ids too.

new_session_id() -> str

A longer one, for a session or a socket.io sid, where a collision matters more than the length does.

request_id_for(headers: bytes) -> str

The id a request is answered and logged under: the client's own X-Request-Id when it is 1-64 characters of [A-Za-z0-9._-], so...

jobs.sl#

Background jobs: work on a schedule, and work queued to run later.

gc struct JobCtx[S] { state: S, name: str, payload: str, attempt: int, due: timez.Time, deadline: until }

What a run gets: the service's state, which run this is, and the deadline to pass to everything that can block.

gc struct Jobs[S] { state: S, zone: timez.Zone, clock: timez.Clock, logger: Logger, default_timeout: int, scheduled: [ScheduledJob[S]], kinds: [JobKind[S]], queue: [QueuedJob], lock: mutex, in_flight: int, stopping: bool, failures: int }
new_jobs[S](state: S) -> Jobs[S]
job_backoff(attempt: int) -> int

The wait before try attempt + 1: 1 s, 2 s, 4 s ...

Jobs.every(name: str, spec: str, run: fn(JobCtx[S]) -> result[int, str], timeout: int) -> result[int, str]

Runs run on the cron schedule spec (see timez.parse_cron), each run given timeout nanoseconds as its deadline.

Jobs.handle(kind: str, run: fn(JobCtx[S]) -> result[int, str], attempts: int) -> result[int, str]

Registers what runs a queued job of kind, and how many tries it gets (at least 1).

Jobs.enqueue(kind: str, payload: str, at: timez.Time) -> result[int, str]

Queues one run of kind with payload, due at at (now or in the past runs at the next tick).

Jobs.queued() -> int

Queued runs not yet started.

Jobs.running() -> int

Runs started and not yet finished.

Jobs.tick(now: timez.Time) -> int

Starts every run due at now -- scheduled turns and queued jobs -- each in its own task, and returns how many it started.

Jobs.start() -> int

Starts the scheduler: a task that ticks at each due time, and at least every quarter second so a stop or SIGTERM is seen quickly.

Jobs.stop() -> int

Stops starting runs.

Jobs.wait_runs(timeout: int) -> bool

Waits until no run is in progress, or timeout nanoseconds pass; true when none is.

json.sl#

JSON, for the shapes you know and the ones you do not.

enum JsonKind { Null, Bool, Number, String, Array, Object }
gc struct Json { kind: JsonKind, bool_val: bool, raw: str, text: str, items: [Json], keys: [str], values: [Json] }
jnull() -> Json
jbool(v: bool) -> Json
jint(v: int) -> Json
jfloat(v: float) -> Json
jstr(v: str) -> Json
jobj() -> Json
jarr() -> Json
user_json(id: str) -> bytes
user_json_bytes(id: bytes) -> bytes

user_json_bytes: the id as BYTES (already sliced out of a path).

message_json(message: str) -> bytes
Json.set(key: str, v: Json) -> Json

Sets a key, replacing it if present, and returns the object so calls chain.

Json.set_str(key: str, v: str) -> Json
Json.set_int(key: str, v: int) -> Json
Json.set_float(key: str, v: float) -> Json
Json.set_bool(key: str, v: bool) -> Json
Json.set_opt_str(key: str, v: opt[str]) -> Json

Only when the value is there: the difference between "absent" and "null" belongs to the caller, not to the encoder.

Json.add(v: Json) -> Json
Json.add_str(v: str) -> Json
Json.add_int(v: int) -> Json
Json.get(key: str) -> Json

A missing key, or an index that is not there, gives Null rather than an error -- so a chain of lookups never has to be guarded...

Json.at(i: int) -> Json
Json.path(p: str) -> Json

"user.address.city", "items.0.name": one call instead of five.

Json.has_key(key: str) -> bool

Present, even if its value is null -- which is how a PATCH tells "set this to nothing" from "leave it alone".

Json.is_null() -> bool
Json.is_object() -> bool
Json.is_array() -> bool
Json.is_string() -> bool
Json.is_number() -> bool
Json.is_bool() -> bool
Json.size() -> int

Items in an array, keys in an object, bytes in a string.

Json.keys_of() -> [str]
Json.items_of() -> [Json]
Json.str_or(fallback: str) -> str
Json.int_or(fallback: int) -> int
Json.float_or(fallback: float) -> float
Json.bool_or(fallback: bool) -> bool
Json.render() -> str

The JSON text of this value.

Json.render_bytes() -> bytes

Renders straight into a Bytes builder: no two-element parts list, no join, no str in between.

parse(text: str) -> result[Json, str]
snake_keys(text: str) -> str

slang has no struct tags, so a wire name is a field name.

camel_keys(text: str) -> str
to_snake_case(s: str) -> str
to_camel(s: str) -> str
parse_body(content_type: str, body: bytes) -> result[Json, FormError]

The body as a value you can walk, with the failure already carrying a registered code.

decode_failed(reg: Registry, why: str, request_id: str) -> http.Response

The failure of slang's own json.decode, mapped into the envelope with the offending field named where the message carries...

gc struct Checker { fields: [FieldError] }

Decoding says a field is a string; it does not say the string is an email, or between 3 and 40 characters, or one of four words.

checker() -> Checker
Checker.failed() -> bool
Checker.note(field: str, reason: str) -> int
Checker.req_str(j: Json, key: str, min: int, max: int) -> str

A required string, with a length range.

Checker.opt_str(j: Json, key: str, max: int, fallback: str) -> str
Checker.req_int(j: Json, key: str, min: int, max: int) -> int
Checker.opt_int(j: Json, key: str, min: int, max: int, fallback: int) -> int
Checker.req_bool(j: Json, key: str) -> bool
Checker.req_email(j: Json, key: str) -> str
Checker.req_url(j: Json, key: str) -> str
Checker.req_uuid(j: Json, key: str) -> str
Checker.req_one_of(j: Json, key: str, allowed: [str]) -> str
Checker.req_strs(j: Json, key: str, max_items: int) -> [str]

A list of strings, with a cap on how many: the shape of "tags", "recipients", "ids".

Checker.req_obj(j: Json, key: str) -> Json

A nested object, so a checker can walk into it and report "address.city" rather than "address".

Checker.under(prefix: str, other: Checker) -> int

Everything found so far, re-reported under a prefix: for a nested object checked by its own function.

log.sl#

Logging.

enum LogLevel { DEBUG, LOG, WARN, ERROR, OFF }
level_rank(l: LogLevel) -> int
level_name(l: LogLevel) -> str
parse_level(s: str) -> opt[LogLevel]
gc struct LogEvent { time: int, level: LogLevel, context: str, message: str, request: bool, method: str, route: str, path: str, status: int, duration_ns: int, size: int, request_id: str, client_ip: str, trace_id: str, ack: opt[chan[int]] }
gc struct Logger { events: opt[chan[LogEvent]], level: LogLevel, requests: bool, console: bool, json: bool, color: bool, capacity: int, lock: mutex, dropped: int, no_ack: opt[chan[int]] }
default_log_capacity() -> int

How many events may wait for the writer: enough to absorb a burst of a few thousand requests between two wake-ups.

no_logger() -> Logger

Logs nothing.

console_logger(json: bool, color: bool, level: LogLevel, requests: bool) -> Logger

zokor's own writer, to stdout, configured but not started: start (which listen_and_serve_with calls) makes the channel and...

channel_logger(ch: chan[LogEvent], level: LogLevel, requests: bool) -> Logger

A logger whose events go to ch, drained by the caller's own task.

default_logger() -> Logger

The default: pretty and coloured on a terminal, JSON otherwise, level LOG, request lines on.

logger_from(cfg: Config) -> Logger

LOG_FORMAT, LOG_LEVEL, LOG_REQUESTS and NO_COLOR over the default.

Logger.start()

Starts the console writer if this is an unstarted console logger; otherwise does nothing.

Logger.own_tasks() -> int

Tasks this logger runs for itself: 1 once the console writer is started.

Logger.enabled(l: LogLevel) -> bool

Whether an event at l would be sent at all: lets a caller skip building one.

Logger.emit(ev: LogEvent)

Offers ev without waiting: dropped and counted when the channel is full or below the level.

Logger.record_request(method: str, route: str, raw_path: str, status: int, duration_ns: int, size: int, request_id: str, client_ip: str)

The serve loop's call: one request's record, if requests are logged at its level.

Logger.debug(context: str, message: str)
Logger.log(context: str, message: str)
Logger.warn(context: str, message: str)
Logger.error(context: str, message: str)
Logger.take_dropped() -> int

Events dropped and not yet reported; resets the count.

Logger.flush(timeout_ns: int)

Waits until everything emitted before this call is written, or timeout_ns passes.

request_event(method: str, route: str, raw_path: str, status: int, duration_ns: int, size: int, request_id: str, client_ip: str) -> LogEvent

A request's record.

logs_requests(lg: Logger, status: int) -> bool

Whether the serve loop should build a request record at all.

safe_path(raw: str) -> str

A client-supplied path made safe to print: the query dropped, at most 64 bytes kept, and every byte outside printable ASCII --...

utc_parts(unix_ns: int) -> [int]

Unix nanoseconds as UTC civil time: [year, month, day, hour, minute, second, millisecond].

gc struct LogFormat { pid: str, three: [str], plain: [str], sec: int, stamp_t: str, stamp_sp: str, quoted: map[str]str }

Formatting state, made once and reused for every line: tables of the numbers a line needs (so a status, a size or a...

new_log_format(pid: int) -> LogFormat
LogFormat.stamp(unix_ns: int, t: bool) -> str

"2026-10-12T18:47:01.123Z" (t) or "2026-10-12 18:47:01.123Z".

LogFormat.json_into(sb: builder.Str, ev: LogEvent) -> builder.Str

One JSON object and a newline, appended to sb.

LogFormat.pretty_into(sb: builder.Str, ev: LogEvent, color: bool, since_ns: int) -> builder.Str

The NestJS layout and a newline, appended to sb: [zokor] <pid> - <utc time> LOG [Context] message +3.21ms The...

LogFormat.json(ev: LogEvent) -> str

The same as single strings, without the newline: for a consumer that wants one line at a time.

LogFormat.pretty(ev: LogEvent, color: bool, since_ns: int) -> str

metrics.sl#

Opt-in Prometheus metrics with bounded request series and custom scalars.

gc struct MetricsConfig { path: str, max_series: int, max_custom: int, buckets_ns: [int] }
metrics_config() -> MetricsConfig
enum MetricKind { Counter, Gauge }
gc struct Metric { name: str, help: str, kind: MetricKind, lock: mutex, value: int }
gc struct Metrics { path: str, max_series: int, max_custom: int, bounds: [int], lock: mutex, active: int, dropped: int, routes: map[str]int, series_index: map[int]int, series: [RequestSeries], custom: [Metric] }
new_metrics(cfg: MetricsConfig) -> result[Metrics, str]

Copies configuration so later caller mutations cannot change bucket shapes.

Metrics.enter()
Metrics.leave(method: str, route: str, status: int, ns: int)

One completion for each enter, including a panic or failed socket write.

Metrics.counter(name: str, help: str) -> result[Metric, str]
Metrics.gauge(name: str, help: str) -> result[Metric, str]
Metric.add(delta: int) -> result[bool, str]

Counters only increase; gauges can add a signed delta.

Metric.set(value: int) -> result[bool, str]
Metric.read() -> int
metrics_text(m: Metrics) -> str

A consistent snapshot is copied under the lock; text formatting happens after unlocking so scrapes do not hold up request...

migrations.sl#

Forward-only SQL migrations: immutable history and an atomic pending batch.

gc struct Migration { version: int, name: str, sql: str, checksum: str }
gc struct MigrationLimits { max_files: int, max_file_bytes: int, max_total_bytes: int }

Limits for reading files, chosen by the caller before startup.

migration_limits() -> MigrationLimits
migration_checksum(sql: str) -> str
new_migration(version: int, name: str, sql: str) -> Migration
load_migrations(dir: str, limits: MigrationLimits) -> result[[Migration], str]

Load N_description.sql files, sort numerically, and reject duplicates.

migrate(p: pg.Pool, ms: [Migration], deadline: until) -> result[int, str]

Validate all historical files, then apply the pending batch in one transaction under a database-wide advisory lock.

negotiation.sl#

Startup-compiled representations and bounded RFC 9110 Accept negotiation.

gc struct AcceptLimits { max_bytes: int, max_ranges: int, max_params: int, max_offers: int, max_offer_bytes: int }
accept_limits() -> AcceptLimits
gc struct Negotiator { offers: [str], types: [str], subtypes: [str], params: [[str]], max_bytes: int, max_ranges: int, max_params: int }

Treat the compiled vectors as immutable after startup.

new_negotiator(offers: [str], limits: AcceptLimits) -> result[Negotiator, str]

Offer order breaks otherwise equal preferences.

Negotiator.choose(header: opt[str]) -> result[opt[str], str]

none means no acceptable offer; err means malformed Accept.

negotiate[S](c: Ctx[S], n: Negotiator) -> result[str, http.Response]

A selected media type, or a registered 400/406 response.

openapi.sl#

Startup OpenAPI generation from registered routes and explicit wire schemas.

gc struct OpenApiLimits { max_routes: int, max_schemas: int, max_schema_bytes: int, max_total_schema_bytes: int, max_schema_depth: int, max_document_bytes: int, max_collection_entries: int, max_metadata_bytes: int }

Resource limits for startup metadata and the generated document.

openapi_limits() -> OpenApiLimits
gc struct ApiParameter { name: str, location: str, schema: str, required: bool, description: str }

schema is a registered DTO name or a primitive JSON Schema type.

gc struct ApiResponse { status: i32, description: str, schema: str, media_type: str }

An empty schema declares a response without a body.

gc struct ApiOperation { method: Method, pattern: str, operation_id: str, summary: str, description: str, tags: [str], parameters: [ApiParameter], request_schema: str, request_media_type: str, request_required: bool, responses: [ApiResponse], errors: [str] }

Metadata cannot create routes: method/pattern must occur in routes_list.

gc struct OpenApi { title: str, version: str, description: str, server_urls: [str], schemas: map[str]str, operations: [ApiOperation], omitted: [str], error_schema: str, error_media_type: str, limits: OpenApiLimits }

Configure before generation.

new_openapi(title: str, version: str) -> OpenApi
api_operation(method: Method, pattern: str) -> ApiOperation
api_parameter(name: str, location: str, schema: str) -> ApiParameter
api_response(status: i32, schema: str) -> ApiResponse
OpenApi.schema(name: str, definition: str) -> int

A JSON Schema definition, parsed within limits at generation.

OpenApi.operation(op: ApiOperation) -> int
OpenApi.omit(method: Method, pattern: str) -> int

Explicitly exclude an unsupported or private route from this document.

api_default_error_schema() -> str

Embed this definition when the application's renderer uses the default envelope.

api_ref(name: str) -> str

A self-contained reference, or an inline primitive schema.

openapi_document(routes: [str], reg: Registry, api: OpenApi) -> result[str, str]

Generate at startup; returns every detected metadata problem together.

pagination.sl#

Pagination: offset pages and keyset (cursor) pages, one envelope for both, so a client reads every list the same way:

struct Page { limit: int, offset: int, cursor: str, by_cursor: bool }

What the request asked for, checked.

Page.fetch() -> int

How many rows to ask the database for: one more than the page, so the response knows whether there is a next page without a count.

page_of[S](c: Ctx[S], default_limit: int, max_limit: int) -> result[Page, http.Response]

The page c asks for: limit (default default_limit, at most max_limit), and either offset or cursor.

encode_cursor(parts: [str]) -> str

The opaque cursor for a row whose sort key is parts.

decode_cursor(cursor: str) -> result[[str], str]

The key parts of a cursor, err when it is not one encode_cursor made.

cursor_parts(p: Page) -> [str]

The key parts of the cursor p came with ([] on the first page).

offset_page[S, T](c: Ctx[S], p: Page, rows: [T], total: opt[int]) -> http.Response

An offset page of rows -- up to p.fetch() of them; one past the limit means there is a next page -- in the envelope, with Link.

cursor_page[S, T](c: Ctx[S], p: Page, rows: [T], key: fn(T) -> [str]) -> http.Response

A keyset page of rows -- up to p.fetch(); one past the limit means there is a next page, whose cursor is key of the last...

postgres.sl#

Postgres, wired to zokor's config, results and error envelope.

pg_from_config(cfg: Config) -> result[pg.Pool, str]

DATABASE_URL (postgres://user:pass@host:port/db?sslmode=require) and DB_POOL_SIZE (default 10).

has_postgres(cfg: Config) -> bool
pg_close(p: pg.Pool)
row_json(rows: pg.Rows, r: int) -> str

Row r as a JSON object keyed by column name.

rows_as[T](rows: pg.Rows) -> result[[T], str]

Every row as a T, matched by column name without intermediate JSON text.

query_all[T](p: pg.Pool, sql: str, args: [pg.Arg], deadline: until) -> result[[T], str]

Runs sql with args on a pooled connection and returns every row as a T.

query_one[T](p: pg.Pool, sql: str, args: [pg.Arg], deadline: until) -> result[opt[T], str]

The first row as a T, none when the query returned no row.

conn_query_all[T](c: pg.Conn, sql: str, args: [pg.Arg], deadline: until) -> result[[T], str]

The same on a connection you hold -- inside a transaction body.

transaction[A, R](p: pg.Pool, arg: A, body: fn(pg.Conn, A) -> result[R, str], deadline: until) -> result[R, str]

Runs body(conn, arg) inside BEGIN ...

is_unique_violation(e: str) -> bool
db_failure(reg: Registry, e: str, request_id: str) -> http.Response

A database error as the answer to send, in the service's envelope.

ratelimit.sl#

Rate limiting.

gc struct RateShard { hits: map[str][int], lock: mutex, since_sweep: int }
gc struct RateLimiter { limit: int, window: int, shards: [RateShard], sweep_every: int }
new_rate_limiter(limit: int, window_ns: int) -> RateLimiter

At most limit requests per client in any window_ns nanoseconds (1000000000 is one second).

retry_after_secs(wait_ns: int) -> int

Whole seconds until wait_ns has passed, never below 1: a Retry-After of 0 invites an immediate retry that will be refused.

struct RateCheck { wait: int, remaining: int, reset: int }

One check's answer, for the RateLimit-* headers.

RateLimiter.take_at(key: str, now: int) -> int

Counts one request for key at now (time.mono nanoseconds).

RateLimiter.check_at(key: str, now: int) -> RateCheck

take_at, with what is left: what the RateLimit-* headers report.

RateLimiter.take(key: str) -> int

The same, now.

RateLimiter.tracked() -> int

How many client keys are being tracked.

rate_limit[S](c: Ctx[S], l: RateLimiter, key: str) -> opt[http.Response]

The body of a rate-limiting before: none to continue, or the registered rate_limited failure (429 unless re-registered)...

rate_limit_client[S](c: Ctx[S], l: RateLimiter) -> opt[http.Response]

rate_limit keyed on the client's address: the socket's peer, or, behind a proxy listed in TRUSTED_PROXIES, the address that...

redirect.sl#

Checked redirects for variable destinations; redirect remains the raw constructor.

gc struct RedirectRules { max_location_bytes: int, allow_external: bool }
redirect_rules() -> RedirectRules

Relative references by default.

redirect_checked(status: i32, location: str, rules: RedirectRules) -> result[http.Response, str]

Validate status and destination together without including destination secrets in errors.

redirect_to(location: str) -> result[http.Response, str]

A checked 303 for the usual POST/redirect/GET flow, using relative references.

redis.sl#

Redis, wired to zokor's config system.

redis_from_config(cfg: Config) -> result[redis.Pool, str]

REDIS_URL, read the same way every other optional dependency in this package is: config.optional_dsn, so a malformed value (a...

has_redis(cfg: Config) -> bool

true iff REDIS_URL is set -- for a service that wants to know whether to wire Redis-backed features in at all before trying to...

redis_close(p: redis.Pool)

Closes every idle connection and marks the pool closed; an in-flight acquire finishes normally, and the next one fails with...

respond.sl#

Success responses.

json_response(status: i32, body: str) -> http.Response
json_response_bytes(status: i32, body: bytes) -> http.Response

json_response with a BYTES body: same shape, no to_bytes copy.

ok_json(body: str) -> http.Response

ok is a slang builtin (the result constructor), so the 200 helper is named for what it sends.

ok_json_bytes(body: bytes) -> http.Response

ok_json with a BYTES body: the render_bytes path -- no str, no to_bytes, no copy between the renderer and the socket.

created(body: str, location: str) -> http.Response
created_bytes(body: bytes, location: str) -> http.Response
accepted(body: str) -> http.Response
accepted_bytes(body: bytes) -> http.Response

accepted with a BYTES body: same shape, no to_bytes copy.

no_content() -> http.Response
text(status: i32, body: str) -> http.Response
text_bytes(status: i32, body: bytes) -> http.Response

text with a BYTES body: same shape, no to_bytes copy.

html(status: i32, body: str) -> http.Response
html_bytes(status: i32, body: bytes) -> http.Response

html with a BYTES body: same shape, no to_bytes copy.

bytes_of(status: i32, content_type: str, body: bytes) -> http.Response
redirect(status: i32, location: str) -> http.Response

303 after a POST, 307/308 to preserve the method, 301/302 for the older shapes.

with_header(r: http.Response, name: str, value: str) -> http.Response
no_store(r: http.Response) -> http.Response

Cache-Control in the two forms a service actually needs.

cache_for(r: http.Response, seconds: int) -> http.Response

router.sl#

The router: a request goes in, a response comes out, and the application's own state comes with it.

enum Method { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT }

The HTTP methods, as a closed set.

gc struct Ctx[S] { state: S, req: http.Request, params: opt[map[str]str], param1_name: str, param1_value: str, route: str, request_id: str, client_ip: str, out: opt[[str]], locals: opt[map[str]str], errors: Registry, upgrade: opt[Socket] }
struct Served { resp: http.Response, route: str, upgrade: opt[Socket] }

A response and the route pattern that produced it, for the request log.

gc struct Route[S] { method: Method, pattern: str, befores: [fn(Ctx[S]) -> opt[http.Response]], afters: [fn(Ctx[S], http.Response) -> http.Response], segs: [str], names: [str], wild: int, fkind: int, fpath: str, handler: fn(Ctx[S]) -> http.Response, has_static: bool, static_status: i32, static_ctype: str, static_body: bytes, static_rendered: bytes, static_hit: opt[http.StaticBody], static_close_hit: opt[http.StaticBody], no_map: opt[map[str]str], ws: int }
gc struct Router[S] { routes: [Route[S]], befores: [fn(Ctx[S]) -> opt[http.Response]], afters: [fn(Ctx[S], http.Response) -> http.Response], state: S, errors: Registry, auto_options: bool, auto_head: bool, ready: Readiness[S], cors: Cors, security: Security, sanitize: SanitizeMode, auto_etag: bool, rindex: [RNode], static_paths: map[str]int, idem: opt[Idempotency], gzip: opt[Compression], mounts: [FileMount], no_lines: opt[[str]], observations: opt[Metrics], tracer: opt[Tracer], no_tracer: opt[Tracer], request_locals: opt[map[str]str], sockets: [WsRoute[S]], no_socket: opt[Socket] }

A before runs ahead of the handler and may end the request by returning a response; none carries on.

new_router[S](state: S) -> Router[S]

An application builds its router from its state alone; everything else starts empty or at the default a correct server should...

new_group[S](r: Router[S], prefix: str) -> Group[S]

The function-call equivalent of r.group(prefix), for a service that wants its setup to read as a list of constructors rather...

Router.handle(method: Method, pattern: str, h: fn(Ctx[S]) -> http.Response) -> int

Every method goes through here, including the ones with their own helper below, so a pattern is split exactly once and the...

Router.handle_with(method: Method, pattern: str, h: fn(Ctx[S]) -> http.Response, befores: [fn(Ctx[S]) -> opt[http.Response]], afters: [fn(Ctx[S], http.Response) -> http.Response]) -> int

The one registration path: a pattern is split exactly once, and the rules about :params and *rest live here alone.

Router.static_bytes(p: str, status: i32, content_type: str, body: bytes, h: fn(Ctx[S]) -> http.Response) -> int

The full set of HTTP methods, so no service has to fall back to handle with a string for anything standard.

Router.websocket(p: str, open: fn(Ctx[S]) -> http.Response, w: WsRoute[S]) -> int

A WebSocket route (ws_serve.sl).

Router.socketio(p: str, open: fn(Ctx[S]) -> http.Response, s: SioRoute[S]) -> int

A Socket.IO route (sio_serve.sl), usually at "/socket.io/".

Router.get(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.head(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.post(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.put(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.patch(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.delete(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.options(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.trace(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.connect(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Router.any(methods: [Method], p: str, h: fn(Ctx[S]) -> http.Response) -> int

One handler for several methods on one pattern.

Router.group(prefix: str) -> Group[S]

A prefix with its own middleware.

Router.guarded(method: Method, pattern: str, h: fn(Ctx[S]) -> http.Response, befores: [fn(Ctx[S]) -> opt[http.Response]]) -> int

Middleware for one route, which is the other half of the same need: `r.guarded(Method.DELETE, "/orgs/:id", drop,...

Router.before(f: fn(Ctx[S]) -> opt[http.Response]) -> int
Router.after(f: fn(Ctx[S], http.Response) -> http.Response) -> int
Router.etags() -> int

Tags every 200 answer to a GET or HEAD with a strong ETag -- a hash of the body it sends -- and answers a matching...

Router.idempotency(store: Idempotency) -> int

Answers a retried POST or PATCH that carries an Idempotency-Key with the first answer, from store (see idempotency.sl).

Router.compress(z: Compression) -> int

Gzips answers worth it for clients that accept it (see compression.sl).

Router.files(m: FileMount) -> int

Serves the directory of m under its prefix (see files.sl): a GET or HEAD no route claims.

Router.security_headers(h: SecurityHeaders) -> int

Replaces the security headers (see security.sl); no_security_headers() turns them off.

Router.mongo_sanitize(mode: SanitizeMode) -> int

Turns the operator-injection guard on (REJECT, STRIP) or off; see sanitize.sl.

Router.cors(p: CorsPolicy) -> int

Turns CORS on with p (see cors.sl).

Router.health() -> int

Answers GET /healthz (liveness) and GET /readyz (the checks below).

Router.health_at(live_path: str, ready_path: str) -> int

The same at paths of the service's choosing.

Router.ready_check(name: str, f: fn(S) -> result[int, str]) -> int

Registers a dependency /readyz waits on.

Router.ready_timeout(ns: int) -> int

How long /readyz waits for its checks, in nanoseconds; below 1ms is raised to 1ms.

Router.routes_list() -> [str]

"METHOD /pattern" per route, in registration order: for a boot log, for zokor check, and for a test that pins down the...

Router.metrics(m: Metrics) -> int

Opt-in Prometheus endpoint; a service's explicit route still wins.

Router.tracing(driver: Tracer) -> int
Router.allowed(p: str) -> [Method]

The methods registered for a path, for Allow on a 405 and for an automatic OPTIONS.

Router.serve(req: http.Request) -> http.Response

Dispatch: befores, the handler, then afters -- and the afters run even when a before ended the request, so a logging or header...

Router.match_static(f: http.WireFrame) -> opt[http.StaticBody]

Frame dispatch: route on the ALREADY-PARSED method/path strs, with no raw bytes, no offsets, no segs list, no params map...

Router.serve_static(f: http.WireFrame) -> result[http.StaticBody, http.Response]
Router.serve_frame(f: http.WireFrame, request_id: str) -> http.Response

serve() stays for callers that already hold a Request (tests, middleware fixtures); serve_conn uses this via serve_static.

Router.serve_frame_for(f: http.WireFrame, request_id: str, client_ip: str) -> http.Response

serve_frame, with the client's address for Ctx.client_ip.

Router.serve_frame_routed(f: http.WireFrame, request_id: str, client_ip: str) -> Served

What the serve loop calls: the response, and the route pattern that answered it for the request log ("" when none did).

Router.serve_id(req: http.Request, request_id: str) -> http.Response
Router.serve_id_for(req: http.Request, request_id: str, client_ip: str) -> http.Response
Router.serve_id_routed(req: http.Request, request_id: str, client_ip: str) -> Served
Router.fail(c: Ctx[S], code: str) -> http.Response

The registry's codes, rendered with this request's id.

Router.fail_with(c: Ctx[S], code: str, detail: str) -> http.Response
Router.fail_fields(c: Ctx[S], code: str, fields: [FieldError]) -> http.Response
join_methods(ms: [Method]) -> str
gc struct Group[S] { router: Router[S], prefix: str, befores: [fn(Ctx[S]) -> opt[http.Response]], afters: [fn(Ctx[S], http.Response) -> http.Response] }

A prefix and the middleware that goes with it.

Group.group(prefix: str) -> Group[S]

A group inside this one: the prefixes join and the middleware accumulates.

Group.before(f: fn(Ctx[S]) -> opt[http.Response]) -> int
Group.after(f: fn(Ctx[S], http.Response) -> http.Response) -> int
Group.handle(method: Method, pattern: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.websocket(p: str, open: fn(Ctx[S]) -> http.Response, w: WsRoute[S]) -> int

r.websocket, under the group's prefix and with its hooks.

Group.get(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.head(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.post(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.put(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.patch(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.delete(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Group.options(p: str, h: fn(Ctx[S]) -> http.Response) -> int
Ctx.trace() -> TraceContext

Empty identifiers when tracing is off.

Ctx.trace_request(req: httpc.Request) -> result[httpc.Request, str]
Ctx.logger(log: Logger) -> TraceLogger
Ctx.param(name: str) -> str
Ctx.param_int(name: str, fallback: int) -> int
Ctx.header(name: str) -> str
Ctx.add_header(name: str, value: str) -> int

Adds a header to whatever response this request ends with: the handler's, or a before's short-circuit, or an error.

Ctx.cookies() -> map[str]str

Every cookie the request carried.

Ctx.set_local(key: str, v: str) -> int

What a before resolved, read by the handler: the user a token belongs to, the tenant a subdomain names, the plan a customer...

Ctx.socket() -> Socket

The socket a WebSocket route's handshake is opening: put it in a hub, set its locals.

Ctx.accept_ws() -> http.Response

The 101 that accepts the upgrade.

Ctx.local(key: str) -> str
Ctx.has_local(key: str) -> bool
Ctx.body_str() -> str
Ctx.body_bytes() -> bytes
Ctx.method() -> Method

The verb as a checked value.

Ctx.method_str() -> str

The verb exactly as the client sent it, for logs.

Ctx.path() -> str
Ctx.query(name: str) -> str

One query parameter, percent-decoded, or "".

Ctx.query_int(name: str, fallback: int) -> int
Ctx.bearer() -> str

The bearer token without its scheme, or "".

Ctx.content_type() -> str
Ctx.json_body() -> result[Json, FormError]

The body as a JSON value, for a shape you have not declared.

Ctx.form(rules: UploadRules) -> result[Form, FormError]

The multipart form this request carries, under the caller's rules.

Ctx.upload(rules: UploadRules) -> result[Form, FormError]

The same, refused unless the request is a POST/PUT/PATCH: a body on a GET is a client doing something strange.

Ctx.wants_close() -> bool
dto[S, T](c: Ctx[S]) -> result[T, http.Response]

The body decoded into a declared shape, in one call: the wire's camelCase keys become the struct's snake_case fields (slang's...

query_as[S, T](c: Ctx[S]) -> result[T, http.Response]

The query string bound into a declared shape, in one call, with the same field-level errors a bad body gets -- let r:...

sanitize.sl#

Operator injection: the NoSQL half of input sanitisation.

enum SanitizeMode { OFF, REJECT, STRIP }
sanitize_from(cfg: Config) -> SanitizeMode

MONGO_SANITIZE: off (default), reject or strip.

operator_key(k: str) -> bool

A key MongoDB would read as an operator or a path.

find_operator_key(j: Json) -> str

The first operator key in j, as a path ("filter.$where", "items[2].$gt"), or "" when there is none.

strip_operator_keys(j: Json) -> int

Removes every operator key in j, at any depth, in place; returns how many were removed.

sanitize_request(req: http.Request, mode: SanitizeMode) -> str

What the guard says about req under mode: "" to carry on (after stripping in place, in STRIP mode), or the offending key's...

security.sl#

Security headers.

gc struct SecurityHeaders { csp: str, hsts_max_age: int, hsts_subdomains: bool, hsts_preload: bool, nosniff: bool, frame_options: str, referrer_policy: str, coop: str, corp: str }
default_security_headers() -> SecurityHeaders
no_security_headers() -> SecurityHeaders
security_problem(h: SecurityHeaders) -> str

What is wrong with h, or "".

security_lines(h: SecurityHeaders) -> [str]

The header lines, "name: value", in a fixed order.

security_from(cfg: Config) -> SecurityHeaders

SECURITY_HEADERS (false turns all of them off), CONTENT_SECURITY_POLICY ("off" for none), HSTS_MAX_AGE (seconds, 0 = off),...

gc struct Security { lines: [str], prefixes: [str] }

What the router keeps: the lines, and each line's lowercase name for the "a handler set its own" check.

security_compile(h: SecurityHeaders) -> Security
Security.apply(resp: http.Response) -> http.Response

resp with every line it does not already carry.

static_render_with(status: i32, content_type: str, lines: [str], body: bytes, keep_alive: bool) -> bytes

A static route's bytes: status line, content type, the security lines, length, connection, the date line, body.

serve.sl#

The server edge: the accept loop that turns a Router into a running server.

gc struct ServerConfig { idle_timeout: int, header_timeout: int, body_timeout: int, write_timeout: int, max_request_bytes: int, max_requests_per_conn: int, shutdown_timeout: int, request_ids: bool, trusted_proxies: TrustedProxies, logger: Logger, max_connections: int, tls: TlsSettings }

Four deadlines, not one, because "how long should this wait" has different honest answers depending on what a connection is...

gc struct TlsSettings { on: bool, cert_file: str, key_file: str, client_ca_file: str, handshake_timeout: int }

A certificate chain and its private key, as PEM files, read once when the server starts: a missing or mismatched pair stops it...

no_tls() -> TlsSettings
tls_files(cert_file: str, key_file: str) -> TlsSettings

HTTPS from these two PEM files, with every other setting at its default.

default_server_config() -> ServerConfig

Safe rather than infinite, per the todo item this answers.

server_config_from(cfg: Config) -> ServerConfig

The same defaults, overridable per field from config: IDLE_TIMEOUT, READ_HEADER_TIMEOUT, READ_BODY_TIMEOUT, WRITE_TIMEOUT,...

serve_conn[S](r: Router[S], c: link, sc: ServerConfig)

One connection, until the client goes away or asks to close.

gc struct ConnGate { lock: mutex, open: int, max: int, refused: int, reported_at: int }

The server-wide connection count, shared by every acceptor.

new_conn_gate(max: int) -> ConnGate
ConnGate.enter() -> bool

Takes a slot for a new connection; false (and counted as refused) when every slot is taken.

ConnGate.leave()
ConnGate.open_now() -> int
ConnGate.refusals_to_report(now: int) -> int

The refusals to report, at most once per second of now: 0 when there are none or the last report was under a second ago.

serve_tls_conn[S](r: Router[S], c: link, sc: ServerConfig, ctx: rawptr)

serve_conn over TLS: the handshake first, under its own deadline, then the same loop.

accept_backoff(prev_ns: int) -> int

The pause after a failed accept: 5ms, doubling to 1s while it keeps failing.

listen_and_serve[S](r: Router[S], port: int) -> result[int, str]

Bind, then accept until the process is asked to stop.

listen_and_serve_with[S](r: Router[S], port: int, sc: ServerConfig) -> result[int, str]

Same, with timeouts, limits, and the shutdown drain window under the caller's control -- built from config with...

sio_serve.sl#

Socket.IO routes, served: Engine.IO v4 and Socket.IO v5 (what socket.io-client 3 and 4 speak), on both of its transports.

gc struct SioRoute[S] { settings: SioSettings, event: fn(S, Socket, SioEvent), connect: opt[fn(S, Socket, str, str) -> result[bool, str]], disconnect: opt[fn(S, Socket, str, str)], sessions: SioSessions }
sio_route[S](event: fn(S, Socket, SioEvent)) -> SioRoute[S]
SioRoute.on_connect(f: fn(S, Socket, str, str) -> result[bool, str]) -> SioRoute[S]
SioRoute.on_disconnect(f: fn(S, Socket, str, str)) -> SioRoute[S]

socket.sl#

A live connection the serve loop holds open after an upgrade, and the hub that lets one connection's handler reach the others.

gc struct Socket { id: str, route: str, protocol: str, client_ip: str, locals: map[str]str, lock: mutex, out: chan[Out], open: bool, ended: str, close_code: int, close_reason: str, hubs: [Hub], namespaces: map[str]bool, handler: int, handshake: http.Response, sio: bool, accepted: bool }
Socket.send_text(s: str) -> bool

Queues a text message.

Socket.send_binary(b: bytes) -> bool
Socket.emit(namespace: str, name: str, args_json: str) -> bool

Socket.IO: emit(name, ...args) on a namespace ("/" by default).

Socket.emit_with_ack(namespace: str, name: str, args_json: str, id: int) -> bool

The same, asking the client to acknowledge: its answer arrives as an Ack event carrying id.

Socket.ack(namespace: str, id: int, args_json: str) -> bool

The answer to a client event that asked for one (SioEvent.ack >= 0).

Socket.emit_binary(namespace: str, name: str, args_json: str, data: [bytes]) -> bool

emit with binary attachments: args_json is the other arguments, as for emit, and each of data follows them as a binary...

Socket.disconnect_ns(namespace: str) -> bool

Disconnects the client from one namespace; the session stays.

Socket.close(code: int, reason: str) -> bool

Starts the closing handshake with code (1000 normal, 1001 going away, 1008 policy, 4000-4999 the application's own) and...

Socket.is_open() -> bool
Socket.local(key: str) -> str

A value the handshake's hooks set on the request, "" when none did.

Socket.set_local(key: str, v: str) -> int
gc struct Hub { id: str, lock: mutex, sockets: map[str]Socket, rooms: map[str]map[str]bool }

Sockets by id, and rooms of them: what a chat, a live dashboard or a notification fan-out needs to reach more than the one...

new_hub() -> Hub
Hub.add(s: Socket) -> int
Hub.remove(s: Socket) -> int

Takes the socket out of the hub and every room in it.

Hub.join_room(s: Socket, room: str) -> int

Puts the socket in a room (adding it to the hub first if needed).

Hub.leave_room(s: Socket, room: str) -> int
Hub.count() -> int
Hub.room_size(room: str) -> int
Hub.get(id: str) -> opt[Socket]

The socket with this id, if it is in the hub.

Hub.broadcast(text: str, except: str) -> int

Text to every WebSocket in the hub but except (an id, or "").

Hub.broadcast_binary(b: bytes, except: str) -> int
Hub.to_room(room: str, text: str, except: str) -> int
Hub.to_room_binary(room: str, b: bytes, except: str) -> int
Hub.emit_all(namespace: str, name: str, args_json: str, except: str) -> int

Socket.IO: one event to every Socket.IO session in the hub, encoded once.

Hub.emit_to_room(room: str, namespace: str, name: str, args_json: str, except: str) -> int
Hub.emit_binary_to_room(room: str, namespace: str, name: str, args_json: str, data: [bytes], except: str) -> int

emit_binary to every Socket.IO session in a room ("" for the hub).

testkit.sl#

Building a request, and reading a response.

gc struct TestRequest { method: Method, path: str, headers: map[str]str, body: bytes }
request(method: Method, path: str) -> TestRequest
get_request(path: str) -> TestRequest
TestRequest.header(name: str, value: str) -> TestRequest
TestRequest.bearer(token: str) -> TestRequest
TestRequest.basic(credentials: str) -> TestRequest
TestRequest.content_type(ct: str) -> TestRequest
TestRequest.body_str(body: str) -> TestRequest
TestRequest.body_bytes(body: bytes) -> TestRequest
TestRequest.json_body(body: str) -> TestRequest

Sets the content type too, because a body without one is a body a real client would not send.

TestRequest.json(body: Json) -> TestRequest
TestRequest.multipart(boundary: str, fields: map[str]str, files: [Upload]) -> TestRequest

A multipart body, built from the parts given: each file is name:filename:content-type:content, each field name:value.

TestRequest.build() -> http.Request

Not fallible: a header that fails http.request()'s validation (CR or LF in a name or value, a colon in a name) here is a bug...

upload_part(field: str, filename: str, content_type: str, content: str) -> Upload

One part for multipart, without going near a real encoder.

resp_status(r: http.Response) -> int

status_of is taken by the registry, so responses read as resp_status, resp_text, resp_header, resp_json.

resp_text(r: http.Response) -> str
resp_header(r: http.Response, name: str) -> str
resp_json(r: http.Response) -> Json

The body as JSON, or Null when it is not JSON at all -- so an assertion reads json_of(resp).get("id") without a guard.

resp_error_code(r: http.Response) -> str

The error code in zokor's envelope: "not_found", "validation_failed".

resp_error_fields(r: http.Response) -> [str]

The field names an error envelope complains about, in order, so a test asserts on the shape rather than on an exact string.

tokens.sl#

Opaque bearer tokens.

gc struct TokenStore { subjects: map[str]str, expires: map[str]int, ttl: int, lock: mutex, since_sweep: int }
new_token_store(ttl_ns: int) -> TokenStore

ttl_ns is how long a token stays valid after it is issued, in nanoseconds; 0 means until it is revoked.

new_token() -> result[str, str]

32 random bytes as 64 lowercase hex characters.

TokenStore.issue(subject: str) -> result[str, str]

A new token for subject.

TokenStore.issue_at(subject: str, now: int) -> result[str, str]
TokenStore.lookup(token: str) -> opt[str]

The subject a live token was issued to; none for an unknown, revoked or expired token (an expired one is dropped on the way).

TokenStore.lookup_at(token: str, now: int) -> opt[str]
TokenStore.revoke(token: str) -> bool

Ends one token (logout).

TokenStore.revoke_subject(subject: str) -> int

Ends every token issued to subject ("log out everywhere", a password change).

TokenStore.count() -> int

How many tokens are held (expired ones count until swept).

bearer_subject[S](c: Ctx[S], store: TokenStore) -> opt[str]

The subject behind the request's Authorization: Bearer <token>, or none when the header is missing or the token is not live.

tracing.sl#

Request-scoped W3C context, carried explicitly across tasks and calls.

gc struct TraceConfig { max_parent_bytes: int, max_state_bytes: int, max_state_members: int }

Startup limits for incoming W3C headers.

gc struct Tracer { max_parent_bytes: int, max_state_bytes: int, max_state_members: int }

Validated, immutable startup tracing settings.

struct TraceContext { trace_id: str, parent_id: str, span_id: str, sampled: bool, state: str }

Explicit request context; pass this value when spawning background work.

trace_config() -> TraceConfig

Default limits: 256 parent bytes, 512 state bytes, 32 state members.

new_tracer(cfg: TraceConfig) -> result[Tracer, str]

Validate all startup limits together and copy them.

trace_context(t: Tracer, headers: bytes) -> result[TraceContext, str]

Invalid, absent or duplicate parents start a new trace.

trace_header(ctx: TraceContext) -> result[str, str]

A validated version-00 header; reserved flags are always zero.

trace_request(ctx: TraceContext, req: httpc.Request) -> result[httpc.Request, str]

Copy the request and its headers; never mutate a shared caller request.

trace_registry(reg: Registry, ctx: TraceContext) -> Registry

Bind a registry by copying its startup references, never changing the shared registry as concurrent requests arrive with...

struct TraceLogger { logger: Logger, trace_id: str, request_id: str }

A request binding over a shared Logger, without changing its configuration.

traced_logger(log: Logger, ctx: TraceContext) -> TraceLogger

Bind application records to a validated trace id.

upload.sl#

File uploads.

gc struct UploadRules { limits: multipart.Limits, allowed_types: [str], allowed_extensions: [str], require_content_type: bool }
gc struct Upload { field: str, filename: str, raw_filename: str, content_type: str, data: bytes }
gc struct Form { values: map[str]str, files: [Upload] }
gc struct FormError { code: str, detail: str }

The reason a form was refused: a registered error code, and a sentence naming what was wrong.

default_uploads() -> UploadRules
uploads_allowing(types: [str]) -> UploadRules
with_max_file_bytes(r: UploadRules, n: int) -> UploadRules
with_max_total_bytes(r: UploadRules, n: int) -> UploadRules
with_max_files(r: UploadRules, n: int) -> UploadRules
with_extensions(r: UploadRules, exts: [str]) -> UploadRules
parse_form(content_type: str, body: bytes, rules: UploadRules) -> result[Form, FormError]
value(f: Form, name: str) -> str
has_value(f: Form, name: str) -> bool
file(f: Form, field: str) -> opt[Upload]

The first file posted under a field name, which is the usual case.

files_for(f: Form, field: str) -> [Upload]

Every file under one field name: <input multiple> posts repeats.

total_bytes(f: Form) -> int
safe_filename(raw: str) -> str

A client's filename, made safe to use as ONE path segment.

extension_of(filename: str) -> str

".png" for "a.png", lowercased; "" when there is no extension.

media_type_of(header: str) -> str

The media type without its parameters: "image/png" from "image/png; charset=binary".

ws.sl#

WebSocket, and Socket.IO on top of it.

gc struct WsRules { max_frame_bytes: int, max_message_bytes: int, protocols: [str], ping_interval: int, pong_timeout: int, max_queue: int, close_timeout: int }
default_ws() -> WsRules
ws_allowing(protocols: [str]) -> WsRules
enum Incoming { Text, Binary, Ping, Pong, Close }
gc struct Message { kind: Incoming, text: str, data: bytes, code: int, reason: str }
gc struct Conn { rules: WsRules, inbox: builder.Bytes, have: int, need: int, partial: builder.Bytes, partial_op: ws.Op, fragmenting: bool, outbox: bytes, closed: bool, sid: str }
new_conn(rules: WsRules) -> Conn
is_upgrade(req: http.Request) -> bool

Is this request asking to become a WebSocket? Cheap enough to call on every request, so a route can serve both.

upgrade(req: http.Request, rules: WsRules) -> result[http.Response, str]

The 101 to send back, or the failure to report.

receive(c: Conn, chunk: bytes) -> result[[Message], str]

Feeds bytes in and takes whole messages out.

pending(c: Conn) -> bytes

Bytes the connection owes the peer (pongs, the close echo), taken out: call it after receive and write whatever it returns.

is_closed(c: Conn) -> bool
text_frame(s: str) -> bytes
binary_frame(b: bytes) -> bytes
ping_frame(payload: bytes) -> bytes
pong_frame(payload: bytes) -> bytes
close_frame(code: int, reason: str) -> bytes
close_because(c: Conn, why: str) -> bytes

The close a protocol error calls for: 1002, with the reason, and the connection marked shut so nothing else is sent.

gc struct SioSettings { ping_interval_ms: int, ping_timeout_ms: int, max_payload: int, max_polling_sessions: int, max_attachments: int }
default_sio() -> SioSettings
enum SioKind { Open, Connect, Event, Ack, Ping, Disconnect, Other }
gc struct SioEvent { kind: SioKind, namespace: str, name: str, args_json: str, ack: int, attachments: [bytes], expecting: int }
sio_open(c: Conn, sid: str, s: SioSettings) -> bytes

The first thing a socket.io connection needs: its session id and the keepalive timings, as a text frame.

sio_receive(c: Conn, m: Message) -> result[SioEvent, str]

Turns one WebSocket text message into a socket.io event.

sio_connect_ok(c: Conn, namespace: str) -> bytes

The answer to a CONNECT.

sio_connect_error(namespace: str, why: str) -> bytes
sio_emit(namespace: str, name: str, args_json: str) -> bytes

emit(name, args).

sio_emit_with_ack(namespace: str, name: str, args_json: str, id: int) -> bytes
sio_ack(namespace: str, id: int, args_json: str) -> bytes

The reply to an event that carried an ack id.

sio_disconnect(namespace: str) -> bytes

ws_serve.sl#

WebSocket routes, served: the handshake goes through the router like any request -- its befores run, so authentication and...

gc struct WsRoute[S] { rules: WsRules, message: opt[fn(S, Socket, Message)], closed: opt[fn(S, Socket, int, str)], sio: opt[SioRoute[S]] }
ws_route[S](rules: WsRules, message: fn(S, Socket, Message)) -> WsRoute[S]

The message handler is the one a route cannot do without; on_close is optional.

WsRoute.on_close(f: fn(S, Socket, int, str)) -> WsRoute[S]

Called once when the connection ends, however it ended, with the code and reason: the peer's, the ones this side closed with,...

internal/multipart (reached as UploadRules.limits)#

gc struct Limits { max_parts: int, max_files: int, max_file_bytes: int, max_field_bytes: int, max_total_bytes: int, max_headers_bytes: int, max_filename_bytes: int }