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,
BitArray$BitArray,
BitArray$BitArray$data,
} from "./gleam.mjs";
import { to_list } from "../gleam_javascript/gleam/javascript/array.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";
let runtimeResult;
export function detect_runtime() {
if (runtimeResult) return runtimeResult;
if (typeof globalThis.Bun !== "undefined") {
runtimeResult = Result$Ok(Runtime$Bun());
} else if (typeof globalThis.Deno !== "undefined") {
runtimeResult = Result$Ok(Runtime$Deno());
} else if (globalThis.process?.versions?.node) {
runtimeResult = Result$Ok(Runtime$Node());
} else {
runtimeResult = Result$Error(undefined);
}
return runtimeResult;
}
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 ---------------------------------------------------------
let runtimeModulePromise;
function importRuntimeModule() {
if (runtimeModulePromise) return runtimeModulePromise;
const runtime = detect_runtime();
if (!Result$isOk(runtime)) {
runtimeModulePromise = Promise.resolve(undefined);
return runtimeModulePromise;
}
const rt = Result$Ok$0(runtime);
if (Runtime$isBun(rt)) {
runtimeModulePromise = import("./bun.ffi.mjs");
} else if (Runtime$isDeno(rt)) {
runtimeModulePromise = import("./deno.ffi.mjs");
} else if (Runtime$isNode(rt)) {
runtimeModulePromise = import("./node.ffi.mjs");
} else {
runtimeModulePromise = Promise.resolve(undefined);
}
return runtimeModulePromise;
}
export async function from_file(path, offset, limit) {
const runtime = await importRuntimeModule();
if (!runtime) {
return Result$Error(FileError$RuntimeNotSupportedFileError());
}
return await runtime.from_file(path, offset, limit);
}
export async function start(builder) {
const runtime = await importRuntimeModule();
if (!runtime) {
return Result$Error(undefined);
}
return await runtime.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.byteLength;
if (totalSize > max_body_limit) {
return Result$Error(undefined);
}
chunks.push(BitArray$BitArray(chunk));
}
return Result$Ok(to_list(chunks));
} catch (e) {
return Result$Error(undefined);
}
}
export function bit_array_list_to_string(chunks) {
try {
const decoder = new TextDecoder("utf-8", { fatal: true });
let text = "";
for (const chunk of chunks) {
text += decoder.decode(BitArray$BitArray$data(chunk), { stream: true });
}
return Result$Ok(text + decoder.decode());
} catch (_error) {
return Result$Error(undefined);
}
}
export function from_bit_array(bytes) {
return new ReadableStream({
start(controller) {
const data = BitArray$BitArray$data(bytes);
controller.enqueue(
new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
);
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);
const data = BitArray$BitArray$data(bytes);
controller.enqueue(
new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
);
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)
: WebsocketMessage$Binary(
BitArray$BitArray(
message instanceof Uint8Array ? message : new Uint8Array(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)) {
const bytes = WebsocketMessage$Binary$bytes(message);
this.send?.(BitArray$BitArray$data(bytes));
}
}
#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;
}
}
}