Skip to content

✨ Add WebSocket server to @effectionx/websocket#222

Open
taras wants to merge 9 commits into
mainfrom
feat/websocket-server
Open

✨ Add WebSocket server to @effectionx/websocket#222
taras wants to merge 9 commits into
mainfrom
feat/websocket-server

Conversation

@taras

@taras taras commented Jul 6, 2026

Copy link
Copy Markdown
Member

Motivation

The @effectionx/websocket package shipped only a client (useWebSocket()). There was no server counterpart, even though the package's own test file already implemented the full server-side mechanic by hand — a ws WebSocketServer firing connection events, each raw socket wrapped with useWebSocket(). This PR formalizes that pattern into a reusable server that pairs naturally with the existing client.

Approach

New useWebSocketServer() (websocket/server.ts) — the server counterpart of useWebSocket():

  • Returns a WebSocketServerResource<T>: an Effection Stream whose items are the same full-duplex WebSocketResource the client produces, so both sides share one handle type (iterate to receive, yield* connection.send() to reply).
  • Reuses useWebSocket() to wrap each incoming socket.
  • Buffers connections in a createQueue, so none are dropped between the server starting to listen and the consumer iterating.
  • Crashes the resource scope on a server error, mirroring the client.
  • Auto-closes the server and every live connection when the resource passes out of scope.
  • Takes a () => WebSocketServerLike factory, so the package never imports a concrete server implementation and stays platform-agnostic — no new runtime/peer dependency (ws remains a devDependency for tests only).

send is now an Operation — invoked as yield* resource.send(...) on both client and server, keeping the shared WebSocketResource symmetric and letting sends participate in structured concurrency.

Warning

Breaking change: WebSocketResource.send() changed from a synchronous void call to an Operation. Callers must now write yield* socket.send(...). Version bumped 2.3.3 → 3.0.0.

Also included:

  • mod.ts re-exports the server; updated client README/tests to yield* their sends.
  • New server.test.ts (5 tests): connection delivery, message round-trips in both directions, close-propagation on client disconnect, server teardown closing live clients, and multiple simultaneous clients as distinct connections.
  • README gains a "WebSocket Server" section with a full client↔server echo example.

Verification

  • All 9 tests pass (client + server) — a real ws server against a real native WebSocket client, exercising the pairing end-to-end.
  • biome lint clean, biome format clean, tsc -b passes.

Summary by CodeRabbit

  • New Features
    • Added WebSocket server support with a server API that streams accepted connections and enables full-duplex messaging.
    • Updated WebSocketResource.send to return an operation, and added an explicit WebSocketResource.close(code?, reason?).
  • Documentation
    • Refreshed Basic Usage to use async-style sending.
    • Added a WebSocket Server guide with end-to-end examples and lifecycle notes.
  • Bug Fixes
    • Improved shutdown behavior: active connections close cleanly; silent peers won’t cause teardown hangs.
    • Isolated per-connection failures so one client won’t stop new connections.
  • Tests
    • Added comprehensive WebSocket server test coverage and updated messaging assertions.

Add useWebSocketServer(), the server counterpart of useWebSocket(). It
yields a stream of incoming connections, each a full-duplex
WebSocketResource, so client and server share the same handle type.

The underlying server is supplied via a factory, keeping the package free
of any concrete server dependency and platform-agnostic.

BREAKING CHANGE: WebSocketResource.send() is now an Operation, invoked as
`yield* socket.send(...)` on both client and server. Bumped to 3.0.0.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a WebSocket server stream API, updates send to be awaited as an Effection operation, and updates tests, docs, exports, package metadata, and TypeScript references.

Changes

WebSocket server and send() operation

