Skip to content

Instantly share code, notes, and snippets.

@tmcw
Created December 21, 2023 20:53
Show Gist options
  • Save tmcw/47f98394bbfebb896879b04b0824ebc3 to your computer and use it in GitHub Desktop.
Save tmcw/47f98394bbfebb896879b04b0824ebc3 to your computer and use it in GitHub Desktop.
import {
encodeBase64,
decodeBase64,
} from "https://deno.land/std@0.210.0/encoding/base64.ts";
import process from "node:process";
import SuperJSON from "npm:superjson@2.1.0";
SuperJSON.registerCustom<Function, string>(
{
isApplicable: (v: any): v is Function => typeof v === "function",
serialize: (_v: Function) => "",
deserialize: () => {
// This will cause typeOutput to detect a function
// and trigger the right behavior in ValEditor.
return Symbol.for("function") as unknown as Function;
},
},
"function"
);
/**
* Given an object of environment variables, create a stub
* that simulates the same interface as Deno.env
*/
export function createDenoEnvStub(
input: Record<string, string>
): typeof Deno.env {
return {
get(key: string) {
return input[key];
},
has(key: string) {
return input[key] !== undefined;
},
toObject() {
return { ...input };
},
set(_key: string, _value: string) {
// Stub
},
delete(_key: string) {
// Stub
},
};
}
/**
* Given a Response object, serialize it.
* Note: if you try this twice on the same Response, it'll
* crash! Streams, like resp.arrayBuffer(), can only
* be consumed once.
*/
export async function serializeResponse(resp: Response) {
return {
headers: [...resp.headers.entries()],
body: resp.body ? encodeBase64(await resp.arrayBuffer()) : null,
status: resp.status,
statusText: resp.statusText,
};
}
/**
* Express API mocks ----------------------------------------------------------
*/
export class RequestLike {
#options: SerializedExpressRequest;
constructor(options: SerializedExpressRequest) {
this.#options = options;
}
// https://expressjs.com/en/api.html#req.method
get method() {
return this.#options.method;
}
// https://expressjs.com/en/api.html#req.query
get query() {
return this.#options.query;
}
get body() {
return this.#options.body;
}
/**
* Stubs
*/
// https://expressjs.com/en/api.html#req.baseUrl
get baseUrl() {
return "/";
}
// https://expressjs.com/en/api.html#req.params
get params() {
return {};
}
// https://expressjs.com/en/api.html#req.secure
get secure() {
return true;
}
// https://expressjs.com/en/api.html#req.subdomains
get subdomains() {
return [];
}
// https://expressjs.com/en/api.html#req.fresh
get fresh() {
return true;
}
// https://expressjs.com/en/api.html#req.protocol
get protocol() {
return "https";
}
// https://expressjs.com/en/api.html#req.path
get path() {
return this.#options.path;
}
// https://expressjs.com/en/4x/api.html#req.originalUrl
get originalUrl() {
return this.#options.originalUrl;
}
acceptsCharsets() {
return true;
}
acceptsEncodings() {
return true;
}
acceptsLanguages() {
return true;
}
accepts() {
return true;
}
get(name: string) {
return this.#options.headers[String(name).toLowerCase()];
}
}
interface DoneMessage {
type: "done";
wallTime: number;
}
interface ErrorMessage {
type: "error";
value: unknown;
}
interface ReadyMessage {
type: "ready";
}
interface ReturnMessage {
type: "return";
value: unknown;
}
interface ExportsMessage {
type: "exports";
value: unknown;
}
interface ExpressResponseMessage {
type: "expressresponse";
name: string;
args: unknown[];
}
type Message =
| DoneMessage
| ErrorMessage
| ReadyMessage
| ReturnMessage
| ExportsMessage
| ExpressResponseMessage;
export function send(message: Message) {
// We used to have a much smaller limit here.
// This one is just to ensure folks aren't sending crazy amount of data
if (JSON.stringify(message).length > 10_000_000) {
message = {
type: "error",
value: {
name: "WS_PAYLOAD_TOO_LARGE: " + message.type,
message: `The ${message.type} is too large to process`,
},
};
}
(self as any).postMessage(message);
}
export class ResponseLike {
#stub(name: string, args: unknown[]) {
send({
type: "expressresponse",
name,
args,
});
return this;
}
json(...args: unknown[]) {
return this.#stub("json", args);
}
jsonp(...args: unknown[]) {
return this.#stub("jsonp", args);
}
status(...args: unknown[]) {
return this.#stub("status", args);
}
send(...args: unknown[]) {
return this.#stub("send", args);
}
type(...args: unknown[]) {
return this.#stub("type", args);
}
get(...args: unknown[]) {
return this.#stub("get", args);
}
redirect(...args: unknown[]) {
return this.#stub("redirect", args);
}
end(...args: unknown[]) {
return this.#stub("end", args);
}
set(...args: unknown[]) {
return this.#stub("set", args);
}
}
/**
* Deserialization ------------------------------------------------------------
*/
const TAG = "@@valtown-type";
function getDeserializationTag(arg: unknown): string | undefined {
if (
arg &&
typeof arg === "object" &&
TAG in arg &&
typeof arg[TAG] === "string"
) {
return arg[TAG];
}
}
type SerializedRequest = {
[TAG]: "request";
url: string;
method: string;
headers: [string, string][];
body?: string;
};
type SerializedExpressRequest = {
[TAG]: "express-request";
method: string;
protocol: string;
hostname: string;
xhr: boolean;
body: string;
query: Record<string, string>;
headers: Record<string, string>;
path: string;
originalUrl: string;
};
type SerializedExpressResponse = {
[TAG]: "express-response";
};
function deserializeRequest(arg: SerializedRequest) {
return new Request(arg.url, {
method: arg.method,
headers: arg.headers,
...(arg.body ? { body: decodeBase64(arg.body) } : {}),
});
}
function deserializeExpressRequest(arg: SerializedExpressRequest) {
return new RequestLike(arg);
}
function deserializeExpressResponse(_arg: SerializedExpressResponse) {
return new ResponseLike();
}
export function deserializeCustom(arg: unknown) {
// Sniff for custom types that can't make it through
// structuredClone and deserialize them into custom
// objects.
switch (getDeserializationTag(arg)) {
case "request": {
return deserializeRequest(arg as SerializedRequest);
}
case "express-request": {
return deserializeExpressRequest(arg as SerializedExpressRequest);
}
case "express-response": {
return deserializeExpressResponse(arg as SerializedExpressResponse);
}
default: {
return arg;
}
}
}
const WALL_TIME_START = Date.now();
function getMainExport(
mod: any
): { ok: true; value: any } | { ok: false; error: Error } {
if ("default" in mod) {
return { ok: true, value: mod.default };
}
// If the val has exactly one named export, we run that.
const exports = Object.keys(mod);
if (exports.length > 1) {
const error = new Error(
`Vals require a default export, or exactly one named export. This val exports: ${exports.join(
", "
)}`
);
error.name = "ImportValError";
return { ok: false, error };
} else if (exports.length === 0) {
const error = new Error(
"Vals require a default export, or exactly one named export. This val has none."
);
error.name = "ImportValError";
return { ok: false, error };
}
return { ok: true, value: mod[exports[0]] };
}
/**
* Send a message to the host.
*/
self.addEventListener("message", async (msg: any) => {
if (msg.data.entrypoint) {
try {
// Override the environment
process.env = { ...msg.data.env };
Object.defineProperty(Deno, "env", {
value: createDenoEnvStub({ ...msg.data.env }),
});
const mod = await import(msg.data.entrypoint);
if (Array.isArray(msg.data.args)) {
const exp = getMainExport(mod);
if (!exp.ok) {
throw exp.error;
}
if (typeof exp.value === "function") {
let value = await exp.value(...msg.data.args.map(deserializeCustom));
if (value instanceof Response) {
value = await serializeResponse(value);
}
send({ type: "return", value: SuperJSON.serialize(value) });
} else {
send({ type: "return", value: SuperJSON.serialize(await exp.value) });
}
} else {
// Await every exported value
const awaitedMod = Object.fromEntries(
await Promise.all(
Object.entries(mod).map(async ([key, v]) => {
const resolved = await v;
if (resolved instanceof Response) {
return [key, await serializeResponse(resolved)];
}
return [key, resolved];
})
)
);
send({ type: "exports", value: SuperJSON.serialize(awaitedMod) });
const output = getMainExport(awaitedMod);
send({
type: "return",
value: SuperJSON.serialize(output.ok ? output.value : undefined),
});
}
// Communicate the result to the parent.
} catch (e) {
send({ type: "error", value: e });
}
send({
type: "done",
wallTime: Date.now() - WALL_TIME_START,
});
}
});
send({ type: "ready" });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment