mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-21 09:29:44 -06:00
Minor: Write out js runtime to traildepot and rely on relative includes. Also some opportunistic cleanups along the way.
This commit is contained in:
6
Makefile
6
Makefile
@@ -7,12 +7,14 @@ format:
|
||||
pnpm -r format; \
|
||||
cargo +nightly fmt; \
|
||||
dart format client/trailbase-dart/ examples/blog/flutter/; \
|
||||
txtpbfmt `find . -regex ".*.textproto"`
|
||||
txtpbfmt `find . -regex ".*.textproto"`; \
|
||||
dotnet format client/trailbase-dotnet
|
||||
|
||||
check:
|
||||
pnpm -r check; \
|
||||
cargo clippy --workspace --no-deps; \
|
||||
dart analyze client/trailbase-dart examples/blog/flutter
|
||||
dart analyze client/trailbase-dart examples/blog/flutter; \
|
||||
dotnet format client/trailbase-dotnet --verify-no-changes
|
||||
|
||||
docker:
|
||||
docker build . -t trailbase/trailbase
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { addRoute, parsePath, query, htmlHandler, jsonHandler, stringHandler } from "trailbase:main";
|
||||
import type { JsonRequestType, ParsedPath, StringRequestType } from "../../../trailbase-core/js/src/index.ts";
|
||||
import { addRoute, parsePath, query, htmlHandler, jsonHandler, stringHandler } from "../trailbase.js";
|
||||
import type { JsonRequestType, ParsedPath, StringRequestType } from "../trailbase.d.ts";
|
||||
|
||||
addRoute("GET", "/test", stringHandler(async (req: StringRequestType) => {
|
||||
const uri : ParsedPath = parsePath(req.uri);
|
||||
const uri: ParsedPath = parsePath(req.uri);
|
||||
|
||||
const table = uri.query.get("table");
|
||||
if (table) {
|
||||
|
||||
120
client/testfixture/trailbase.d.ts
vendored
Normal file
120
client/testfixture/trailbase.d.ts
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
export type HeaderMapType = {
|
||||
[key: string]: string;
|
||||
};
|
||||
export type PathParamsType = {
|
||||
[key: string]: string;
|
||||
};
|
||||
export type RequestType = {
|
||||
uri: string;
|
||||
params: PathParamsType;
|
||||
headers: HeaderMapType;
|
||||
body?: Uint8Array;
|
||||
};
|
||||
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>;
|
||||
export declare enum StatusCodes {
|
||||
CONTINUE = 100,
|
||||
SWITCHING_PROTOCOLS = 101,
|
||||
PROCESSING = 102,
|
||||
EARLY_HINTS = 103,
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NON_AUTHORITATIVE_INFORMATION = 203,
|
||||
NO_CONTENT = 204,
|
||||
RESET_CONTENT = 205,
|
||||
PARTIAL_CONTENT = 206,
|
||||
MULTI_STATUS = 207,
|
||||
MULTIPLE_CHOICES = 300,
|
||||
MOVED_PERMANENTLY = 301,
|
||||
MOVED_TEMPORARILY = 302,
|
||||
SEE_OTHER = 303,
|
||||
NOT_MODIFIED = 304,
|
||||
USE_PROXY = 305,
|
||||
TEMPORARY_REDIRECT = 307,
|
||||
PERMANENT_REDIRECT = 308,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
PAYMENT_REQUIRED = 402,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
METHOD_NOT_ALLOWED = 405,
|
||||
NOT_ACCEPTABLE = 406,
|
||||
PROXY_AUTHENTICATION_REQUIRED = 407,
|
||||
REQUEST_TIMEOUT = 408,
|
||||
CONFLICT = 409,
|
||||
GONE = 410,
|
||||
LENGTH_REQUIRED = 411,
|
||||
PRECONDITION_FAILED = 412,
|
||||
REQUEST_TOO_LONG = 413,
|
||||
REQUEST_URI_TOO_LONG = 414,
|
||||
UNSUPPORTED_MEDIA_TYPE = 415,
|
||||
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
|
||||
EXPECTATION_FAILED = 417,
|
||||
IM_A_TEAPOT = 418,
|
||||
INSUFFICIENT_SPACE_ON_RESOURCE = 419,
|
||||
METHOD_FAILURE = 420,
|
||||
MISDIRECTED_REQUEST = 421,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
LOCKED = 423,
|
||||
FAILED_DEPENDENCY = 424,
|
||||
UPGRADE_REQUIRED = 426,
|
||||
PRECONDITION_REQUIRED = 428,
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
NOT_IMPLEMENTED = 501,
|
||||
BAD_GATEWAY = 502,
|
||||
SERVICE_UNAVAILABLE = 503,
|
||||
GATEWAY_TIMEOUT = 504,
|
||||
HTTP_VERSION_NOT_SUPPORTED = 505,
|
||||
INSUFFICIENT_STORAGE = 507,
|
||||
NETWORK_AUTHENTICATION_REQUIRED = 511
|
||||
}
|
||||
export type StringRequestType = {
|
||||
uri: string;
|
||||
params: PathParamsType;
|
||||
headers: HeaderMapType;
|
||||
body?: string;
|
||||
};
|
||||
export type StringResponseType = {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body: string;
|
||||
};
|
||||
export declare function stringHandler(f: (req: StringRequestType) => MaybeResponse<StringResponseType | string>): CallbackType;
|
||||
export type HtmlResponseType = {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body: string;
|
||||
};
|
||||
export declare function htmlHandler(f: (req: StringRequestType) => MaybeResponse<HtmlResponseType | string>): CallbackType;
|
||||
export type JsonRequestType = {
|
||||
uri: string;
|
||||
params: PathParamsType;
|
||||
headers: HeaderMapType;
|
||||
body?: object | string;
|
||||
};
|
||||
export interface JsonResponseType {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body: object;
|
||||
}
|
||||
export declare function jsonHandler(f: (req: JsonRequestType) => MaybeResponse<JsonRequestType | object>): CallbackType;
|
||||
export declare function addRoute(method: string, route: string, callback: CallbackType): void;
|
||||
export declare function dispatch(method: string, route: string, uri: string, pathParams: [string, string][], headers: [string, string][], body: Uint8Array): Promise<ResponseType>;
|
||||
export declare function query(queryStr: string, params: unknown[]): Promise<unknown[][]>;
|
||||
export declare function execute(queryStr: string, params: unknown[]): Promise<number>;
|
||||
export type ParsedPath = {
|
||||
path: string;
|
||||
query: URLSearchParams;
|
||||
};
|
||||
export declare function parsePath(path: string): ParsedPath;
|
||||
export declare function decodeFallback(bytes: Uint8Array): string;
|
||||
export declare function encodeFallback(string: string): Uint8Array;
|
||||
632
client/testfixture/trailbase.js
Normal file
632
client/testfixture/trailbase.js
Normal file
@@ -0,0 +1,632 @@
|
||||
/// HTTP status codes.
|
||||
///
|
||||
// source: https://github.com/prettymuchbryce/http-status-codes/blob/master/src/status-codes.ts
|
||||
export var StatusCodes;
|
||||
(function (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.
|
||||
StatusCodes[StatusCodes["CONTINUE"] = 100] = "CONTINUE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PROCESSING"] = 102] = "PROCESSING";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["EARLY_HINTS"] = 103] = "EARLY_HINTS";
|
||||
/// 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
|
||||
StatusCodes[StatusCodes["OK"] = 200] = "OK";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["CREATED"] = 201] = "CREATED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["ACCEPTED"] = 202] = "ACCEPTED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NO_CONTENT"] = 204] = "NO_CONTENT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["RESET_CONTENT"] = 205] = "RESET_CONTENT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["MULTI_STATUS"] = 207] = "MULTI_STATUS";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["MULTIPLE_CHOICES"] = 300] = "MULTIPLE_CHOICES";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["MOVED_TEMPORARILY"] = 302] = "MOVED_TEMPORARILY";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["SEE_OTHER"] = 303] = "SEE_OTHER";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NOT_MODIFIED"] = 304] = "NOT_MODIFIED";
|
||||
/// @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.
|
||||
StatusCodes[StatusCodes["USE_PROXY"] = 305] = "USE_PROXY";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["FORBIDDEN"] = 403] = "FORBIDDEN";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NOT_FOUND"] = 404] = "NOT_FOUND";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["CONFLICT"] = 409] = "CONFLICT";
|
||||
///
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["GONE"] = 410] = "GONE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["REQUEST_TOO_LONG"] = 413] = "REQUEST_TOO_LONG";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["REQUEST_URI_TOO_LONG"] = 414] = "REQUEST_URI_TOO_LONG";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["IM_A_TEAPOT"] = 418] = "IM_A_TEAPOT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["INSUFFICIENT_SPACE_ON_RESOURCE"] = 419] = "INSUFFICIENT_SPACE_ON_RESOURCE";
|
||||
/// @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.
|
||||
StatusCodes[StatusCodes["METHOD_FAILURE"] = 420] = "METHOD_FAILURE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["MISDIRECTED_REQUEST"] = 421] = "MISDIRECTED_REQUEST";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.4
|
||||
///
|
||||
/// The resource that is being accessed is locked.
|
||||
StatusCodes[StatusCodes["LOCKED"] = 423] = "LOCKED";
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.5
|
||||
///
|
||||
/// The request failed due to failure of a previous request.
|
||||
StatusCodes[StatusCodes["FAILED_DEPENDENCY"] = 424] = "FAILED_DEPENDENCY";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["UPGRADE_REQUIRED"] = 426] = "UPGRADE_REQUIRED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["PRECONDITION_REQUIRED"] = 428] = "PRECONDITION_REQUIRED";
|
||||
/// 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").
|
||||
StatusCodes[StatusCodes["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["REQUEST_HEADER_FIELDS_TOO_LARGE"] = 431] = "REQUEST_HEADER_FIELDS_TOO_LARGE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["UNAVAILABLE_FOR_LEGAL_REASONS"] = 451] = "UNAVAILABLE_FOR_LEGAL_REASONS";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["INSUFFICIENT_STORAGE"] = 507] = "INSUFFICIENT_STORAGE";
|
||||
/// 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.
|
||||
StatusCodes[StatusCodes["NETWORK_AUTHENTICATION_REQUIRED"] = 511] = "NETWORK_AUTHENTICATION_REQUIRED";
|
||||
})(StatusCodes || (StatusCodes = {}));
|
||||
export function stringHandler(f) {
|
||||
return async (req) => {
|
||||
try {
|
||||
let body = req.body;
|
||||
let resp = 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 {
|
||||
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 function htmlHandler(f) {
|
||||
return async (req) => {
|
||||
try {
|
||||
let body = req.body;
|
||||
let resp = 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 function jsonHandler(f) {
|
||||
return async (req) => {
|
||||
try {
|
||||
let body = req.body;
|
||||
let resp = 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;
|
||||
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, route, callback) {
|
||||
rustyscript.functions.route(method, route);
|
||||
callbacks.set(`${method}:${route}`, callback);
|
||||
console.debug("JS: Added route:", method, route);
|
||||
}
|
||||
export async function dispatch(method, route, uri, pathParams, headers, body) {
|
||||
const key = `${method}:${route}`;
|
||||
const cb = 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(queryStr, params) {
|
||||
return await rustyscript.async_functions.query(queryStr, params);
|
||||
}
|
||||
export async function execute(queryStr, params) {
|
||||
return await rustyscript.async_functions.execute(queryStr, params);
|
||||
}
|
||||
export function parsePath(path) {
|
||||
const queryIndex = path.indexOf("?");
|
||||
if (queryIndex >= 0) {
|
||||
return {
|
||||
path: path.slice(0, queryIndex),
|
||||
query: new URLSearchParams(path.slice(queryIndex + 1)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
path,
|
||||
query: new URLSearchParams(),
|
||||
};
|
||||
}
|
||||
/// @param {Uint8Array} bytes
|
||||
/// @return {string}
|
||||
///
|
||||
/// source: https://github.com/samthor/fast-text-encoding
|
||||
export function decodeFallback(bytes) {
|
||||
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;
|
||||
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) {
|
||||
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;
|
||||
@@ -24,10 +24,18 @@ use trailbase_cli::{
|
||||
type BoxError = Box<dyn std::error::Error>;
|
||||
|
||||
fn init_logger(dev: bool) {
|
||||
// SWC is very spammy in in debug builds and complaints about source maps when compiling
|
||||
// typescript to javascript. Since we don't care about source maps and didn't find a better
|
||||
// option to mute the errors, turn it off in debug builds.
|
||||
#[cfg(debug_assertions)]
|
||||
const DEFAULT: &str = "info,refinery_core=warn,tracing::span=warn,swc_ecma_codegen=off";
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEFAULT: &str = "info,refinery_core=warn,tracing::span=warn";
|
||||
|
||||
env_logger::init_from_env(if dev {
|
||||
env_logger::Env::new().default_filter_or("info,trailbase_core=debug,refinery_core=warn")
|
||||
env_logger::Env::new().default_filter_or(format!("{DEFAULT},trailbase_core=debug"))
|
||||
} else {
|
||||
env_logger::Env::new().default_filter_or("info,refinery_core=warn")
|
||||
env_logger::Env::new().default_filter_or(DEFAULT)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -279,7 +287,7 @@ async fn async_main(runtime: Rc<tokio::runtime::Runtime>) -> Result<(), BoxError
|
||||
|
||||
let (to, subject, body) = (cmd.to.clone(), cmd.subject.clone(), cmd.body.clone());
|
||||
|
||||
let (_new_db, state) = init_app_state(DataDir(args.data_dir), None, false, runtime).await?;
|
||||
let (_new_db, state) = init_app_state(DataDir(args.data_dir), None, false).await?;
|
||||
let email = Email::new(&state, to, subject, body)?;
|
||||
email.send().await?;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use libsql::Connection;
|
||||
use log::*;
|
||||
use object_store::ObjectStore;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::auth::jwt::JwtHelper;
|
||||
@@ -55,9 +54,6 @@ pub(crate) struct AppStateArgs {
|
||||
pub conn: Connection,
|
||||
pub logs_conn: Connection,
|
||||
pub jwt: JwtHelper,
|
||||
|
||||
#[allow(unused)]
|
||||
pub tokio_runtime: Rc<tokio::runtime::Runtime>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -274,8 +270,8 @@ impl AppState {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) fn script_runtime(&self) -> &RuntimeHandle {
|
||||
return &self.state.runtime;
|
||||
pub(crate) fn script_runtime(&self) -> RuntimeHandle {
|
||||
return self.state.runtime.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
use rust_embed::RustEmbed;
|
||||
use rustyscript::deno_core::{
|
||||
anyhow::Error, ModuleSource, ModuleSourceCode, ModuleSpecifier, RequestedModuleType,
|
||||
ResolutionKind,
|
||||
anyhow::{anyhow, Error},
|
||||
ModuleSpecifier, RequestedModuleType, ResolutionKind,
|
||||
};
|
||||
use rustyscript::module_loader::ImportProvider;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::assets::cow_to_string;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct MemoryCache {
|
||||
cache: HashMap<String, String>,
|
||||
}
|
||||
pub(crate) struct ImportProviderImpl;
|
||||
|
||||
impl MemoryCache {
|
||||
/// Set a module in the cache
|
||||
pub fn set(&mut self, specifier: &str, source: String) {
|
||||
self.cache.insert(specifier.to_string(), source);
|
||||
}
|
||||
|
||||
/// Get a module from the cache
|
||||
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<String> {
|
||||
self.cache.get(specifier.as_str()).cloned()
|
||||
}
|
||||
|
||||
pub fn has(&self, specifier: &ModuleSpecifier) -> bool {
|
||||
self.cache.contains_key(specifier.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportProvider for MemoryCache {
|
||||
impl ImportProvider for ImportProviderImpl {
|
||||
fn resolve(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
_referrer: &str,
|
||||
_kind: ResolutionKind,
|
||||
) -> Option<Result<ModuleSpecifier, Error>> {
|
||||
// println!("resolve: {specifier:?}");
|
||||
// Tell the loader to allow the import if the module is in the cache
|
||||
self.get(specifier).map(|_| Ok(specifier.clone()))
|
||||
log::trace!("resolve: {specifier:?}");
|
||||
|
||||
// Specifier is just a URL.
|
||||
match specifier.scheme() {
|
||||
"file" | "trailbase" => {
|
||||
return Some(Ok(specifier.clone()));
|
||||
}
|
||||
scheme => {
|
||||
return Some(Err(anyhow!("Unsupported schema: '{scheme}'")));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn import(
|
||||
@@ -45,27 +37,23 @@ impl ImportProvider for MemoryCache {
|
||||
_is_dyn_import: bool,
|
||||
_requested_module_type: RequestedModuleType,
|
||||
) -> Option<Result<String, Error>> {
|
||||
// println!("import : {specifier:?}");
|
||||
// Return the source code if the module is in the cache
|
||||
self.get(specifier).map(Ok)
|
||||
}
|
||||
log::trace!("import: {specifier:?}");
|
||||
|
||||
fn post_process(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
source: ModuleSource,
|
||||
) -> Result<ModuleSource, Error> {
|
||||
log::debug!("post_process: {specifier:?}");
|
||||
// Cache the source code
|
||||
if !self.has(specifier) {
|
||||
match &source.code {
|
||||
ModuleSourceCode::String(s) => {
|
||||
self.set(specifier.as_str(), s.to_string());
|
||||
}
|
||||
ModuleSourceCode::Bytes(_) => {}
|
||||
match specifier.scheme() {
|
||||
"trailbase" => {
|
||||
return Some(Ok(cow_to_string(
|
||||
JsRuntimeAssets::get("index.js")
|
||||
.expect("Failed to read rt/index.js")
|
||||
.data,
|
||||
)));
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RustEmbed, Clone)]
|
||||
#[folder = "js/dist/"]
|
||||
pub(crate) struct JsRuntimeAssets;
|
||||
|
||||
@@ -5,7 +5,6 @@ use axum::response::{IntoResponse, Response};
|
||||
use axum::Router;
|
||||
use libsql::Connection;
|
||||
use parking_lot::Mutex;
|
||||
use rust_embed::RustEmbed;
|
||||
use rustyscript::{init_platform, json_args, Module, Runtime};
|
||||
use serde::Deserialize;
|
||||
use serde_json::from_value;
|
||||
@@ -15,8 +14,9 @@ use std::sync::{Arc, LazyLock};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::assets::cow_to_string;
|
||||
use crate::js::import_provider::JsRuntimeAssets;
|
||||
use crate::records::sql_to_json::rows_to_json_arrays;
|
||||
use crate::AppState;
|
||||
use crate::{AppState, DataDir};
|
||||
|
||||
mod import_provider;
|
||||
|
||||
@@ -63,6 +63,8 @@ impl RuntimeSingleton {
|
||||
|x| x.get(),
|
||||
);
|
||||
|
||||
log::info!("Starting v8 JavaScript runtime with {n_threads} workers.");
|
||||
|
||||
let (shared_sender, shared_receiver) = crossbeam_channel::unbounded::<Message>();
|
||||
|
||||
let (state, receivers): (Vec<State>, Vec<crossbeam_channel::Receiver<Message>>) = (0
|
||||
@@ -89,7 +91,12 @@ impl RuntimeSingleton {
|
||||
let shared_receiver = shared_receiver.clone();
|
||||
|
||||
return std::thread::spawn(move || {
|
||||
let mut runtime = Self::init_runtime(index).unwrap();
|
||||
let mut runtime = match Self::init_runtime(index) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(err) => {
|
||||
panic!("Failed to init v8 runtime on thread {index}: {err}");
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
crossbeam_channel::select! {
|
||||
@@ -140,18 +147,11 @@ impl RuntimeSingleton {
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.thread_name("v8-runtime")
|
||||
.thread_stack_size(4 * 1024 * 1024)
|
||||
.build()?;
|
||||
|
||||
let mut cache = import_provider::MemoryCache::default();
|
||||
cache.set(
|
||||
"trailbase:main",
|
||||
cow_to_string(JsRuntimeAssets::get("index.js").unwrap().data),
|
||||
);
|
||||
|
||||
let mut runtime = rustyscript::Runtime::with_tokio_runtime(
|
||||
rustyscript::RuntimeOptions {
|
||||
import_provider: Some(Box::new(cache)),
|
||||
import_provider: Some(Box::new(import_provider::ImportProviderImpl)),
|
||||
schema_whlist: HashSet::from(["trailbase".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -224,6 +224,7 @@ impl RuntimeSingleton {
|
||||
// make it work. Thus, we're making the V8 VM a singleton (like Dart's).
|
||||
static RUNTIME: LazyLock<RuntimeSingleton> = LazyLock::new(RuntimeSingleton::new);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RuntimeHandle;
|
||||
|
||||
impl RuntimeHandle {
|
||||
@@ -265,6 +266,10 @@ impl RuntimeHandle {
|
||||
return Self {};
|
||||
}
|
||||
|
||||
fn state(&self) -> &'static Vec<State> {
|
||||
return &RUNTIME.state;
|
||||
}
|
||||
|
||||
async fn apply<T>(
|
||||
&self,
|
||||
f: impl (FnOnce(&mut rustyscript::Runtime) -> T) + Send + Sync + 'static,
|
||||
@@ -340,13 +345,17 @@ impl IntoResponse for JsResponseError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get's called from JS to `addRoute`.
|
||||
fn route_callback(
|
||||
/// Get's called from JS during `addRoute` and installs an axum HTTP handler.
|
||||
///
|
||||
/// The axum HTTP handler will then call back into the registered callback in JS.
|
||||
fn add_route_to_router(
|
||||
runtime_handle: RuntimeHandle,
|
||||
router: Arc<Mutex<Option<Router<AppState>>>>,
|
||||
method: String,
|
||||
route: String,
|
||||
) -> Result<(), AnyError> {
|
||||
let method_uppercase = method.to_uppercase();
|
||||
|
||||
let route_path = route.clone();
|
||||
let handler = move |params: RawPathParams, req: Request| async move {
|
||||
let (parts, body) = req.into_parts();
|
||||
@@ -356,12 +365,7 @@ fn route_callback(
|
||||
"request deserialization failed".to_string(),
|
||||
));
|
||||
};
|
||||
let Parts {
|
||||
method: _,
|
||||
uri,
|
||||
headers,
|
||||
..
|
||||
} = parts;
|
||||
let Parts { uri, headers, .. } = parts;
|
||||
|
||||
let path_params: Vec<(String, String)> = params
|
||||
.iter()
|
||||
@@ -386,7 +390,7 @@ fn route_callback(
|
||||
body: Option<bytes::Bytes>,
|
||||
}
|
||||
|
||||
let js_response = RuntimeHandle::new()
|
||||
let js_response = runtime_handle
|
||||
.apply(move |runtime| -> Result<JsResponse, rustyscript::Error> {
|
||||
let tokio_runtime = runtime.tokio_runtime();
|
||||
return tokio_runtime.block_on(async {
|
||||
@@ -461,47 +465,68 @@ where
|
||||
return from_value::<T>(arg.clone()).map_err(|err| Error::Runtime(err.to_string()));
|
||||
}
|
||||
|
||||
pub(crate) async fn install_routes(module: Module) -> Result<Option<Router<AppState>>, AnyError> {
|
||||
pub(crate) async fn install_routes(
|
||||
runtime_handle: RuntimeHandle,
|
||||
module: Module,
|
||||
) -> Result<Option<Router<AppState>>, AnyError> {
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
let receivers: Vec<_> = RUNTIME
|
||||
.state
|
||||
let receivers: Vec<_> = runtime_handle
|
||||
.state()
|
||||
.iter()
|
||||
.map(move |s| -> oneshot::Receiver<Option<Router<AppState>>> {
|
||||
let module = module.clone();
|
||||
let (sender, receiver) = oneshot::channel::<Option<Router<AppState>>>();
|
||||
s.sender
|
||||
.send(Message::Run(Box::new(move |runtime: &mut Runtime| {
|
||||
let router = Arc::new(Mutex::new(Some(Router::<AppState>::new())));
|
||||
.enumerate()
|
||||
.map(
|
||||
move |(index, state)| -> oneshot::Receiver<Option<Router<AppState>>> {
|
||||
let (sender, receiver) = oneshot::channel::<Option<Router<AppState>>>();
|
||||
|
||||
// First install a native callback that builds an axum router.
|
||||
let router_clone = router.clone();
|
||||
runtime
|
||||
.register_function("route", move |args: &[serde_json::Value]| {
|
||||
let method: String = get_arg(args, 0)?;
|
||||
let route: String = get_arg(args, 1)?;
|
||||
let module = module.clone();
|
||||
let runtime_handle = runtime_handle.clone();
|
||||
|
||||
route_callback(router_clone.clone(), method, route)
|
||||
.map_err(|err| rustyscript::Error::Runtime(err.to_string()))?;
|
||||
if let Err(err) = state
|
||||
.sender
|
||||
.send(Message::Run(Box::new(move |runtime: &mut Runtime| {
|
||||
let router = Arc::new(Mutex::new(Some(Router::<AppState>::new())));
|
||||
|
||||
Ok(serde_json::Value::Null)
|
||||
})
|
||||
.unwrap();
|
||||
// First install a native callback that builds an axum router.
|
||||
let router_clone = router.clone();
|
||||
runtime
|
||||
.register_function("route", move |args: &[serde_json::Value]| {
|
||||
let method: String = get_arg(args, 0)?;
|
||||
let route: String = get_arg(args, 1)?;
|
||||
|
||||
// Then execute the script/module, i.e. statements in the file scope.
|
||||
runtime.load_module(&module).unwrap();
|
||||
add_route_to_router(runtime_handle.clone(), router_clone.clone(), method, route)
|
||||
.map_err(|err| rustyscript::Error::Runtime(err.to_string()))?;
|
||||
|
||||
let router: Router<AppState> = router.lock().take().unwrap();
|
||||
if router.has_routes() {
|
||||
sender.send(Some(router)).unwrap();
|
||||
} else {
|
||||
sender.send(None).unwrap();
|
||||
}
|
||||
})))
|
||||
.unwrap();
|
||||
Ok(serde_json::Value::Null)
|
||||
})
|
||||
.expect("Failed to register 'route' function");
|
||||
|
||||
return receiver;
|
||||
})
|
||||
// Then execute the script/module, i.e. statements in the file scope.
|
||||
//
|
||||
// TODO: SWC is very spammy (at least in debug builds). Ideally, we'd lower the tracing
|
||||
// filter level within this scope. Haven't found a good way, thus filtering it
|
||||
// env-filter at the CLI level. We could try to use a dedicated reload layer:
|
||||
// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/reload/index.html
|
||||
if let Err(err) = runtime.load_module(&module) {
|
||||
panic!("Failed to load '{:?}': {err}", module.filename());
|
||||
}
|
||||
|
||||
let router: Router<AppState> = router.lock().take().unwrap();
|
||||
sender
|
||||
.send(if router.has_routes() {
|
||||
Some(router)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.expect("Failed to comm with parent");
|
||||
})))
|
||||
{
|
||||
panic!("Failed to comm with v8 rt'{index}': {err}");
|
||||
}
|
||||
|
||||
return receiver;
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
let mut receivers = futures::future::join_all(receivers).await;
|
||||
@@ -510,9 +535,35 @@ pub(crate) async fn install_routes(module: Module) -> Result<Option<Router<AppSt
|
||||
return Ok(receivers.swap_remove(0)?);
|
||||
}
|
||||
|
||||
#[derive(RustEmbed, Clone)]
|
||||
#[folder = "js/dist/"]
|
||||
struct JsRuntimeAssets;
|
||||
pub(crate) async fn write_js_runtime_files(data_dir: &DataDir) {
|
||||
if let Err(err) = tokio::fs::write(
|
||||
data_dir.root().join("trailbase.js"),
|
||||
cow_to_string(
|
||||
JsRuntimeAssets::get("index.js")
|
||||
.expect("Failed to read rt/index.js")
|
||||
.data,
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to write 'trailbase.js': {err}");
|
||||
}
|
||||
|
||||
if let Err(err) = tokio::fs::write(
|
||||
data_dir.root().join("trailbase.d.ts"),
|
||||
cow_to_string(
|
||||
JsRuntimeAssets::get("index.d.ts")
|
||||
.expect("Failed to read rt/index.d.ts")
|
||||
.data,
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to write 'trailbase.d.ts': {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -606,20 +606,15 @@ impl TryFrom<sqlite3_parser::ast::Stmt> for Table {
|
||||
tbl_name,
|
||||
args: _args,
|
||||
..
|
||||
} => {
|
||||
#[cfg(debug_assertions)]
|
||||
debug!("vTable args: {_args:?}");
|
||||
|
||||
Ok(Table {
|
||||
name: tbl_name.name.to_string(),
|
||||
strict: false,
|
||||
columns: vec![],
|
||||
foreign_keys: vec![],
|
||||
unique: vec![],
|
||||
virtual_table: true,
|
||||
temporary: false,
|
||||
})
|
||||
}
|
||||
} => Ok(Table {
|
||||
name: tbl_name.name.to_string(),
|
||||
strict: false,
|
||||
columns: vec![],
|
||||
foreign_keys: vec![],
|
||||
unique: vec![],
|
||||
virtual_table: true,
|
||||
temporary: false,
|
||||
}),
|
||||
_ => Err(SchemaError::Precondition(
|
||||
format!("expected 'CREATE TABLE', got: {value:?}").into(),
|
||||
)),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use libsql::Connection;
|
||||
use log::*;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use thiserror::Error;
|
||||
use trailbase_sqlite::{connect_sqlite, query_one_row};
|
||||
|
||||
@@ -9,6 +8,7 @@ use crate::app_state::{AppState, AppStateArgs};
|
||||
use crate::auth::jwt::{JwtHelper, JwtHelperError};
|
||||
use crate::config::load_or_init_config_textproto;
|
||||
use crate::constants::USER_TABLE;
|
||||
use crate::js::write_js_runtime_files;
|
||||
use crate::migrations::{apply_logs_migrations, apply_main_migrations};
|
||||
use crate::rand::generate_random_string;
|
||||
use crate::server::DataDir;
|
||||
@@ -42,7 +42,6 @@ pub async fn init_app_state(
|
||||
data_dir: DataDir,
|
||||
public_dir: Option<PathBuf>,
|
||||
dev: bool,
|
||||
tokio_runtime: Rc<tokio::runtime::Runtime>,
|
||||
) -> Result<(bool, AppState), InitError> {
|
||||
// First create directory structure.
|
||||
data_dir.ensure_directory_structure().await?;
|
||||
@@ -101,6 +100,9 @@ pub async fn init_app_state(
|
||||
debug!("Failed to load maxmind geoip DB '{geoip_db_path:?}': {err}");
|
||||
}
|
||||
|
||||
// Write out the latest .js/.d.ts runtime files.
|
||||
write_js_runtime_files(&data_dir).await;
|
||||
|
||||
let app_state = AppState::new(AppStateArgs {
|
||||
data_dir: data_dir.clone(),
|
||||
public_dir,
|
||||
@@ -110,7 +112,6 @@ pub async fn init_app_state(
|
||||
conn: main_conn.clone(),
|
||||
logs_conn,
|
||||
jwt,
|
||||
tokio_runtime,
|
||||
});
|
||||
|
||||
if new_db {
|
||||
|
||||
@@ -94,13 +94,8 @@ impl Server {
|
||||
where
|
||||
O: std::future::Future<Output = Result<(), Box<dyn std::error::Error + Sync + Send>>>,
|
||||
{
|
||||
let (new_data_dir, state) = init::init_app_state(
|
||||
opts.data_dir.clone(),
|
||||
opts.public_dir.clone(),
|
||||
opts.dev,
|
||||
opts.tokio_runtime.clone(),
|
||||
)
|
||||
.await?;
|
||||
let (new_data_dir, state) =
|
||||
init::init_app_state(opts.data_dir.clone(), opts.public_dir.clone(), opts.dev).await?;
|
||||
|
||||
if new_data_dir {
|
||||
on_first_init(state.clone())
|
||||
@@ -113,7 +108,7 @@ impl Server {
|
||||
let mut js_router = Some(Router::new());
|
||||
for module in modules {
|
||||
let fname = module.filename().to_owned();
|
||||
let router = install_routes(module)
|
||||
let router = install_routes(state.script_runtime(), module)
|
||||
.await
|
||||
.map_err(|err| InitError::ScriptError(err.to_string()))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user