Add conversions and convenience for various content-types to the js runtime. Make handlers async. Also extend the test spin up wait time with the longer build times with v8.

This commit is contained in:
Sebastian Jeltsch
2024-11-13 22:31:23 +01:00
parent 18dd596478
commit 7465203174
9 changed files with 787 additions and 73 deletions

1
Cargo.lock generated
View File

@@ -5571,6 +5571,7 @@ dependencies = [
"axum-extra",
"axum-test",
"base64 0.22.1",
"bytes",
"chrono",
"cookie",
"criterion",

View File

@@ -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",
};
});

View File

@@ -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>
<body>
<h1>Html Handler</h1>
</body>
</html>
`;
}));
addRoute("GET", "/json", jsonHandler((_req: JsonRequestType) => {
return {
int: 5,
real: 4.2,
msg: "foo",
obj: {
nested: true,
}
};
}));

View File

@@ -44,7 +44,7 @@ Future<Process> 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'));

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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<T> = Promise<T | undefined> | T | undefined;
export type CallbackType = (req: RequestType) => MaybeResponse<ResponseType>;
/// 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<string, CbType>();
export function stringHandler(
f: (req: StringRequestType) => MaybeResponse<StringResponseType | string>,
): CallbackType {
return async (req: RequestType): Promise<ResponseType | undefined> => {
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<HtmlResponseType | string>,
): CallbackType {
return async (req: RequestType): Promise<ResponseType | undefined> => {
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<JsonRequestType | object>,
): CallbackType {
return async (req: RequestType): Promise<ResponseType | undefined> => {
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<string, CallbackType>();
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<ResponseType> {
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;

View File

@@ -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<dyn std::error::Error + Send + Sync>;
#[derive(Default, Deserialize)]
struct JsResponse {
headers: Option<HashMap<String, String>>,
status: Option<u16>,
body: Option<String>,
}
enum Message {
Run(Box<dyn (FnOnce(&mut Runtime)) + Send + Sync>),
}
@@ -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<String, String> = 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<Vec<(String, String)>>,
status: Option<u16>,
body: Option<bytes::Bytes>,
}
let js_response = RuntimeHandle::new()
.apply(move |runtime| -> Result<JsResponse, rustyscript::Error> {
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::<JsResponse>(
None,
"__dispatch",
json_args!(
method,
route_path,
uri.to_string(),
path_params,
headers,
body_bytes
),
)
.await;
});
})
.await
.map_err(JsResponseError::Internal)?