Current section

Files

Jump to
smol src smol.ffi.mjs
Raw

src/smol.ffi.mjs

import {
Result$Ok,
Result$Error,
Result$isOk,
Result$Ok$0,
toList,
toBitArray,
} from "./gleam.mjs";
import * as bytes_tree from "../gleam_stdlib/gleam/bytes_tree.mjs";
import {
// FileError
FileError$RuntimeNotSupportedFileError,
// Runtime
Runtime$Bun,
Runtime$isDeno,
Runtime$isBun,
Runtime$isNode,
Runtime$Deno,
Runtime$Node,
// Step
Step$Send,
Step$isSend,
Step$Send$sending,
Step$Send$state,
Step$Step,
Step$isStep,
Step$Step$state,
Step$Close,
Step$isClose,
// WebsocketMessage
WebsocketMessage$Text,
WebsocketMessage$isText,
WebsocketMessage$Text$text,
WebsocketMessage$Binary,
WebsocketMessage$isBinary,
WebsocketMessage$Binary$bytes,
} from "./smol.mjs";
export function detect_runtime() {
if (typeof globalThis.Bun !== "undefined") {
return Result$Ok(Runtime$Bun());
} else if (typeof globalThis.Deno !== "undefined") {
return Result$Ok(Runtime$Deno());
} else if (globalThis.process?.versions?.node) {
return Result$Ok(Runtime$Node());
} else {
return Result$Error(undefined);
}
}
export function error_handler(message, error) {
console.error("smol: " + message, error?.stack ?? error);
}
export function rescue(on_error, inner_handler) {
return inner_handler().catch(on_error);
}
// -- RUNTIME-SPECIFIC ---------------------------------------------------------
export async function from_file(path, offset, limit) {
const runtime = detect_runtime();
if (!Result$isOk(runtime)) {
return Result$Error(FileError$RuntimeNotSupportedFileError());
}
const rt = Result$Ok$0(runtime);
if (Runtime$isBun(rt)) {
const { from_file } = await import("./bun.ffi.mjs");
return await from_file(path, offset, limit);
} else if (Runtime$isDeno(rt)) {
const { from_file } = await import("./deno.ffi.mjs");
return await from_file(path, offset, limit);
} else if (Runtime$isNode(rt)) {
const { from_file } = await import("./node.ffi.mjs");
return await from_file(path, offset, limit);
}
}
export async function start(builder) {
const runtime = detect_runtime();
if (!Result$isOk(runtime)) {
return Result$Error(undefined);
}
const rt = Result$Ok$0(runtime);
if (Runtime$isBun(rt)) {
const { start } = await import("./bun.ffi.mjs");
return await start(builder);
} else if (Runtime$isDeno(rt)) {
const { start } = await import("./deno.ffi.mjs");
return await start(builder);
} else if (Runtime$isNode(rt)) {
const { start } = await import("./node.ffi.mjs");
return await start(builder);
}
}
// -- BODY ---------------------------------------------------------------------
export async function read(request, max_body_limit) {
try {
const chunks = [];
let totalSize = 0;
for await (const chunk of request.body) {
totalSize += chunk.length; // TODO WHAT IS THE TYPE OF THIS
if (totalSize > max_body_limit) {
return Result$Error(undefined);
}
chunks.push(toBitArray([chunk]));
}
return Result$Ok(bytes_tree.concat_bit_arrays(toList(chunks)));
} catch (e) {
return Result$Error(undefined);
}
}
export function from_bit_array(bytes) {
if (bytes.bitOffset !== 0 || bytes.bitSize % 8 !== 0) {
throw new globalThis.Error("Can only send byte-aligned BitArrays");
}
return new ReadableStream({
start(controller) {
// TODO: migrate to new ffi api
controller.enqueue(bytes.rawBuffer ?? bytes.buffer);
controller.close();
},
});
}
export function from_unfold(state, unfold) {
return new ReadableStream({
async pull(controller) {
try {
while (true) {
const step = await unfold(state);
if (Step$isSend(step)) {
const bytes = Step$Send$sending(step);
if (bytes.bitOffset || bytes.bitSize % 8 !== 0) {
throw new globalThis.Error("Can only send byte-aligned BitArrays");
}
// TODO: migrate bitarray ffi
controller.enqueue(bytes.rawBuffer ?? bytes.buffer);
state = Step$Send$state(step);
return;
} else if (Step$isStep(step)) {
state = Step$Step$state(step);
continue;
} else if (Step$isClose(step)) {
controller.close();
return;
}
}
} catch (e) {
controller.error(e);
}
},
});
}
// -- WEBSOCKETS ---------------------------------------------------------------
export const upgrade = Symbol("smol-websocket");
export function from_upgrade(init, update, on_open, on_message, on_close) {
const stream = new ReadableStream();
const runtime = new WebsocketRuntime({
init,
update,
on_message,
on_close,
on_open,
});
stream[upgrade] = runtime;
return stream;
}
export class WebsocketRuntime {
#options;
#state;
#queue = [];
#isRunning = false;
#isClosed = true;
send = null;
close = null;
constructor(options) {
this.#options = options;
this.#state = options.init;
this.onOpen = this.onOpen.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onClose = this.onClose.bind(this);
this.dispatch = this.dispatch.bind(this);
}
dispatch(message) {
this.#receive(message);
}
onOpen() {
this.#isClosed = false;
const appMsg = this.#options.on_open(this.dispatch);
this.#receive(appMsg);
}
onMessage(message) {
const smolMsg =
typeof message === "string"
? WebsocketMessage$Text(message)
: // NOTE: we pass uint8array or _ArrayBuffer_ here -
// technially, ArrayBuffer is not mentioned in the implementation as being
// supported, but the way this function is implemented means it still works!
//
// TODO: replace toBitArray
WebsocketMessage$Binary(toBitArray([message]));
const appMsg = this.#options.on_message(smolMsg);
this.#receive(appMsg);
}
onClose() {
const appMsg = this.#options.on_close;
this.#receive(appMsg);
queueMicrotask(() => (this.#isClosed = true));
}
#close() {
this.close?.();
}
#send(message) {
if (WebsocketMessage$isText(message)) {
this.send?.(WebsocketMessage$Text$text(message));
} else if (WebsocketMessage$isBinary(message)) {
// TODO: BitArray FFI
const bytes = WebsocketMessage$Binary$bytes(message);
if (bytes.bitOffset || bytes.bitSize % 8 !== 0) {
throw new globalThis.Error("Can only send byte-aligned BitArrays");
}
this.send?.(bytes.rawBuffer ?? bytes.buffer);
}
}
#receive(msg) {
this.#queue.push(msg);
if (!this.#isRunning && !this.#isClosed) {
this.#run();
}
}
async #run() {
this.#isRunning = true;
try {
let msg;
while ((msg = this.#queue.shift())) {
const result = await this.#options.update(this.#state, msg);
if (Step$isSend(result)) {
this.#state = Step$Send$state(result);
this.#send(Step$Send$sending(result));
} else if (Step$isStep(result)) {
this.#state = Step$Step$state(result);
} else if (Step$isClose(result)) {
this.#close();
}
}
} finally {
this.#isRunning = false;
}
}
}