adapter bynk {
uses bynk.locale.types
exports capability { Clock, Random, Logger, Fetch, Secrets, Locale, Idempotency, Events }
exports transparent { Uuid, Method, FetchError, Request, Response, EventEnvelope }
---
A UUID in canonical 8-4-4-4-12 lowercase-hex form.
---
type Uuid = String where Matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
---
An HTTP request method.
---
type Method = enum { Get, Post, Put, Delete }
---
Why an outbound `Fetch.send` failed: a network error, a timeout, or a
request whose headers `Fetch.send` refused to send (`InvalidHeader`). An
`InvalidHeader` request never reaches the network: its `headers` named
Content-Type or Authorization while the matching typed slot was `Some`,
named a header the platform owns (`host`, `content-length`, and the other
framing/hop-by-hop headers), named one header twice in different case, or
held a name or value that is not a legal HTTP header; or a typed slot
(`contentType`, `authorization`) held a value that is not a legal header
value — a secret with a stray newline, say. Retrying cannot fix it.
---
type FetchError = enum { Network, Timeout, InvalidHeader }
---
An outbound HTTP request, as passed to `Fetch.send`. `contentType` and
`authorization` are the typed slots for the two common headers; `headers`
carries any other (`User-Agent`, `Accept`, `If-None-Match`, …), and an empty
map (`Map.empty()`) sends none. Header names are case-insensitive. A
`headers` entry for Content-Type or Authorization is accepted only while the
matching typed slot is `None` — when both are given, `Fetch.send` returns
`Err(InvalidHeader)` rather than let one silently win. Names the platform
owns — `host`, `content-length`, `connection`, `keep-alive`, `te`,
`trailer`, `transfer-encoding`, `upgrade`, `expect` — are refused the same
way, as is a duplicate name differing only in case.
---
type Request = {
method: Method,
url: String,
contentType: Option[String],
authorization: Option[String],
headers: Map[String, String],
body: Option[String],
}
---
An HTTP response: a status code, its headers, and a body. `headers` keys
are lowercased (`res.headers.get("etag")`); a header the server repeated
arrives as one value, joined with ", ". That includes `set-cookie`, and
there the join is lossy: a cookie's `Expires` attribute holds a comma, so
a joined `set-cookie` cannot be split back into its cookies reliably.
---
type Response = {
status: Int,
headers: Map[String, String],
body: String,
}
---
Runtime metadata carried alongside an event payload (Events track, slice
2; design/tracks/events.md, spine #936), available as an `on event`
handler's optional second parameter. `eventId` is minted once per
emission — every subscriber of that emission sees the same value, making
it the dedup key for the `Idempotency` capability's `dedup`/`remember`
idiom. `publisherId` is the emitting context's qualified name, not the
emitting agent's instance identity: `Events.emit` is legal from a plain,
keyless service handler with no agent to report, so a context-scoped
identifier is the only one available uniformly at every legal emission
site (an amendment to design/bynk-design-notes.md §7's "the publisher is
the emitting agent" framing). `schemaVersion` is the version the
cross-build schema registry (`bynk.schema.lock`, Events slice 3c) computes
for the emitting event: unchanged shape keeps its stored version, a purely
additive shape change (only new defaulted fields) auto-bumps it, and a
non-additive change fails the build. An explicit `@schema(N)` annotation
(slice 3b) is checked against that computed value rather than trusted
outright — a mismatch is a build error, not a silent override.
---
type EventEnvelope = {
eventId: String,
publisherId: String,
emittedAt: Instant,
schemaVersion: Int,
}
---
Reads the current wall-clock time, as Unix milliseconds.
---
capability Clock {
fn now() -> Effect[Instant]
}
---
A source of randomness — fresh UUIDs, bounded integers and random bytes.
`int` draws from a general-purpose generator and is **not** for secrets:
use `bytes` for tokens, nonces and keys. `bytes(n)` returns `n` bytes from
the platform's cryptographically secure generator (`crypto.getRandomValues`);
`n` must be between 1 and 65,536, and anything else fails the effect with an
error naming the bound.
---
capability Random {
fn uuid() -> Effect[Uuid]
fn int(lo: Int, hi: Int) -> Effect[Int]
fn bytes(n: Int) -> Effect[Bytes]
}
---
Structured logging at info and error levels.
---
capability Logger {
fn info(msg: String) -> Effect[()]
fn error(msg: String) -> Effect[()]
}
---
Performs outbound HTTP requests.
---
capability Fetch {
fn send(req: Request) -> Effect[Result[Response, FetchError]]
}
---
Reads named secrets from the platform's secret store.
---
capability Secrets {
fn get(name: String) -> Effect[Option[String]]
}
---
Reads the current locale for this request/session. Slice 1: always
returns a fixed reference tag (`"en"`) on every platform.
---
capability Locale {
fn current() -> Effect[LocaleTag]
}
---
Mechanical dedup for at-least-once delivery (design notes §12). `dedup`
checks for a cached outcome under `key`; on a cache miss the caller
computes its outcome and writes it back with `remember` (`record` is a
reserved keyword), which the caller must call to actually cache anything
— a `dedup` miss with no matching `remember` call simply recomputes every
time. `expiresAfter` sets the retention window a call to `remember`
establishes. Slice 0 (design/tracks/idempotency-capability.md): a single
in-memory provider, lost on process restart — no durability guarantee yet.
---
capability Idempotency {
fn dedup[T](key: String) -> Effect[Option[T]]
fn remember[T](key: String, value: T, expiresAfter: Duration) -> Effect[()]
}
---
Emits an event declared by the calling context (Events track, slice 0;
design/tracks/events.md, spine #936). Owner-only: the compiler rejects
`emit[E]` when `E` is not an event declared in the emitting context, even
when `E` is otherwise visible via `consumes`. Fire-and-forget: releases at
handler commit, so an aborted handler emits nothing — a `dedup`/`remember`-
style write-then-flush the provider itself is responsible for, not a
distinct control-flow form here. The explicit `[E]` type argument follows
the `Idempotency.remember[T]` precedent: a generic capability operation's
type parameter is never inferred, even where an argument's type would
determine it.
---
capability Events {
fn emit[E](event: E) -> Effect[()]
}
provides Clock = ClockProvider
provides Random = RandomProvider
provides Logger = LoggerProvider
provides Fetch = FetchProvider
provides Secrets = SecretsProvider
provides Locale = LocaleProvider
provides Idempotency = IdempotencyProvider given Clock
provides Events = EventsProvider
}