Streamline JS/TS WASM HTTP handler, introduce HttpResponse.(status|ok|text|json).

This commit is contained in:
Sebastian Jeltsch 2026-02-18 18:10:56 +01:00
parent 69ee3775ea
commit 9365d86d81
22 changed files with 885 additions and 369 deletions

View file

@ -2,22 +2,21 @@ import { defineConfig } from "trailbase-wasm";
import {
HttpError,
HttpHandler,
OutgoingResponse,
Request,
HttpRequest,
HttpResponse,
StatusCode,
buildJsonResponse,
} from "trailbase-wasm/http";
import { execute, query, Transaction } from "trailbase-wasm/db";
export default defineConfig({
httpHandlers: [
HttpHandler.get("/fibonacci", (req: Request): string => {
HttpHandler.get("/fibonacci", (req: HttpRequest): string => {
const n = req.getQueryParam("n");
return `${fibonacci(n ? parseInt(n) : 40)}\n`;
}),
HttpHandler.get("/json", jsonHandler),
HttpHandler.post("/json", jsonHandler),
HttpHandler.get("/fetch", async (req: Request): Promise<string> => {
HttpHandler.get("/fetch", async (req: HttpRequest): Promise<string> => {
const url = req.getQueryParam("url");
if (url) {
return await (await fetch(url)).text();
@ -94,9 +93,9 @@ export default defineConfig({
],
});
function jsonHandler(req: Request): OutgoingResponse {
function jsonHandler(req: HttpRequest): HttpResponse {
const json = req.json();
return buildJsonResponse(
return HttpResponse.json(
json ?? {
int: 5,
real: 4.2,

View file

@ -23,7 +23,7 @@ const ignoredStarlightCustomTailwindClasses = [
export default [
{
ignores: ["dist/", "node_modules/", ".astro/", "src/env.d.ts"],
ignores: ["**/dist/", "**/node_modules/", ".astro/", "src/env.d.ts"],
},
jsPlugin.configs.recommended,
...tsPlugin.configs.recommended,

View file

@ -0,0 +1,3 @@
dist/
node_modules/
traildepot/

View file

@ -0,0 +1,30 @@
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist/", "node_modules/"],
},
{
files: ["src/**/*.{js,mjs,cjs,mts,ts,tsx,jsx}"],
rules: {
// https://typescript-eslint.io/rules/no-explicit-any/
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-wrapper-object-types": "warn",
"@typescript-eslint/no-namespace": "off",
"no-var": "off",
// http://eslint.org/docs/rules/no-unused-vars
"@typescript-eslint/no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
},
];

View file

@ -0,0 +1,29 @@
{
"name": "docs-example-wasm-guest-ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build:wasm": "jco componentize dist/index.js -w node_modules/trailbase-wasm/wit -o dist/component.wasm",
"build:wasm:aot": "jco componentize dist/index.js -w node_modules/trailbase-wasm/wit --aot -o dist/component.wasm",
"build": "vite build && npm run build:wasm",
"check": "tsc --noEmit --skipLibCheck && eslint",
"dev": "node --experimental-strip-types hot-reload.ts",
"format": "prettier -w src"
},
"dependencies": {
"trailbase-wasm": "workspace:*"
},
"devDependencies": {
"@bytecodealliance/jco": "^1.16.1",
"@eslint/js": "^9.39.2",
"@types/node": "^25.2.1",
"commander": "^14.0.3",
"eslint": "^9.39.2",
"nano-spawn": "^2.0.0",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1"
}
}

View file

@ -0,0 +1,3 @@
import e from "./index";
export const { initEndpoint, incomingHandler, sqliteFunctionEndpoint } = e;

View file

@ -0,0 +1,21 @@
import { defineConfig } from "trailbase-wasm";
import { query } from "trailbase-wasm/db";
import { HttpHandler, HttpResponse, StatusCode } from "trailbase-wasm/http";
import type { HttpRequest } from "trailbase-wasm/http";
async function countRecordsHandler(req: HttpRequest): Promise<HttpResponse> {
const table = req.getPathParam("table");
if (!table) {
return HttpResponse.status(
StatusCode.BAD_REQUEST,
`Table not found for '?table=${table}'`,
);
}
const rows = await query(`SELECT COUNT(*) FROM ${table}`, []);
return HttpResponse.text(`count: ${rows[0][0]}`);
}
export default defineConfig({
httpHandlers: [HttpHandler.get("/count/{table}", countRecordsHandler)],
});

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "es2022" /* Specify what module code is generated. */,
"moduleResolution": "bundler",
"paths": {},
"outDir": "./dist/" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View file

@ -0,0 +1,18 @@
import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: "./dist",
minify: false,
lib: {
entry: "./src/component.ts",
name: "runtime",
fileName: "index",
formats: ["es"],
},
rollupOptions: {
external: (source) =>
source.startsWith("wasi:") || source.startsWith("trailbase:"),
},
},
});

View file

@ -2,40 +2,68 @@
title: Custom APIs
---
import { Aside } from "@astrojs/starlight/components";
import { Aside, Code } from "@astrojs/starlight/components";
import { githubPath } from "@/lib/github.ts"
On startup TrailBase will automatically load any WASM component, i.e. `*.wasm`
files in `traildepot/wasm`.
files, that it finds in `<traildepot>/wasm`.
This can be used to implement arbitrary HTTP APIs with custom handlers.
## Example HTTP Endpoint
The following TypeScript WASM example illustrates a few things:
## Examples
* How to register a parameterized route with `{table}`.
* How to query the database.
* How to return an HTTP error.
At this point the documentation is embarrassingly "thin", take a look at the examples
for further context. Some are provided here, more can be found in `/examples` including
project templates:
```ts
import { defineConfig } from "trailbase-wasm";
import { Request, HttpError, HttpHandler, StatusCode } from "trailbase-wasm/http";
import { query } from "trailbase-wasm/db";
async function handler(req: Request): Promise<string> {
const table = req.getPathParam("table");
if (table) {
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
return `entries: ${rows[0][0]}`;
}
throw new HttpError(
StatusCode.BAD_REQUEST, "Missing '?table=' search query param");
}
* TypeScript: <a href={githubPath("examples/wasm-guest-ts")}><code>/examples/wasm-guest-ts</code></a>
* JavaScript: <a href={githubPath("examples/wasm-guest-js")}><code>/examples/wasm-guest-js</code></a>
* Rust: <a href={githubPath("examples/wasm-guest-rust")}><code>/examples/wasm-guest-rust</code></a>
export default defineConfig({
httpHandlers: [ HttpHandler.get("/test/{table}", handler) ],
});
```
### (Java|Type)Script
More examples can be found in the repository under `examples/wasm-guest-ts/`,
`examples/wasm-guest-js/` and `examples/wasm-guest-rust/`.
The following example demonstrates how to:
* register a parameterized route with `{table}`,
* query the database,
* return a string body or HTTP error.
import tsDocsExample from "@examples/wasm-guest-ts/src/index.ts?raw";
<Code
code={tsDocsExample}
lang="typescript"
title={"docs/examples/wasm-guest-ts/src/index.ts"}
mark={[]}
/>
Another example reading query parameters and producing a JSON response.
import tsCoffeeExample from "@root/examples/coffee-vector-search/guests/typescript/src/index.ts?raw";
<Code
code={tsCoffeeExample}
lang="typescript"
title={"examples/coffee-vector-search/guests/typescript/src/index.ts"}
mark={[]}
/>
### Rust
The following example demonstrates how to:
* register an HTTP route,
* read query parameters,
* query the database,
* and return a JSON response or errors.
import rustExample from "@root/examples/coffee-vector-search/guests/rust/src/lib.rs?raw";
<Code
code={rustExample}
lang="rust"
title={"examples/coffee-vector-search/guests/rust/src/lib.rs"}
mark={[]}
/>

View file

@ -8,18 +8,8 @@ use trailbase_wasm::{Guest, export};
type SearchResponse = (String, f64, f64, f64, f64);
fn as_real(v: &Value) -> Result<f64, String> {
if let Value::Real(f) = v {
return Ok(*f);
}
return Err(format!("Not a real: {v:?}"));
}
async fn search_handler(req: Request) -> Result<Json<Vec<SearchResponse>>, HttpError> {
let mut aroma: i64 = 8;
let mut flavor: i64 = 8;
let mut acidity: i64 = 8;
let mut sweetness: i64 = 8;
let (mut aroma, mut flavor, mut acidity, mut sweetness) = (8, 8, 8, 8);
for (param, value) in req.url().query_pairs() {
match param.as_ref() {
@ -31,7 +21,7 @@ async fn search_handler(req: Request) -> Result<Json<Vec<SearchResponse>>, HttpE
}
}
// Query with vector-search for the closest match.
// Query the closest match using vector-search.
let results: Vec<SearchResponse> = query(
r#"
SELECT Owner, Aroma, Flavor, Acidity, Sweetness
@ -39,9 +29,8 @@ async fn search_handler(req: Request) -> Result<Json<Vec<SearchResponse>>, HttpE
ORDER BY vec_distance_L2(
embedding, FORMAT("[%f, %f, %f, %f]", $1, $2, $3, $4))
LIMIT 100
"#
.to_string(),
vec![
"#,
[
Value::Integer(aroma),
Value::Integer(flavor),
Value::Integer(acidity),
@ -54,7 +43,7 @@ async fn search_handler(req: Request) -> Result<Json<Vec<SearchResponse>>, HttpE
.map(|row| {
// Convert to json response.
let Value::Text(owner) = row[0].clone() else {
panic!("unreachable");
panic!("invariant");
};
return (
@ -70,13 +59,20 @@ async fn search_handler(req: Request) -> Result<Json<Vec<SearchResponse>>, HttpE
return Ok(Json(results));
}
// Implement the function exported in this world (see above).
struct Endpoints;
fn as_real(v: &Value) -> Result<f64, String> {
return match v {
Value::Real(f) => Ok(*f),
_ => Err(format!("Not a real: {v:?}")),
};
}
impl Guest for Endpoints {
// Lastly, implement and export a TrailBase component.
struct GuestImpl;
impl Guest for GuestImpl {
fn http_handlers() -> Vec<HttpRoute> {
return vec![routing::get("/search", search_handler)];
}
}
export!(Endpoints);
export!(GuestImpl);

View file

@ -1,8 +1,8 @@
import { defineConfig } from "trailbase-wasm";
import { Request, HttpHandler } from "trailbase-wasm/http";
import { HttpHandler, HttpRequest, HttpResponse } from "trailbase-wasm/http";
import { query } from "trailbase-wasm/db";
async function searchHandler(req: Request): Promise<string> {
async function searchHandler(req: HttpRequest): Promise<HttpResponse> {
// Get the query params from the url, e.g. '/search?aroma=4&acidity=7'.
const aroma = req.getQueryParam("aroma") ?? 8;
const flavor = req.getQueryParam("flavor") ?? 8;
@ -19,7 +19,7 @@ async function searchHandler(req: Request): Promise<string> {
[+aroma, +flavor, +acid, +sweet],
);
return JSON.stringify(rows);
return HttpResponse.json(rows);
}
export default defineConfig({

View file

@ -1,5 +1,5 @@
import { defineConfig, addPeriodicCallback } from "trailbase-wasm";
import { HttpHandler, buildJsonResponse } from "trailbase-wasm/http";
import { HttpHandler, JsonResponse } from "trailbase-wasm/http";
import { JobHandler } from "trailbase-wasm/job";
import { query } from "trailbase-wasm/db";
@ -43,7 +43,7 @@ export default defineConfig({
});
function jsonHandler(req) {
return buildJsonResponse(
return JsonResponse.from(
req.json() ?? {
int: 5,
real: 4.2,

View file

@ -1,17 +1,17 @@
import { defineConfig, addPeriodicCallback } from "trailbase-wasm";
import { HttpHandler, Request, buildJsonResponse } from "trailbase-wasm/http";
import { HttpHandler, HttpRequest, HttpResponse } from "trailbase-wasm/http";
import { JobHandler } from "trailbase-wasm/job";
import { query } from "trailbase-wasm/db";
export default defineConfig({
httpHandlers: [
HttpHandler.get("/fibonacci", (req: Request) => {
HttpHandler.get("/fibonacci", (req: HttpRequest) => {
const n = req.getQueryParam("n");
return fibonacci(n ? parseInt(n) : 40).toString();
}),
HttpHandler.get("/json", jsonHandler),
HttpHandler.post("/json", jsonHandler),
HttpHandler.get("/a", (req: Request) => {
HttpHandler.get("/a", (req: HttpRequest) => {
const n = req.getQueryParam("n");
return "a".repeat(n ? parseInt(n) : 5000);
}),
@ -25,13 +25,13 @@ export default defineConfig({
}
});
}),
HttpHandler.get("/sleep", async (req: Request) => {
HttpHandler.get("/sleep", async (req: HttpRequest) => {
const param = req.getQueryParam("ms");
const ms: number = param ? parseInt(param) : 500;
await sleep(ms);
return `slept: ${ms}ms`;
}),
HttpHandler.get("/count/{table}/", async (req: Request) => {
HttpHandler.get("/count/{table}/", async (req: HttpRequest) => {
const table = req.getPathParam("table");
if (table) {
const rows = await query(`SELECT COUNT(*) FROM ${table}`, []);
@ -42,8 +42,8 @@ export default defineConfig({
jobHandlers: [JobHandler.minutely("myjob", () => console.log("Hello Job!"))],
});
function jsonHandler(req: Request) {
return buildJsonResponse(
function jsonHandler(req: HttpRequest) {
return HttpResponse.json(
req.json() ?? {
int: 5,
real: 4.2,

View file

@ -2,7 +2,7 @@
"name": "trailbase-wasm",
"description": "WASM Guest Runtime for custom JS/TS endpoints in TrailBase",
"homepage": "https://trailbase.io",
"version": "0.4.0",
"version": "0.5.0",
"license": "OSL-3.0",
"type": "module",
"exports": {

View file

@ -7,8 +7,13 @@ import {
import type { HttpContext } from "@common/HttpContext";
import type { HttpHandlerInterface, ResponseType } from "./index";
import { HttpError, StatusCode, buildResponse } from "./index";
import { type Method, RequestImpl } from "./request";
import { StatusCode } from "./index";
import {
HttpError,
responseToOutgoingResponse,
errorToOutgoingResponse,
} from "./response";
import { type Method, HttpRequestImpl } from "./request";
import { JobHandlerInterface } from "../job";
import { awaitPendingTimers } from "../timer";
@ -53,7 +58,7 @@ export function buildIncomingHttpHandler(args: {
}
return await handler(
new RequestImpl(
new HttpRequestImpl(
wasiMethodToMethod(req.method()),
req.pathWithQuery() ?? "",
context.path_params,
@ -70,30 +75,10 @@ export function buildIncomingHttpHandler(args: {
return async function (req: IncomingRequest, respOutparam: ResponseOutparam) {
try {
const resp: ResponseType = await handle(req);
writeResponse(
respOutparam,
resp instanceof OutgoingResponse
? resp
: buildResponse(
resp instanceof Uint8Array ? resp : encodeBytes(resp ?? ""),
),
);
const outgoingResp = responseToOutgoingResponse(resp);
writeResponse(respOutparam, outgoingResp);
} catch (err) {
if (err instanceof HttpError) {
writeResponse(
respOutparam,
buildResponse(encodeBytes(`${err.message}\n`), {
status: err.statusCode,
}),
);
} else {
writeResponse(
respOutparam,
buildResponse(encodeBytes(`Other: ${err}\n`), {
status: StatusCode.INTERNAL_SERVER_ERROR,
}),
);
}
writeResponse(respOutparam, errorToOutgoingResponse(err));
} finally {
await awaitPendingTimers();
}

View file

@ -1,19 +1,19 @@
import { Fields, OutgoingBody, OutgoingResponse } from "wasi:http/types@0.2.3";
import { StatusCode } from "./status";
import { Request, type Method } from "./request";
import { encodeBytes } from "./incoming";
import { HttpRequest } from "./request";
import type { Method } from "./request";
import type { ResponseType } from "./response";
// Override setInterval/setTimeout.
import "../timer";
// Exports:
export { OutgoingResponse } from "wasi:http/types@0.2.3";
export { StatusCode } from "./status";
export type { Method, Request, Scheme, User } from "./request";
export type { Method, HttpRequest, Scheme, User } from "./request";
export { HttpResponse, HttpError } from "./response";
export type { ResponseType } from "./response";
export type ResponseType = string | Uint8Array | OutgoingResponse | void;
export type HttpHandlerCallback = (
req: Request,
req: HttpRequest,
) => ResponseType | Promise<ResponseType>;
export interface HttpHandlerInterface {
@ -57,158 +57,3 @@ export class HttpHandler implements HttpHandlerInterface {
return new HttpHandler(path, "put", handler);
}
}
export class HttpError extends Error {
readonly statusCode: number;
readonly headers: [string, string][] | undefined;
constructor(
statusCode: number,
message?: string,
headers?: [string, string][],
) {
super(message);
this.statusCode = statusCode;
this.headers = headers;
}
public override toString(): string {
return `HttpError(${this.statusCode}, ${this.message})`;
}
}
export type ResponseOptions = {
status?: StatusCode;
headers?: [string, Uint8Array][];
};
export function buildJsonResponse(
body: object,
opts?: ResponseOptions,
): OutgoingResponse {
return buildResponse(encodeBytes(JSON.stringify(body)), {
...opts,
headers: [
["Content-Type", encodeBytes("application/json")],
...(opts?.headers ?? []),
],
});
}
export function buildResponse(
body: Uint8Array,
opts?: ResponseOptions,
): OutgoingResponse {
// NOTE: `outputStream.blockingWriteAndFlush` only writes up to 4kB, see documentation.
if (body.length <= 4096) {
return buildSmallResponse(body, opts);
}
return buildLargeResponse(body, opts);
}
function buildSmallResponse(
body: Uint8Array,
opts?: ResponseOptions,
): OutgoingResponse {
const outgoingResponse = new OutgoingResponse(
Fields.fromList(opts?.headers ?? []),
);
const outgoingBody = outgoingResponse.body();
{
// Create a stream for the response body
const outputStream = outgoingBody.write();
outputStream.blockingWriteAndFlush(body);
outputStream[Symbol.dispose]?.();
}
outgoingResponse.setStatusCode(opts?.status ?? StatusCode.OK);
OutgoingBody.finish(outgoingBody, undefined);
return outgoingResponse;
}
function buildLargeResponse(
body: Uint8Array,
opts?: ResponseOptions,
): OutgoingResponse {
const outgoingResponse = new OutgoingResponse(
Fields.fromList(opts?.headers ?? []),
);
const outgoingBody = outgoingResponse.body();
{
const outputStream = outgoingBody.write();
// Retrieve a Preview 2 I/O pollable to coordinate writing to the output stream
const pollable = outputStream.subscribe();
let written = 0n;
let remaining = BigInt(body.length);
while (remaining > 0) {
// Wait for the stream to become writable
pollable.block();
// Get the amount of bytes that we're allowed to write
let writableByteCount = outputStream.checkWrite();
if (remaining <= writableByteCount) {
writableByteCount = BigInt(remaining);
}
// If we are not allowed to write any more, but there are still bytes
// remaining then flush and try again
if (writableByteCount === 0n && remaining !== 0n) {
outputStream.flush();
continue;
}
outputStream.write(
new Uint8Array(body.buffer, Number(written), Number(writableByteCount)),
);
written += writableByteCount;
remaining -= written;
// While we can track *when* to flush separately and implement our own logic,
// the simplest way is to flush the written chunk immediately
outputStream.flush();
}
pollable[Symbol.dispose]?.();
outputStream[Symbol.dispose]?.();
}
outgoingResponse.setStatusCode(opts?.status ?? StatusCode.OK);
OutgoingBody.finish(outgoingBody, undefined);
return outgoingResponse;
}
// function writeResponseOriginal(
// responseOutparam: ResponseOutparam,
// status: number,
// body: Uint8Array,
// ) {
// /* eslint-disable prefer-const */
// const outgoingResponse = new OutgoingResponse(new Fields());
//
// let outgoingBody = outgoingResponse.body();
// {
// // Create a stream for the response body
// let outputStream = outgoingBody.write();
// outputStream.blockingWriteAndFlush(body);
//
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore: This is required in order to dispose the stream before we return
// outputStream[Symbol.dispose]();
// //outputStream[Symbol.dispose]?.();
// }
//
// outgoingResponse.setStatusCode(status);
// OutgoingBody.finish(outgoingBody, undefined);
//
// ResponseOutparam.set(responseOutparam, { tag: "ok", val: outgoingResponse });
// }

View file

@ -15,7 +15,7 @@ export type User = {
csrf: string;
};
export interface Request {
export interface HttpRequest {
path(): string;
method(): Method;
scheme(): Scheme | undefined;
@ -36,7 +36,7 @@ export interface Request {
json(): object | undefined;
}
export class RequestImpl implements Request {
export class HttpRequestImpl implements HttpRequest {
constructor(
private readonly _method: Method,
private readonly _path: string,

View file

@ -0,0 +1,219 @@
import { Fields, OutgoingBody, OutgoingResponse } from "wasi:http/types@0.2.3";
import { StatusCode } from "./status";
import { encodeBytes } from "./incoming";
export type ResponseType =
| string
| Uint8Array
| HttpResponse
| OutgoingResponse
| void;
export class HttpResponse {
protected constructor(
public readonly status: StatusCode,
public body?: Uint8Array,
public headers: [string, Uint8Array][] = [],
) {}
public static status(
status: StatusCode | number,
body?: string | Uint8Array,
): HttpResponse {
return new HttpResponse(
status,
typeof body === "string" ? encodeBytes(body) : body,
);
}
public static ok(body?: string | Uint8Array): HttpResponse {
return new HttpResponse(
StatusCode.OK,
typeof body === "string" ? encodeBytes(body) : body,
);
}
public static text(body: string | Uint8Array): HttpResponse {
return new HttpResponse(
StatusCode.OK,
typeof body === "string" ? encodeBytes(body) : body,
[["Content-Type", encodeBytes("text/plain; charset=utf-8")]],
);
}
public static json(value: object): HttpResponse {
return new HttpResponse(StatusCode.OK, encodeBytes(JSON.stringify(value)), [
["Content-Type", encodeBytes("application/json")],
]);
}
public setBody(body: string | Uint8Array): HttpResponse {
this.body = typeof body === "string" ? encodeBytes(body) : body;
return this;
}
public setHeader(key: string, value: string | Uint8Array): HttpResponse {
this.headers.push([
key,
typeof value === "string" ? encodeBytes(value) : value,
]);
return this;
}
}
export class HttpError extends Error {
public constructor(
public readonly status: StatusCode,
message?: string,
) {
super(message);
}
public static from(status: StatusCode | number, message?: string): HttpError {
return new HttpError(status, message);
}
public override toString(): string {
return `HttpError(${this.status}, ${this.message})`;
}
}
export function responseToOutgoingResponse(
resp: ResponseType,
): OutgoingResponse {
if (resp instanceof OutgoingResponse) {
return resp;
} else if (resp instanceof HttpResponse) {
return buildResponse({
status: resp.status,
headers: resp.headers,
body: resp.body ?? new Uint8Array(),
});
} else if (resp instanceof Uint8Array) {
return buildResponse({
status: StatusCode.OK,
headers: [],
body: resp,
});
} else if (typeof resp === "string") {
return buildResponse({
status: StatusCode.OK,
headers: [],
body: encodeBytes(resp),
});
} else {
// void case.
return buildResponse({
status: StatusCode.OK,
headers: [],
body: new Uint8Array(),
});
}
}
export function errorToOutgoingResponse(err: unknown): OutgoingResponse {
if (err instanceof HttpError) {
return buildResponse({
status: err.status,
headers: [["Content-Type", encodeBytes("text/plain; charset=utf-8")]],
body: err.message ? encodeBytes(err.message) : new Uint8Array(),
});
}
return buildResponse({
body: encodeBytes(`uncaught: ${err}`),
status: StatusCode.INTERNAL_SERVER_ERROR,
headers: [],
});
}
type ResponseOptions = {
status: StatusCode;
headers: [string, Uint8Array][];
body: Uint8Array;
};
function buildResponse(opts: ResponseOptions): OutgoingResponse {
// NOTE: `outputStream.blockingWriteAndFlush` only writes up to 4kB, see documentation.
if (opts.body.length <= 4096) {
return buildSmallResponse(opts);
}
return buildLargeResponse(opts);
}
function buildSmallResponse({
status,
headers,
body,
}: ResponseOptions): OutgoingResponse {
const outgoingResponse = new OutgoingResponse(Fields.fromList(headers));
const outgoingBody = outgoingResponse.body();
{
// Create a stream for the response body
const outputStream = outgoingBody.write();
outputStream.blockingWriteAndFlush(body);
outputStream[Symbol.dispose]?.();
}
outgoingResponse.setStatusCode(status);
OutgoingBody.finish(outgoingBody, undefined);
return outgoingResponse;
}
function buildLargeResponse({
status,
headers,
body,
}: ResponseOptions): OutgoingResponse {
const outgoingResponse = new OutgoingResponse(Fields.fromList(headers));
const outgoingBody = outgoingResponse.body();
{
const outputStream = outgoingBody.write();
// Retrieve a Preview 2 I/O pollable to coordinate writing to the output stream
const pollable = outputStream.subscribe();
let written = 0n;
let remaining = BigInt(body.length);
while (remaining > 0) {
// Wait for the stream to become writable
pollable.block();
// Get the amount of bytes that we're allowed to write
let writableByteCount = outputStream.checkWrite();
if (remaining <= writableByteCount) {
writableByteCount = BigInt(remaining);
}
// If we are not allowed to write any more, but there are still bytes
// remaining then flush and try again
if (writableByteCount === 0n && remaining !== 0n) {
outputStream.flush();
continue;
}
outputStream.write(
new Uint8Array(body.buffer, Number(written), Number(writableByteCount)),
);
written += writableByteCount;
remaining -= written;
// While we can track *when* to flush separately and implement our own logic,
// the simplest way is to flush the written chunk immediately
outputStream.flush();
}
pollable[Symbol.dispose]?.();
outputStream[Symbol.dispose]?.();
}
outgoingResponse.setStatusCode(status);
OutgoingBody.finish(outgoingBody, undefined);
return outgoingResponse;
}

View file

@ -29,8 +29,8 @@ export default defineConfig({
// NOTE: Needs to be in `rollupOptions` rather than vite's plugins to apply to each input.
dts({
rollupTypes: true,
// NOTE: Inlucde .d.ts files generated by `jco`.
copyDtsFiles: true,
// NOTE: Include .d.ts files generated by `jco`.
copyDtsFiles: false,
// NOTE: The .d.ts files generated by `jco` contain dynamic imports.
staticImport: true,
}),

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ packages:
- 'crates/auth-ui/ui'
- 'docs'
- 'docs/examples/record_api_ts'
- 'docs/examples/wasm-guest-ts'
- 'examples/blog/web'
- 'examples/coffee-vector-search'
- 'examples/coffee-vector-search/guests/typescript'