Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-alarms-remember-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"partyserver": patch
---

Persist a `__ps_name` fallback for name-based Durable Objects during initialization. This lets alarm handlers recover `this.name` even when firing on a stale on-disk alarm record that was scheduled by an older workerd version that didn't yet persist `name` into the alarm record. See cloudflare/partykit#390.
88 changes: 88 additions & 0 deletions fixtures/alarm-restart-e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# `alarm-restart-e2e`

Reproducer for the runtime contract that motivates partyserver's
`__ps_name` fallback record. Pins down behavior reported in
[cloudflare/partykit#390](https://github.com/cloudflare/partykit/issues/390)
across three Durable Objects in the same Worker:

| DO | Class | Extends |
| ------------ | --------------------------------- | -------------------------------------------------------------------------- |
| `RawAlarm` | `RawAlarm` | `DurableObject` (no PartyServer) |
| `StockAlarm` | `StockAlarm` (built from a mixin) | `Server` from `partyserver@0.5.3` (aliased as `partyserver-stock`) |
| `FixedAlarm` | `FixedAlarm` (built from a mixin) | `Server` from this workspace's local `partyserver` (with the fallback fix) |

Each DO records an observation (`{source, ctxIdName, storedPsName,
partyName, partyNameError, at}`) to its own SQLite-backed storage on
every entry through `fetch()` or `alarm()`. Observations accumulate
across dev-server restarts.

## Run the experiment

```bash
npm install
npm run start
```

In a second shell, schedule an alarm into a fresh room and observe:

```bash
ROOM="cold-strict-$(date +%s)"

# Session A: schedule into a fresh room. This is the only entry into
# the DO instances during session A. After this, the alarm record on
# disk is what carries the DO across the restart.
curl -s "http://localhost:5173/raw/$ROOM?schedule=45"
curl -s "http://localhost:5173/parties/stock-alarm/$ROOM?schedule=45"
curl -s "http://localhost:5173/parties/fixed-alarm/$ROOM?schedule=45"
```

Then kill `vite dev` (Ctrl-C), restart it (`npm run start`), and
**don't touch the room** until well past the 45-second mark. Then:

```bash
curl -s "http://localhost:5173/raw/$ROOM?snapshot=1" | jq
curl -s -i "http://localhost:5173/parties/stock-alarm/$ROOM?snapshot=1" | head -n 12
curl -s "http://localhost:5173/parties/fixed-alarm/$ROOM?snapshot=1" | jq
```

Observed behavior on `workerd@1.20260424.1`,
`compatibility_date: "2026-01-28"`:

- `RawAlarm`: alarm observation has no `ctxIdName` (i.e. `ctx.id.name`
is `undefined`). Subsequent fetches via `idFromName(...)` ALSO see
`ctx.id.name === undefined` for the lifetime of that DO instance —
the instance is "born nameless" and stays that way.

- `StockAlarm`: `Server.fetch` returns 500 with the "Cannot determine
the name" error. Reproduces the failure reported in cloudflare/partykit#390.

- `FixedAlarm`: `alarm()` runs successfully. `ctx.id.name` is
`undefined` in the observation, but `this.name` resolves from the
on-disk `__ps_name` record that PartyServer wrote during session
A's fetch. `partyserver` recovers the name; the DO continues
working normally.

## Why three DOs

`RawAlarm` pins down what workerd actually does, free of any
framework. `StockAlarm` reproduces the user-reported bug under
`partyserver@0.5.3`. `FixedAlarm` validates that the workspace fix
restores normal operation under the same conditions.

## Critical: don't warm the DOs before the alarm fires

Any HTTP fetch or websocket message sent to a DO between session B
startup and the alarm firing time will wake the DO via that entry
point first. workerd captures `ctx.id.name` from the first entry
point and that value persists for the instance's lifetime. So a
pre-alarm fetch silently warms `ctx.id.name` and masks the bug. The
critical window is from `vite dev` starting back up until the
expected alarm fire time. Don't open the page in a browser, don't
curl `?snapshot`, don't let any client reconnect to the room. Just
wait.

The frontend `index.html` exists for manual exploration but is
deliberately separate from the cold-DO experiment so a developer
running the page won't accidentally warm a different room. To run
the cold experiment, drive everything from `curl` against rooms the
frontend isn't subscribed to.
10 changes: 10 additions & 0 deletions fixtures/alarm-restart-e2e/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types env.d.ts --include-runtime false` (hash: 9d5eb238d4dbfdedf1bf7b0674d6a12c)
declare namespace Cloudflare {
interface Env {
RawAlarm: DurableObjectNamespace /* RawAlarm */;
StockAlarm: DurableObjectNamespace /* StockAlarm */;
FixedAlarm: DurableObjectNamespace /* FixedAlarm */;
}
}
interface Env extends Cloudflare.Env {}
137 changes: 137 additions & 0 deletions fixtures/alarm-restart-e2e/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>partyserver alarm-restart e2e</title>
<style>
body {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
monospace;
margin: 24px;
background: #0b0b0e;
color: #e6e6ea;
}
h1 {
font-size: 16px;
margin: 0 0 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.card {
background: #14141a;
border: 1px solid #2a2a33;
border-radius: 6px;
padding: 12px;
font-size: 12px;
}
.card h2 {
font-size: 13px;
margin: 0 0 8px;
}
.row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
}
.ok {
background: #133b1f;
color: #8be9a3;
}
.warn {
background: #4a2a08;
color: #f5b061;
}
.err {
background: #4a1818;
color: #ef9494;
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
background: #0a0a0e;
border: 1px solid #1f1f27;
padding: 8px;
border-radius: 4px;
font-size: 11px;
max-height: 320px;
overflow: auto;
}
input,
button {
background: #1f1f27;
border: 1px solid #2f2f3a;
color: #e6e6ea;
padding: 4px 8px;
border-radius: 4px;
font: inherit;
}
button {
cursor: pointer;
}
button:hover {
background: #292935;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
</style>
</head>
<body>
<h1>partyserver alarm-restart e2e</h1>
<div class="controls">
Room name:
<input id="room" value="default-room" />
Schedule alarm in
<input id="seconds" type="number" value="20" style="width: 64px" />s
<button id="schedule-all">Schedule on all 3</button>
<button id="snapshot-all">Snapshot all 3</button>
</div>
<div class="grid">
<section class="card" id="card-raw">
<h2>RawAlarm <span class="badge" data-status>?</span></h2>
<div class="row">
<small data-summary></small>
</div>
<pre data-log>(no events yet)</pre>
</section>
<section class="card" id="card-stock">
<h2>
StockAlarm (partyserver@0.5.3)
<span class="badge" data-status>?</span>
</h2>
<div class="row">
<small data-summary></small>
</div>
<pre data-log>(no events yet)</pre>
</section>
<section class="card" id="card-fixed">
<h2>
FixedAlarm (workspace partyserver)
<span class="badge" data-status>?</span>
</h2>
<div class="row">
<small data-summary></small>
</div>
<pre data-log>(no events yet)</pre>
</section>
</div>
<script src="/src/client.ts" type="module"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions fixtures/alarm-restart-e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@partyserver/fixture-alarm-restart-e2e",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "vite dev",
"types": "wrangler types env.d.ts --include-runtime false"
},
"dependencies": {
"partyserver": "*",
"partyserver-stock": "npm:partyserver@0.5.3",
"partysocket": "^1.1.18"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.33.2",
"@cloudflare/workers-types": "^4.20260424.1",
"vite": "^8.0.10",
"wrangler": "^4.85.0"
}
}
Loading
Loading