Current section

Files

Jump to
smol src smol.ffi.mjs
Raw

src/smol.ffi.mjs

import { Ok, Error, toList, toBitArray } from './gleam.mjs'
import * as bytes_tree from '../gleam_stdlib/gleam/bytes_tree.mjs'
import {
// FileError
RuntimeNotSupportedFileError,
// ReadError
ExcessBody, MalformedBody,
// Runtime
Bun, Deno, Node,
// Step
Send, Step, Close,
// WebsocketMessage
Text, Binary,
} from './smol.mjs';
export function detect_runtime() {
if (typeof globalThis.Bun !== 'undefined') {
return new Ok(new Bun());
} else if (typeof globalThis.Deno !== 'undefined') {
return new Ok(new Deno());
} else if (globalThis.process?.versions?.node) {
return new Ok(new Node());
} else {
return new Error();
}
}
// -- RUNTIME-SPECIFIC ---------------------------------------------------------
export async function from_file(path, offset, limit) {
const runtime = detect_runtime();
if (!runtime.isOk()) {
return new Error(new RuntimeNotSupportedFileError());
}
switch (runtime[0].constructor) {
case Bun: {
const { from_file } = await import('./bun.ffi.mjs')
return await from_file(path, offset, limit)
}
case Deno: {
const { from_file } = await import('./deno.ffi.mjs')
return await from_file(path, offset, limit)
}
case Node: {
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 (!runtime.isOk()) {
return new Error();
}
switch (runtime[0].constructor) {
case Bun: {
const { start } = await import('./bun.ffi.mjs')
return await start(builder)
}
case Deno: {
const { start } = await import('./deno.ffi.mjs')
return await start(builder)
}
case Node: {
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 new Error(new ExcessBody());
}
chunks.push(toBitArray([chunk]));
}
return new Ok(bytes_tree.concat_bit_arrays(toList(chunks)));
} catch(e) {
console.error('smol:', e.stack)
return new Error(new MalformedBody());
}
}
export function from_unfold(state, unfold) {
return new ReadableStream({
async pull(controller) {
try {
while (true) {
const step = await unfold(state);
if (step instanceof Send) {
const bytes = step.sending;
if (bytes.bitOffset) {
throw new globalThis.Error("Can only send byte-aligned BitArrays");
}
controller.enqueue(bytes.rawBuffer ?? bytes.buffer)
state = step.state;
return;
} else if (step instanceof Step) {
state = step.state;
continue;
} else if(step instanceof Close) {
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, runtime.dispatch];
}
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.#receive(appMsg);
}
onMessage(message) {
const smolMsg = typeof message === 'string'
? new 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!
: new 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 (message instanceof Text) {
this.send?.(message.text);
} else if (message instanceof Binary) {
const bytes = message.bytes;
if (bytes.bitOffset) {
throw new globalThis.Error("Can only send byte-aligned BitArrays");
}
this.send?.(message.rawBuffer ?? message.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 (result instanceof Send) {
this.#state = result.state;
this.#send(result.sending);
} else if (result instanceof Step) {
this.#state = result.state;
} else if (result instanceof Close) {
this.#close();
}
}
} finally {
this.#isRunning = false;
}
}
}