|
9 | 9 | On compatibility dates `>= 2026-04-07` the `web_socket_auto_reply_to_close` flag makes the runtime send a reciprocal Close frame and tear the socket down automatically. For a non-hibernating PartyServer (`hibernate: false`), the Durable Object sits on the server end of a connection that the runtime tunnels back to the client, so that auto-teardown could fire through an already-severed tunnel — surfacing as a spurious retryable `Network connection lost.` rejection (for example when a Durable Object is reset while a connection is still open). Half-open mode keeps PartyServer's existing close handling in control; it already reciprocates the peer's Close frame on every compatibility date, so client behavior is unchanged. |
10 | 10 |
|
11 | 11 | Also in this release, two related WebSocket fixes that keep behavior consistent across all compatibility dates: |
12 | | - |
13 | 12 | - **Pin `binaryType` to `"arraybuffer"` for non-hibernating connections.** On compatibility dates `>= 2026-03-17` the `websocket_standard_binary_type` flag flips the default server-side `binaryType` from `"arraybuffer"` to `"blob"`, so binary frames arrived as `Blob` instead of `ArrayBuffer` on the in-memory path. PartyServer (and frameworks built on it, e.g. Cloudflare Agents) have always received `ArrayBuffer`, so it is now pinned back in `accept()`. This is a no-op on older dates and corrective on newer ones; the Hibernation API is unaffected (it always delivers `ArrayBuffer`). |
14 | 13 | - **Stop reporting transport-teardown errors as `onError`.** A retryable `Network connection lost.` / `WebSocket peer disconnected` error that fires on an already closing/closed connection is the socket going away during the close handshake, not an application error. It is now suppressed when the connection is `CLOSING`/`CLOSED` (detected via the structured `retryable` flag, with a message fallback), so it no longer spams logs on abrupt client disconnects. Genuine mid-connection (`OPEN`) errors still reach `onError`. |
15 | 14 |
|
|
44 | 43 | The fix is at the call site, not in PartyServer: pass `id: someBoundDONamespace.idFromName(facetName)` to `ctx.facets.get(...)`. The facet then gets its own native `ctx.id.name === facetName` and PartyServer's `name` getter does the right thing automatically. No `setName()` is required, no `__ps_name` storage record is written, and cold-wake recovery happens for free because the factory re-runs and `idFromName` is deterministic. |
45 | 44 |
|
46 | 45 | This release adds: |
47 | | - |
48 | 46 | - **A "Using PartyServer with Durable Object Facets" section in the README** that walks through the recommended pattern with a code example, calls out the implicit-id footgun explicitly, and documents that plain-string `id` values are not a substitute for `idFromName(facetName)` (workerd treats string ids as `idFromString`-like, so the resulting facet has no `ctx.id.name`). |
49 | 47 | - **`setName()` docstring updated** to clarify that facets are NOT a `setName()` use case — point to the explicit-`id` pattern instead. The original `setName()` `ctx.id.name` mismatch throw is preserved as a typo guard for the `idFromName` happy path. |
50 | 48 | - **End-to-end facet test coverage** against the real workerd `ctx.facets.get(...)` API. A `FacetParent` / `FacetChild` fixture exercises both the implicit-id path (pinning the runtime contract that `this.name` returns the parent's name in that flow — i.e., behavior-as-documentation so framework authors are unsurprised) and the explicit-id path (recommended; verifies that all reasonable id-construction strategies work and that cold wake recovers without any storage record). Plain-string `id` is also tested; the test asserts it does NOT carry a name, pinning the contract so callers don't get tempted by the type signature. |
|
73 | 71 | ``` |
74 | 72 |
|
75 | 73 | Backward compatible: |
76 | | - |
77 | 74 | - 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. |
78 | 75 | - The pre-existing direct-storage-write pattern keeps working — the storage write becomes idempotent with what `setName()` would do. |
79 | 76 |
|
|
88 | 85 | 0.5.0 moved the legacy storage hydrate into `alarm()` only, breaking Cloudflare Agents facets and any other framework that writes `__ps_name` directly before calling `__unsafe_ensureInitialized()`. Facet DOs are spawned via `ctx.facets.get(...)` rather than `idFromName()` and therefore have `ctx.id.name === undefined`; they relied on PartyServer reading the storage record back to populate `this.name` before `onStart()`. |
89 | 86 |
|
90 | 87 | Changes: |
91 | | - |
92 | 88 | - Move the legacy `__ps_name` hydrate from `alarm()` into `#ensureInitialized()`, still gated on `!ctx.id.name && !#_name` so it costs nothing on the happy path (normal `idFromName()`/`getByName()` DOs skip the storage read entirely). |
93 | 89 | - `Server.fetch()` now delegates to `#ensureInitialized()` for the hydrate instead of doing its own. The `x-partykit-room` header fallback remains as a last resort when neither `ctx.id.name` nor a legacy storage record is available. |
94 | 90 | - `Server.alarm()` is simplified — it no longer needs its own hydrate call since `#ensureInitialized()` handles it. |
|
103 | 99 | Durable Objects now expose `ctx.id.name` on every entry point (constructor, fetch, alarm, hibernating websocket handlers) when the DO is addressed via `idFromName()`/`getByName()`. PartyServer now uses this as the primary source of `this.name`, which simplifies routing, eliminates storage writes, and makes `this.name` available inside the constructor. |
104 | 100 |
|
105 | 101 | Changes in `partyserver`: |
106 | | - |
107 | 102 | - `this.name` resolves from `this.ctx.id.name`. The apologetic `workerd#2240` error message is gone. |
108 | 103 | - `this.name` is now available **inside the constructor** and from class field initializers, not just after `setName()`/`fetch()` has run. |
109 | 104 | - `routePartykitRequest` no longer issues a `setName()`/`_initAndFetch()` RPC before `fetch()`. The WebSocket path goes from 2 RPCs to 1; the HTTP path remains 1 RPC. Props, when supplied, are delivered to the DO via the `x-partykit-props` request header, set after `onBeforeConnect`/`onBeforeRequest` hooks run. |
|
115 | 110 | - When reading `this.name` throws, it is because `ctx.id.name` is undefined and no legacy fallback has populated the name: the DO was addressed via `idFromString()` or `newUniqueId()` (both unsupported), the runtime is too old to expose `ctx.id.name`, or a pre-2026-03-15 alarm fired before the legacy storage fallback ran. |
116 | 111 |
|
117 | 112 | Changes in all affected packages (`partyserver`, `partysub`, `partysync`, `y-partyserver`, `hono-party`): |
118 | | - |
119 | 113 | - `@cloudflare/workers-types` peer dependency bumped from `^4.20240729.0` to `^4.20260424.1`. The old range predates `ctx.id.name` in the type surface. |
120 | 114 |
|
121 | 115 | Not supported: addressing PartyServer DOs via `idFromString()` or `newUniqueId()`. These paths return `ctx.id.name === undefined` inside the DO and will surface as a clear error from `this.name`. PartyServer has always assumed name-based addressing via `getServerByName` / `routePartykitRequest`; this release makes that assumption explicit. |
|
436 | 430 | ### Patch Changes |
437 | 431 |
|
438 | 432 | - [`528adea`](https://github.com/threepointone/partyserver/commit/528adeaced6dce6e888d2f54cc75c3569bf2c277) Thanks [@threepointone](https://github.com/threepointone)! - some fixes and tweaks |
439 | | - |
440 | 433 | - getServerByName was throwing on all requests |
441 | 434 | - `Env` is now an optional arg when defining `Server` |
442 | 435 | - `y-partyserver/provider` can now take an optional `prefix` arg to use a custom url to connect |
443 | 436 | - `routePartyKitRequest`/`getServerByName` now accepts `jurisdiction` |
444 | 437 |
|
445 | 438 | bonus: |
446 | | - |
447 | 439 | - added a bunch of fixtures |
448 | 440 | - added stubs for docs |
449 | 441 |
|
|
0 commit comments