Current section

Files

Jump to
smol src node.ffi.mjs
Raw

src/node.ffi.mjs

import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises'
import { Readable } from 'node:stream';
import http, { ServerResponse } from 'node:http';
import WebSocket from './vendor/ws.js';
import { Ok, Error } from './gleam.mjs';
import { None } from '../gleam_stdlib/gleam/option.mjs';
import {
adapt as adaptFetch, adaptNode, nodeToRequest, responseToNode
} from './adapt.ffi.mjs';
import { upgrade } from './smol.ffi.mjs';
import {
// Error
IsDir, NoEntry, NoAccess, UnknownFileError,
//
started, File
} from './smol.mjs';
export async function start(builder) {
try {
const listener = adaptNode(builder.handler);
const options = {};
const server = http.createServer(options, listener);
server.addListener('upgrade', adaptUpgrade(builder.handler));
let disposed = false;
const stop = () => {
if (disposed) {
return;
}
disposed = true;
console.log('smol: Gracefully shutting down. Please wait...');
process.off('SIGINT', onExit)
process.off('SIGTERM', onExit)
process.off('exit', onExit)
return new Promise((resolve, reject) => {
server.close(err => err ? reject(err) : resolve())
});
}
const onExit = async () => {
try {
await stop()
} finally {
process.exit()
}
}
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(builder.port, builder.interface, 511, resolve)
})
process.on('SIGINT', onExit);
process.on('SIGTERM', onExit);
process.on('exit', onExit);
server.on('error', err => {
console.log('smol:', err.stack);
// FIXME: can we not just die here? maybe restart the server?
process.exit(1);
});
const addressInfo = server.address();
const port = addressInfo.port;
const address = addressInfo.address;
builder.after_start(port, address);
return started(port, address, stop);
} catch(e) {
return new Error();
}
}
function adaptUpgrade(handler) {
const fetchHandler = adaptFetch(handler);
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
const upgradeHandler = async (req, socket, head) => {
const request = nodeToRequest({ toWebStream: Readable.toWeb }, req);
const response = await fetchHandler(request);
if (response.body[upgrade]) {
const runtime = response.body[upgrade]
wss.handleUpgrade(req, socket, head, (ws) => {
runtime.send = message => ws.send(message);
runtime.close = () => ws.close();
runtime.onOpen();
ws.on('message', (data, isBinary) => runtime.onMessage(isBinary ? data : data.toString()))
ws.on('close', runtime.onClose)
})
} else {
const res = new ServerResponse(req)
res.assignSocket(socket)
responseToNode(response, res)
}
}
return upgradeHandler
}
export async function from_file(path, offset, limit) {
try {
const fileInfo = await stat(path)
if (fileInfo.isDirectory()) {
return new Error(new IsDir());
}
const body = Readable.toWeb(createReadStream(path, {
start: offset,
end: limit > 0 ? offset + limit : Infinity
}));
return new Ok(new File(body, fileInfo.size, new None()));
} catch(e) {
if (e.code === 'ENOENT') {
return new Error(new NoEntry());
} else if (e.code === 'EACCES') {
return new Error(new NoAccess());
} else {
return new Error(new UnknownFileError());
}
}
}