Current section

Files

Jump to
smol src adapt.ffi.mjs
Raw

src/adapt.ffi.mjs

import { Ok, toList } from './gleam.mjs'
import * as option from '../gleam_stdlib/gleam/option.mjs';
import * as uri from '../gleam_stdlib/gleam/uri.mjs';
import * as http from '../gleam_http/gleam/http.mjs';
import { Request } from '../gleam_http/gleam/http/request.mjs';
import { status_text } from './smol.mjs'
export function adapt(handler) {
return async function(request) {
return gleamToResponse(await handler(requestToGleam(request)));
}
}
export function adaptNode(handler) {
const fetchHandler = adapt(handler);
const nodeHandler = async function (req, res) {
try {
const { Readable } = await import('node:stream');
const request = nodeToRequest({ toWebStream: Readable.toWeb }, req);
const response = await fetchHandler(request);
await responseToNode(response, res);
} catch (error) {
console.error('smol:', error.stack)
if (!res.headersSent) {
res.statusCode = 500;
res.end('Internal Server Error');
} else {
res.destroy(error);
}
}
}
return nodeHandler;
}
export function requestToGleam(request) {
const method = http.parse_method(request.method);
if (!method.isOk()) {
throw new globalThis.Error('Invalid method');
}
const headers = toList([...request.headers.entries()]);
const body = request.body;
const url = uri.parse(request.url);
if (!url.isOk()) {
throw new globalThis.Error('Invalid url');
}
const scheme = url[0].scheme instanceof option.Some
? http.scheme_from_string(url[0].scheme[0])
: new Ok(new http.Http());
if (!scheme.isOk()) {
throw new globalThis.Error('Invalid scheme');
}
const host = option.unwrap(url[0].host, "localhost");
return new Request(method[0], headers, body, scheme[0], host, url[0].port, url[0].path, url[0].query);
}
export function gleamToResponse(response) {
const headers = new globalThis.Headers();
for(const [name, value] of response.headers) {
headers.append(name, value);
}
return new globalThis.Response(response.body, {
status: response.status,
statusText: status_text(response.status),
headers: headers
})
}
export function nodeToRequest({ toWebStream }, req) {
const protocol = req.socket?.encrypted ? 'https:' : 'http:';
const host = req.headers.host || 'localhost';
const url = new URL(req.url, `${protocol}//${host}`);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (Array.isArray(value)) {
for (const v of value) {
headers.append(key, v);
}
} else {
headers.append(key, value);
}
}
// Only include a body if this is not a GET or HEAD request
const body = (req.method !== 'GET' && req.method !== 'HEAD')
? toWebStream(req)
: null;
const request = new globalThis.Request(url, {
method: req.method,
headers: headers,
body: body,
duplex: 'half' // Required for compatibility with Node's http streams
});
return request;
}
export async function responseToNode(response, res) {
// Set headers
res.statusCode = response.status;
for (const [key, value] of response.headers.entries()) {
res.setHeader(key, value);
}
if (response.body) {
const reader = response.body.getReader();
try {
// Process the stream chunk by chunk
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const canContinue = res.write(Buffer.from(value));
if (!canContinue) {
// Wait for the drain event if backpressure is detected
await new Promise(resolve => res.once('drain', resolve));
}
}
res.end();
} catch (error) {
console.error('smol: sending response failed:', error.stack);
if (!res.headersSent) {
res.statusCode = 500;
res.end('Internal Server Error: Stream processing failed');
} else {
res.destroy(error);
}
}
} else {
res.end();
}
}