Skip to content

Commit 13103e8

Browse files
committed
feat: implement async module with HTTP support and testing capabilities
1 parent d4efced commit 13103e8

14 files changed

Lines changed: 717 additions & 63 deletions

alias.mbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
///|
22
using @core {type ClosureInterpreter}
33

4+
///|
5+
using @core {type RuntimeModule}
6+
47
///|
58
using @core {type RuntimeValue}

async/module.mbt

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
///|
2+
let async_pkg : @core.RuntimePackage = @core.RuntimePackage::new(
3+
"moonbitlang/async",
4+
files={
5+
"async.mbt": (
6+
#|pub(all) enum RetryMethod {
7+
#| Immediate
8+
#| FixedDelay(Int)
9+
#| ExponentialDelay(initial~ : Int, factor~ : Double, maximum~ : Int)
10+
#|}
11+
#|
12+
#|pub async fn[X] retry(
13+
#| _method : RetryMethod,
14+
#| max_retry? : Int = 0,
15+
#| fatal_error? : (Error) -> Bool,
16+
#| f : async () -> X,
17+
#|) -> X = "%eval.async.retry"
18+
#|
19+
#|pub async fn[X] with_timeout(
20+
#| _time : Int,
21+
#| f : async () -> X,
22+
#| error? : Error,
23+
#|) -> X = "%eval.async.with_timeout"
24+
),
25+
},
26+
)
27+
28+
///|
29+
let async_io_pkg : @core.RuntimePackage = @core.RuntimePackage::new(
30+
"moonbitlang/async/io",
31+
files={
32+
"data.mbt": (
33+
#|pub struct Data {
34+
#| text : String
35+
#|}
36+
#|
37+
#|pub fn Data::text(self : Data) -> String {
38+
#| self.text
39+
#|}
40+
),
41+
},
42+
)
43+
44+
///|
45+
let async_http_pkg : @core.RuntimePackage = @core.RuntimePackage::new(
46+
"moonbitlang/async/http",
47+
files={
48+
"http.mbt": (
49+
#|import {
50+
#| "moonbitlang/async/io"
51+
#|}
52+
#|
53+
#|pub struct Cookie {
54+
#| name : String
55+
#| value : String
56+
#|}
57+
#|
58+
#|pub struct Response {
59+
#| code : Int
60+
#| reason : String
61+
#| headers : Map[String, String]
62+
#| cookies : Array[Cookie]
63+
#|}
64+
#|
65+
#|pub async fn get(
66+
#| url : String,
67+
#| headers? : Map[String, String],
68+
#| body? : Data,
69+
#| proxy? : Unit,
70+
#|) -> (Response, Data) = "%eval.async.http.get"
71+
),
72+
},
73+
)
74+
75+
///|
76+
fn run_async_main(f : async () -> Unit) -> Unit {
77+
@moon_async.run_async_main(f)
78+
}
79+
80+
///|
81+
async fn http_get(url : String) -> (@http.Response, &@io.Data) {
82+
@http.get(url)
83+
}
84+
85+
///|
86+
let _keep_public_async_bindings : Unit = {
87+
ignore(run_async_main)
88+
ignore(http_get)
89+
}
90+
91+
///|
92+
#external
93+
priv type JsHttpResult
94+
95+
///|
96+
extern "js" fn sync_http_get(url : String) -> JsHttpResult =
97+
#| (url) => {
98+
#| const { Worker } = require("node:worker_threads")
99+
#| const maxBytes = 1024 * 1024
100+
#| const headerBytes = 16
101+
#| const buffer = new SharedArrayBuffer(headerBytes + maxBytes)
102+
#| const state = new Int32Array(buffer, 0, 4)
103+
#| const body = new Uint8Array(buffer, headerBytes, maxBytes)
104+
#| const worker = new Worker(`
105+
#| const { parentPort, workerData } = require("node:worker_threads")
106+
#| const state = new Int32Array(workerData.buffer, 0, 4)
107+
#| const body = new Uint8Array(workerData.buffer, workerData.headerBytes, workerData.maxBytes)
108+
#| ;(async () => {
109+
#| try {
110+
#| const response = await fetch(workerData.url)
111+
#| const text = await response.text()
112+
#| const bytes = new TextEncoder().encode(text)
113+
#| const len = Math.min(bytes.length, workerData.maxBytes)
114+
#| body.set(bytes.subarray(0, len))
115+
#| Atomics.store(state, 1, response.status)
116+
#| Atomics.store(state, 2, len)
117+
#| Atomics.store(state, 3, bytes.length > workerData.maxBytes ? 1 : 0)
118+
#| } catch (error) {
119+
#| const bytes = new TextEncoder().encode(String(error && error.message || error))
120+
#| const len = Math.min(bytes.length, workerData.maxBytes)
121+
#| body.set(bytes.subarray(0, len))
122+
#| Atomics.store(state, 1, -1)
123+
#| Atomics.store(state, 2, len)
124+
#| } finally {
125+
#| Atomics.store(state, 0, 1)
126+
#| Atomics.notify(state, 0, 1)
127+
#| }
128+
#| })()
129+
#| `, { eval: true, workerData: { url, buffer, headerBytes, maxBytes } })
130+
#| const waitResult = Atomics.wait(state, 0, 0, 10000)
131+
#| worker.terminate()
132+
#| const len = Atomics.load(state, 2)
133+
#| const text = new TextDecoder().decode(body.subarray(0, len))
134+
#| if (waitResult === "timed-out") {
135+
#| return { code: -1, text: "HTTP request timed out", truncated: false }
136+
#| }
137+
#| return {
138+
#| code: Atomics.load(state, 1),
139+
#| text,
140+
#| truncated: Atomics.load(state, 3) === 1,
141+
#| }
142+
#| }
143+
144+
///|
145+
extern "js" fn JsHttpResult::code(self : JsHttpResult) -> Int =
146+
#| (result) => result.code
147+
148+
///|
149+
extern "js" fn JsHttpResult::text(self : JsHttpResult) -> String =
150+
#| (result) => result.text
151+
152+
///|
153+
fn last_fn_arg(
154+
ctx : @core.RuntimeFunctionContext,
155+
) -> @core.WithType[@core.RuntimeFunction]? {
156+
let mut result = None
157+
for arg in ctx.args {
158+
if arg.val is @core.RuntimeValue::Fn(func) {
159+
result = Some(func)
160+
}
161+
}
162+
result
163+
}
164+
165+
///|
166+
fn string_pos_arg(ctx : @core.RuntimeFunctionContext, index : Int) -> String? {
167+
let mut pos = 0
168+
for arg in ctx.args {
169+
if arg.kind is @core.RuntimeArgumentKind::Positional {
170+
if pos == index {
171+
return match arg.val {
172+
@core.RuntimeValue::String(value) => Some(value)
173+
_ => None
174+
}
175+
}
176+
pos = pos + 1
177+
}
178+
}
179+
None
180+
}
181+
182+
///|
183+
let eval_async_with_timeout_fn : @core.RuntimeFunction = ctx => {
184+
match last_fn_arg(ctx) {
185+
Some(func) => ctx.context.call(func.val, ctx.pkg, [])
186+
None => @core.RuntimeValue::Unit
187+
}
188+
}
189+
190+
///|
191+
let eval_async_retry_fn : @core.RuntimeFunction = ctx => {
192+
match last_fn_arg(ctx) {
193+
Some(func) => ctx.context.call(func.val, ctx.pkg, [])
194+
None => @core.RuntimeValue::Unit
195+
}
196+
}
197+
198+
///|
199+
fn response_to_value(
200+
pkg : @core.RuntimePackage,
201+
response : @http.Response,
202+
) -> @core.RuntimeValue {
203+
pkg.cons_with_labels("Response", [
204+
(Some("code"), @core.RuntimeValue::Int(response.code, raw=None), false),
205+
(Some("reason"), @core.RuntimeValue::String(response.reason), false),
206+
(Some("headers"), @core.RuntimeValue::Map({}), false),
207+
(Some("cookies"), @core.RuntimeValue::Array([]), false),
208+
])
209+
}
210+
211+
///|
212+
fn data_to_value(
213+
pkg : @core.RuntimePackage,
214+
text : String,
215+
) -> @core.RuntimeValue {
216+
pkg.cons_with_labels("Data", [
217+
(Some("text"), @core.RuntimeValue::String(text), false),
218+
])
219+
}
220+
221+
///|
222+
let eval_async_http_get_fn : @core.RuntimeFunction = ctx => {
223+
match string_pos_arg(ctx, 0) {
224+
Some(url) => {
225+
let http_pkg = ctx.context.find_pkg("http")
226+
let io_pkg = http_pkg.deps
227+
.get("moonbitlang/async/io")
228+
.unwrap_or(http_pkg.deps.get("io").unwrap_or(http_pkg))
229+
let result = sync_http_get(url)
230+
let code = result.code()
231+
if code < 0 {
232+
raise @core.control_error(result.text())
233+
}
234+
@core.RuntimeValue::Tuple([
235+
response_to_value(http_pkg, {
236+
code,
237+
reason: "",
238+
headers: {},
239+
cookies: [],
240+
}),
241+
data_to_value(io_pkg, result.text()),
242+
])
243+
}
244+
None => @core.RuntimeValue::Unit
245+
}
246+
}
247+
248+
///|
249+
#alias(module)
250+
pub fn module_() -> @core.RuntimeModule {
251+
{
252+
meta: @core.ModuleInfo::new(
253+
"moonbitlang/async",
254+
version="0.19.0",
255+
description="Injected async runtime bindings for oboard/eval",
256+
),
257+
pkgs: {
258+
"async": async_pkg,
259+
"moonbitlang/async": async_pkg,
260+
"io": async_io_pkg,
261+
"moonbitlang/async/io": async_io_pkg,
262+
"http": async_http_pkg,
263+
"moonbitlang/async/http": async_http_pkg,
264+
},
265+
embedded_fns: {
266+
"%eval.async.retry": eval_async_retry_fn,
267+
"%eval.async.with_timeout": eval_async_with_timeout_fn,
268+
"%eval.async.http.get": eval_async_http_get_fn,
269+
},
270+
}
271+
}

