///|
pub fn run(f : async () -> Unit noraise) -> Unit = "%async.run"
///|
pub suberror IOError derive(Show)
///|
pub suberror NetworkError derive(Show)
///|
pub suberror StreamError derive(Show)
///|
pub suberror UnimplementedError derive(Show)
///|
pub suberror ExecError derive(Show)
///|
pub(all) enum FetchCredentials {
Omit
SameOrigin
Include
} derive(ToJson(style="flat"), Show, Eq)
///|
pub(all) enum FetchMode {
CORS
NoCORS
SameOrigin
Navigate
} derive(ToJson(style="flat"), Show, Eq)
///|
pub struct HttpResponse {
mut statusCode : Int
mut headers : Map[String, String]
data : Bytes
} derive(Show, Eq)
///|
pub struct SSEEvent {
mut event_type : String?
mut data : String
mut id : String?
mut retry : Int?
} derive(Show, Eq)
///|
pub fn HttpResponse::json(self : HttpResponse) -> Json raise @json.ParseError {
@json.parse(self.text())
}
///|
pub fn HttpResponse::unwrap_json(self : HttpResponse) -> Json {
(try? self.json()).or(Json::null())
}
///|
pub fn HttpResponse::text(self : HttpResponse) -> String {
let buf = StringBuilder::new()
try! @encoding.decoder(UTF8).decode_to(self.data, buf)
buf.to_string()
}
///|
pub fn SSEEvent::parse(data : String) -> Array[SSEEvent] {
let events = []
let lines = data.split("\n")
let mut current_event = SSEEvent::{
event_type: None,
data: "",
id: None,
retry: None,
}
let mut has_data = false
for line in lines {
let trimmed = line.to_string()
if trimmed == "" {
if has_data {
events.push(current_event)
current_event = SSEEvent::{
event_type: None,
data: "",
id: None,
retry: None,
}
has_data = false
}
} else if trimmed.strip_prefix(":") != None {
// Comment line, ignore
continue
} else if trimmed.find(":") is Some(colon_pos) {
let field = trimmed.substring(start=0, end=colon_pos)
let value = if colon_pos + 1 < trimmed.length() {
trimmed.substring(start=colon_pos + 1, end=trimmed.length())
} else {
""
}
match field {
"event" => current_event.event_type = Some(value)
"data" => {
if current_event.data != "" {
current_event.data = current_event.data + "\n" + value
} else {
current_event.data = value
}
has_data = true
}
"id" => current_event.id = Some(value)
"retry" =>
try {
current_event.retry = Some(@strconv.parse_int(value))
} catch {
_ => continue
}
_ => continue
}
} else {
// Field without colon, treat as data
if current_event.data != "" {
current_event.data = current_event.data + "\n" + trimmed
} else {
current_event.data = trimmed
}
has_data = true
}
}
if has_data {
events.push(current_event)
}
events
}
///|
pub(all) enum HttpMethod {
GET
POST
PUT
DELETE
PATCH
OPTIONS
HEAD
CONNECT
TRACE
} derive(ToJson(style="legacy"), Show, Eq)
///|
pub(all) struct HttpRequest {
request_method : HttpMethod
url : String?
path : String
query : String?
body : Json?
} derive(ToJson, Show, Eq)
///|
pub fn listen(_port : Int) -> Unit raise Error {
fail("Invalid backend")
}
///|
pub fn handle(_reqMethod : String, _mapping : String) -> Unit {
}
///|
pub fn write_head(
self : HttpResponse,
statusCode : Int,
headers : Map[String, String],
) -> Unit {
self.statusCode = statusCode
self.headers = headers
}
///|
pub async fn get(
url : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(url, http_method=HttpMethod::GET, headers?, credentials?, mode?)
}
///|
pub async fn post(
url : String,
body? : String,
data? : Json,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
let mHeaders = {}
if headers is Some(h) {
for k, v in h {
mHeaders.set(k, v)
}
}
if data is Some(_) {
mHeaders.set("Content-Type", "application/json")
}
request(
url,
http_method=HttpMethod::POST,
body=if data is Some(d) {
d.stringify()
} else if body is Some(b) {
b
} else {
""
},
headers=mHeaders,
credentials?,
mode?,
)
}
///|
pub async fn put(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::PUT,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn delete(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::DELETE,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn patch(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::PATCH,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn options(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::OPTIONS,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn head(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::HEAD,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn connect(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::CONNECT,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn trace(
url : String,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
request(
url,
http_method=HttpMethod::TRACE,
body?,
headers?,
credentials?,
mode?,
)
}
///|
pub async fn request(
url : String,
http_method? : HttpMethod,
body? : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
stream? : Bool = false,
) -> HttpResponse raise Error {
let options : Map[String, Json] = {}
if http_method is Some(m) {
options.set("method", m.to_string().to_json())
}
if body is Some(b) {
options.set("body", b.to_json())
}
if headers is Some(h) {
options.set("headers", h.to_json())
}
if credentials is Some(c) {
options.set("credentials", c.to_json())
}
if mode is Some(m) {
options.set("mode", m.to_string().to_json())
}
if stream {
options.set("stream", stream.to_json())
}
let (response_info, headers_json, data) = request_internal_ffi(
[url.to_json(), options.to_json()].to_json().stringify(),
)
match (try? @json.parse(response_info), try? @json.parse(headers_json)) {
(Ok({ "status": Number(status, ..), .. }), Ok(Object(headers))) =>
HttpResponse::{
statusCode: status.to_int(),
headers: headers
.iter()
.map(fn(i : (String, Json)) -> (String, String) {
(i.0, i.1.as_string().unwrap_or(""))
})
.to_array()
|> Map::from_array,
data,
}
_ => {
println("Error fetching url: " + url)
raise NetworkError
}
}
}
///|
pub async fn download(
url : String,
save_path? : String,
save_path_fn? : (Map[String, String]) -> String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
) -> HttpResponse raise Error {
let res = request(
url,
http_method=HttpMethod::GET,
headers?,
credentials?,
mode?,
)
@fs.write_bytes_to_file(
if save_path is Some(s) {
s
} else if save_path_fn is Some(f) {
f(res.headers)
} else {
"downloaded_file"
},
res.data,
)
res
}
///|
pub async fn request_stream(
url : String,
http_method? : HttpMethod,
body? : Json,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
on_chunk~ : (String) -> Unit,
) -> HttpResponse raise NetworkError {
let options : Map[String, Json] = {}
if http_method is Some(m) {
options.set("method", m.to_string().to_json())
}
if body is Some(b) {
options.set("body", b.stringify().to_json())
}
if headers is Some(h) {
options.set("headers", h.to_json())
}
if credentials is Some(c) {
options.set("credentials", c.to_json())
}
if mode is Some(m) {
options.set("mode", m.to_string().to_json())
}
match
(try? @json.parse(
request_stream_internal_ffi(
[url.to_json(), options.to_json()].to_json().stringify(),
on_chunk,
),
)) {
Ok(
{
"headers": Object(headers),
"status": Number(status, ..),
"data": String(data),
..
}
) =>
HttpResponse::{
statusCode: status.to_int(),
headers: headers
.iter()
.map(fn(i : (String, Json)) -> (String, String) {
(i.0, i.1.as_string().unwrap_or(""))
})
.to_array()
|> Map::from_array,
data: data.to_bytes(),
}
_ => raise NetworkError
}
}
///|
pub async fn sse_connect(
url : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
on_event : (SSEEvent) -> Unit,
) -> HttpResponse raise Error {
let mut buffer = ""
request_stream(
url,
http_method=HttpMethod::GET,
headers={ "Accept": "text/event-stream" },
credentials?,
mode?,
on_chunk=fn(chunk : String) {
buffer = buffer + chunk
// Process complete events (separated by double newlines)
let parts = buffer.split("\n\n").to_array()
if parts.length() > 1 {
for i = 0; i < parts.length() - 1; i = i + 1 {
let event_data = parts[i].to_string()
if event_data != "" {
let events = SSEEvent::parse(event_data + "\n\n")
for event in events {
on_event(event)
}
}
}
// Keep the last incomplete part in buffer
buffer = parts[parts.length() - 1].to_string()
}
},
)
}
///|
pub async fn get_stream(
url : String,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
on_chunk : (String) -> Unit,
) -> HttpResponse raise NetworkError {
request_stream(
url,
http_method=HttpMethod::GET,
headers?,
credentials?,
mode?,
on_chunk~,
)
}
///|
pub async fn post_stream(
url : String,
data~ : Json,
headers? : Map[String, String],
credentials? : FetchCredentials,
mode? : FetchMode,
on_chunk : (String) -> Unit,
) -> HttpResponse raise NetworkError {
request_stream(
url,
http_method=HttpMethod::POST,
body=data,
headers?,
credentials?,
mode?,
on_chunk~,
)
}