-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHttp.affine
More file actions
171 lines (153 loc) · 7.57 KB
/
Copy pathHttp.affine
File metadata and controls
171 lines (153 loc) · 7.57 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
// SPDX-License-Identifier: PMPL-1.0-or-later
// 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) }
}