diff --git a/Cargo.lock b/Cargo.lock
index 3d53b2ad..e6de0b6c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5571,6 +5571,7 @@ dependencies = [
"axum-extra",
"axum-test",
"base64 0.22.1",
+ "bytes",
"chrono",
"cookie",
"criterion",
diff --git a/client/testfixture/index.ts b/client/testfixture/index.ts
deleted file mode 100644
index 036a2abd..00000000
--- a/client/testfixture/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { addRoute } from "trailbase:main";
-
-type Headers = { [key: string]: string };
-type Request = {
- uri: string;
- headers: Headers;
- body: string;
-};
-type Response = {
- headers?: Headers;
- status?: number;
- body?: string;
-};
-
-addRoute("GET", "/test", (req: Request) : Response => {
- console.log("Request", req);
- return {
- body: "js response",
- };
-});
diff --git a/client/testfixture/scripts/index.ts b/client/testfixture/scripts/index.ts
new file mode 100644
index 00000000..f1917d4c
--- /dev/null
+++ b/client/testfixture/scripts/index.ts
@@ -0,0 +1,45 @@
+import { addRoute, parsePath, query, htmlHandler, jsonHandler, stringHandler } from "trailbase:main";
+import type { JsonRequestType, ParsedPath, StringRequestType } from "../../../trailbase-core/js/src/index.ts";
+
+addRoute("GET", "/test", stringHandler(async (req: StringRequestType) => {
+ const uri : ParsedPath = parsePath(req.uri);
+
+ const table = uri.query.get("table");
+ if (table) {
+ const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
+ return `entries: ${rows[0][0]}`;
+ }
+
+ return `test: ${req.uri}`;
+}));
+
+addRoute("GET", "/test/:table", stringHandler(async (req: StringRequestType) => {
+ const table = req.params["table"];
+ if (table) {
+ const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
+ return `entries: ${rows[0][0]}`;
+ }
+
+ return `test: ${req.uri}`;
+}));
+
+addRoute("GET", "/html", htmlHandler((_req: StringRequestType) => {
+ return `
+
+
+ Html Handler
+
+
+ `;
+}));
+
+addRoute("GET", "/json", jsonHandler((_req: JsonRequestType) => {
+ return {
+ int: 5,
+ real: 4.2,
+ msg: "foo",
+ obj: {
+ nested: true,
+ }
+ };
+}));
diff --git a/client/trailbase-dart/test/trailbase_test.dart b/client/trailbase-dart/test/trailbase_test.dart
index ec04fe7a..ec19d15f 100644
--- a/client/trailbase-dart/test/trailbase_test.dart
+++ b/client/trailbase-dart/test/trailbase_test.dart
@@ -44,7 +44,7 @@ Future initTrailBase() async {
]);
final dio = Dio();
- for (int i = 0; i < 50; ++i) {
+ for (int i = 0; i < 100; ++i) {
try {
final response = await dio.fetch(
RequestOptions(path: 'http://127.0.0.1:${port}/api/healthcheck'));
diff --git a/client/trailbase-dotnet/ClientTest.cs b/client/trailbase-dotnet/ClientTest.cs
index 519f2bfa..a5a9c49d 100644
--- a/client/trailbase-dotnet/ClientTest.cs
+++ b/client/trailbase-dotnet/ClientTest.cs
@@ -53,7 +53,7 @@ public class ClientTestFixture : IDisposable {
var client = new HttpClient();
Task.Run(async () => {
- for (int i = 0; i < 50; ++i) {
+ for (int i = 0; i < 100; ++i) {
try {
var response = await client.GetAsync($"http://{address}/api/healthcheck");
if (response.StatusCode == System.Net.HttpStatusCode.OK) {
@@ -61,7 +61,7 @@ public class ClientTestFixture : IDisposable {
}
}
catch (Exception e) {
- Console.WriteLine($"Caught exception: {e}");
+ Console.WriteLine($"Caught exception: {e.Message}");
}
await Task.Delay(500);
diff --git a/client/trailbase-ts/tests/integration_test_runner.ts b/client/trailbase-ts/tests/integration_test_runner.ts
index f5045b6d..b6506ecf 100644
--- a/client/trailbase-ts/tests/integration_test_runner.ts
+++ b/client/trailbase-ts/tests/integration_test_runner.ts
@@ -22,7 +22,7 @@ async function initTrailBase(): Promise<{ subprocess: Subprocess }> {
const subprocess = execa`cargo run -- --data-dir ../testfixture run --dev -a 127.0.0.1:${port}`;
- for (let i = 0; i < 50; ++i) {
+ for (let i = 0; i < 100; ++i) {
if ((subprocess.exitCode ?? 0) > 0) {
break;
}
diff --git a/trailbase-core/Cargo.toml b/trailbase-core/Cargo.toml
index 6fda7664..eefb2517 100644
--- a/trailbase-core/Cargo.toml
+++ b/trailbase-core/Cargo.toml
@@ -19,6 +19,7 @@ axum = { version = "^0.7.5", features=["multipart"] }
axum-client-ip = "0.6.0"
axum-extra = { version = "^0.9.3", default-features = false, features=["protobuf"] }
base64 = { version = "0.22.1", default-features = false }
+bytes = "1.8.0"
chrono = "^0.4.38"
cookie = "0.18.1"
crossbeam-channel = "0.5.13"
diff --git a/trailbase-core/js/src/index.ts b/trailbase-core/js/src/index.ts
index e6e32d06..89de1e1f 100644
--- a/trailbase-core/js/src/index.ts
+++ b/trailbase-core/js/src/index.ts
@@ -1,26 +1,555 @@
declare var rustyscript: any;
declare var globalThis: any;
-type Headers = { [key: string]: string };
-type Request = {
+export type HeaderMapType = { [key: string]: string };
+export type PathParamsType = { [key: string]: string };
+export type RequestType = {
uri: string;
- headers: Headers;
- body: string;
+ params: PathParamsType;
+ headers: HeaderMapType;
+ body?: Uint8Array;
};
-type Response = {
- headers?: Headers;
+export type ResponseType = {
+ headers?: [string, string][];
status?: number;
+ body?: Uint8Array;
+};
+export type MaybeResponse = Promise | T | undefined;
+export type CallbackType = (req: RequestType) => MaybeResponse;
+
+/// HTTP status codes.
+///
+// source: https://github.com/prettymuchbryce/http-status-codes/blob/master/src/status-codes.ts
+export enum StatusCodes {
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.1
+ ///
+ /// This interim response indicates that everything so far is OK and that the
+ /// client should continue with the request or ignore it if it is already
+ /// finished.
+ CONTINUE = 100,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.2
+ ///
+ /// This code is sent in response to an Upgrade request header by the client,
+ /// and indicates the protocol the server is switching too.
+ SWITCHING_PROTOCOLS = 101,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.1
+ ///
+ /// This code indicates that the server has received and is processing the
+ /// request, but no response is available yet.
+ PROCESSING = 102,
+ /// Official Documentation @ https://www.rfc-editor.org/rfc/rfc8297#page-3
+ ///
+ /// This code indicates to the client that the server is likely to send a
+ /// final response with the header fields included in the informational
+ /// response.
+ EARLY_HINTS = 103,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.1
+ ///
+ /// The request has succeeded. The meaning of a success varies depending on the HTTP method:
+ /// GET: The resource has been fetched and is transmitted in the message body.
+ /// HEAD: The entity headers are in the message body.
+ /// POST: The resource describing the result of the action is transmitted in the message body.
+ /// TRACE: The message body contains the request message as received by the server
+ OK = 200,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.2
+ ///
+ /// The request has succeeded and a new resource has been created as a result
+ /// of it. This is typically the response sent after a PUT request.
+ CREATED = 201,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.3
+ ///
+ /// The request has been received but not yet acted upon. It is
+ /// non-committal, meaning that there is no way in HTTP to later send an
+ /// asynchronous response indicating the outcome of processing the request. It
+ /// is intended for cases where another process or server handles the request,
+ /// or for batch processing.
+ ACCEPTED = 202,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.4
+ ///
+ /// This response code means returned meta-information set is not exact set
+ /// as available from the origin server, but collected from a local or a third
+ /// party copy. Except this condition, 200 OK response should be preferred
+ /// instead of this response.
+ NON_AUTHORITATIVE_INFORMATION = 203,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.5
+ ///
+ /// There is no content to send for this request, but the headers may be
+ /// useful. The user-agent may update its cached headers for this resource with
+ /// the new ones.
+ NO_CONTENT = 204,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.6
+ ///
+ /// This response code is sent after accomplishing request to tell user agent
+ /// reset document view which sent this request.
+ RESET_CONTENT = 205,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.1
+ ///
+ /// This response code is used because of range header sent by the client to
+ /// separate download into multiple streams.
+ PARTIAL_CONTENT = 206,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.2
+ ///
+ /// A Multi-Status response conveys information about multiple resources in
+ /// situations where multiple status codes might be appropriate.
+ MULTI_STATUS = 207,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.1
+ ///
+ /// The request has more than one possible responses. User-agent or user
+ /// should choose one of them. There is no standardized way to choose one of
+ /// the responses.
+ MULTIPLE_CHOICES = 300,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.2
+ ///
+ /// This response code means that URI of requested resource has been changed.
+ /// Probably, new URI would be given in the response.
+ MOVED_PERMANENTLY = 301,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.3
+ ///
+ /// This response code means that URI of requested resource has been changed
+ /// temporarily. New changes in the URI might be made in the future. Therefore,
+ /// this same URI should be used by the client in future requests.
+ MOVED_TEMPORARILY = 302,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.4
+ ///
+ /// Server sent this response to directing client to get requested resource
+ /// to another URI with an GET request.
+ SEE_OTHER = 303,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.1
+ ///
+ /// This is used for caching purposes. It is telling to client that response
+ /// has not been modified. So, client can continue to use same cached version
+ /// of response.
+ NOT_MODIFIED = 304,
+ /// @deprecated
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.6
+ ///
+ /// Was defined in a previous version of the HTTP specification to indicate
+ /// that a requested response must be accessed by a proxy. It has been
+ /// deprecated due to security concerns regarding in-band configuration of a
+ /// proxy.
+ USE_PROXY = 305,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.7
+ ///
+ /// Server sent this response to directing client to get requested resource
+ /// to another URI with same method that used prior request. This has the same
+ /// semantic than the 302 Found HTTP response code, with the exception that the
+ /// user agent must not change the HTTP method used: if a POST was used in the
+ /// first request, a POST must be used in the second request.
+ TEMPORARY_REDIRECT = 307,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7538#section-3
+ ///
+ /// This means that the resource is now permanently located at another URI,
+ /// specified by the Location: HTTP Response header. This has the same
+ /// semantics as the 301 Moved Permanently HTTP response code, with the
+ /// exception that the user agent must not change the HTTP method used: if a
+ /// POST was used in the first request, a POST must be used in the second
+ /// request.
+ PERMANENT_REDIRECT = 308,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.1
+ ///
+ /// This response means that server could not understand the request due to invalid syntax.
+ BAD_REQUEST = 400,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.1
+ ///
+ /// Although the HTTP standard specifies "unauthorized", semantically this
+ /// response means "unauthenticated". That is, the client must authenticate
+ /// itself to get the requested response.
+ UNAUTHORIZED = 401,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.2
+ ///
+ /// This response code is reserved for future use. Initial aim for creating
+ /// this code was using it for digital payment systems however this is not used
+ /// currently.
+ PAYMENT_REQUIRED = 402,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.3
+ ///
+ /// The client does not have access rights to the content, i.e. they are
+ /// unauthorized, so server is rejecting to give proper response. Unlike 401,
+ /// the client's identity is known to the server.
+ FORBIDDEN = 403,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.4
+ ///
+ /// The server can not find requested resource. In the browser, this means
+ /// the URL is not recognized. In an API, this can also mean that the endpoint
+ /// is valid but the resource itself does not exist. Servers may also send this
+ /// response instead of 403 to hide the existence of a resource from an
+ /// unauthorized client. This response code is probably the most famous one due
+ /// to its frequent occurence on the web.
+ NOT_FOUND = 404,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.5
+ ///
+ /// The request method is known by the server but has been disabled and
+ /// cannot be used. For example, an API may forbid DELETE-ing a resource. The
+ /// two mandatory methods, GET and HEAD, must never be disabled and should not
+ /// return this error code.
+ METHOD_NOT_ALLOWED = 405,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.6
+ ///
+ /// This response is sent when the web server, after performing server-driven
+ /// content negotiation, doesn't find any content following the criteria given
+ /// by the user agent.
+ NOT_ACCEPTABLE = 406,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.2
+ ///
+ /// This is similar to 401 but authentication is needed to be done by a proxy.
+ PROXY_AUTHENTICATION_REQUIRED = 407,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.7
+ ///
+ /// This response is sent on an idle connection by some servers, even without
+ /// any previous request by the client. It means that the server would like to
+ /// shut down this unused connection. This response is used much more since
+ /// some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection
+ /// mechanisms to speed up surfing. Also note that some servers merely shut
+ /// down the connection without sending this message.
+ REQUEST_TIMEOUT = 408,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.8
+ ///
+ /// This response is sent when a request conflicts with the current state of the server.
+ CONFLICT = 409,
+ ///
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.9
+ ///
+ /// This response would be sent when the requested content has been
+ /// permenantly deleted from server, with no forwarding address. Clients are
+ /// expected to remove their caches and links to the resource. The HTTP
+ /// specification intends this status code to be used for "limited-time,
+ /// promotional services". APIs should not feel compelled to indicate resources
+ /// that have been deleted with this status code.
+ GONE = 410,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.10
+ ///
+ /// The server rejected the request because the Content-Length header field
+ /// is not defined and the server requires it.
+ LENGTH_REQUIRED = 411,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.2
+ ///
+ /// The client has indicated preconditions in its headers which the server
+ /// does not meet.
+ PRECONDITION_FAILED = 412,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.11
+ ///
+ /// Request entity is larger than limits defined by server; the server might
+ /// close the connection or return an Retry-After header field.
+ REQUEST_TOO_LONG = 413,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.12
+ ///
+ /// The URI requested by the client is longer than the server is willing to interpret.
+ REQUEST_URI_TOO_LONG = 414,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.13
+ ///
+ /// The media format of the requested data is not supported by the server, so
+ /// the server is rejecting the request.
+ UNSUPPORTED_MEDIA_TYPE = 415,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.4
+ ///
+ /// The range specified by the Range header field in the request can't be
+ /// fulfilled; it's possible that the range is outside the size of the target
+ /// URI's data.
+ REQUESTED_RANGE_NOT_SATISFIABLE = 416,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.14
+ ///
+ /// This response code means the expectation indicated by the Expect request
+ /// header field can't be met by the server.
+ EXPECTATION_FAILED = 417,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2324#section-2.3.2
+ ///
+ /// Any attempt to brew coffee with a teapot should result in the error code
+ /// "418 I'm a teapot". The resulting entity body MAY be short and stout.
+ IM_A_TEAPOT = 418,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
+ ///
+ /// The 507 (Insufficient Storage) status code means the method could not be
+ /// performed on the resource because the server is unable to store the
+ /// representation needed to successfully complete the request. This condition
+ /// is considered to be temporary. If the request which received this status
+ /// code was the result of a user action, the request MUST NOT be repeated
+ /// until it is requested by a separate user action.
+ INSUFFICIENT_SPACE_ON_RESOURCE = 419,
+ /// @deprecated
+ /// Official Documentation @ https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt
+ ///
+ /// A deprecated response used by the Spring Framework when a method has failed.
+ METHOD_FAILURE = 420,
+ /// Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7540#section-9.1.2
+ ///
+ /// Defined in the specification of HTTP/2 to indicate that a server is not
+ /// able to produce a response for the combination of scheme and authority that
+ /// are included in the request URI.
+ MISDIRECTED_REQUEST = 421,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3
+ ///
+ /// The request was well-formed but was unable to be followed due to semantic errors.
+ UNPROCESSABLE_ENTITY = 422,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.4
+ ///
+ /// The resource that is being accessed is locked.
+ LOCKED = 423,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.5
+ ///
+ /// The request failed due to failure of a previous request.
+ FAILED_DEPENDENCY = 424,
+ /// Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.15
+ ///
+ /// The server refuses to perform the request using the current protocol but
+ /// might be willing to do so after the client upgrades to a different
+ /// protocol.
+ UPGRADE_REQUIRED = 426,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-3
+ ///
+ /// The origin server requires the request to be conditional. Intended to
+ /// prevent the 'lost update' problem, where a client GETs a resource's state,
+ /// modifies it, and PUTs it back to the server, when meanwhile a third party
+ /// has modified the state on the server, leading to a conflict.
+ PRECONDITION_REQUIRED = 428,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-4
+ ///
+ /// The user has sent too many requests in a given amount of time ("rate limiting").
+ TOO_MANY_REQUESTS = 429,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-5
+ ///
+ /// The server is unwilling to process the request because its header fields
+ /// are too large. The request MAY be resubmitted after reducing the size of
+ /// the request header fields.
+ REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7725
+ ///
+ /// The user-agent requested a resource that cannot legally be provided, such
+ /// as a web page censored by a government.
+ UNAVAILABLE_FOR_LEGAL_REASONS = 451,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.1
+ ///
+ /// The server encountered an unexpected condition that prevented it from
+ /// fulfilling the request.
+ INTERNAL_SERVER_ERROR = 500,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.2
+ ///
+ /// The request method is not supported by the server and cannot be handled.
+ /// The only methods that servers are required to support (and therefore that
+ /// must not return this code) are GET and HEAD.
+ NOT_IMPLEMENTED = 501,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.3
+ ///
+ /// This error response means that the server, while working as a gateway to
+ /// get a response needed to handle the request, got an invalid response.
+ BAD_GATEWAY = 502,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.4
+ ///
+ /// The server is not ready to handle the request. Common causes are a server
+ /// that is down for maintenance or that is overloaded. Note that together with
+ /// this response, a user-friendly page explaining the problem should be sent.
+ /// This responses should be used for temporary conditions and the Retry-After:
+ /// HTTP header should, if possible, contain the estimated time before the
+ /// recovery of the service. The webmaster must also take care about the
+ /// caching-related headers that are sent along with this response, as these
+ /// temporary condition responses should usually not be cached.
+ SERVICE_UNAVAILABLE = 503,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.5
+ ///
+ /// This error response is given when the server is acting as a gateway and
+ /// cannot get a response in time.
+ GATEWAY_TIMEOUT = 504,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.6
+ ///
+ /// The HTTP version used in the request is not supported by the server.
+ HTTP_VERSION_NOT_SUPPORTED = 505,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
+ ///
+ /// The server has an internal configuration error: the chosen variant
+ /// resource is configured to engage in transparent content negotiation itself,
+ /// and is therefore not a proper end point in the negotiation process.
+ INSUFFICIENT_STORAGE = 507,
+ /// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-6
+ ///
+ /// The 511 status code indicates that the client needs to authenticate to
+ /// gain network access.
+ NETWORK_AUTHENTICATION_REQUIRED = 511,
+}
+
+export type StringRequestType = {
+ uri: string;
+ params: PathParamsType;
+ headers: HeaderMapType;
body?: string;
};
-type CbType = (req: Request) => Response | undefined;
+export type StringResponseType = {
+ headers?: [string, string][];
+ status?: number;
+ body: string;
+};
-const callbacks = new Map();
+export function stringHandler(
+ f: (req: StringRequestType) => MaybeResponse,
+): CallbackType {
+ return async (req: RequestType): Promise => {
+ try {
+ let body = req.body;
+ let resp: StringResponseType | string | undefined = await f({
+ uri: req.uri,
+ params: req.params,
+ headers: req.headers,
+ body: body && decodeFallback(body),
+ });
-export function addRoute(method: string, route: string, callback: CbType) {
+ if (resp === undefined) {
+ return undefined;
+ }
+
+ if (typeof resp === "string") {
+ return {
+ status: StatusCodes.OK,
+ body: encodeFallback(resp),
+ };
+ }
+
+ const respBody = resp.body;
+ return {
+ headers: resp.headers,
+ status: resp.status,
+ body: respBody ? encodeFallback(respBody) : undefined,
+ };
+ } catch (err) {
+ return {
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ body: encodeFallback(`Uncaught error: ${err}`),
+ };
+ }
+ };
+}
+
+export type HtmlResponseType = {
+ headers?: [string, string][];
+ status?: number;
+ body: string;
+};
+
+export function htmlHandler(
+ f: (req: StringRequestType) => MaybeResponse,
+): CallbackType {
+ return async (req: RequestType): Promise => {
+ try {
+ let body = req.body;
+ let resp: HtmlResponseType | string | undefined = await f({
+ uri: req.uri,
+ params: req.params,
+ headers: req.headers,
+ body: body && decodeFallback(body),
+ });
+
+ if (resp === undefined) {
+ return undefined;
+ }
+
+ if (typeof resp === "string") {
+ return {
+ headers: [["content-type", "text/html"]],
+ status: StatusCodes.OK,
+ body: encodeFallback(resp),
+ };
+ }
+
+ const respBody = resp.body;
+ return {
+ headers: [["content-type", "text/html"], ...(resp.headers ?? [])],
+ status: resp.status,
+ body: respBody ? encodeFallback(respBody) : undefined,
+ };
+ } catch (err) {
+ return {
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ body: encodeFallback(`Uncaught error: ${err}`),
+ };
+ }
+ };
+}
+
+export type JsonRequestType = {
+ uri: string;
+ params: PathParamsType;
+ headers: HeaderMapType;
+ body?: object | string;
+};
+export interface JsonResponseType {
+ headers?: [string, string][];
+ status?: number;
+ body: object;
+}
+
+export function jsonHandler(
+ f: (req: JsonRequestType) => MaybeResponse,
+): CallbackType {
+ return async (req: RequestType): Promise => {
+ try {
+ let body = req.body;
+ let resp: JsonResponseType | object | undefined = await f({
+ uri: req.uri,
+ params: req.params,
+ headers: req.headers,
+ body: body && decodeFallback(body),
+ });
+
+ if (resp === undefined) {
+ return undefined;
+ }
+
+ if ("body" in resp) {
+ const r = resp as JsonResponseType;
+ const rBody = r.body;
+ return {
+ headers: [["content-type", "application/json"], ...(r.headers ?? [])],
+ status: r.status,
+ body: rBody ? encodeFallback(JSON.stringify(rBody)) : undefined,
+ };
+ }
+
+ return {
+ headers: [["content-type", "application/json"]],
+ status: StatusCodes.OK,
+ body: encodeFallback(JSON.stringify(resp)),
+ };
+ } catch (err) {
+ return {
+ headers: [["content-type", "application/json"]],
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ body: encodeFallback(`Uncaught error: ${err}`),
+ };
+ }
+ };
+}
+
+const callbacks = new Map();
+
+export function addRoute(
+ method: string,
+ route: string,
+ callback: CallbackType,
+) {
rustyscript.functions.route(method, route);
callbacks.set(`${method}:${route}`, callback);
+ console.debug("JS: Added route:", method, route);
+}
- console.log("JS: Added route:", method, route);
+export async function dispatch(
+ method: string,
+ route: string,
+ uri: string,
+ pathParams: [string, string][],
+ headers: [string, string][],
+ body: Uint8Array,
+): Promise {
+ const key = `${method}:${route}`;
+ const cb: CallbackType | undefined = callbacks.get(key);
+ if (!cb) {
+ throw Error(`Missing callback: ${key}`);
+ }
+
+ return (
+ (await cb({
+ uri,
+ params: Object.fromEntries(pathParams),
+ headers: Object.fromEntries(headers),
+ body,
+ })) ?? { status: StatusCodes.OK }
+ );
}
export async function query(
@@ -37,24 +566,173 @@ export async function execute(
return await rustyscript.async_functions.execute(queryStr, params);
}
-export function dispatch(
- method: string,
- route: string,
- uri: string,
- headers: Headers,
- body: string,
-): Response | undefined {
- const key = `${method}:${route}`;
- const cb = callbacks.get(key);
- if (!cb) {
- throw Error(`Missing callback: ${key}`);
+export type ParsedPath = {
+ path: string;
+ query: URLSearchParams;
+};
+
+export function parsePath(path: string): ParsedPath {
+ const queryIndex = path.indexOf("?");
+ if (queryIndex >= 0) {
+ return {
+ path: path.slice(0, queryIndex),
+ query: new URLSearchParams(path.slice(queryIndex + 1)),
+ };
}
- return cb({
- uri,
- headers,
- body,
- });
+ return {
+ path,
+ query: new URLSearchParams(),
+ };
+}
+
+/// @param {Uint8Array} bytes
+/// @return {string}
+///
+/// source: https://github.com/samthor/fast-text-encoding
+export function decodeFallback(bytes: Uint8Array): string {
+ var inputIndex = 0;
+
+ // Create a working buffer for UTF-16 code points, but don't generate one
+ // which is too large for small input sizes. UTF-8 to UCS-16 conversion is
+ // going to be at most 1:1, if all code points are ASCII. The other extreme
+ // is 4-byte UTF-8, which results in two UCS-16 points, but this is still 50%
+ // fewer entries in the output.
+ var pendingSize = Math.min(256 * 256, bytes.length + 1);
+ var pending = new Uint16Array(pendingSize);
+ var chunks = [];
+ var pendingIndex = 0;
+
+ for (;;) {
+ var more = inputIndex < bytes.length;
+
+ // If there's no more data or there'd be no room for two UTF-16 values,
+ // create a chunk. This isn't done at the end by simply slicing the data
+ // into equal sized chunks as we might hit a surrogate pair.
+ if (!more || pendingIndex >= pendingSize - 1) {
+ // nb. .apply and friends are *really slow*. Low-hanging fruit is to
+ // expand this to literally pass pending[0], pending[1], ... etc, but
+ // the output code expands pretty fast in this case.
+ // These extra vars get compiled out: they're just to make TS happy.
+ // Turns out you can pass an ArrayLike to .apply().
+ var subarray = pending.subarray(0, pendingIndex);
+ var arraylike = subarray as unknown as number[];
+ chunks.push(String.fromCharCode.apply(null, arraylike));
+
+ if (!more) {
+ return chunks.join("");
+ }
+
+ // Move the buffer forward and create another chunk.
+ bytes = bytes.subarray(inputIndex);
+ inputIndex = 0;
+ pendingIndex = 0;
+ }
+
+ // The native TextDecoder will generate "REPLACEMENT CHARACTER" where the
+ // input data is invalid. Here, we blindly parse the data even if it's
+ // wrong: e.g., if a 3-byte sequence doesn't have two valid continuations.
+
+ var byte1 = bytes[inputIndex++];
+ if ((byte1 & 0x80) === 0) {
+ // 1-byte or null
+ pending[pendingIndex++] = byte1;
+ } else if ((byte1 & 0xe0) === 0xc0) {
+ // 2-byte
+ var byte2 = bytes[inputIndex++] & 0x3f;
+ pending[pendingIndex++] = ((byte1 & 0x1f) << 6) | byte2;
+ } else if ((byte1 & 0xf0) === 0xe0) {
+ // 3-byte
+ var byte2 = bytes[inputIndex++] & 0x3f;
+ var byte3 = bytes[inputIndex++] & 0x3f;
+ pending[pendingIndex++] = ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3;
+ } else if ((byte1 & 0xf8) === 0xf0) {
+ // 4-byte
+ var byte2 = bytes[inputIndex++] & 0x3f;
+ var byte3 = bytes[inputIndex++] & 0x3f;
+ var byte4 = bytes[inputIndex++] & 0x3f;
+
+ // this can be > 0xffff, so possibly generate surrogates
+ var codepoint =
+ ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
+ if (codepoint > 0xffff) {
+ // codepoint &= ~0x10000;
+ codepoint -= 0x10000;
+ pending[pendingIndex++] = ((codepoint >>> 10) & 0x3ff) | 0xd800;
+ codepoint = 0xdc00 | (codepoint & 0x3ff);
+ }
+ pending[pendingIndex++] = codepoint;
+ } else {
+ // invalid initial byte
+ }
+ }
+}
+
+/// @param {string} string
+/// @return {Uint8Array}
+////
+/// source: https://github.com/samthor/fast-text-encoding
+export function encodeFallback(string: string): Uint8Array {
+ var pos = 0;
+ var len = string.length;
+
+ var at = 0; // output position
+ var tlen = Math.max(32, len + (len >>> 1) + 7); // 1.5x size
+ var target = new Uint8Array((tlen >>> 3) << 3); // ... but at 8 byte offset
+
+ while (pos < len) {
+ var value = string.charCodeAt(pos++);
+ if (value >= 0xd800 && value <= 0xdbff) {
+ // high surrogate
+ if (pos < len) {
+ var extra = string.charCodeAt(pos);
+ if ((extra & 0xfc00) === 0xdc00) {
+ ++pos;
+ value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
+ }
+ }
+ if (value >= 0xd800 && value <= 0xdbff) {
+ continue; // drop lone surrogate
+ }
+ }
+
+ // expand the buffer if we couldn't write 4 bytes
+ if (at + 4 > target.length) {
+ tlen += 8; // minimum extra
+ tlen *= 1.0 + (pos / string.length) * 2; // take 2x the remaining
+ tlen = (tlen >>> 3) << 3; // 8 byte offset
+
+ var update = new Uint8Array(tlen);
+ update.set(target);
+ target = update;
+ }
+
+ if ((value & 0xffffff80) === 0) {
+ // 1-byte
+ target[at++] = value; // ASCII
+ continue;
+ } else if ((value & 0xfffff800) === 0) {
+ // 2-byte
+ target[at++] = ((value >>> 6) & 0x1f) | 0xc0;
+ } else if ((value & 0xffff0000) === 0) {
+ // 3-byte
+ target[at++] = ((value >>> 12) & 0x0f) | 0xe0;
+ target[at++] = ((value >>> 6) & 0x3f) | 0x80;
+ } else if ((value & 0xffe00000) === 0) {
+ // 4-byte
+ target[at++] = ((value >>> 18) & 0x07) | 0xf0;
+ target[at++] = ((value >>> 12) & 0x3f) | 0x80;
+ target[at++] = ((value >>> 6) & 0x3f) | 0x80;
+ } else {
+ continue; // out of range
+ }
+
+ target[at++] = (value & 0x3f) | 0x80;
+ }
+
+ // Use subarray if slice isn't supported (IE11). This will use more memory
+ // because the original array still exists.
+ return target.slice ? target.slice(0, at) : target.subarray(0, at);
}
globalThis.__dispatch = dispatch;
diff --git a/trailbase-core/src/js/mod.rs b/trailbase-core/src/js/mod.rs
index 1dd276a5..eedd341e 100644
--- a/trailbase-core/src/js/mod.rs
+++ b/trailbase-core/src/js/mod.rs
@@ -1,5 +1,5 @@
use axum::body::Body;
-use axum::extract::Request;
+use axum::extract::{RawPathParams, Request};
use axum::http::{header::CONTENT_TYPE, request::Parts, HeaderName, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Router;
@@ -9,7 +9,7 @@ use rust_embed::RustEmbed;
use rustyscript::{init_platform, json_args, Module, Runtime};
use serde::Deserialize;
use serde_json::from_value;
-use std::collections::{HashMap, HashSet};
+use std::collections::HashSet;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use thiserror::Error;
@@ -22,13 +22,6 @@ mod import_provider;
type AnyError = Box;
-#[derive(Default, Deserialize)]
-struct JsResponse {
- headers: Option>,
- status: Option,
- body: Option,
-}
-
enum Message {
Run(Box),
}
@@ -355,7 +348,7 @@ fn route_callback(
) -> Result<(), AnyError> {
let method_uppercase = method.to_uppercase();
let route_path = route.clone();
- let handler = move |req: Request| async move {
+ let handler = move |params: RawPathParams, req: Request| async move {
let (parts, body) = req.into_parts();
let Ok(body_bytes) = axum::body::to_bytes(body, usize::MAX).await else {
@@ -370,7 +363,11 @@ fn route_callback(
..
} = parts;
- let headers: HashMap = headers
+ let path_params: Vec<(String, String)> = params
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+ let headers: Vec<(String, String)> = headers
.into_iter()
.filter_map(|(key, value)| {
if let Some(key) = key {
@@ -382,20 +379,32 @@ fn route_callback(
})
.collect();
+ #[derive(Deserialize)]
+ struct JsResponse {
+ headers: Option>,
+ status: Option,
+ body: Option,
+ }
+
let js_response = RuntimeHandle::new()
.apply(move |runtime| -> Result {
- let response: JsResponse = runtime.call_function(
- None,
- "__dispatch",
- json_args!(
- method,
- route_path,
- uri.to_string(),
- headers,
- String::from_utf8_lossy(&body_bytes)
- ),
- )?;
- return Ok(response);
+ let tokio_runtime = runtime.tokio_runtime();
+ return tokio_runtime.block_on(async {
+ return runtime
+ .call_function_async::(
+ None,
+ "__dispatch",
+ json_args!(
+ method,
+ route_path,
+ uri.to_string(),
+ path_params,
+ headers,
+ body_bytes
+ ),
+ )
+ .await;
+ });
})
.await
.map_err(JsResponseError::Internal)?