Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 97 additions & 1 deletion websocket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ await main(function* () {
let socket = yield* useWebSocket("ws://websocket.example.org");

// Send messages to the server
socket.send("Hello World");
yield* socket.send("Hello World");

// Receive messages using a simple iterator
for (let message of yield* each(socket)) {
Expand All @@ -36,6 +36,16 @@ await main(function* () {
});
```

By default, teardown waits up to one second for the peer's close handshake.
Configure that deadline when creating the resource if your environment needs a
different shutdown policy:

```typescript
let socket = yield* useWebSocket("ws://websocket.example.org", {
closeTimeout: 5_000,
});
```

## Features

- **Ready-to-use Connections**: `useWebSocket()` returns only after the
Expand All @@ -46,6 +56,92 @@ await main(function* () {
- **Clean Resource Management**: Connections are properly cleaned up when the
operation completes

## WebSocket Server

`useWebSocketServer()` is the server counterpart of `useWebSocket()`. It hands
back a subscription of incoming connections, where **each connection is the same
full-duplex `WebSocketResource`** produced by the client — you receive messages
by iterating it and reply with `yield* connection.send()`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

The underlying server is supplied through a factory, so this package never
imports a concrete server implementation and stays platform-agnostic. On Node
this is typically the [`ws`](https://github.com/websockets/ws) `WebSocketServer`.

```typescript
import { each, main, spawn } from "effection";
import { WebSocketServer } from "ws";
import { useWebSocketServer } from "@effectionx/websocket";

await main(function* () {
let connections = yield* useWebSocketServer<string>(
() => new WebSocketServer({ port: 3000 }),
{ closeTimeout: 5_000 },
);

// Connections are read one at a time, so spawn a handler per connection to
// serve many clients concurrently.
while (true) {
let { value: connection } = yield* connections.next();
yield* spawn(function* () {
for (let message of yield* each(connection)) {
yield* connection.send(`echo: ${message.data}`);
yield* each.next();
}
});
}
});
```

A client — using `useWebSocket()` from the same package — pairs with it directly.
Because `send` is an `Operation`, invoke it with `yield*` on both sides:

```typescript
import { each, main } from "effection";
import { useWebSocket } from "@effectionx/websocket";

await main(function* () {
let socket = yield* useWebSocket<string>("ws://localhost:3000");

yield* socket.send("hello"); // client -> server

for (let message of yield* each(socket)) {
console.log(message.data); // "echo: hello" (server -> client)
yield* each.next();
}
});
```

Connections are buffered from the moment the resource is created, so none are
dropped before you start reading. That is why the server is a subscription rather
than a stream: reading a connection consumes it, and every consumer draws from
the same buffer instead of getting an independent replay.

The server — and every live connection it produced — is automatically closed when
the resource passes out of scope, with close code `1001` ("going away"). The
server's second argument configures the close-handshake timeout for every
accepted connection.

### Observing connection failures

The two kinds of failure are reported differently. An error on the server itself
crashes the resource's scope, reaching your error boundary like any other
failure. An error on a single connection is isolated so it cannot take the server
down, and is published on `server.errors` instead — spawn a task to watch that
stream if you want to see them:

```typescript
yield* spawn(function* () {
for (let error of yield* each(connections.errors)) {
// a socket failure throws the DOM `error` event, which is not an `Error`;
// effection 4.1+ boxes it and keeps the original on `cause`
console.error("connection failed:", (error as Error)?.cause ?? error);
yield* each.next();
}
});
```

`errors` is lossy: failures raised while nobody is subscribed are not buffered.

## Advanced Usage

### Custom WebSocket Implementations
Expand Down
1 change: 1 addition & 0 deletions websocket/mod.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./websocket.ts";
export * from "./server.ts";
8 changes: 6 additions & 2 deletions websocket/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@effectionx/websocket",
"description": "WebSocket client with stream-based message handling and automatic cleanup",
"version": "2.3.4",
"description": "WebSocket client and server with stream-based message handling and automatic cleanup",
"version": "3.0.0",
"keywords": ["io", "streams"],
"type": "module",
"main": "./dist/mod.js",
Expand All @@ -15,6 +15,10 @@
}
},
"files": ["dist"],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"dependencies": {
"@effectionx/node": "workspace:*",
"@effectionx/timebox": "workspace:*"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"peerDependencies": {
"effection": "^3 || ^4"
},
Expand Down
Loading
Loading