Skip to content

Instantly share code, notes, and snippets.

@iameli
Last active May 28, 2024 16:03
Show Gist options
  • Save iameli/aeaa025d96ec6aa453b1b568a4d26ca5 to your computer and use it in GitHub Desktop.
Save iameli/aeaa025d96ec6aa453b1b568a4d26ca5 to your computer and use it in GitHub Desktop.
polyfill for running express app from a cloudflare worker
import express from "./express-poly";
import Router from "express/lib/router";
import cardcoreApp from "@cardcore/server/dist/router";
import creds from "./azure-sql.json";
import CosmosDB from "./cosmosdb";
import slackNotify from "./slack-notify";
import { v4 as uuid } from "uuid";
import { version } from "../version.json";
import pkg from "../package.json";
import stringify from "json-stable-stringify";
self.setImmediate = fn => setTimeout(fn, 0);
const cosmos = new CosmosDB({ ...creds, app: "cardcore-test" });
let instanceId;
const outer = Router();
outer.use((req, res, next) => {
if (!instanceId) {
instanceId = uuid();
slackNotify(
`instance ${instanceId} booting up, version ${version}, handling ${
req.method
} ${req.path}`
);
}
res.header("access-control-allow-origin", "*");
res.header("access-control-allow-methods", "*");
res.header("access-control-allow-headers", "content-type");
res.header("cardcore-version", version);
const notFound = () => {
const err = new Error("NotFoundError");
err.name = "NotFoundError";
return err;
};
req.store = {
async get(key) {
const thing = await KV.get(key);
if (thing === null || thing === "null") {
throw notFound();
}
return JSON.parse(await KV.get(key));
},
async put(key, value) {
const thing = await KV.get(key);
if (thing) {
throw new Error("Can't overwrite");
}
const start = Date.now();
const result = await KV.put(key, value);
slackNotify(`write took ${Date.now() - start}ms`);
return result;
}
};
next();
});
outer.options((req, res) => {
res.sendStatus(200);
});
outer.use(cardcoreApp);
// proxy
const ccVersion = pkg.dependencies["@cardcore/server"];
const root = `https://unpkg.com/@cardcore/frontend@${ccVersion}/build`;
outer.get("*", async (req, res) => {
let pathname = new URL(req.url).pathname;
if (pathname === "/") {
return res.sendResponse(fetch(root + "/index.html"));
}
const staticReq = await fetch(root + pathname);
if (staticReq.ok) {
return res.sendResponse(staticReq);
}
return res.sendResponse(fetch(root + "/index.html"));
});
const app = express();
app.use(outer);
app.listen();
import { resolve } from "path";
import parseurl from "parseurl";
import slackNotify from "./slack-notify";
export class ExpressRequest {
constructor({ path, method, url, headers, body }) {
this.path = path;
this.method = method;
this.url = url;
this.headers = headers;
this.body = body;
this.params = this.path.split("/").filter(x => x);
const { host, protocol } = parseurl(this);
this.headers.host = host;
this.protocol = protocol.slice(0, protocol.length - 1);
}
get(headerName) {
return this.headers[headerName];
}
}
export class ExpressResponse {
constructor({ resolve, reject }) {
this._resolve = resolve;
this._reject = reject;
this._status = 200;
this._headers = {};
}
status(code) {
this._status = code;
}
json(obj) {
const str = JSON.stringify(obj, null, 2);
this.header("content-type", "application/json");
return this.send(str);
}
send(data) {
if (data !== null && typeof data === "object") {
return this.json(data);
}
if (data === null || data === undefined) {
data = "";
}
if (typeof data !== "string") {
throw new Error("can't handle data type " + typeof data);
}
if (this._status === 500) {
slackNotify(
JSON.stringify({
status: 500,
data
})
);
}
this.sendResponse(
new Response(data, {
status: this._status,
headers: {
...this._headers
}
})
);
}
end() {
this.send();
}
// worker-specific one if we want to manage Response ourselves
sendResponse(workerResponse) {
this._resolve(workerResponse);
}
header(field, value) {
this._headers[field] = value;
}
set(...args) {
return this.header(...args);
}
sendStatus(code) {
this.status(code);
if (code === 204) {
return this.send("");
}
this.send(`${this._status}`);
}
}
export class ExpressApp {
constructor() {}
use(fn) {
this.router = fn;
}
listen() {
// port is ignored, we're a service worker
addEventListener("fetch", event => {
event.respondWith(this._handleRequest(event.request));
});
}
async _handleRequest(workerReq) {
let body;
if (workerReq.method === "POST") {
try {
body = await workerReq.json();
} catch (e) {
return new Response(e.message, {
status: 500,
headers: {
"content-type": "text/plain"
}
});
}
}
return new Promise((resolve, reject) => {
const path = new URL(workerReq.url).pathname;
const req = new ExpressRequest({
path,
method: workerReq.method,
url: workerReq.url,
headers: workerReq.headers,
body: body
});
const res = new ExpressResponse({ resolve, reject });
if (!this.router) {
return reject(new Error("no route found"));
}
this.router(req, res, err => {
if (err) {
return reject(err);
}
reject(new Error("request got past the router"));
});
}).catch(err => {
if (!err) {
err = new Error("undefined error");
}
return new Response([err.message, err.stack].join("\n"), {
status: 500,
headers: {
"content-type": "text/plain"
}
});
});
}
}
export default function(...args) {
return new ExpressApp(...args);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment