Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/core/src/actor-configuration.test.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, never>> {
static override configuration(): ActorConfiguration {
return {
sockets: {
upgradePath: "/custom",
},
};
}

protected override async shouldUpgradeWebSocket(
request: Request,
): Promise<boolean> {
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<Response> {
return Promise.resolve(new Response("fallback", { status: 418 }));
}
}

const actor = new CustomPathActor(undefined, undefined);
(actor as Record<string, unknown>)["_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);
});
});
13 changes: 8 additions & 5 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ export abstract class Actor<E> extends DurableObject<E> {

/**
* 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: {
Expand Down Expand Up @@ -237,7 +238,8 @@ export abstract class Actor<E> extends DurableObject<E> {
async fetch(request: Request): Promise<Response> {
// 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);
Expand Down Expand Up @@ -562,7 +564,8 @@ export function getActor<T extends Actor<any>>(
): DurableObjectStub<T> {
const className = ActorClass.name;
const envObj = env as unknown as Record<string, DurableObjectNamespace>;
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];
Expand Down