Skip to content

feat: WebSocket support via functional events#82

Closed
brainkim wants to merge 49 commits into
mainfrom
feat/websocket-events
Closed

feat: WebSocket support via functional events#82
brainkim wants to merge 49 commits into
mainfrom
feat/websocket-events

Conversation

@brainkim

Copy link
Copy Markdown
Member

Summary

  • Adds websocketmessage and websocketclose as new ServiceWorker functional events (ExtendableEvent subclasses)
  • FetchEvent.upgradeWebSocket() upgrades connections — no HTTP Response created (101 is invalid in Node.js)
  • Pool returns typed WebSocketUpgradeResult instead of monkeypatched Response objects
  • Node.js adapter uses ws package with lazy loading; Bun uses native Bun.serve WebSocket support
  • createDirectModePool() provides pool-compatible interface for single-worker deployments
  • Full TypeScript types in globals.d.ts (WebSocketClient, event types, upgradeWebSocket on FetchEvent)

User-facing API

self.addEventListener("fetch", (event) => {
  if (event.request.headers.get("upgrade") === "websocket") {
    event.upgradeWebSocket({ data: { room: "lobby" } });
    return;
  }
  event.respondWith(new Response("Hello"));
});

self.addEventListener("websocketmessage", (event) => {
  event.source.send(`Echo: ${event.data}`);
});

self.addEventListener("websocketclose", (event) => {
  console.log(`Closed: ${event.code} ${event.reason}`);
});

Test plan

  • All 194 package tests pass
  • 24 build tests pass
  • 19 e2e direct mode tests pass
  • 17 e2e bundling tests pass
  • TypeScript clean across all 3 platform packages
  • ESLint clean
  • Manual WebSocket echo server test
  • Multi-worker WebSocket sticky routing test

🤖 Generated with Claude Code

brainkim and others added 26 commits April 3, 2026 11:18
Implements WebSocket support as two new ServiceWorker functional events
(websocketmessage, websocketclose) following the ExtendableEvent pattern.
Users call event.upgradeWebSocket() in fetch handlers — no Response is
created (status 101 is invalid in Node.js and semantically wrong).

Pool signals upgrades via WebSocketUpgradeResult discriminated union
instead of monkeypatching Response objects. Platform adapters (Node.js
with ws package, Bun with native WebSocket) handle the actual upgrade
handshake. Direct mode uses createDirectModePool() adapter for the
same interface without message passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests call pool.handleRequest() which now returns
Response | WebSocketUpgradeResult. Cast to Response since
these tests only make HTTP requests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- FetchEvent.upgradeWebSocket() behavior (responded, kGetUpgradeResult, UUID, data)
- WebSocketMessageEvent/WebSocketCloseEvent construction
- ShovelWebSocketClient send/close via relay
- ShovelClients WebSocket tracking (register, remove, get, matchAll)
- Event dispatch through registration (websocketmessage, websocketclose)
- dispatchFetchEvent returns null response for upgrades
- createDirectModePool upgrade flow and message relay
- ServiceWorkerPool WebSocket upgrade (real worker process)
- Pool message relay (echo), binary messages, concurrent connections
- Fix stale dispatchRequest assertions in e2e-direct-mode tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… slicing

P1: Node adapter was destroying the socket when pool.handleRequest()
returned a Response instead of an upgrade result (e.g. 401/403 auth
rejection). Now writes the HTTP response so clients see the status/body.

P2: Bun adapter forwarded message.buffer directly for binary WebSocket
frames, ignoring byteOffset/byteLength of the typed array view. This
corrupts binary messages when the view doesn't cover the full backing
buffer. Now slices to the correct range.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…pgrade header

P2: upgradeWebSocket() now throws if respondWith() or upgradeWebSocket()
was already called, preventing silent overwrite of the response path.

P2: Bun adapter now checks the Upgrade header case-insensitively, so
"WebSocket", "WEBSOCKET", etc. all trigger the upgrade path per HTTP spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, Bun upgrade failure

P2: Send websocketclose event to worker before removing connection state
during shutdown/reload, so app cleanup handlers run.

P2: Direct-mode dispatch callbacks now catch and log errors from user
websocketmessage/websocketclose handlers instead of surfacing unhandled
rejections.

P2: Bun adapter checks server.upgrade() return value — on failure, cleans
up worker-side state and returns a 500 response instead of leaving a
phantom connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Direct-mode WebSocket clients now carry their own relay instead of
using the process-global relay, so multiple in-process pools don't
stomp each other's send()/close() routing.

Simplified #closeWorkerWebSockets to let the adapter's normal close
path deliver websocketclose while ownership is still tracked, instead
of posting to a worker that's about to be terminated.

Added regression test for per-pool relay isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…TP error on upgrade failure

P1: Pass WebSocket relay to ShovelFetchEvent via wsRelay option so
clients created by upgradeWebSocket() can send()/close() immediately
inside the fetch handler, before dispatchFetchEvent returns.

P1: Buffer outbound ws:send/ws:close messages in Node and Bun adapters
when the real WebSocket hasn't completed handshake yet. Flush when
handleUpgrade completes (Node) or open fires (Bun).

P2: Node upgrade error handler now writes a proper HTTP error response
(using HTTPError.toResponse) instead of destroying the socket, so
clients see the intended status code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: When a fetch handler calls upgradeWebSocket() but the request isn't
a WebSocket upgrade (no Upgrade header), the pool returns a
WebSocketUpgradeResult through the normal HTTP path. Both Node and Bun
adapters now detect this and return 426 Upgrade Required instead of
crashing on the cast to Response.

P3: Both adapters hardcoded wasClean=true for all close events. Now
derived from the close code — 1006 (abnormal closure / no close frame)
reports wasClean=false, all others report true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: All four generated direct-mode templates (Node/Bun × index/platform)
now check for WebSocketUpgradeResult and return 426 Upgrade Required
when a fetch handler calls upgradeWebSocket() on a non-WebSocket request.

P2: Augment Clients.get() return type in globals.d.ts to include
WebSocketClient, so typed code can access WebSocket clients without
manual casting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1: Worker-initiated client.close() sends ws:close to the pool, which
was immediately deleting the connectionID from #wsConnections. The
adapter's close callback then called sendWebSocketClose() but couldn't
find the owning worker, so websocketclose never dispatched back.

