These libraries are experimental. APIs may change without notice. Generated from source with koruc 0.1.7 on 8/19/2026.
cURL
@korulang/curl@0.0.1Type-safe libcurl wrapper with phantom obligation types
curl/index.kz · 13 tors
@koru/curl - HTTP Client for Koru
Wraps industry-standard libcurl with phantom obligation types
to enforce connection cleanup at compile-time.
Phantom lifecycles
Derived from the phantom labels in the declarations below — state! issues an
obligation the compiler will chase, !state discharges it, a bare state holds it without moving it. Nothing here is hand-drawn.
Response 1 state open!Multi 3 states empty!open!owned!Pending 1 state open!// GET Request
~pub tor get { url: string, allocator: ?std.mem.Allocator }
| ok *Response<open!>
| err Error// POST Request
~pub tor post { url: string, body: string, allocator: ?std.mem.Allocator }
| ok *Response<open!>
| err Error~pub tor post.with-headers {
url: string,
body: string,
headers: []const Header,
allocator: ?std.mem.Allocator
}
| ok *Response<open!>
| err Error// Close (discharges obligation)
~pub tor close { resp: *Response<!open> }~pub tor multi.new { allocator: ?std.mem.Allocator } -> *Multi<empty!>// One URL per call, mirroring curl_multi_add_handle. Consumes EITHER state and
// issues `open` — not because adding destroys the batch, but because it moves
// it: a fresh batch is `empty`, and `multi.start` accepts only `<!open>`. So a
// batch with no transfers in it cannot be started, and the compiler says so
// rather than libcurl returning nothing at runtime. Same shape as
// raylib:frames consuming `<!opened|!active>`.
~pub tor multi.add { m: *Multi<!empty|!open>, url: string } -> *Multi<open!>// The authenticated-POST twin of `multi.add` — the request shape
// `post.with-headers` speaks, on the batch surface `poll`/`await` can pump
// under a live UI. The slot OWNS its copies of the body and the built header
// list (libcurl requires both to outlive the transfer; the caller's strings
// are free to die the moment this returns), released in `multi.close` with
// the url. No FOLLOWLOCATION here: a redirected POST silently degrades to a
// GET, and an API endpoint that redirects deserves a loud status over a
// quietly wrong verb.
~pub tor multi.add.post {
m: *Multi<!empty|!open>,
url: string,
body: string,
headers: []const Header
} -> *Multi<open!>// Starts nothing by itself — the multi interface begins work on the first
// perform. What it does is hand back a promise, so the batch handle can no
// longer be added to or closed while transfers are in flight.
// `! ?started-multi` pulses ONCE PER TRANSFER before the promise is handed back
// — the one moment where every slot exists and none has moved. It is the twin of
// `poll`'s `! progress`, at the other end of the batch's life: progress reports
// a transfer that has advanced, this reports a transfer that now exists.
//
// Optional, so a caller that does not care about per-transfer setup writes
// nothing. A caller that does gets its rows built by the same act that built the
// transfers, instead of hand-counting a second list into agreement (the three
// literal `insert(xfers)` rows this arm retires in koru-examples/downloads).
~pub tor multi.start { m: *Multi<!open> }
! ?started-multi *Transfer
| started *Pending<open!>
| failed Error// NON-BLOCKING. One pass: advance every transfer as far as it can go without
// waiting, report what moved, return.
//
// It BORROWS the promise (`<open>`, no bang), and that is the whole point of
// the shape. Advancing a batch does not end it: the handle that comes back is
// the one that went in. The earlier spelling consumed `<!open>` and re-issued
// `<open!>` on a `| pending` branch that returned the identical pointer — a
// consume-and-reissue of the SAME state on the SAME handle, which is a borrow
// wearing a consume's clothes. (Contrast `multi.add`, which also returns its
// argument but moves it `empty` → `open`: there the consume-and-reissue IS the
// state transition, and is right.)
//
// The cost of the old shape was not cosmetic. A caller holding the promise
// across calls — a frame loop, a store — had to surrender the obligation and
// take it back every single pass, which for a store means removing the row and
// re-inserting it. Borrowing lets the owner keep `<open!>` for the whole run,
// which is what is actually true: polling never made the batch not-theirs.
//
// So this reports and returns nothing. `| complete` says the batch has stopped
// running; `finish` is what ends it, because ending is a consuming act and
// consumption happens on INPUT, never on a branch.
// `! ?chunk` is the streaming arm, and PRESENCE IS THE SWITCH: provide it and
// every drain pass hands you the bytes that arrived since the last one, with
// the slot's buffer cleared behind each firing — `finished` then carries only
// the status and whatever trailed the final drain. Omit it and nothing drains:
// bodies buffer whole and arrive on `finished`, exactly as before. The arm
// fires from THIS proc body during the pass (the same shape as `! progress`),
// never from libcurl's C callback — the callback only ever appends to the
// slot buffer, which is the boundary this section's header commits to.
~pub tor poll { p: *Pending<open> }
! ?chunk *Chunk
! ?progress *Progress
! finished *Response<open!>
| running
| complete// Ends a promise that has stopped running, handing the batch back. The
// consuming half of `poll`: `<!open>` dies HERE, on an input, because that is
// the only position a phantom obligation can be consumed in. One `finish` per
// promise, however many `poll`s came before it.
//
// Sibling of `cancel`, and the difference is intent rather than mechanism —
// `finish` collects a batch that ran to the end, `cancel` abandons one that did
// not. Both are legal exits, which is why a store holding a Pending must be
// told which it wants (`! discharge`) rather than being left to guess.
~pub tor finish { p: *Pending<!open> } -> *Multi<owned!>// BLOCKING. The same pass in a loop with a real poll timeout, until nothing is
// left running. The loop is here in the proc, not a koru fold — so this needs
// no back-edge and no Handlers threading.
~pub tor await { p: *Pending<!open> }
! ?chunk *Chunk
! ?progress *Progress
! finished *Response<open!>
| done *Multi<owned!>// Abandon. The other way out of a promise, and it means something different
// from finishing — which is why the compiler names both rather than choosing.
~pub tor cancel { p: *Pending<!open> } -> *Multi<owned!>~pub tor multi.close { m: *Multi<!owned> }