Layer / File(s) Summary
send() Operation contract and existing client tests/docs
websocket/websocket.ts, websocket/websocket.test.ts, websocket/README.md
WebSocketResource.send now returns Operation<void>, useWebSocket implements it as a generator method, and existing tests/basic usage docs now yield* send calls.
WebSocketServer interfaces and implementation
websocket/server.ts
Adds WebSocketServerLike and WebSocketServerResource<T>, and implements useWebSocketServer as a scoped resource that buffers connections, forwards errors, wraps sockets with useWebSocket, and closes the server on teardown.
WebSocketServer test suite and helpers
websocket/server.test.ts
Adds tests for connection/message exchange, disconnect completion, teardown cleanup, buffered delivery, error isolation, and simultaneous client isolation, plus useTestServer, useHttp, connect, and drain helpers.
Module exports, README server docs, package metadata
websocket/mod.ts, websocket/README.md, websocket/package.json, websocket/tsconfig.json
Re-exports server.ts, adds server usage docs, updates package description/version, adds node/timebox dependencies, and adds node/timebox project references.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a WebSocket server to the websocket package.
Description check ✅ Passed The description follows the template with Motivation and Approach sections and adequately explains the feature and breaking change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Policy Compliance ✅ Passed websocket/package.json has valid description/keywords, source changes are version-bumped to 3.0.0, and the PR description contains no AI marketing footer.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/websocket-server

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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@effectionx/websocket@222

commit: 3822ca5

Comment thread websocket/server.ts Outdated
// client's `throw yield* once(socket, "error")` behavior
let errored = withResolvers<Error>();
let onError = (error: Error) => errored.resolve(error);
server.on("error", onError);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we should be using once here from either @effectionx/node or effection depending on if it's TargetEvent or EventEmitter interface

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 61f849d — now using once from @effectionx/node (the ws server is an EventEmitter): let [error] = yield* once<[Error]>(server, "error"); throw error;. Added @effectionx/node as a dependency.