Now the ws:close handler only closes the real socket — sendWebSocketClose()
(called by the adapter's close callback) handles both dispatching the
event and cleaning up #wsConnections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: If handleUpgrade() throws after pool.handleRequest() already
registered a WebSocket connection, the catch block now sends
sendWebSocketClose(1006) to clean up the worker-side clients registry
and #wsConnections, preventing phantom connections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: When a WebSocket handshake takes longer than requestTimeout, the
pending request is removed before ws:upgrade arrives. The upgrade was
silently dropped, leaving the worker with a phantom connection. Now
sends ws:close back to the worker to clean up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: When upgradeWebSocket() fires on a plain HTTP request, all code
paths (listen handlers + generated direct-mode templates) now call
sendWebSocketClose() before returning 426, preventing leaked connection
state in ShovelClients and #wsConnections.

P3: upgradeWebSocket() now checks kCanExtend() and throws
InvalidStateError if called after dispatch phase ends, matching
respondWith() contract.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…per connection

P2: dispatchRequest() now throws an explicit error when the fetch handler
calls upgradeWebSocket(), instead of silently returning null through a
Response-typed API. Callers must use dispatchFetchEvent() for upgrade
support.

P2: WebSocket message dispatch is now serialized per connection using a
promise chain, in both direct-mode (createDirectModePool) and worker
message loop (startWorkerMessageLoop). Back-to-back frames on the same
connection are processed in order even with async handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1: startWorkerMessageLoop was calling dispatchRequest() which now
throws on WebSocket upgrades. Switched to dispatchFetchEvent() so
multi-worker mode correctly handles upgradeWebSocket() and sends
ws:upgrade back to the pool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloudflare adapter now handles upgradeWebSocket() using Cloudflare's
native WebSocketPair API. The server-side socket is wired to dispatch
websocketmessage/websocketclose events, and the client's send/close
relay forwards to the real socket.

Also updated dispatchFetchEvent() to accept a pre-built ShovelFetchEvent
(or subclass like CloudflareFetchEvent) so platform-specific event
properties (env bindings, platformWaitUntil) are preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: Pass WebSocket relay into CloudflareFetchEvent via wsRelay option
so upgradeWebSocket() returns a client with working send()/close()
immediately inside the fetch handler.

P2: Register upgraded WebSocket clients with ShovelClients so
self.clients.get()/matchAll({type: "websocket"}) work on Cloudflare.

P2: Add per-connection dispatch queue for message ordering parity
with Node/Bun adapters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WebSocketPair is the wrong abstraction — the whole point of functional
events is surviving hibernation/restarts. Cloudflare WebSocket support
needs proper design work (likely Durable Object integration), not a
quick WebSocketPair shim. Reverting to main's Cloudflare runtime.

On Cloudflare, calling upgradeWebSocket() will throw via
dispatchRequest(), which is the correct behavior for an unsupported
platform feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements Cloudflare WebSocket support using the correct abstraction —
the Durable Object Hibernation API (acceptWebSocket, webSocketMessage,
webSocketClose) instead of WebSocketPair closures that don't survive
hibernation.

Architecture:
- Worker routes Upgrade: websocket requests to ShovelWebSocketDO when
  env.SHOVEL_WS binding is present
- DO initializes Shovel runtime, dispatches fetch events, uses
  ctx.acceptWebSocket() for hibernation-aware upgrades
- After hibernation wake-up, runtime re-initializes and client state
  is reconstructed from ws.deserializeAttachment()
- Per-connection dispatch queues for ordered message delivery
- ShovelWebSocketDO in separate file (websocket-do.ts) to avoid
  importing cloudflare:workers outside workerd

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a persistence

Rename websocketmessage → wsmessage, websocketclose → wsclose.
Shorter names following the dblclick/auxclick precedent for abbreviated
standard-style event names.

P1: Cloudflare DO now buffers messages sent during the fetch handler
(e.g. client.send("welcome")) and flushes after the WebSocketPair is
wired up, matching Node/Bun behavior.

P1: Cloudflare DO re-serializes ws attachment after each message
dispatch so client.data mutations persist across events and survive
hibernation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1: Cloudflare DO no longer re-runs runLifecycle() — the Worker's
createFetchHandler already activated the registration before forwarding.
Re-running threw "ServiceWorker must be installed before activation".

P2: Bun binary WebSocket frames can arrive as raw ArrayBuffer (not just
typed array views). Now checks instanceof ArrayBuffer first and only
slices for typed array views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… in envStorage

P1: After hibernation wake-up, the module is re-evaluated with a fresh
registration that hasn't been activated. Now checks registration.ready
and only runs lifecycle when needed (skips on initial request where
Worker already activated).

P1: webSocketMessage/webSocketClose now wrap dispatch in envStorage.run()
so env-backed APIs (directories, etc.) work in WebSocket handlers,
matching the fetch path behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…DO fetch

P1: After hibernation wake-up, rehydrate all existing WebSocket
connections from ctx.getWebSockets() into self.clients, so matchAll
and get see all live connections (not just the one that triggered
the event).

P2: Add platformWaitUntil hook to CloudflareFetchEvent in the DO's
fetch handler, so event.waitUntil() work during upgrade isn't dropped
when the 101 response returns.

Note: Multi-worker self.clients.matchAll({type: "websocket"}) is
worker-local by design (same as standard ServiceWorker Clients API).
Cross-worker client enumeration would need pool-level relay protocol.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brainkim brainkim force-pushed the feat/websocket-events branch from 45a4fbd to 25b0c84 Compare April 3, 2026 15:20
brainkim and others added 3 commits April 3, 2026 11:56
…inding

P1: webSocketMessage/webSocketClose now return the dispatch promise
chain so Cloudflare keeps the handler alive for async work and
hibernation data persistence.

P2: WebSocket upgrade requests without the SHOVEL_WS Durable Object
binding now return 501 with a clear configuration error message
instead of falling through to dispatchRequest() which throws.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tchAll

P1: Cloudflare adapter only intercepts WebSocket upgrades when SHOVEL_WS
binding is configured. Without it, requests pass through to user code
for manual WebSocketPair handling — no breaking change for existing apps.

P2: clients.matchAll() and matchAll({type: "worker"}) now include
WebSocket clients since their type is "worker". Previously they were
only returned for the nonstandard {type: "websocket"} query.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: matchAll() without type now returns empty again (window clients per
spec). WebSocket clients only returned for {type: "websocket"} or
{type: "all"}.

P2: Each Cloudflare WebSocket connection gets its own Durable Object via
newUniqueId() instead of all sharing one "default" instance. Hibernation
keeps idle DOs cheap; this avoids single-object CPU/memory bottleneck.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
brainkim and others added 11 commits April 3, 2026 12:25
…n dispatch queue

P1: Without SHOVEL_WS binding, Cloudflare adapter now falls back to
WebSocketPair (non-hibernation) instead of crashing via dispatchRequest.
Works for dev/local and apps that don't need hibernation.

P1: DO message handler now reconstructs ShovelWebSocketClient INSIDE
the dispatch queue chain (after previous handler completes), so
client.data mutations from back-to-back messages don't get overwritten
by stale attachment data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… registration

The WebSocketPair fallback path (without SHOVEL_WS binding) now has
parity with the DO/Node/Bun paths:
- wsRelay passed to event so send()/close() work in fetch handler
- Per-connection dispatch queues for message ordering
- Register/remove WebSocket clients in self.clients
- Buffer early messages until socket is wired up

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: The DO isolate has its own module state, so the worker's
setBroadcastChannelBackend call doesn't carry over. Now configures
the PubSub backend in #ensureRuntime when SHOVEL_PUBSUB is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: The dev/Miniflare handleRequest path re-wrapped responses into
new Response(), dropping the nonstandard webSocket property needed
for WebSocket upgrades. Now passes through the original response
when webSocket is present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2: serializeAttachment() now catches non-cloneable client.data and
falls back to null with a warning, instead of crashing the upgrade
or message handler.

P2: reloadWorkers/terminate now yield to the event loop after closing
WebSocket connections, giving close callbacks time to deliver wsclose
events to workers before shutdown begins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…l to after wsclose

P2: upgradeWebSocket() now throws if the request doesn't have
Upgrade: websocket header, preventing cross-platform behavior
differences where some adapters return 426 and others try to upgrade.

P2: Direct-mode sendWebSocketClose removes the client from
self.clients in the finally block (after dispatch) instead of before,
so wsclose listeners can still see the connection via clients.get().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1: Removed automatic interception of WebSocket requests when SHOVEL_WS
is configured. The DO is exported for explicit use — users route to it
from their fetch handler when they want hibernation. Default path uses
WebSocketPair fallback, preserving existing manual WebSocket code.

P1: DO no longer runs install/activate lifecycle. With per-connection
DOs, lifecycle would run once per connection (migrations, cache warming
repeated). The worker already ran lifecycle; the DO only needs event
handlers which are re-registered on module re-evaluation.

P2: Existing manual WebSocketPair routes no longer broken by adding
the SHOVEL_WS binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloudflare WebSocket requires SHOVEL_WS Durable Object binding.
No WebSocketPair fallback — hibernation is the only supported path.

DO marks registration as activated without re-running lifecycle,
avoiding duplicate migrations/cache warming per connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
workerd occasionally fails to spawn on CI runners (child_process
connect error). Added createMiniflare() helper with 3 retries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brainkim brainkim force-pushed the feat/websocket-events branch from 90f6e4f to 80f2925 Compare April 8, 2026 16:49
brainkim and others added 9 commits April 8, 2026 13:01
The ShovelWebSocketDO import pulls in cloudflare:workers which
crashes workerd on Miniflare when no DO bindings are configured.
Users who want hibernation WebSocket import and re-export the DO
class from their own code alongside their wrangler.toml config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DO class must be exported from the worker bundle for Cloudflare
to instantiate it. The earlier Miniflare crash was the flaky workerd
spawn issue (fixed by retry helper), not the cloudflare:workers import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e-during-upgrade

Two regressions found by Codex review:

P1: ws:upgrade postMessage included client.data which throws DataCloneError
for non-structured-cloneable values (functions, symbols, class instances).
The supervisor never reads this field — removed it.

P2: Node adapter registered ws.on("close") after flushPending(), so a
buffered close frame during upgrade would miss the listener. Moved
flushPending() after all listener registrations.

Also fixes direct-mode close-during-upgrade: added onUpgrade callback to
ShovelFetchEventInit so clients are registered immediately when
upgradeWebSocket() is called, not after dispatchFetchEvent returns.

Adds regression tests for both issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…al to after wsclose

Two more Codex review findings:

P1: Cloudflare upgrade without SHOVEL_WS binding fell through to
dispatchRequest() which throws on upgrades → opaque 500. Now uses
dispatchFetchEvent() and returns 426 with a clear error message.

P2: Pooled worker mode removed the client from self.clients before
dispatching wsclose, so handlers couldn't see the disconnecting client
via clients.get/matchAll. Moved removal to finally{} after dispatch,
matching direct-mode and Cloudflare DO behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…l connections

Per-connection DOs via newUniqueId() meant self.clients.matchAll()
only saw one connection per DO instance. Switch to idFromName() with
a fixed name so all WebSocket connections share one DO and client
lookup works the same as Node/Bun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… mode

If upgradeWebSocket() was called but the fetch handler then threw,
the client stayed registered in ShovelClients — visible via
clients.get() and matchAll() despite the upgrade never completing.
Now wrapped in try/catch to remove the phantom registration on error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If client.data becomes non-structured-cloneable after a wsmessage
handler, serializeAttachment fails silently — leaving the previous
attachment intact. On hibernation wake-up this resurrects stale data.
Now nulls out the attachment on failure so rehydration gets null
instead of incorrect old state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bun's child_process.spawn fails with "connect ENOENT" on the 4th
stdio pipe (control-fd=3) when multiple workerd processes spawn
concurrently. Root cause is a Bun runtime bug with socketpair
creation under load.

Fix: exclude cloudflare-*.test.* from parallel bun test via
bunfig.toml pathIgnorePatterns, then run them sequentially via
the test script. Also adds explanatory comment in the retry helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pathIgnorePatterns in bunfig.toml applies to explicit file arguments too,
so "bun test ./test/cloudflare-build.test.js" returned "no test files matched"
on CI. Override it on the CLI for the sequential runs, keeping the wpt
exclusion intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brainkim

Copy link
Copy Markdown
Member Author

Closing in favor of a redesign

After extensive design discussion, we're not merging this PR. The core issue: it shoehorns WebSockets into the ServiceWorker Clients abstraction (WebSocketClient extends Client, matchAll({type: "websocket"}), ClientQueryOptions extension), and that's a direction we can't defend as standards-adjacent. The Client interface models browsing contexts the SW controls; an inbound WebSocket connection isn't one, and making it pretend to be breaks the abstraction in several places (postMessage semantics, frameType, type enum).

Design principle we landed on

Extend via new event types and methods on new event classes we own. Never modify standards classes (WebSocket, FetchEvent, Client, Clients, BroadcastChannel).

This preserves the option for future standards to evolve without conflicting with Shovel, keeps the "portable web standards" promise honest, and surgically contains the invention to surfaces we own.

New direction (sketch)

Three new event types on ServiceWorkerGlobalScope:

"websocketupgrade"  -> WebSocketUpgradeEvent extends ExtendableEvent
"websocketmessage"  -> WebSocketMessageEvent extends ExtendableEvent
"websocketclose"    -> WebSocketCloseEvent extends ExtendableEvent

All interaction happens via methods on these new classes. Users never see a WebSocket instance. IDs and send/close/subscribe/unsubscribe live on the event objects. Cross-isolate fanout piggybacks on BroadcastChannel via one semantic rule (BC messages published on a channel name are forwarded as send() to every WebSocket that subscribe()'d to that channel). No extensions to BroadcastChannel itself.

No touches to WebSocket, FetchEvent, Client, Clients, ClientQueryOptions, or BroadcastChannel. Estimated surface: ~20-30 lines in globals.d.ts, vs 63 here.

What's preserved

The commits on feat/websocket-events remain on the branch. The 35 regression tests encode real behavior we want the new implementation to maintain (echo, binary frames, close-during-upgrade, non-cloneable data, hibernation rehydration, phantom client cleanup). They'll port over with API substitution.

The Cloudflare DO hibernation plumbing in packages/platform-cloudflare/src/websocket-do.ts is largely reusable — the semantics change (subscription-based forwarding instead of Client registration) but the DO lifecycle and serializeAttachment patterns carry forward.

Next step

New PR against a fresh branch, starting from globals.d.ts and writing types first to ground every API question before touching runtime code.

@brainkim brainkim closed this Apr 22, 2026
brainkim added a commit that referenced this pull request Apr 23, 2026
Running multiple cloudflare-*.test.* files concurrently in the same
\`bun test\` invocation triggers intermittent workerd failures:
  \`error: disconnected: miniposix::write(fd, pos, size): Broken pipe; fd = 3\`
  (workerd control socket write failing during bun+miniflare setup)

Workaround carried over from PR #82 (commit 1f154fd):
- bunfig.toml: exclude \`**/cloudflare-*.test.*\` from the default walk
- package.json: run each cloudflare-*.test.js in its own \`bun test\`
  invocation, sequentially, via the root \`test\` script

No code changes — purely test orchestration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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