Skip to content

Commit fe030f6

Browse files
committed
setName() persists __ps_name for non-idFromName DOs
For DOs where `ctx.id.name` is undefined (Cloudflare Agents facets and similar framework-level bootstrap patterns), `setName(name)` now writes the name to the legacy `__ps_name` storage key in addition to stashing it in memory. This makes `setName()` the sanctioned bootstrap API: frameworks no longer need to write `__ps_name` directly themselves — PartyServer manages its own storage layout. For DOs addressed via idFromName/getByName, behavior is unchanged: ctx.id.name carries the name, no storage write happens. Migration for framework authors who currently do: await this.ctx.storage.put("__ps_name", name); await this.__unsafe_ensureInitialized(); becomes: await this.setName(name); Backward compatible — the existing direct-storage pattern keeps working since the storage write becomes idempotent. Adds SetNameBootstrapServer + 3 tests covering: 1. setName() persists __ps_name for newUniqueId DOs 2. Cold-wake fetch recovers the name via the storage fallback 3. setName() does NOT write storage when ctx.id.name is set (regression guard for the 0.5.0 zero-storage-write win on the happy path) Made-with: Cursor
1 parent de99473 commit fe030f6

5 files changed

Lines changed: 143 additions & 14 deletions

File tree

.changeset/setname-bootstrap.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"partyserver": patch
3+
---
4+
5+
`setName()` is now the sanctioned bootstrap API for non-`idFromName` DOs.
6+
7+
When `ctx.id.name` is undefined (e.g. Cloudflare Agents facets, which are spawned via `ctx.facets.get(...)` rather than `idFromName()`), `setName(name)` now persists the name to the legacy `__ps_name` storage key in addition to stashing it in memory. This means cold-wake invocations of the DO recover the name through `#ensureInitialized()`'s legacy storage fallback without the framework having to reach into PartyServer's private storage layout.
8+
9+
Frameworks that previously did:
10+
11+
```ts
12+
await this.ctx.storage.put("__ps_name", name);
13+
await this.__unsafe_ensureInitialized();
14+
```
15+
16+
can now do:
17+
18+
```ts
19+
await this.setName(name);
20+
```
21+
22+
Backward compatible:
23+
24+
- For DOs addressed via `idFromName()` / `getByName()` (the happy path), `setName()` continues to NOT write storage — `ctx.id.name` is the source of truth and `setName()` is just a no-op-plus-onStart.
25+
- The pre-existing direct-storage-write pattern keeps working — the storage write becomes idempotent with what `setName()` would do.
26+
27+
`setName()`'s `@deprecated` docblock has been clarified: it remains the supported API for framework-level bootstrap of non-`idFromName` DOs and for delivering initial `props` to `onStart()`. The deprecation only applies to redundant calls on DOs that were addressed via `idFromName()`.

packages/partyserver/src/index.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -670,19 +670,27 @@ export class Server<
670670
}
671671

672672
/**
673-
* @deprecated for callers that address DOs via `idFromName()` /
674-
* `getByName()` — `this.name` is available automatically from
675-
* `ctx.id.name` and calling `setName()` is redundant.
673+
* Establish this server's name and trigger `onStart()`.
674+
*
675+
* Two distinct use cases:
676676
*
677-
* Still appropriate for two use cases:
678-
* 1. Delivering initial `props` to `onStart()` (via the optional
679-
* second argument).
680-
* 2. Framework-level bootstrap of non-`idFromName` DOs where
677+
* 1. **Framework-level bootstrap of non-`idFromName` DOs** where
681678
* `ctx.id.name` is undefined — for example, Cloudflare Agents
682-
* facets. Calling `setName(name)` from inside such a DO stashes
683-
* the name in memory for the current instance; for
684-
* survival-across-eviction, write `__ps_name` to storage as
685-
* well.
679+
* facets (spawned via `ctx.facets.get(...)`). `setName()` is the
680+
* sanctioned bootstrap primitive: it stashes the name in memory
681+
* AND persists it to storage (under `__ps_name`) so the name
682+
* survives DO eviction and is recovered on cold wake by
683+
* `#ensureInitialized()`.
684+
* 2. **Delivering initial `props` to `onStart()`** via the optional
685+
* second argument.
686+
*
687+
* For DOs addressed via `idFromName()` / `getByName()`, calling
688+
* `setName()` is redundant — `this.name` is available automatically
689+
* from `ctx.id.name`. Throws if `name` does not match `ctx.id.name`.
690+
*
691+
* @deprecated for callers that address DOs via `idFromName()` /
692+
* `getByName()`. Still the supported API for framework-level
693+
* bootstrap and props delivery.
686694
*/
687695
async setName(name: string, props?: Props) {
688696
if (!name) {
@@ -703,10 +711,14 @@ export class Server<
703711
this.#_props = props;
704712
}
705713
if (!this.#_name && ctxName === undefined) {
706-
// Legacy path only (DO was addressed without idFromName). Stash the
707-
// name in memory so subsequent handlers can read `this.name`.
708-
// No storage write: PartyServer no longer persists the name itself.
714+
// Bootstrap path (DO was addressed without idFromName, e.g.
715+
// Cloudflare Agents facets). Stash the name in memory AND
716+
// persist to storage so that subsequent cold-wake invocations
717+
// (fetch, alarm, websocket handlers, RPC via
718+
// `__unsafe_ensureInitialized`) can recover the name through
719+
// `#ensureInitialized()`'s legacy fallback.
709720
this.#_name = name;
721+
await this.ctx.storage.put(NAME_STORAGE_KEY, name);
710722
}
711723
await this.#ensureInitialized();
712724
}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,49 @@ describe("x-partykit-room header applied before onStart", () => {
722722
});
723723
});
724724

