-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHttp.affine
More file actions
253 lines (223 loc) · 11.7 KB
/
Copy pathHttp.affine
File metadata and controls
253 lines (223 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 hyperpolymath
//
// Http.affine — portable HTTP requests (issue #160).
//
// The C-spine migration primitive that replaces ReScript's
// `fetch`/`Promise`-based networking with a typed, effect-rowed surface.
//
// Portability: the single `http_request` extern is lowered by the
// Deno-ESM backend (lib/codegen_deno.ml) to a `globalThis.fetch`
// round-trip, which runs unmodified on Deno, Node 18+, and browser ESM.
// The WasmGC / Node-CJS host-import path is a tracked follow-up
// (the runtime there has no `fetch` import yet) — see the issue thread.
//
// Headers are an association list `[(String, String)]` rather than a
// `Dict<String, String>`: this lets #160 land independently of #162
// (Dict) while keeping the stated spine order #160 -> #161 -> #162.
// Upgrading `headers` to `Dict` later is an additive, source-compatible
// change for callers that build/read headers through the helpers here.
module Http;
// `Option`/`Some`/`None` are owned by `prelude` (ADR-011).
use prelude::{Option, Some, None};
// Networking uses the reserved built-in effect `Net` (no declaration
// needed — it is one of the language's reserved effect names alongside
// `Random`/`Time`). Every signature that can touch the network carries
// `/ Net`; the host round-trip is also `Async`, so the effect row
// stays honest end to end. A bespoke `effect Http;` would not be
// visible to importing modules' effect checker, so `Net` is both more
// correct and the only cross-module-sound choice.
/// An outgoing HTTP request.
///
/// `headers` is an ordered association list of `(name, value)` pairs.
/// `body` is absent for verbs that take none (GET/HEAD).
pub type Request = {
url: String,
method: String,
headers: [(String, String)],
body: Option<String>
}
/// A host HTTP response. `body` is the raw response text; structured
/// decoding (JSON) is the caller's concern and is the subject of the
/// next spine primitive, #161.
pub type Response = {
status: Int,
headers: [(String, String)],
body: String
}
// Core boundary primitive. No AffineScript body: the Deno-ESM backend
// lowers a call here to `(await __as_httpFetch(url, method, headers,
// body))`. The `Async` effect is real — callers compile to
// `async function` (codegen_deno async-fn detection).
extern fn http_request(url: String,
method: String,
headers: [(String, String)],
body: Option<String>) -> Response / { Net, Async };
/// Build a `Request` value (ergonomic constructor for the record).
pub fn request(method: String,
url: String,
headers: [(String, String)],
body: Option<String>) -> Request {
#{ url: url, method: method, headers: headers, body: body }
}
/// Perform the HTTP request described by `req`.
pub fn fetch(req: Request) -> Response / { Net, Async } {
http_request(req.url, req.method, req.headers, req.body)
}
/// `GET url` with no extra request headers.
pub fn get(url: String) -> Response / { Net, Async } {
http_request(url, "GET", [], None)
}
/// `POST body` to `url` with no extra request headers.
pub fn post(url: String, body: String) -> Response / { Net, Async } {
http_request(url, "POST", [], Some(body))
}
/// True for a 2xx status.
pub fn is_ok(resp: Response) -> Bool {
resp.status >= 200 && resp.status < 300
}
// ── Lower-level wasm-consumable surface (issue #225, ADR-013) ─────────
//
// On the WasmGC backend an async result cannot be returned synchronously
// across the i32 boundary. The established convergence convention (#205)
// is a `Thenable` host handle the side resolving it observes via the
// shared continuation protocol. `http_request_thenable` mirrors the
// proven `Vscode.httpPostJson` shape exactly: it returns a `Thenable`
// handle the host registers; settlement carries the JSON-encoded
// `{ status, headers, body }`.
//
// This is NOT a second public surface: `fetch`/`get`/`post` above remain
// the single portable source API. On Deno the source-to-source backend
// awaits directly and never touches this. On wasm the transparent CPS
// transform (ADR-013) lowers the high-level surface ONTO this verified
// primitive — callers still write `fetch(req) -> Response`. This
// declaration is the host-import contract + the foundation that
// transform is built and tested on (#225 PR 1).
pub extern type Thenable;
/// Issue an HTTP request, returning a host `Thenable` that settles with
/// the JSON-encoded response `{ status, headers, body }`. `body` is the
/// empty string for verbs that carry none. Wasm-path primitive; see the
/// module note. Resolved via the shared #205 continuation protocol.
pub extern fn http_request_thenable(url: String,
method: String,
body: String) -> Thenable / { Net, Async };
/// Register a guest continuation to run once, after `t` settles (issue
/// #225 ADR-013, mirroring the proven #205 `Vscode.thenableThen` shape
/// but on the Http wasm-path module). The host invokes `on_settle`
/// exactly once via the #199 closure-pointer ABI; the returned `Int`
/// is an opaque disposable token. The transparent CPS transform
/// (ADR-013) emits calls to this — source authors never write it; they
/// write `fetch`/`get`/`post` and the WasmGC backend lowers onto this
/// verified primitive. PR 2 base case: single async boundary, no
/// live-local capture; the continuation is fired at most once (host
/// single-fire + a guest-side defensive trap on re-entry).
pub extern fn thenableThen(t: Thenable,
on_settle: fn(Unit) -> Int) -> Int / { Async };
// ── Typed Response reader (issue #225 PR3d, ADR-013 §Value-reconstruction) ──
//
// After a `Thenable` settles, the continuation needs the full
// `Response`. `jsonField` (one top-level *scalar* String) cannot decode
// `headers: [(String, String)]` — a JSON array of pairs. ADR-013
// §Value-reconstruction calls for a *minimal typed reader for the fixed
// `Response` shape*, deferring general JSON decode to #161.
//
// These host-mediated primitives expose the settled payload as
// scalars/strings — the same proven extern convention as
// `thenableResultJson`/`jsonField` (the host reads its own settled
// record, keyed by the `Thenable` handle; no structured value crosses
// the i32 boundary). The guest reconstructs the fixed record in
// `readResponse`; its header loop exercises the #255-fixed `while`
// codegen.
pub extern fn responseStatus(t: Thenable) -> Int / { Async };
pub extern fn responseBody(t: Thenable) -> String / { Async };
pub extern fn responseHeaderCount(t: Thenable) -> Int / { Async };
pub extern fn responseHeaderName(t: Thenable, index: Int) -> String / { Async };
pub extern fn responseHeaderValue(t: Thenable, index: Int) -> String / { Async };
/// Reconstruct the fixed `Response` shape from a settled `Thenable`.
///
/// The ADR-013 *minimal typed reader*: it performs the structured
/// `headers` decode `jsonField` cannot, for exactly the fixed
/// `{ status, headers, body }` shape (no silent lossiness — those are
/// the only `Response` fields). General JSON decode stays #161's
/// concern. Call only after `t` has settled (from a `thenableThen`
/// continuation / the CPS-lowered surface).
pub fn readResponse(t: Thenable) -> Response {
let mut headers = [];
let mut i = 0;
let n = responseHeaderCount(t);
while i < n {
headers = headers ++ [(responseHeaderName(t, i), responseHeaderValue(t, i))];
i = i + 1;
}
#{ status: responseStatus(t), headers: headers, body: responseBody(t) }
}
// ── HTTP server primitives (hpm-http-rsr Zig FFI surface) ────────────────────
//
// The server-side counterpart to `http_request`. Backed by the 10
// `hpm_http_*` exports at
// `hyperpolymath/http-capability-gateway/ffi/zig/src/main.zig`.
//
// Designed for the OikosBot webhook receiver path: bind once, accept in a
// loop, inspect the request, reply, free. No keep-alive (every response
// closes the connection). No TLS — designed to sit behind a reverse proxy
// (Caddy / nginx).
//
// **Native-only.** These externs have no Deno-ESM lowering and will fail
// at runtime there (consistent with the existing `http_request_thenable`
// + `response*` wasm-path externs above). The natural deployment is the
// WasmGC / native-C path linking libhttp_capability_gateway alongside
// libhpm_crypto.
//
// Native lowering adapts the Zig size-query convention (`isize`, NULL/0
// → required size, then re-call to write into a caller-provided buffer)
// into AS-friendly `String` / `Option<String>` returns; AS callers never
// see the two-step protocol.
pub extern type HpmHttpServer;
pub extern type HpmHttpRequest;
/// Bind a TCP listener on `host:port`. Host is an IPv4/IPv6 string
/// (e.g. `"0.0.0.0"`, `"127.0.0.1"`, `"::1"`). Pass `port = 0` to let
/// the kernel pick a free port (then read it back with
/// `hpm_http_server_port`). Returns `None` on host-parse / bind /
/// allocator failure. Pair every `Some(s)` with `hpm_http_server_free(s)`.
pub extern fn hpm_http_server_listen(host: String, port: Int) -> Option<HpmHttpServer> / { Net };
/// The bound port (useful when `listen` was called with `port: 0`).
/// Returns 0 on a null handle.
pub extern fn hpm_http_server_port(server: HpmHttpServer) -> Int / { Net };
/// Close the listener and free the handle. Does NOT affect requests
/// already returned by `accept` — those must be freed independently
/// with `hpm_http_request_free`. Returns 0.
pub extern fn hpm_http_server_free(server: HpmHttpServer) -> Int / { Net };
/// Block until a request arrives, parse its head, return a request
/// handle. Returns `None` if accept failed, the client sent a
/// malformed head, or the allocator failed. The TCP connection is
/// closed automatically on failure. Pair every `Some(r)` with
/// `hpm_http_request_free(r)`.
pub extern fn hpm_http_server_accept(server: HpmHttpServer) -> Option<HpmHttpRequest> / { Net, Async };
/// Request method ordinal, matching `std.http.Method`:
/// `0=GET 1=HEAD 2=POST 3=PUT 4=DELETE 5=CONNECT 6=OPTIONS 7=TRACE 8=PATCH`.
/// Returns -1 on a null handle.
pub extern fn hpm_http_request_method(req: HpmHttpRequest) -> Int / { Net };
/// Request target (URI path + query). Returns the empty string on a
/// null handle.
pub extern fn hpm_http_request_path(req: HpmHttpRequest) -> String / { Net };
/// Look up a request header by case-insensitive name. Returns `None`
/// if the header is absent. The native lowering hides the Zig
/// size-query convention — AS callers see the decoded value
/// directly.
pub extern fn hpm_http_request_header(req: HpmHttpRequest, name: String) -> Option<String> / { Net };
/// Read the entire request body. May only be called once per request
/// — subsequent calls return the empty string. Native lowering caps
/// the body at the Zig-side `HTTP_MAX_BODY_BYTES` (1 MiB); over-cap
/// requests return the empty string.
pub extern fn hpm_http_request_body(req: HpmHttpRequest) -> String / { Net };
/// Send a complete HTTP response. `status` is the numeric status
/// code (e.g. `200`, `404`, `500`). `headers` is a
/// `"Name: Value\r\nName: Value"`-formatted extra-headers buffer
/// (max 16 entries; pass the empty string for none). Connection is
/// always closed after (no keep-alive). Returns 0 on success, -1 on
/// error (already-responded / over-16-headers / IO failure).
pub extern fn hpm_http_request_respond(req: HpmHttpRequest, status: Int, headers: String, body: String) -> Int / { Net };
/// Close the TCP connection and free the request handle. Must be
/// called exactly once for every `Some(r)` returned by
/// `hpm_http_server_accept`. Returns 0.
pub extern fn hpm_http_request_free(req: HpmHttpRequest) -> Int / { Net };