Comment thread websocket/server.ts Outdated
}
});
};
server.on("connection", onConnection);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we can use on here from effectionx/node or effection

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 61f849d — connections now come from on(server, "connection") (from @effectionx/node), consumed with each.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@websocket/server.test.ts`:
- Around line 117-120: The test is accessing `.data` on `firstMessages.next()`
and `secondMessages.next()` without narrowing the iterator result, but the
`.value` can also be a `CloseEvent`. Update the `received` setup in
`server.test.ts` to first unwrap each `IteratorResult` and assert/narrow that
the yielded value is a `MessageEvent<string>` before reading `.data`, using the
existing `firstMessages` and `secondMessages` iterators so the type checker can
distinguish them from `CloseEvent`.

In `@websocket/server.ts`:
- Around line 85-140: Add a test for connection-level error isolation in
useWebSocketServer: verify that a single accepted socket error after the
connection is established does not take down the server or other active
connections. Use the useWebSocketServer, onConnection, and the per-connection
useWebSocket<T> task behavior to assert the errored socket is isolated while the
server keeps accepting/serving remaining connections.
- Around line 108-119: The per-connection handling in onConnection currently
runs inside scope.run without its own failure boundary, so errors from
useWebSocket or the subscription drain loop can bubble up and affect the server.
Wrap the body of onConnection in a connection-local error boundary or
try/catch-style effect handler so each raw WebSocket failure is isolated, and
make sure the connection setup, connections.add(connection), and
subscription.next() loop all stay within that per-client scope.

In `@websocket/websocket.ts`:
- Around line 161-163: The public send method in websocket.ts is relying on
contextual typing for both its parameter and its Operation<void> return, so make
the types explicit on the send method in the websocket object literal. Update
the send signature in the websocket factory/adapter implementation to declare
the data parameter type and the void-returning operation type directly, keeping
the method compatible with the surrounding interface while following the
explicit public function typing guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2406db94-4af3-4cb6-8f23-3452e452d764

📥 Commits

Reviewing files that changed from the base of the PR and between 387f725 and aee6d2f.

📒 Files selected for processing (7)
  • websocket/README.md
  • websocket/mod.ts
  • websocket/package.json
  • websocket/server.test.ts
  • websocket/server.ts
  • websocket/websocket.test.ts
  • websocket/websocket.ts

Comment thread websocket/server.test.ts
Comment thread websocket/server.ts
Comment thread websocket/server.ts Outdated
Comment thread websocket/websocket.ts Outdated

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
websocket/server.ts (1)

85-103: 🚀 Performance & Scalability | 🔵 Trivial

Unbounded connection buffering could exhaust resources under connection floods.

createQueue is unlimited in size, and the accept loop wraps and stores every incoming socket via useWebSocket regardless of whether anything ever reads from server.socket. If the consumer stalls or never iterates the stream, the server keeps accepting and holding live sockets (each with its own background tasks/listeners) indefinitely — a stalled or malicious client population could grow memory/FD usage without bound. Worth considering a cap on buffered connections (or backpressure signal) as a hardening follow-up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@websocket/server.ts` around lines 85 - 103, The accept path in
websocket/server.ts can buffer an unlimited number of live WebSocketResource
entries, which risks unbounded memory and FD growth if server.socket is not
being consumed. Update the createQueue/useWebSocket flow in the server accept
loop to enforce a bounded connection buffer or apply backpressure so new sockets
are not wrapped and retained indefinitely when the consumer stalls. Use the
existing spawn, each(on(server, "connection")), and connections.add logic as the
place to gate or reject excess connections.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@websocket/server.ts`:
- Around line 85-103: The accept path in websocket/server.ts can buffer an
unlimited number of live WebSocketResource entries, which risks unbounded memory
and FD growth if server.socket is not being consumed. Update the
createQueue/useWebSocket flow in the server accept loop to enforce a bounded
connection buffer or apply backpressure so new sockets are not wrapped and
retained indefinitely when the consumer stalls. Use the existing spawn,
each(on(server, "connection")), and connections.add logic as the place to gate
or reject excess connections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e247a1c9-f01f-47c8-9884-ac73b73888d2

📥 Commits

Reviewing files that changed from the base of the PR and between aee6d2f and 9191267.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • websocket/package.json
  • websocket/server.test.ts
  • websocket/server.ts
  • websocket/tsconfig.json
  • websocket/websocket.ts

taras added 3 commits July 6, 2026 19:53
Phase 1: replace the unbounded wait for the peer close handshake in
useWebSocket with timebox(), so a silent peer can no longer hang scope
teardown. Adds @effectionx/timebox.
Phase 2: WebSocketResource gains a composable close(code?, reason?)
operation; useWebSocketServer composes a 1001 "server shutting down"
close for live connections on teardown. First-close-wins, so it takes
precedence over the scope-exit 1000.
Phase 3: each accepted connection now runs inside a scoped() error
boundary, so one socket erroring is contained instead of crashing the
server. Failures are surfaced compositionally on a new server.errors
stream rather than a callback.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
websocket/server.test.ts (1)

24-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated "accept a connection" boilerplate.

The let { server, port } = yield* useTestServer(); let incoming = yield* server; let client = yield* connect(port); let connection = (yield* incoming.next()).value; sequence repeats across most tests in this file. A small acceptConnection() helper (alongside the existing useTestServer/connect/drain helpers) would reduce duplication and keep new tests concise.

Also applies to: 38-50, 101-113, 133-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@websocket/server.test.ts` around lines 24 - 36, The test setup in
server.test.ts repeats the same “start server, wait for incoming, connect
client, accept connection” boilerplate in multiple cases. Extract that sequence
into a small helper such as acceptConnection() near the existing useTestServer,
connect, and drain helpers, and update the affected tests to use it so the
websocket tests stay concise and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@websocket/package.json`:
- Around line 17-19: The websocket package currently depends on the top-level
`@effectionx/node` entrypoint, which pulls in Node-only stream code through its
reexports and breaks browser/Deno consumers. Update the websocket package to
avoid importing the full `@effectionx/node` package and instead reference the
narrower `@effectionx/node/events` entrypoint, or separate the event helpers from
the Node stream adapter so `@effectionx/websocket` stays environment-agnostic.

In `@websocket/server.ts`:
- Around line 159-161: The teardown loop in the server shutdown path currently
closes each live connection sequentially via `connection.close(...)`, which can
stretch shutdown to N times the close timeout. Update the shutdown logic to run
the per-connection close effects concurrently using Effection’s `all` in the
`server.ts` teardown flow, and add `all` to the Effection import. Keep the
existing `close(1001, "server shutting down")` behavior inside the
per-connection task, and verify that the `close()` calls remain safe when
executed in parallel.

---

Outside diff comments:
In `@websocket/server.test.ts`:
- Around line 24-36: The test setup in server.test.ts repeats the same “start
server, wait for incoming, connect client, accept connection” boilerplate in
multiple cases. Extract that sequence into a small helper such as
acceptConnection() near the existing useTestServer, connect, and drain helpers,
and update the affected tests to use it so the websocket tests stay concise and
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8430e035-504d-4dd1-aea7-18e16862e950

📥 Commits

Reviewing files that changed from the base of the PR and between 9191267 and 3822ca5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • websocket/package.json
  • websocket/server.test.ts
  • websocket/server.ts
  • websocket/tsconfig.json
  • websocket/websocket.test.ts
  • websocket/websocket.ts

Comment thread websocket/package.json
Comment on lines +17 to +19
"dependencies": {
"@effectionx/node": "workspace:*",
"@effectionx/timebox": "workspace:*"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether `@effectionx/node` itself depends on Node.js-specific runtime APIs
# or is purely a type/interface package usable in any JS environment.
fd package.json node --exec cat {}
rg -n "process\.|node:" node/*.ts node/mod.ts 2>/dev/null

Repository: thefrontside/effectionx

Length of output: 1604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect websocket package metadata and its source imports.
fd -a 'package.json' websocket
echo '--- websocket/package.json ---'
cat websocket/package.json

echo '--- websocket source imports ---'
rg -n 'from "`@effectionx/`(node|timebox)"|import .*`@effectionx/`(node|timebox)|require\\("`@effectionx/`(node|timebox)"\\)' websocket

Repository: thefrontside/effectionx

Length of output: 1497


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- websocket file map ---'
fd -a 'mod.ts|server.ts|client.ts|socket.ts|*.ts' websocket

echo '--- websocket/mod.ts ---'
cat -n websocket/mod.ts

echo '--- websocket/server.ts ---'
cat -n websocket/server.ts

echo '--- `@effectionx/timebox` package.json ---'
fd -a 'package.json' timebox
cat timebox/package.json

echo '--- `@effectionx/timebox` exports/imports ---'
rg -n 'export|import|setTimeout|clearTimeout|AbortController|node:' timebox

echo '--- `@effectionx/node` exports/imports ---'
cat -n node/mod.ts
rg -n 'export .*on|export .*once|import .*node:|from "node:' node

Repository: thefrontside/effectionx

Length of output: 546


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- websocket/mod.ts ---'
cat -n websocket/mod.ts

echo '--- websocket/server.ts ---'
cat -n websocket/server.ts

echo '--- websocket/websocket.ts ---'
cat -n websocket/websocket.ts

echo '--- timebox/package.json ---'
cat timebox/package.json

echo '--- timebox/mod.ts ---'
cat -n timebox/mod.ts

Repository: thefrontside/effectionx

Length of output: 20994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- node/mod.ts ---'
cat -n node/mod.ts

echo '--- node/events.ts ---'
cat -n node/events.ts

echo '--- node/stream.ts ---'
cat -n node/stream.ts

Repository: thefrontside/effectionx

Length of output: 9186


Avoid a hard dependency on @effectionx/node in websocket/package.json. Its top-level entrypoint reexports node/stream.ts, so importing @effectionx/websocket pulls in node:stream even for browser/Deno consumers. Import @effectionx/node/events instead, or split the event helpers from the Node-only stream adapter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@websocket/package.json` around lines 17 - 19, The websocket package currently
depends on the top-level `@effectionx/node` entrypoint, which pulls in Node-only
stream code through its reexports and breaks browser/Deno consumers. Update the
websocket package to avoid importing the full `@effectionx/node` package and
instead reference the narrower `@effectionx/node/events` entrypoint, or separate
the event helpers from the Node stream adapter so `@effectionx/websocket` stays
environment-agnostic.

