diff --git a/packages/core/src/actor-configuration.test.ts b/packages/core/src/actor-configuration.test.ts new file mode 100644 index 0000000..5b10a97 --- /dev/null +++ b/packages/core/src/actor-configuration.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("cloudflare:workers", () => { + class DurableObject { + constructor(_state?: unknown, _env?: unknown) {} + } + + class WorkerEntrypoint {} + + return { + DurableObject, + WorkerEntrypoint, + env: {}, + }; +}); + +import { Actor, type ActorConfiguration } from "./index"; + +describe("Actor configuration overrides", () => { + it("respects custom upgrade paths defined by subclasses", async () => { + let upgradeCalls = 0; + + class CustomPathActor extends Actor> { + static override configuration(): ActorConfiguration { + return { + sockets: { + upgradePath: "/custom", + }, + }; + } + + protected override async shouldUpgradeWebSocket( + request: Request, + ): Promise { + return request.headers.get("Upgrade")?.toLowerCase() === "websocket"; + } + + protected override onWebSocketUpgrade(_request: Request): Response { + upgradeCalls += 1; + return new Response("upgraded", { status: 200 }); + } + + protected override onRequest(): Promise { + return Promise.resolve(new Response("fallback", { status: 418 })); + } + } + + const actor = new CustomPathActor(undefined, undefined); + (actor as Record)["_setNameCalled"] = true; + + const upgradeResponse = await actor.fetch( + new Request("https://example.com/custom/game", { + headers: { Upgrade: "websocket" }, + }), + ); + // Node/undici Response objects cannot emit 101, so we just ensure the response we returned flows through. + expect(upgradeResponse.status).toBe(200); + expect(upgradeCalls).toBe(1); + + const fallbackResponse = await actor.fetch( + new Request("https://example.com/ws/game"), + ); + expect(fallbackResponse.status).toBe(418); + expect(upgradeCalls).toBe(1); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 69bec8b..16b7010 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -107,10 +107,11 @@ export abstract class Actor extends DurableObject { /** * Static method to configure the actor. - * @param options - * @returns + * Subclasses can override this to customize upgrade paths or other options. + * @param request - Incoming request (optional for subclasses that need context) + * @returns Actor configuration values */ - static configuration = (request: Request): ActorConfiguration => { + static configuration(request?: Request): ActorConfiguration { return { locationHint: undefined, sockets: { @@ -237,7 +238,8 @@ export abstract class Actor extends DurableObject { async fetch(request: Request): Promise { // If the request route is `/ws` then we should upgrade the connection to a WebSocket // Get configuration from the static property - const config = (this.constructor as typeof Actor).configuration(request); + const ActorClass = this.constructor as typeof Actor; + const config = ActorClass.configuration(request); // Parse the URL to check if the path component matches the upgradePath const url = new URL(request.url); @@ -562,7 +564,8 @@ export function getActor>( ): DurableObjectStub { const className = ActorClass.name; const envObj = env as unknown as Record; - const locationHint = (ActorClass as any).configuration().locationHint; + const actorConfig = (ActorClass as unknown as typeof Actor).configuration(); + const locationHint = actorConfig.locationHint; const bindingName = Object.keys(envObj).find(key => { const binding = (env as any).__DURABLE_OBJECT_BINDINGS?.[key];