Skip to content

fix(api): buffer live events during SSE backlog replay for ordered delivery#361

Draft
duyet wants to merge 1 commit into
mainfrom
claude/w5-sse-backlog-ordering
Draft

fix(api): buffer live events during SSE backlog replay for ordered delivery#361
duyet wants to merge 1 commit into
mainfrom
claude/w5-sse-backlog-ordering

Conversation

@duyet

@duyet duyet commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Status: DRAFT — in progress, functional fix not yet committed

Opening now to hit a compressed deadline. Per instruction: not rushing the
correctness of this MED-risk Durable Object streaming change. This commit is
documentation-only (an inline FIXME note); the actual fix and tests follow
in subsequent commits on this same branch.

Task

Executing plans/008-sse-backlog-ordering.md: StateStreamHub.watch()
registers the SSE writer in this.writers before replaying the
D1-backed backlog, so a concurrent /notify broadcast during replay can
interleave or duplicate events ahead of the ordered backlog stream. Planned
fix: buffer-then-drain — hold live broadcasts in a pending buffer while
backlog replays, flush pending events (deduped/sorted by sequence), then
register the writer atomically (no await between flush and registration).

Bigger finding from investigation (verified against the real DO runtime

via @cloudflare/vitest-pool-workers, not a mock)

The current /watch DO path (the non-once=true path) deadlocks
indefinitely whenever the backlog has 1+ rows
. Repro: seed one state
event, then GET /watch?after=0 through the DO — the request never
resolves (confirmed hung past 10s; an empty backlog resolves in ~295ms).

Root cause: watch() does await this.writeBacklog(writer, ...) before
return new Response(stream.readable, ...). A TransformStream's writes
cannot resolve until something reads from stream.readable (initial
backpressure per the WHATWG streams spec), but nothing can ever read it
because the Response wrapping that readable stream isn't returned until
writeBacklog finishes — a structural circular wait for any non-empty
backlog.

This means the plan's literal "Target design" (buffer → await writeBacklog
→ flush → register writer, Response still returned only at the end) would
still deadlock before a client observes anything, so it can't be
implemented or tested as written. The real fix needs to return
Response(stream.readable, ...) immediately, then do backlog-write +
pending-flush + writer-registration as background work (e.g. via
this.ctx.waitUntil), keeping heartbeat/abort wiring intact for both
phases. Public SSE wire format and the /notify+/watch route contract
stay unchanged — only the internal sequencing changes.

Verified so far

  • Drift check: git diff --stat ce9a1fa..HEAD -- packages/api/src/state-stream-hub.ts — no drift.
  • Deadlock repro confirmed empirically (0-row backlog ~295ms OK; 1-row and 3-row backlog hang past 10s) via a throwaway test against SELF.fetch in the real vitest-pool-workers/workerd environment. That scratch test was not committed.

NOT done / NOT verified yet

  • Functional fix not written.
  • Lint/typecheck/vitest not run against a real change (nothing to run yet beyond a comment-only diff — biome passed on the touched file as part of the pre-commit hook, nothing else claimed).
  • No new test file yet.

Next steps (follow-up commits, same branch)

  1. Implement buffer-then-drain + immediate-Response/background-backlog design in packages/api/src/state-stream-hub.ts.
  2. Add packages/api/test/state-stream-hub.test.ts: backlog-only ordering, live broadcast, concurrent-notify-during-replay regression, abort-during-replay cleanup.
  3. Run bunx biome check packages/api/src/, bunx tsc --noEmit -p packages/api/tsconfig.json, cd packages/api && bunx vitest run and report actual results.
  4. Mark ready for review once verified.

Co-Authored-By: Duyet Le me@duyet.net
Co-Authored-By: duyetbot bot@duyet.net

…m-hub

Inline note for reviewers ahead of the fix: `watch()` currently registers
the writer before the backlog replay completes (the known ordering bug
from plans/008), and separately, `writeBacklog`'s writes cannot resolve
until a reader attaches to `stream.readable`, which never happens because
the Response wrapping it is only returned after the backlog write loop
finishes -- deadlocking any request with a non-empty backlog. Verified
against the real Durable Object runtime. Functional fix follows in a
subsequent commit on this branch.

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @duyet, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8dcfeaee-21ec-4c89-8a47-eb85af477b7e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/w5-sse-backlog-ordering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant