From 355822f61857cfe21330aec4869f4bed6ede2ff2 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Tue, 29 Jul 2025 00:18:32 +0530 Subject: [PATCH 1/4] chore: remove redundant initialize code --- packages/partyserver/src/index.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/partyserver/src/index.ts b/packages/partyserver/src/index.ts index b82313b..9a17c49 100644 --- a/packages/partyserver/src/index.ts +++ b/packages/partyserver/src/index.ts @@ -483,12 +483,6 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam await this.setName(connection.server); // TODO: ^ this shouldn't be async - if (this.#status !== "started") { - // This means the server "woke up" after hibernation - // so we need to hydrate it again - await this.#initialize(); - } - return this.onMessage(connection, message); } @@ -508,11 +502,6 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam await this.setName(connection.server); // TODO: ^ this shouldn't be async - if (this.#status !== "started") { - // This means the server "woke up" after hibernation - // so we need to hydrate it again - await this.#initialize(); - } return this.onClose(connection, code, reason, wasClean); } @@ -527,11 +516,6 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam await this.setName(connection.server); // TODO: ^ this shouldn't be async - if (this.#status !== "started") { - // This means the server "woke up" after hibernation - // so we need to hydrate it again - await this.#initialize(); - } return this.onError(connection, error); } @@ -612,9 +596,7 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam this.#_name = name; if (this.#status !== "started") { - await this.ctx.blockConcurrencyWhile(async () => { - await this.#initialize(); - }); + await this.#initialize(); } } From 83943a982da3f0e9d6be37d0b7fa4d6d94e29446 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Tue, 29 Jul 2025 00:22:59 +0530 Subject: [PATCH 2/4] add changeset --- .changeset/evil-carrots-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/evil-carrots-grab.md diff --git a/.changeset/evil-carrots-grab.md b/.changeset/evil-carrots-grab.md new file mode 100644 index 0000000..684882f --- /dev/null +++ b/.changeset/evil-carrots-grab.md @@ -0,0 +1,5 @@ +--- +"partyserver": patch +--- + +remove redundant initialize code as setName takes care of it, along with the nested blockConcurrencyWhile call From 8577320f07464bd1a92b35e2484253df5647de1e Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Mon, 9 Feb 2026 03:00:21 +0000 Subject: [PATCH 3/4] Add hibernating onStart server tests and impl Add tests for a hibernating server that verify setName/onStart initialization behavior: onStart runs before processing connections, runs only once with concurrent connections/requests, and that websocket messages work after initialization. Implement HibernatingOnStartServer (hibernate: true) with a delayed onStart that increments a counter, plus onConnect/onMessage/onRequest handlers that expose the counter. Update tests/worker imports and wrangler.toml to register the new Durable Object binding and include it in migrations. --- packages/partyserver/src/tests/index.test.ts | 100 ++++++++++++++++++ packages/partyserver/src/tests/worker.ts | 38 ++++++- packages/partyserver/src/tests/wrangler.jsonc | 8 ++ 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/packages/partyserver/src/tests/index.test.ts b/packages/partyserver/src/tests/index.test.ts index 127ba26..903ec5b 100644 --- a/packages/partyserver/src/tests/index.test.ts +++ b/packages/partyserver/src/tests/index.test.ts @@ -301,6 +301,106 @@ describe("Server", () => { // describe("in-memory"); }); +describe("Hibernating Server (setName handles initialization)", () => { + it("calls onStart before processing connections", async () => { + const ctx = createExecutionContext(); + const request = new Request( + "http://example.com/parties/hibernating-on-start-server/h-test1", + { + headers: { Upgrade: "websocket" } + } + ); + const response = await worker.fetch(request, env, ctx); + const ws = response.webSocket!; + ws.accept(); + + const { promise, resolve, reject } = Promise.withResolvers(); + ws.addEventListener("message", (message) => { + try { + // counter should be 1 because onStart completed before onConnect + expect(message.data).toEqual("1"); + resolve(); + } catch (e) { + reject(e); + } finally { + ws.close(); + } + }); + + return promise; + }); + + it("calls onStart only once with concurrent connections and requests", async () => { + const ctx = createExecutionContext(); + + async function makeConnection() { + const request = new Request( + "http://example.com/parties/hibernating-on-start-server/h-test2", + { + headers: { Upgrade: "websocket" } + } + ); + const response = await worker.fetch(request, env, ctx); + const ws = response.webSocket!; + ws.accept(); + const { promise, resolve, reject } = Promise.withResolvers(); + ws.addEventListener("message", (message) => { + try { + expect(message.data).toEqual("1"); + resolve(); + } catch (e) { + reject(e); + } finally { + ws.close(); + } + }); + return promise; + } + + async function makeRequest() { + const request = new Request( + "http://example.com/parties/hibernating-on-start-server/h-test2" + ); + const response = await worker.fetch(request, env, ctx); + expect(await response.text()).toEqual("1"); + } + + await Promise.all([makeConnection(), makeConnection(), makeRequest()]); + }); + + it("handles websocket messages after initialization", async () => { + const ctx = createExecutionContext(); + const request = new Request( + "http://example.com/parties/hibernating-on-start-server/h-test3", + { + headers: { Upgrade: "websocket" } + } + ); + const response = await worker.fetch(request, env, ctx); + const ws = response.webSocket!; + ws.accept(); + + // Wait for the onConnect message + const connectMessage = await new Promise((resolve) => { + ws.addEventListener("message", (e) => resolve(e.data as string), { + once: true + }); + }); + expect(connectMessage).toEqual("1"); + + // Send a message and verify the server is still initialized + ws.send("hello"); + const echoMessage = await new Promise((resolve) => { + ws.addEventListener("message", (e) => resolve(e.data as string), { + once: true + }); + }); + expect(echoMessage).toEqual("counter:1"); + + ws.close(); + }); +}); + describe("CORS", () => { it("returns CORS headers on OPTIONS preflight for matched routes", async () => { const ctx = createExecutionContext(); diff --git a/packages/partyserver/src/tests/worker.ts b/packages/partyserver/src/tests/worker.ts index a84f2c3..5d27b93 100644 --- a/packages/partyserver/src/tests/worker.ts +++ b/packages/partyserver/src/tests/worker.ts @@ -1,6 +1,6 @@ import { routePartykitRequest, Server } from "../index"; -import type { Connection, ConnectionContext } from "../index"; +import type { Connection, ConnectionContext, WSMessage } from "../index"; function assert(condition: unknown, message: string): asserts condition { if (!condition) { @@ -11,6 +11,7 @@ function assert(condition: unknown, message: string): asserts condition { export type Env = { Stateful: DurableObjectNamespace; OnStartServer: DurableObjectNamespace; + HibernatingOnStartServer: DurableObjectNamespace; Mixed: DurableObjectNamespace; ConfigurableState: DurableObjectNamespace; ConfigurableStateInMemory: DurableObjectNamespace; @@ -67,6 +68,41 @@ export class OnStartServer extends Server { } } +/** + * Like OnStartServer but with hibernate: true. + * Tests that setName properly initializes the server in the + * hibernating websocket handler path (webSocketMessage, webSocketClose, etc.) + */ +export class HibernatingOnStartServer extends Server { + static options = { + hibernate: true + }; + + counter = 0; + + async onStart() { + assert(this.name, "name is not available inside onStart"); + await new Promise((resolve) => { + setTimeout(() => { + this.counter++; + resolve(); + }, 300); + }); + } + + onConnect(connection: Connection) { + connection.send(this.counter.toString()); + } + + onMessage(connection: Connection, _message: WSMessage) { + connection.send(`counter:${this.counter}`); + } + + onRequest(): Response { + return new Response(this.counter.toString()); + } +} + export class Mixed extends Server { static options = { hibernate: true diff --git a/packages/partyserver/src/tests/wrangler.jsonc b/packages/partyserver/src/tests/wrangler.jsonc index d3ca216..c89347d 100644 --- a/packages/partyserver/src/tests/wrangler.jsonc +++ b/packages/partyserver/src/tests/wrangler.jsonc @@ -42,6 +42,10 @@ { "name": "CustomCorsServer", "class_name": "CustomCorsServer" + }, + { + "name": "HibernatingOnStartServer", + "class_name": "HibernatingOnStartServer" } ] }, @@ -61,6 +65,10 @@ { "tag": "v3", "new_classes": ["CorsServer", "CustomCorsServer"] + }, + { + "tag": "v4", + "new_classes": ["HibernatingOnStartServer"] } ] } From 5e685b7e63b4833522390699fa7d224b57f1d3a5 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Mon, 9 Feb 2026 03:08:09 +0000 Subject: [PATCH 4/4] Add AlarmServer durable object and test Introduce an AlarmServer Durable Object and test to verify that alarms initialize the DO and call onAlarm without redundant blockConcurrencyWhile. Changes: add AlarmServer class (hibernate:true) with counter and alarmCount, implement onStart/onAlarm/onRequest; export AlarmServer in the test Env; import runDurableObjectAlarm and add a test that schedules an alarm, triggers it via runDurableObjectAlarm, and asserts onStart and alarmCount behavior; register AlarmServer in wrangler.jsonc. This ensures alarm-triggered initialization works as expected. --- packages/partyserver/src/tests/index.test.ts | 40 ++++++++++++++++++- packages/partyserver/src/tests/worker.ts | 35 ++++++++++++++++ packages/partyserver/src/tests/wrangler.jsonc | 6 ++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/partyserver/src/tests/index.test.ts b/packages/partyserver/src/tests/index.test.ts index 903ec5b..358dff7 100644 --- a/packages/partyserver/src/tests/index.test.ts +++ b/packages/partyserver/src/tests/index.test.ts @@ -1,6 +1,7 @@ import { createExecutionContext, - env + env, + runDurableObjectAlarm // waitOnExecutionContext } from "cloudflare:test"; import { describe, expect, it } from "vitest"; @@ -401,6 +402,43 @@ describe("Hibernating Server (setName handles initialization)", () => { }); }); +describe("Alarm (initialize without redundant blockConcurrencyWhile)", () => { + it("properly initializes on alarm and calls onAlarm", async () => { + // Use a single stub for the entire test so runDurableObjectAlarm + // sees the same DO instance that has the alarm scheduled. + const id = env.AlarmServer.idFromName("alarm-test1"); + const stub = env.AlarmServer.get(id); + + // Initialize the DO and schedule an alarm in one request + const res = await stub.fetch( + new Request( + "http://example.com/parties/alarm-server/alarm-test1?setAlarm=1", + { + headers: { "x-partykit-room": "alarm-test1" } + } + ) + ); + expect(await res.text()).toEqual("alarm set"); + + // Trigger the alarm + const ran = await runDurableObjectAlarm(stub); + expect(ran).toBe(true); + + // Verify state: onStart called once, alarm was triggered once + const stateRes = await stub.fetch( + new Request("http://example.com/", { + headers: { "x-partykit-room": "alarm-test1" } + }) + ); + const state = (await stateRes.json()) as { + counter: number; + alarmCount: number; + }; + expect(state.counter).toEqual(1); + expect(state.alarmCount).toEqual(1); + }); +}); + describe("CORS", () => { it("returns CORS headers on OPTIONS preflight for matched routes", async () => { const ctx = createExecutionContext(); diff --git a/packages/partyserver/src/tests/worker.ts b/packages/partyserver/src/tests/worker.ts index 5d27b93..c9c9012 100644 --- a/packages/partyserver/src/tests/worker.ts +++ b/packages/partyserver/src/tests/worker.ts @@ -12,6 +12,7 @@ export type Env = { Stateful: DurableObjectNamespace; OnStartServer: DurableObjectNamespace; HibernatingOnStartServer: DurableObjectNamespace; + AlarmServer: DurableObjectNamespace; Mixed: DurableObjectNamespace; ConfigurableState: DurableObjectNamespace; ConfigurableStateInMemory: DurableObjectNamespace; @@ -103,6 +104,40 @@ export class HibernatingOnStartServer extends Server { } } +/** + * Tests that alarm() properly initializes the server + * without the redundant blockConcurrencyWhile wrapper. + */ +export class AlarmServer extends Server { + static options = { + hibernate: true + }; + + counter = 0; + alarmCount = 0; + + async onStart() { + this.counter++; + } + + onAlarm() { + this.alarmCount++; + } + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + if (url.searchParams.get("setAlarm")) { + // Schedule alarm far in the future so it won't auto-fire + await this.ctx.storage.setAlarm(Date.now() + 60_000); + return new Response("alarm set"); + } + return Response.json({ + counter: this.counter, + alarmCount: this.alarmCount + }); + } +} + export class Mixed extends Server { static options = { hibernate: true diff --git a/packages/partyserver/src/tests/wrangler.jsonc b/packages/partyserver/src/tests/wrangler.jsonc index c89347d..7376d1d 100644 --- a/packages/partyserver/src/tests/wrangler.jsonc +++ b/packages/partyserver/src/tests/wrangler.jsonc @@ -46,6 +46,10 @@ { "name": "HibernatingOnStartServer", "class_name": "HibernatingOnStartServer" + }, + { + "name": "AlarmServer", + "class_name": "AlarmServer" } ] }, @@ -68,7 +72,7 @@ }, { "tag": "v4", - "new_classes": ["HibernatingOnStartServer"] + "new_classes": ["HibernatingOnStartServer", "AlarmServer"] } ] }