async/moon.pkg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {
2+
}
3+
4+
options(
5+
targets: { "module.mbt": [ "js" ] },
6+
)

async/pkg.generated.mbti

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Generated using `moon info`, DON'T EDIT IT
2+
package "oboard/eval/async"
3+
4+
// Values
5+
6+
// Errors
7+
8+
// Types and methods
9+
10+
// Type aliases
11+
12+
// Traits
13+

export.mbt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ pub fn value_to_string(value : RuntimeValue) -> String {
99
}
1010

1111
///|
12-
pub fn MoonBitVM::create(log? : Bool = false) -> MoonBitVM {
13-
MoonBitVM::new(log~)
12+
pub fn MoonBitVM::create(
13+
log? : Bool = false,
14+
modules? : Array[RuntimeModule] = [],
15+
) -> MoonBitVM {
16+
MoonBitVM::new(log~, modules~)
1417
}
1518

1619
///|
@@ -21,6 +24,11 @@ pub fn eval_result_to_string(result : EvalResult) -> String {
2124
}
2225
}
2326

27+
///|
28+
pub fn test_result_to_string(result : TestResult) -> String {
29+
result.to_string()
30+
}
31+
2432
///|
2533
pub fn code_to_ast(code : String) -> String {
2634
match @core.parse_eval_code(code) {

0 commit comments

Comments
 (0)