diff --git a/.changeset/fix-lobby-party-type.md b/.changeset/fix-lobby-party-type.md new file mode 100644 index 0000000..ac2a239 --- /dev/null +++ b/.changeset/fix-lobby-party-type.md @@ -0,0 +1,5 @@ +--- +"partyserver": patch +--- + +Add `lobby.className` to `onBeforeConnect`/`onBeforeRequest` callbacks, providing the Durable Object class name (e.g. `"MyAgent"`). The existing `lobby.party` field is now deprecated (it returns the kebab-case URL namespace) and will be changed to return the class name in the next major version. diff --git a/package-lock.json b/package-lock.json index b2ded74..4b623ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12235,7 +12235,7 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", "hono": "^4.11.1", - "partyserver": "^0.1.4" + "partyserver": "^0.1.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240729.0", @@ -12327,7 +12327,7 @@ "license": "ISC", "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", - "partyserver": "^0.1.4", + "partyserver": "^0.1.5", "partysocket": "^1.1.13" }, "peerDependencies": { @@ -12345,7 +12345,7 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", - "partyserver": "^0.1.4" + "partyserver": "^0.1.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240729.0", @@ -12367,7 +12367,7 @@ "license": "ISC", "dependencies": { "cron-parser": "^5.4.0", - "partyserver": "^0.1.4" + "partyserver": "^0.1.5" } }, "packages/y-partyserver": { @@ -12382,7 +12382,7 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", "@types/lodash.debounce": "^4.0.9", - "partyserver": "^0.1.4", + "partyserver": "^0.1.5", "ws": "^8.18.3", "yjs": "^13.6.28" }, diff --git a/packages/hono-party/package.json b/packages/hono-party/package.json index a3f2433..d901a8e 100644 --- a/packages/hono-party/package.json +++ b/packages/hono-party/package.json @@ -37,6 +37,6 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", "hono": "^4.11.1", - "partyserver": "^0.1.4" + "partyserver": "^0.1.5" } } diff --git a/packages/partyserver/src/index.ts b/packages/partyserver/src/index.ts index a7a910c..17e7411 100644 --- a/packages/partyserver/src/index.ts +++ b/packages/partyserver/src/index.ts @@ -31,6 +31,9 @@ const serverMapCache = new WeakMap< Record >(); +// Maps kebab-case namespace -> original env binding name (e.g. "my-agent" -> "MyAgent") +const bindingNameCache = new WeakMap>(); + /** * For a given server namespace, create a server with a name. */ @@ -87,8 +90,20 @@ function camelCaseToKebabCase(str: string): string { // Convert any remaining underscores to hyphens and remove trailing -'s return kebabified.replace(/_/g, "-").replace(/-$/, ""); } +export interface Lobby { + /** + * The kebab-case namespace from the URL path (e.g. `"my-agent"`). + * @deprecated Use `className` instead, which returns the Durable Object class name. + * In the next major version, `party` will return the class name instead of the kebab-case namespace. + */ + party: string; + /** The Durable Object class name / env binding name (e.g. `"MyAgent"`). */ + className: Extract; + /** The room / instance name extracted from the URL. */ + name: string; +} + export interface PartyServerOptions< - // biome-ignore lint/correctness/noUnusedVariables: it's ok, we'll remove this in the next major Env = Cloudflare.Env, Props = Record > { @@ -122,17 +137,11 @@ export interface PartyServerOptions< cors?: boolean | HeadersInit; onBeforeConnect?: ( req: Request, - lobby: { - party: string; - name: string; - } + lobby: Lobby ) => Response | Request | void | Promise; onBeforeRequest?: ( req: Request, - lobby: { - party: string; - name: string; - } + lobby: Lobby ) => | Response | Request @@ -175,26 +184,28 @@ export async function routePartykitRequest< options?: PartyServerOptions ): Promise { if (!serverMapCache.has(env)) { - serverMapCache.set( - env, - Object.entries(env).reduce((acc, [k, v]) => { - if ( - v && - typeof v === "object" && - "idFromName" in v && - typeof v.idFromName === "function" - ) { - Object.assign(acc, { [camelCaseToKebabCase(k)]: v }); - return acc; - } - return acc; - }, {}) - ); + const namespaceMap: Record = {}; + const bindingNames: Record = {}; + for (const [k, v] of Object.entries(env)) { + if ( + v && + typeof v === "object" && + "idFromName" in v && + typeof v.idFromName === "function" + ) { + const kebab = camelCaseToKebabCase(k); + namespaceMap[kebab] = v as DurableObjectNamespace; + bindingNames[kebab] = k; + } + } + serverMapCache.set(env, namespaceMap); + bindingNameCache.set(env, bindingNames); } const map = serverMapCache.get(env) as unknown as Record< string, DurableObjectNamespace >; + const bindingNames = bindingNameCache.get(env) as Record; const prefix = options?.prefix || "parties"; const prefixParts = prefix.split("/"); @@ -271,12 +282,27 @@ Did you forget to add a durable object binding to the class ${namespace[0].toUpp req.headers.set("x-partykit-props", JSON.stringify(options?.props)); } + const className = bindingNames[namespace] as Extract; + let partyDeprecationWarned = false; + const lobby: Lobby = { + get party() { + if (!partyDeprecationWarned) { + partyDeprecationWarned = true; + console.warn( + 'lobby.party is deprecated and currently returns the kebab-case namespace (e.g. "my-agent"). ' + + 'Use lobby.className instead to get the Durable Object class name (e.g. "MyAgent"). ' + + "In the next major version, lobby.party will return the class name." + ); + } + return namespace; + }, + className, + name + }; + if (isWebSocket) { if (options?.onBeforeConnect) { - const reqOrRes = await options.onBeforeConnect(req, { - party: namespace, - name - }); + const reqOrRes = await options.onBeforeConnect(req, lobby); if (reqOrRes instanceof Request) { req = reqOrRes; } else if (reqOrRes instanceof Response) { @@ -285,10 +311,7 @@ Did you forget to add a durable object binding to the class ${namespace[0].toUpp } } else { if (options?.onBeforeRequest) { - const reqOrRes = await options.onBeforeRequest(req, { - party: namespace, - name - }); + const reqOrRes = await options.onBeforeRequest(req, lobby); if (reqOrRes instanceof Request) { req = reqOrRes; } else if (reqOrRes instanceof Response) { diff --git a/packages/partyserver/src/tests/index.test.ts b/packages/partyserver/src/tests/index.test.ts index 1cf9a51..fd11183 100644 --- a/packages/partyserver/src/tests/index.test.ts +++ b/packages/partyserver/src/tests/index.test.ts @@ -150,6 +150,19 @@ describe("Server", () => { expect(response.headers.get("Location")).toBe("https://example3.com"); }); + it("provides className with the Durable Object class name", async () => { + const ctx = createExecutionContext(); + const request = new Request( + "http://example.com/parties/on-start-server/lobby-info" + ); + const response = await worker.fetch(request, env, ctx); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + className: "OnStartServer", + name: "lobby-info" + }); + }); + it("ignores foreign hibernated websockets when broadcasting", async () => { const ctx = createExecutionContext(); diff --git a/packages/partyserver/src/tests/worker.ts b/packages/partyserver/src/tests/worker.ts index 95b74c6..35b929f 100644 --- a/packages/partyserver/src/tests/worker.ts +++ b/packages/partyserver/src/tests/worker.ts @@ -353,8 +353,8 @@ export default { return ( (await routePartykitRequest(request, env, { - onBeforeConnect: async (_request, { party, name }) => { - if (party === "on-start-server") { + onBeforeConnect: async (_request, { className, name }) => { + if (className === "OnStartServer") { if (name === "is-error") { return new Response("Error", { status: 503 }); } else if (name === "is-redirect") { @@ -365,17 +365,23 @@ export default { } } }, - onBeforeRequest: async (_request, { party, name }) => { - if (party === "on-start-server") { - if (name === "is-error") { + onBeforeRequest: async (_request, lobby) => { + if (lobby.className === "OnStartServer") { + if (lobby.name === "is-error") { return new Response("Error", { status: 504 }); - } else if (name === "is-redirect") { + } else if (lobby.name === "is-redirect") { return new Response("Redirect", { status: 302, headers: { Location: "https://example3.com" } }); } } + if (lobby.name === "lobby-info") { + return Response.json({ + className: lobby.className, + name: lobby.name + }); + } } })) || new Response("Not Found", { status: 404 }) ); diff --git a/packages/partysub/package.json b/packages/partysub/package.json index 099d20c..833d52d 100644 --- a/packages/partysub/package.json +++ b/packages/partysub/package.json @@ -46,7 +46,7 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", - "partyserver": "^0.1.4", + "partyserver": "^0.1.5", "partysocket": "^1.1.13" } } diff --git a/packages/partysync/package.json b/packages/partysync/package.json index 2cf6edc..2a3b306 100644 --- a/packages/partysync/package.json +++ b/packages/partysync/package.json @@ -54,6 +54,6 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", - "partyserver": "^0.1.4" + "partyserver": "^0.1.5" } } diff --git a/packages/partywhen/package.json b/packages/partywhen/package.json index cae5ac7..3906256b 100644 --- a/packages/partywhen/package.json +++ b/packages/partywhen/package.json @@ -29,6 +29,6 @@ "description": "A library for scheduling and running tasks in Cloudflare Workers", "dependencies": { "cron-parser": "^5.4.0", - "partyserver": "^0.1.4" + "partyserver": "^0.1.5" } } diff --git a/packages/y-partyserver/package.json b/packages/y-partyserver/package.json index f775eb4..e83f5ad 100644 --- a/packages/y-partyserver/package.json +++ b/packages/y-partyserver/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20251218.0", "@types/lodash.debounce": "^4.0.9", - "partyserver": "^0.1.4", + "partyserver": "^0.1.5", "ws": "^8.18.3", "yjs": "^13.6.28" }