Deploy.
This commit is contained in:
+67
@@ -0,0 +1,67 @@
|
||||
export type Awaitable<T> = T | Promise<T>;
|
||||
// TODO: allow dispatching more events?
|
||||
export type Dispatcher = (
|
||||
type: "scheduled",
|
||||
init: { cron?: string }
|
||||
) => Awaitable<void>;
|
||||
|
||||
export type IncomingRequest = Request<
|
||||
unknown,
|
||||
IncomingRequestCfProperties<unknown>
|
||||
>;
|
||||
|
||||
export interface MiddlewareContext {
|
||||
dispatch: Dispatcher;
|
||||
next(request: IncomingRequest, env: any): Awaitable<Response>;
|
||||
}
|
||||
|
||||
export type Middleware = (
|
||||
request: IncomingRequest,
|
||||
env: any,
|
||||
ctx: ExecutionContext,
|
||||
middlewareCtx: MiddlewareContext
|
||||
) => Awaitable<Response>;
|
||||
|
||||
const __facade_middleware__: Middleware[] = [];
|
||||
|
||||
// The register functions allow for the insertion of one or many middleware,
|
||||
// We register internal middleware first in the stack, but have no way of controlling
|
||||
// the order that addMiddleware is run in service workers so need an internal function.
|
||||
export function __facade_register__(...args: (Middleware | Middleware[])[]) {
|
||||
__facade_middleware__.push(...args.flat());
|
||||
}
|
||||
export function __facade_registerInternal__(
|
||||
...args: (Middleware | Middleware[])[]
|
||||
) {
|
||||
__facade_middleware__.unshift(...args.flat());
|
||||
}
|
||||
|
||||
function __facade_invokeChain__(
|
||||
request: IncomingRequest,
|
||||
env: any,
|
||||
ctx: ExecutionContext,
|
||||
dispatch: Dispatcher,
|
||||
middlewareChain: Middleware[]
|
||||
): Awaitable<Response> {
|
||||
const [head, ...tail] = middlewareChain;
|
||||
const middlewareCtx: MiddlewareContext = {
|
||||
dispatch,
|
||||
next(newRequest, newEnv) {
|
||||
return __facade_invokeChain__(newRequest, newEnv, ctx, dispatch, tail);
|
||||
},
|
||||
};
|
||||
return head(request, env, ctx, middlewareCtx);
|
||||
}
|
||||
|
||||
export function __facade_invoke__(
|
||||
request: IncomingRequest,
|
||||
env: any,
|
||||
ctx: ExecutionContext,
|
||||
dispatch: Dispatcher,
|
||||
finalMiddleware: Middleware
|
||||
): Awaitable<Response> {
|
||||
return __facade_invokeChain__(request, env, ctx, dispatch, [
|
||||
...__facade_middleware__,
|
||||
finalMiddleware,
|
||||
]);
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// This loads all middlewares exposed on the middleware object and then starts
|
||||
// the invocation chain. The big idea is that we can add these to the middleware
|
||||
// export dynamically through wrangler, or we can potentially let users directly
|
||||
// add them as a sort of "plugin" system.
|
||||
|
||||
import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "__ENTRY_POINT__";
|
||||
import { __facade_invoke__, __facade_register__, Dispatcher } from "./common";
|
||||
import type { WorkerEntrypointConstructor } from "__ENTRY_POINT__";
|
||||
|
||||
// Preserve all the exports from the worker
|
||||
export * from "__ENTRY_POINT__";
|
||||
|
||||
class __Facade_ScheduledController__ implements ScheduledController {
|
||||
readonly #noRetry: ScheduledController["noRetry"];
|
||||
|
||||
constructor(
|
||||
readonly scheduledTime: number,
|
||||
readonly cron: string,
|
||||
noRetry: ScheduledController["noRetry"]
|
||||
) {
|
||||
this.#noRetry = noRetry;
|
||||
}
|
||||
|
||||
noRetry() {
|
||||
if (!(this instanceof __Facade_ScheduledController__)) {
|
||||
throw new TypeError("Illegal invocation");
|
||||
}
|
||||
// Need to call native method immediately in case uncaught error thrown
|
||||
this.#noRetry();
|
||||
}
|
||||
}
|
||||
|
||||
function wrapExportedHandler(worker: ExportedHandler): ExportedHandler {
|
||||
// If we don't have any middleware defined, just return the handler as is
|
||||
if (
|
||||
__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
|
||||
__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
|
||||
) {
|
||||
return worker;
|
||||
}
|
||||
// Otherwise, register all middleware once
|
||||
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
|
||||
__facade_register__(middleware);
|
||||
}
|
||||
|
||||
const fetchDispatcher: ExportedHandlerFetchHandler = function (
|
||||
request,
|
||||
env,
|
||||
ctx
|
||||
) {
|
||||
if (worker.fetch === undefined) {
|
||||
throw new Error("Handler does not export a fetch() function.");
|
||||
}
|
||||
return worker.fetch(request, env, ctx);
|
||||
};
|
||||
|
||||
return {
|
||||
...worker,
|
||||
fetch(request, env, ctx) {
|
||||
const dispatcher: Dispatcher = function (type, init) {
|
||||
if (type === "scheduled" && worker.scheduled !== undefined) {
|
||||
const controller = new __Facade_ScheduledController__(
|
||||
Date.now(),
|
||||
init.cron ?? "",
|
||||
() => {}
|
||||
);
|
||||
return worker.scheduled(controller, env, ctx);
|
||||
}
|
||||
};
|
||||
return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function wrapWorkerEntrypoint(
|
||||
klass: WorkerEntrypointConstructor
|
||||
): WorkerEntrypointConstructor {
|
||||
// If we don't have any middleware defined, just return the handler as is
|
||||
if (
|
||||
__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
|
||||
__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
|
||||
) {
|
||||
return klass;
|
||||
}
|
||||
// Otherwise, register all middleware once
|
||||
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
|
||||
__facade_register__(middleware);
|
||||
}
|
||||
|
||||
// `extend`ing `klass` here so other RPC methods remain callable
|
||||
return class extends klass {
|
||||
#fetchDispatcher: ExportedHandlerFetchHandler<Record<string, unknown>> = (
|
||||
request,
|
||||
env,
|
||||
ctx
|
||||
) => {
|
||||
this.env = env;
|
||||
this.ctx = ctx;
|
||||
if (super.fetch === undefined) {
|
||||
throw new Error("Entrypoint class does not define a fetch() function.");
|
||||
}
|
||||
return super.fetch(request);
|
||||
};
|
||||
|
||||
#dispatcher: Dispatcher = (type, init) => {
|
||||
if (type === "scheduled" && super.scheduled !== undefined) {
|
||||
const controller = new __Facade_ScheduledController__(
|
||||
Date.now(),
|
||||
init.cron ?? "",
|
||||
() => {}
|
||||
);
|
||||
return super.scheduled(controller);
|
||||
}
|
||||
};
|
||||
|
||||
fetch(request: Request<unknown, IncomingRequestCfProperties>) {
|
||||
return __facade_invoke__(
|
||||
request,
|
||||
this.env,
|
||||
this.ctx,
|
||||
this.#dispatcher,
|
||||
this.#fetchDispatcher
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined;
|
||||
if (typeof ENTRY === "object") {
|
||||
WRAPPED_ENTRY = wrapExportedHandler(ENTRY);
|
||||
} else if (typeof ENTRY === "function") {
|
||||
WRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY);
|
||||
}
|
||||
export default WRAPPED_ENTRY;
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
__facade_invoke__,
|
||||
__facade_register__,
|
||||
__facade_registerInternal__,
|
||||
Awaitable,
|
||||
Dispatcher,
|
||||
IncomingRequest,
|
||||
Middleware,
|
||||
} from "./common";
|
||||
|
||||
export { __facade_register__, __facade_registerInternal__ };
|
||||
|
||||
// Miniflare 2's `EventTarget` follows the spec and doesn't allow exceptions to
|
||||
// be caught by `dispatchEvent`. Instead it has a custom `ThrowingEventTarget`
|
||||
// class that rethrows errors from event listeners in `dispatchEvent`.
|
||||
// We'd like errors to be propagated to the top-level `addEventListener`, so
|
||||
// we'd like to use `ThrowingEventTarget`. Unfortunately, `ThrowingEventTarget`
|
||||
// isn't exposed on the global scope, but `WorkerGlobalScope` (which extends
|
||||
// `ThrowingEventTarget`) is. Therefore, we get at it in this nasty way.
|
||||
let __FACADE_EVENT_TARGET__: EventTarget;
|
||||
if ((globalThis as any).MINIFLARE) {
|
||||
__FACADE_EVENT_TARGET__ = new (Object.getPrototypeOf(WorkerGlobalScope))();
|
||||
} else {
|
||||
__FACADE_EVENT_TARGET__ = new EventTarget();
|
||||
}
|
||||
|
||||
function __facade_isSpecialEvent__(
|
||||
type: string
|
||||
): type is "fetch" | "scheduled" {
|
||||
return type === "fetch" || type === "scheduled";
|
||||
}
|
||||
const __facade__originalAddEventListener__ = globalThis.addEventListener;
|
||||
const __facade__originalRemoveEventListener__ = globalThis.removeEventListener;
|
||||
const __facade__originalDispatchEvent__ = globalThis.dispatchEvent;
|
||||
|
||||
globalThis.addEventListener = function (type, listener, options) {
|
||||
if (__facade_isSpecialEvent__(type)) {
|
||||
__FACADE_EVENT_TARGET__.addEventListener(
|
||||
type,
|
||||
listener as EventListenerOrEventListenerObject,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
__facade__originalAddEventListener__(type, listener, options);
|
||||
}
|
||||
};
|
||||
globalThis.removeEventListener = function (type, listener, options) {
|
||||
if (__facade_isSpecialEvent__(type)) {
|
||||
__FACADE_EVENT_TARGET__.removeEventListener(
|
||||
type,
|
||||
listener as EventListenerOrEventListenerObject,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
__facade__originalRemoveEventListener__(type, listener, options);
|
||||
}
|
||||
};
|
||||
globalThis.dispatchEvent = function (event) {
|
||||
if (__facade_isSpecialEvent__(event.type)) {
|
||||
return __FACADE_EVENT_TARGET__.dispatchEvent(event);
|
||||
} else {
|
||||
return __facade__originalDispatchEvent__(event);
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
var addMiddleware: typeof __facade_register__;
|
||||
var addMiddlewareInternal: typeof __facade_registerInternal__;
|
||||
}
|
||||
globalThis.addMiddleware = __facade_register__;
|
||||
globalThis.addMiddlewareInternal = __facade_registerInternal__;
|
||||
|
||||
const __facade_waitUntil__ = Symbol("__facade_waitUntil__");
|
||||
const __facade_response__ = Symbol("__facade_response__");
|
||||
const __facade_dispatched__ = Symbol("__facade_dispatched__");
|
||||
|
||||
class __Facade_ExtendableEvent__ extends Event {
|
||||
[__facade_waitUntil__]: Awaitable<unknown>[] = [];
|
||||
|
||||
waitUntil(promise: Awaitable<any>) {
|
||||
if (!(this instanceof __Facade_ExtendableEvent__)) {
|
||||
throw new TypeError("Illegal invocation");
|
||||
}
|
||||
this[__facade_waitUntil__].push(promise);
|
||||
}
|
||||
}
|
||||
|
||||
interface FetchEventInit extends EventInit {
|
||||
request: Request;
|
||||
passThroughOnException: FetchEvent["passThroughOnException"];
|
||||
}
|
||||
|
||||
class __Facade_FetchEvent__ extends __Facade_ExtendableEvent__ {
|
||||
#request: Request;
|
||||
#passThroughOnException: FetchEvent["passThroughOnException"];
|
||||
[__facade_response__]?: Awaitable<Response>;
|
||||
[__facade_dispatched__] = false;
|
||||
|
||||
constructor(type: "fetch", init: FetchEventInit) {
|
||||
super(type);
|
||||
this.#request = init.request;
|
||||
this.#passThroughOnException = init.passThroughOnException;
|
||||
}
|
||||
|
||||
get request() {
|
||||
return this.#request;
|
||||
}
|
||||
|
||||
respondWith(response: Awaitable<Response>) {
|
||||
if (!(this instanceof __Facade_FetchEvent__)) {
|
||||
throw new TypeError("Illegal invocation");
|
||||
}
|
||||
if (this[__facade_response__] !== undefined) {
|
||||
throw new DOMException(
|
||||
"FetchEvent.respondWith() has already been called; it can only be called once.",
|
||||
"InvalidStateError"
|
||||
);
|
||||
}
|
||||
if (this[__facade_dispatched__]) {
|
||||
throw new DOMException(
|
||||
"Too late to call FetchEvent.respondWith(). It must be called synchronously in the event handler.",
|
||||
"InvalidStateError"
|
||||
);
|
||||
}
|
||||
this.stopImmediatePropagation();
|
||||
this[__facade_response__] = response;
|
||||
}
|
||||
|
||||
passThroughOnException() {
|
||||
if (!(this instanceof __Facade_FetchEvent__)) {
|
||||
throw new TypeError("Illegal invocation");
|
||||
}
|
||||
// Need to call native method immediately in case uncaught error thrown
|
||||
this.#passThroughOnException();
|
||||
}
|
||||
}
|
||||
|
||||
interface ScheduledEventInit extends EventInit {
|
||||
scheduledTime: number;
|
||||
cron: string;
|
||||
noRetry: ScheduledEvent["noRetry"];
|
||||
}
|
||||
|
||||
class __Facade_ScheduledEvent__ extends __Facade_ExtendableEvent__ {
|
||||
#scheduledTime: number;
|
||||
#cron: string;
|
||||
#noRetry: ScheduledEvent["noRetry"];
|
||||
|
||||
constructor(type: "scheduled", init: ScheduledEventInit) {
|
||||
super(type);
|
||||
this.#scheduledTime = init.scheduledTime;
|
||||
this.#cron = init.cron;
|
||||
this.#noRetry = init.noRetry;
|
||||
}
|
||||
|
||||
get scheduledTime() {
|
||||
return this.#scheduledTime;
|
||||
}
|
||||
|
||||
get cron() {
|
||||
return this.#cron;
|
||||
}
|
||||
|
||||
noRetry() {
|
||||
if (!(this instanceof __Facade_ScheduledEvent__)) {
|
||||
throw new TypeError("Illegal invocation");
|
||||
}
|
||||
// Need to call native method immediately in case uncaught error thrown
|
||||
this.#noRetry();
|
||||
}
|
||||
}
|
||||
|
||||
__facade__originalAddEventListener__("fetch", (event) => {
|
||||
const ctx: ExecutionContext = {
|
||||
waitUntil: event.waitUntil.bind(event),
|
||||
passThroughOnException: event.passThroughOnException.bind(event),
|
||||
};
|
||||
|
||||
const __facade_sw_dispatch__: Dispatcher = function (type, init) {
|
||||
if (type === "scheduled") {
|
||||
const facadeEvent = new __Facade_ScheduledEvent__("scheduled", {
|
||||
scheduledTime: Date.now(),
|
||||
cron: init.cron ?? "",
|
||||
noRetry() {},
|
||||
});
|
||||
|
||||
__FACADE_EVENT_TARGET__.dispatchEvent(facadeEvent);
|
||||
event.waitUntil(Promise.all(facadeEvent[__facade_waitUntil__]));
|
||||
}
|
||||
};
|
||||
|
||||
const __facade_sw_fetch__: Middleware = function (request, _env, ctx) {
|
||||
const facadeEvent = new __Facade_FetchEvent__("fetch", {
|
||||
request,
|
||||
passThroughOnException: ctx.passThroughOnException,
|
||||
});
|
||||
|
||||
__FACADE_EVENT_TARGET__.dispatchEvent(facadeEvent);
|
||||
facadeEvent[__facade_dispatched__] = true;
|
||||
event.waitUntil(Promise.all(facadeEvent[__facade_waitUntil__]));
|
||||
|
||||
const response = facadeEvent[__facade_response__];
|
||||
if (response === undefined) {
|
||||
throw new Error("No response!"); // TODO: proper error message
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
event.respondWith(
|
||||
__facade_invoke__(
|
||||
event.request as IncomingRequest,
|
||||
globalThis,
|
||||
ctx,
|
||||
__facade_sw_dispatch__,
|
||||
__facade_sw_fetch__
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
__facade__originalAddEventListener__("scheduled", (event) => {
|
||||
const facadeEvent = new __Facade_ScheduledEvent__("scheduled", {
|
||||
scheduledTime: event.scheduledTime,
|
||||
cron: event.cron,
|
||||
noRetry: event.noRetry.bind(event),
|
||||
});
|
||||
|
||||
__FACADE_EVENT_TARGET__.dispatchEvent(facadeEvent);
|
||||
event.waitUntil(Promise.all(facadeEvent[__facade_waitUntil__]));
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import type { Middleware } from "./common";
|
||||
|
||||
const drainBody: Middleware = async (request, env, _ctx, middlewareCtx) => {
|
||||
try {
|
||||
return await middlewareCtx.next(request, env);
|
||||
} finally {
|
||||
try {
|
||||
if (request.body !== null && !request.bodyUsed) {
|
||||
const reader = request.body.getReader();
|
||||
while (!(await reader.read()).done) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to drain the unused request body.", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default drainBody;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import type { Middleware } from "./common";
|
||||
|
||||
interface JsonError {
|
||||
message?: string;
|
||||
name?: string;
|
||||
stack?: string;
|
||||
cause?: JsonError;
|
||||
}
|
||||
|
||||
function reduceError(e: any): JsonError {
|
||||
return {
|
||||
name: e?.name,
|
||||
message: e?.message ?? String(e),
|
||||
stack: e?.stack,
|
||||
cause: e?.cause === undefined ? undefined : reduceError(e.cause),
|
||||
};
|
||||
}
|
||||
|
||||
// See comment in `bundle.ts` for details on why this is needed
|
||||
const jsonError: Middleware = async (request, env, _ctx, middlewareCtx) => {
|
||||
try {
|
||||
return await middlewareCtx.next(request, env);
|
||||
} catch (e: any) {
|
||||
const error = reduceError(e);
|
||||
return Response.json(error, {
|
||||
status: 500,
|
||||
headers: { "MF-Experimental-Error-Stack": "true" },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default jsonError;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare module "config:middleware/patch-console-prefix" {
|
||||
export const prefix: string;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/// <reference path="middleware-patch-console-prefix.d.ts"/>
|
||||
|
||||
import { prefix } from "config:middleware/patch-console-prefix";
|
||||
import type { Middleware } from "./common";
|
||||
|
||||
// Directly patch console methods to add worker prefix.
|
||||
// We capture the original method once and replace with a wrapper.
|
||||
// This approach allows third-party code to wrap console methods
|
||||
(["log", "debug", "info"] as const).forEach((method) => {
|
||||
globalThis.console[method] = new Proxy(globalThis.console[method], {
|
||||
apply(target, thisArg, argumentsList) {
|
||||
return target.apply(thisArg, [prefix, ...argumentsList]);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const passthrough: Middleware = (request, env, _ctx, middlewareCtx) => {
|
||||
return middlewareCtx.next(request, env);
|
||||
};
|
||||
|
||||
export default passthrough;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import type { Middleware } from "./common";
|
||||
|
||||
// A middleware has to be a function of type Middleware
|
||||
const prettyError: Middleware = async (request, env, _ctx, middlewareCtx) => {
|
||||
try {
|
||||
const response = await middlewareCtx.next(request, env);
|
||||
return response;
|
||||
} catch (e: any) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 🚨</title>
|
||||
<style>
|
||||
pre {
|
||||
margin: 16px auto;
|
||||
max-width: 600px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre>${e.stack}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return new Response(html, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "text/html;charset=utf-8" },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default prettyError;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import type { Middleware } from "./common";
|
||||
|
||||
// A middleware has to be a function of type Middleware
|
||||
const scheduled: Middleware = async (request, env, _ctx, middlewareCtx) => {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === "/__scheduled") {
|
||||
const cron = url.searchParams.get("cron") ?? "";
|
||||
await middlewareCtx.dispatch("scheduled", { cron });
|
||||
|
||||
return new Response("Ran scheduled event");
|
||||
}
|
||||
|
||||
const resp = await middlewareCtx.next(request, env);
|
||||
|
||||
// If you open the `/__scheduled` page in a browser, the browser will automatically make a request to `/favicon.ico`.
|
||||
// For scheduled Workers _without_ a fetch handler, this will result in a 500 response that clutters the log with unhelpful error messages.
|
||||
// To avoid this, inject a 404 response to favicon.ico loads on the `/__scheduled` page
|
||||
if (
|
||||
request.headers.get("referer")?.endsWith("/__scheduled") &&
|
||||
url.pathname === "/favicon.ico" &&
|
||||
resp.status === 500
|
||||
) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
export default scheduled;
|
||||
Reference in New Issue
Block a user