This commit is contained in:
2026-07-08 18:29:36 +08:00
parent ad51ced86c
commit 3af8ff9b80
1613 changed files with 701644 additions and 2 deletions
+67
View File
@@ -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
View File
@@ -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
View File
@@ -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__]));
});
@@ -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;
@@ -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;
@@ -0,0 +1,3 @@
declare module "config:middleware/patch-console-prefix" {
export const prefix: string;
}
@@ -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
View File
@@ -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
View File
@@ -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;