Skip to content

Commit bd1d3fd

Browse files
committed
Persist room name to survive alarm-triggered cold starts
1 parent eef891a commit bd1d3fd

5 files changed

Lines changed: 84 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"partyserver": patch
3+
---
4+
5+
Persist room name to DO storage so `this.name` is available in `onAlarm()` after alarm-triggered cold starts. Previously, `this.name` would throw because it was only set via the `x-partykit-room` header in `fetch()`, which alarms don't go through.

packages/partyserver/src/index.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,19 @@ export class Server<
403403
Did you try connecting directly to this Durable Object? Try using getServerByName(namespace, id) instead.`);
404404
}
405405
await this.setName(room);
406-
} else if (this.#status !== "started") {
407-
// Name was set by a previous request but initialization failed.
408-
// Retry initialization so the server can recover from transient
409-
// onStart failures.
410-
await this.#initialize();
406+
} else {
407+
const room = request.headers.get("x-partykit-room");
408+
if (room && room !== this.#_name) {
409+
throw new Error(
410+
`Room name mismatch: this server is "${this.#_name}" but request has room "${room}"`
411+
);
412+
}
413+
if (this.#status !== "started") {
414+
// Name was set by a previous request but initialization failed.
415+
// Retry initialization so the server can recover from transient
416+
// onStart failures.
417+
await this.#initialize();
418+
}
411419
}
412420
const url = new URL(request.url);
413421

@@ -559,6 +567,11 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam
559567
async #initialize(): Promise<void> {
560568
let error: unknown;
561569
await this.ctx.blockConcurrencyWhile(async () => {
570+
if (!this.#_name) {
571+
const stored =
572+
await this.ctx.storage.get<string>("__partyserver_name");
573+
if (stored) this.#_name = stored;
574+
}
562575
this.#status = "starting";
563576
try {
564577
await this.onStart(this.#_props);
@@ -627,9 +640,6 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam
627640
return this.#_name;
628641
}
629642

630-
// We won't have an await inside this function
631-
// but it will be called remotely,
632-
// so we need to mark it as async
633643
async setName(name: string) {
634644
if (!name) {
635645
throw new Error("A name is required.");
@@ -640,6 +650,7 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam
640650
);
641651
}
642652
this.#_name = name;
653+
await this.ctx.storage.put("__partyserver_name", name);
643654

644655
if (this.#status !== "started") {
645656
await this.#initialize();

packages/partyserver/src/tests/index.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
22
createExecutionContext,
33
env,
4-
runDurableObjectAlarm
4+
runDurableObjectAlarm,
5+
runInDurableObject
56
// waitOnExecutionContext
67
} from "cloudflare:test";
78
import { describe, expect, it } from "vitest";
@@ -532,6 +533,42 @@ describe("Alarm (initialize without redundant blockConcurrencyWhile)", () => {
532533
});
533534
});
534535

536+
describe("Alarm cold start (name persisted to storage)", () => {
537+
it("setName persists the room name to storage", async () => {
538+
const id = env.AlarmNameServer.idFromName("persist-write-test");
539+
const stub = env.AlarmNameServer.get(id);
540+
541+
await stub.fetch(
542+
new Request("http://example.com/", {
543+
headers: { "x-partykit-room": "persist-write-test" }
544+
})
545+
);
546+
547+
const stored = await runInDurableObject(stub, async (_instance, state) => {
548+
return state.storage.get("__partyserver_name");
549+
});
550+
expect(stored).toEqual("persist-write-test");
551+
});
552+
553+
it("this.name is available in onAlarm after a cold start", async () => {
554+
const id = env.AlarmNameServer.idFromName("alarm-name-test");
555+
const stub = env.AlarmNameServer.get(id);
556+
557+
// Pre-seed storage directly — no fetch, so #_name is never set in memory.
558+
// This simulates a DO that was previously initialized (name persisted)
559+
// but has since been evicted and is now cold-starting via alarm.
560+
await runInDurableObject(stub, async (_instance, state) => {
561+
await state.storage.put("__partyserver_name", "alarm-name-test");
562+
await state.storage.setAlarm(Date.now() + 60_000);
563+
});
564+
565+
await runDurableObjectAlarm(stub);
566+
567+
const alarmName = await runInDurableObject(stub, (i) => i.alarmName);
568+
expect(alarmName).toEqual("alarm-name-test");
569+
});
570+
});
571+
535572
describe("CORS", () => {
536573
it("returns CORS headers on OPTIONS preflight for matched routes", async () => {
537574
const ctx = createExecutionContext();

packages/partyserver/src/tests/worker.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type Env = {
1313
OnStartServer: DurableObjectNamespace<OnStartServer>;
1414
HibernatingOnStartServer: DurableObjectNamespace<HibernatingOnStartServer>;
1515
AlarmServer: DurableObjectNamespace<AlarmServer>;
16+
AlarmNameServer: DurableObjectNamespace<AlarmNameServer>;
1617
Mixed: DurableObjectNamespace<Mixed>;
1718
ConfigurableState: DurableObjectNamespace<ConfigurableState>;
1819
ConfigurableStateInMemory: DurableObjectNamespace<ConfigurableStateInMemory>;
@@ -352,6 +353,22 @@ export class TagsServerInMemory extends Server {
352353
}
353354
}
354355

356+
/**
357+
* Tests that this.name is available in onAlarm after an alarm-triggered
358+
* cold start (no prior fetch).
359+
*/
360+
export class AlarmNameServer extends Server {
361+
static options = {
362+
hibernate: true
363+
};
364+
365+
alarmName: string | null = null;
366+
367+
onAlarm() {
368+
this.alarmName = this.name;
369+
}
370+
}
371+
355372
export class CorsServer extends Server {
356373
onRequest(): Response | Promise<Response> {
357374
return Response.json({ cors: true });

packages/partyserver/src/tests/wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
"name": "FailingOnStartServer",
5656
"class_name": "FailingOnStartServer"
5757
},
58+
{
59+
"name": "AlarmNameServer",
60+
"class_name": "AlarmNameServer"
61+
},
5862
{
5963
"name": "HibernatingNameInMessage",
6064
"class_name": "HibernatingNameInMessage"
@@ -84,6 +88,7 @@
8488
"HibernatingOnStartServer",
8589
"AlarmServer",
8690
"FailingOnStartServer",
91+
"AlarmNameServer",
8792
"HibernatingNameInMessage",
8893
"TagsServer",
8994
"TagsServerInMemory"

0 commit comments

Comments
 (0)