Last active
May 28, 2024 16:03
-
-
Save iameli/aeaa025d96ec6aa453b1b568a4d26ca5 to your computer and use it in GitHub Desktop.
polyfill for running express app from a cloudflare worker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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