# zokor API (generated by docs/gen_api.sh from zokor/; do not edit) # # `import "zokor";` then `zokor.`. Methods are `value.name(..)`; # `self` is omitted. Times are int nanoseconds, statuses i32. # Recipes: docs/llms-small.txt. ## 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., 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 } new_cookie(name: str, value: str) -> Cookie cookie_problem(c: Cookie) -> str What is wrong with `c`, or "". cookie_string(c: Cookie) -> str The Set-Cookie value for `c`, unchecked: callers check first. set_cookie(resp: http.Response, c: Cookie) -> result[http.Response, str] `resp` with a Set-Cookie header for `c`, or why `c` cannot be sent. clear_cookie(resp: http.Response, name: str, path: str, domain: str) -> result[http.Response, str] Tells the browser to delete `name`. 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] } cookie_keys(keys: [str]) -> result[CookieKeys, str] Keys of at least 32 bytes each, newest first. cookie_keys_from(cfg: Config) -> CookieKeys COOKIE_KEYS, comma-separated, newest first. 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_cookie(name: str, value: str, keys: CookieKeys) -> result[Cookie, str] A cookie whose value is `value.signature`. 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] - 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.cookie(name: str) -> str The value of cookie `name`, "" when the request carried none. 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 `, 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: `` 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 `before`s 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 }