From 93888abc251213521b4d340fce161bb0ff347094 Mon Sep 17 00:00:00 2001 From: Sunil Pai <18808+threepointone@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:48:38 +0100 Subject: [PATCH] inherit server hibernation options --- .changeset/calm-servers-hibernate.md | 5 ++++ packages/partyserver/README.md | 2 ++ packages/partyserver/src/index.ts | 24 ++++++++++++++++++-- packages/partyserver/src/tests/index.test.ts | 22 ++++++++++++++++++ packages/partyserver/src/tests/worker.ts | 8 +++++-- 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 .changeset/calm-servers-hibernate.md diff --git a/.changeset/calm-servers-hibernate.md b/.changeset/calm-servers-hibernate.md new file mode 100644 index 00000000..4fc5b569 --- /dev/null +++ b/.changeset/calm-servers-hibernate.md @@ -0,0 +1,5 @@ +--- +"partyserver": patch +--- + +Inherit hibernation settings when a Server subclass declares partial static options. diff --git a/packages/partyserver/README.md b/packages/partyserver/README.md index 4209a9f3..b23a34a0 100644 --- a/packages/partyserver/README.md +++ b/packages/partyserver/README.md @@ -158,6 +158,8 @@ A connection is a standard WebSocket with the following additional properties: You can enable [hibernation](https://developers.cloudflare.com/durable-objects/reference/websockets/#websocket-hibernation) by setting a static `options` property on your Server class. This allows the server to hibernate when not in use and wake up when a new connection is established. All lifecycle hooks will be called as expected when the server wakes up. +Subclasses inherit the nearest explicitly configured `hibernate` value. Declaring partial static options on a child class does not disable hibernation inherited from its parent. + ```ts export class MyServer extends Server { static options = { diff --git a/packages/partyserver/src/index.ts b/packages/partyserver/src/index.ts index bec3dd0c..4b50b67e 100644 --- a/packages/partyserver/src/index.ts +++ b/packages/partyserver/src/index.ts @@ -586,6 +586,24 @@ Did you forget to add a durable object binding to the class ${namespace[0].toUpp } } +type ServerOptions = { hibernate?: boolean }; + +function resolveServerOptions(serverClass: { + options?: ServerOptions; +}): Required { + let current: { options?: ServerOptions } | null = serverClass; + while (current) { + const hibernate = current.options?.hibernate; + if (hibernate !== undefined) { + return { hibernate }; + } + current = Object.getPrototypeOf(current) as { + options?: ServerOptions; + } | null; + } + return { hibernate: false }; +} + export class Server< Env extends Cloudflare.Env = Cloudflare.Env, Props extends Record = Record @@ -598,7 +616,9 @@ export class Server< #ParentClass: typeof Server = Object.getPrototypeOf(this).constructor; - #connectionManager: ConnectionManager = this.#ParentClass.options.hibernate + #options = resolveServerOptions(this.#ParentClass); + + #connectionManager: ConnectionManager = this.#options.hibernate ? new HibernatingConnectionManager(this.ctx) : new InMemoryConnectionManager(); @@ -709,7 +729,7 @@ export class Server< // Accept the websocket connection connection = this.#connectionManager.accept(connection, { tags }); - if (!this.#ParentClass.options.hibernate) { + if (!this.#options.hibernate) { this.#attachSocketEventHandlers(connection); } await this.onConnect(connection, ctx); diff --git a/packages/partyserver/src/tests/index.test.ts b/packages/partyserver/src/tests/index.test.ts index b4a9159c..1547ee15 100644 --- a/packages/partyserver/src/tests/index.test.ts +++ b/packages/partyserver/src/tests/index.test.ts @@ -1,5 +1,6 @@ import { createExecutionContext, + runInDurableObject, runDurableObjectAlarm // waitOnExecutionContext } from "cloudflare:test"; @@ -68,6 +69,27 @@ describe("Server", () => { return promise; }); + it("inherits hibernation when a child declares partial static options", async () => { + const name = crypto.randomUUID(); + const response = await worker.fetch( + new Request(`http://example.com/parties/stateful/${name}`, { + headers: { Upgrade: "websocket" } + }), + env, + createExecutionContext() + ); + const websocket = response.webSocket!; + websocket.accept(); + + const acceptedWebSockets = await runInDurableObject( + env.Stateful.getByName(name), + (_instance, ctx) => ctx.getWebSockets().length + ); + + expect(acceptedWebSockets).toBe(1); + websocket.close(); + }); + it("calls onStart only once, and does not process messages or requests until it is resolved", async () => { const ctx = createExecutionContext(); diff --git a/packages/partyserver/src/tests/worker.ts b/packages/partyserver/src/tests/worker.ts index a2e44f4a..e9d79836 100644 --- a/packages/partyserver/src/tests/worker.ts +++ b/packages/partyserver/src/tests/worker.ts @@ -60,10 +60,14 @@ export class BinaryTypeProbe extends Server { } } -export class Stateful extends Server { - static options = { +class HibernatingServer extends Server { + static options: typeof Server.options = { hibernate: true }; +} + +export class Stateful extends HibernatingServer { + static options = {}; onConnect( connection: Connection,