Comment thread websocket/server.ts
Comment on lines +159 to +161
for (let connection of [...live]) {
yield* connection.close(1001, "server shutting down");
}

@coderabbitai coderabbitai Bot Jul 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Close live connections concurrently during teardown to bound shutdown latency.

Each yield* connection.close(1001, ...) awaits that connection's close handshake, bounded by CLOSE_TIMEOUT_MS. Done sequentially, a server with N connections whose peers go silent takes up to N * CLOSE_TIMEOUT_MS to release, delaying scope exit. Running them concurrently with all bounds worst-case teardown to a single timeout.

♻️ Proposed fix
-      for (let connection of [...live]) {
-        yield* connection.close(1001, "server shutting down");
-      }
+      yield* all(
+        [...live].map((connection) =>
+          connection.close(1001, "server shutting down")
+        ),
+      );

Add all to the effection import:

 import {
+  all,
   createQueue,
   createSignal,
   each,
   resource,
   scoped,
   spawn,
 } from "effection";

Please confirm all is exported by effection 4.0.0 and that concurrent close() calls don't contend on shared teardown state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let connection of [...live]) {
yield* connection.close(1001, "server shutting down");
}
yield* all(
[...live].map((connection) =>
connection.close(1001, "server shutting down")
),
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@websocket/server.ts` around lines 159 - 161, The teardown loop in the server
shutdown path currently closes each live connection sequentially via
`connection.close(...)`, which can stretch shutdown to N times the close
timeout. Update the shutdown logic to run the per-connection close effects
concurrently using Effection’s `all` in the `server.ts` teardown flow, and add
`all` to the Effection import. Keep the existing `close(1001, "server shutting
down")` behavior inside the per-connection task, and verify that the `close()`
calls remain safe when executed in parallel.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In general this is a good idea, but I believe that .close() is a synchronous operation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd server.ts websocket -x cat -n {}
echo "-----websocket.ts-----"
fd websocket.ts websocket -x cat -n {}

Length of output: 16955


🏁 Script executed:

#!/bin/bash
fd package.json websocket -x cat {}
echo "---"
rg -n "effection" websocket/package.json
echo "--- check effection exports for 'all' ---"
find / -path /proc -prune -o -type d -name "effection" -print 2>/dev/null | head -20

Length of output: 1783


🤖 web_search executed:

Does Effection (effection npm package) version 3 and 4 export an all function for running multiple operations concurrently?

Length of output: 7341


Confirmed all is exported by Effection in both v3 and v4 (it's been a core API since v3, with a fix for its typing in 3.0.1), so that part of my suggestion is safe.

On the substance of your point: looking at websocket.ts, WebSocketResource.close() (Lines 191-193) is a generator method that calls closeSocket(), which does socket.close(code, reason) (the synchronous, fire-and-forget native call) and then yield* timebox(CLOSE_TIMEOUT_MS, () => closed) (Lines 158-160) — waiting up to CLOSE_TIMEOUT_MS (1000ms) for the peer's close handshake to resolve. So while the underlying socket.close() call is synchronous, the WebSocketResource.close() operation itself is not — it suspends until the close event fires or the timebox expires.

That's exactly why sequential yield* connection.close(...) in server.ts's teardown loop (Lines 159-161) can cost up to N * CLOSE_TIMEOUT_MS if there are N live connections with unresponsive peers, since each iteration blocks on its own timebox before starting the next. Running them concurrently with all would bound that to a single CLOSE_TIMEOUT_MS regardless of N.

Given close() calls only touch per-connection state (each connection's own socket and closed resolver), concurrent execution shouldn't contend on shared teardown state — live is only read (via the [...live] snapshot) before the closes are kicked off, not mutated by close() itself.

Comment thread websocket/server.ts
*
* await main(function* () {
* let server = yield* useWebSocketServer<string>(
* () => new WebSocketServer({ port: 3000 }) as unknown as WebSocketServerLike,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need to cast here? Is WebSocketServer not WebSocketServerLike?

Comment thread websocket/server.ts
});

// accept connections. Each is handled in its own task wrapped in `scoped`,
// which is a real error boundary (its trap/delimiter contains a crash) — so

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

trap/delimiter are private concepts and should not be referenced in comments

Comment thread websocket/server.ts
Comment on lines +159 to +161
for (let connection of [...live]) {
yield* connection.close(1001, "server shutting down");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

put inside an ensure() ?

Comment thread websocket/server.ts
Comment on lines +159 to +161
for (let connection of [...live]) {
yield* connection.close(1001, "server shutting down");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In general this is a good idea, but I believe that .close() is a synchronous operation.

Comment thread websocket/server.ts
Comment on lines +148 to +152
yield* provide({
*[Symbol.iterator]() {
return connections;
},
errors,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The problem with this is that it subtly breaks the stream contract which is that it tends to be stateless, and that when you subscribe to a stream, that is when state is activated.

Is it not better to make the web socket server a subscription, which is more honest to the consumer that "this will allocate state and begin filling a buffer"?

Comment thread websocket/server.ts
Comment on lines +124 to +125
live.add(connection);
connections.add(connection);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are we storing these connections in two separate sets?

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.

2 participants