725+
describe("setName() as bootstrap API for non-idFromName DOs", () => {
726+
it("persists __ps_name to storage so the name survives eviction", async () => {
727+
const id = env.SetNameBootstrapServer.newUniqueId();
728+
const stub = env.SetNameBootstrapServer.get(id);
729+
730+
const result = await stub.bootstrap("setname-bootstrap-test");
731+
expect(result.onStartName).toBe("setname-bootstrap-test");
732+
733+
// Verify setName() persisted to storage. Frameworks no longer need
734+
// to write __ps_name themselves — `setName()` covers it.
735+
const stored = await stub.readStoredName();
736+
expect(stored).toBe("setname-bootstrap-test");
737+
});
738+
739+
it("recovers the name on cold-wake fetch via the storage fallback", async () => {
740+
const id = env.SetNameBootstrapServer.newUniqueId();
741+
const stub = env.SetNameBootstrapServer.get(id);
742+
await stub.bootstrap("setname-coldwake-test");
743+
744+
// Subsequent fetch with no header — must hydrate from storage.
745+
const res = await stub.fetch(new Request("http://example.com/"));
746+
expect(res.status).toBe(200);
747+
const data = (await res.json()) as { name: string };
748+
expect(data.name).toBe("setname-coldwake-test");
749+
});
750+
751+
it("does NOT write storage when the DO was addressed via idFromName", async () => {
752+
// For normal idFromName DOs, ctx.id.name carries the name and
753+
// setName() is redundant. It must not write `__ps_name` to
754+
// storage in this path — that would re-introduce the per-setName
755+
// storage write that 0.5.0 eliminated.
756+
const id = env.SetNameBootstrapServer.idFromName("setname-no-write");
757+
const stub = env.SetNameBootstrapServer.get(id);
758+
759+
// Calling setName with the matching name is a no-op on storage;
760+
// it just runs onStart.
761+
await stub.setName("setname-no-write");
762+
763+
const stored = await stub.readStoredName();
764+
expect(stored).toBeUndefined();
765+
});
766+
});
767+
725768
describe("Framework bootstrap fallback (Agents facets etc.)", () => {
726769
it("hydrates this.name from __ps_name storage when ctx.id.name is undefined", async () => {
727770
// Regression guard for Cloudflare Agents facets. Facets are spawned

packages/partyserver/src/tests/worker.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Env = {
1616
AlarmNameServer: DurableObjectNamespace<AlarmNameServer>;
1717
NoNameServer: DurableObjectNamespace<NoNameServer>;
1818
HeaderOnlyOnStartServer: DurableObjectNamespace<HeaderOnlyOnStartServer>;
19+
SetNameBootstrapServer: DurableObjectNamespace<SetNameBootstrapServer>;
1920
FacetLikeBootstrapServer: DurableObjectNamespace<FacetLikeBootstrapServer>;
2021
NameInConstructorServer: DurableObjectNamespace<NameInConstructorServer>;
2122
Mixed: DurableObjectNamespace<Mixed>;
@@ -281,6 +282,47 @@ export class HeaderOnlyOnStartServer extends Server {
281282
}
282283
}
283284

285+
/**
286+
* Same scenario as `FacetLikeBootstrapServer`, but uses the sanctioned
287+
* `setName()` bootstrap API instead of writing `__ps_name` directly to
288+
* storage. Verifies that `setName()` alone is sufficient: it stashes
289+
* `#_name` in memory AND persists it to storage so cold-wake fetches
290+
* recover the name through `#ensureInitialized()`'s legacy fallback.
291+
*/
292+
export class SetNameBootstrapServer extends Server {
293+
static options = { hibernate: true };
294+
295+
onStartName: string | null = null;
296+
297+
async onStart() {
298+
try {
299+
this.onStartName = this.name;
300+
} catch {
301+
this.onStartName = null;
302+
}
303+
}
304+
305+
async bootstrap(name: string): Promise<{ onStartName: string | null }> {
306+
await this.setName(name);
307+
return { onStartName: this.onStartName };
308+
}
309+
310+
/**
311+
* Probe storage from outside the DO to verify `setName()` persisted
312+
* the name under the legacy `__ps_name` key.
313+
*/
314+
async readStoredName(): Promise<string | undefined> {
315+
return this.ctx.storage.get<string>("__ps_name");
316+
}
317+
318+
onRequest(): Response {
319+
return Response.json({
320+
name: this.name,
321+
onStartName: this.onStartName
322+
});
323+
}
324+
}
325+
284326
/**
285327
* Simulates a framework-level bootstrap pattern (e.g. Cloudflare Agents
286328
* facets) where the DO is NOT addressed via `idFromName()` — so

packages/partyserver/src/tests/wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
"name": "HeaderOnlyOnStartServer",
9292
"class_name": "HeaderOnlyOnStartServer"
9393
},
94+
{
95+
"name": "SetNameBootstrapServer",
96+
"class_name": "SetNameBootstrapServer"
97+
},
9498
{
9599
"name": "FacetLikeBootstrapServer",
96100
"class_name": "FacetLikeBootstrapServer"
@@ -125,6 +129,7 @@
125129
"UriServerInMemory",
126130
"PropsServer",
127131
"HeaderOnlyOnStartServer",
132+
"SetNameBootstrapServer",
128133
"FacetLikeBootstrapServer",
129134
"NameInConstructorServer"
130135
]

0 commit comments

Comments
 (0)