{"id":22069957,"url":"https://github.com/napolab/durabcast","last_synced_at":"2025-04-12T22:55:15.277Z","repository":{"id":251171401,"uuid":"836162715","full_name":"napolab/durabcast","owner":"napolab","description":"DurabCast simplifies WebSocket management with Cloudflare Durable Objects, handling complex configurations out of the box.","archived":false,"fork":false,"pushed_at":"2024-10-31T02:30:35.000Z","size":179,"stargazers_count":35,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-12T22:55:13.714Z","etag":null,"topics":["cloudflare-workers","durable-objects","websocket"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/durabcast","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/napolab.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-31T09:24:18.000Z","updated_at":"2025-04-05T10:09:01.000Z","dependencies_parsed_at":"2024-08-01T09:31:08.037Z","dependency_job_id":"ed73cd2d-a5b1-4392-a8f9-9b88b19d1923","html_url":"https://github.com/napolab/durabcast","commit_stats":null,"previous_names":["napolab/duracast"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/napolab%2Fdurabcast","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/napolab%2Fdurabcast/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/napolab%2Fdurabcast/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/napolab%2Fdurabcast/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/napolab","download_url":"https://codeload.github.com/napolab/durabcast/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248643050,"owners_count":21138353,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare-workers","durable-objects","websocket"],"created_at":"2024-11-30T20:14:14.732Z","updated_at":"2025-04-12T22:55:15.250Z","avatar_url":"https://github.com/napolab.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DurabCast\n\n`DurabCast` is a library for easily handling WebSockets with Cloudflare Durable Objects. It simplifies the setup and management of WebSocket connections by implementing complex configurations out of the box.\n\n## Features\n\n- **Connection Monitoring and Auto-Close**\n\n  - Monitor and close idle connections automatically.\n  - `interval` and `timeout` can be configured through options.\n  - Opt-out of auto-closing by setting `autoClose` to `false`.\n\n- **Client-Side Keep-Alive with `pingWebSocket`**\n\n  - Use the `pingWebSocket` function on the client side to send periodic ping messages.\n  - Ensures the connection remains active when `autoClose` is enabled on the server.\n\n- **Message Broadcasting**\n\n  - Broadcast messages to other connected clients.\n  - Override `webSocketMessage` to customize message handling.\n\n- **Connection Alive Check**\n\n  - Use `isAliveSocket` to check if a connection is still alive.\n\n## Installation\n\n```sh\nnpm install -D @cloudflare/workers-types\nnpm install durabcast\n```\n\n## Basic Usage\n\nHere is a simple example to get started with `DurabCast`.\n\n### `wrangler.toml`\n\n```toml\nname = \"sample\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-07-18\"\n\n[[durable_objects.bindings]]\nclass_name = \"BroadcastMessage\"\nname = \"BROADCAST_MESSAGE\"\n\n[[migrations]]\ntag = \"v1\"\nnew_classes = [\"BroadcastMessage\"]\n```\n\n### `index.ts`\n\n```ts\nimport { BroadcastMessage, type BroadcastMessageAppType } from \"durabcast\";\nimport { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport { hc } from \"hono/client\";\nimport { z } from \"zod\";\nimport { upgrade } from \"durabcast/helpers/upgrade\";\n\ntype Env = {\n  Bindings: {\n    BROADCAST_MESSAGE: DurableObjectNamespace\u003cBroadcastMessage\u003e;\n  };\n};\n\nconst app = new Hono\u003cEnv\u003e();\n\nconst route = app\n  .get(\n    \"/rooms/:roomId\",\n    upgrade(),\n    zValidator(\"query\", z.object({ uid: z.string() })),\n    async (c) =\u003e {\n      const roomId = c.req.param(\"roomId\");\n      const uid = c.req.valid(\"query\").uid;\n      const id = c.env.BROADCAST_MESSAGE.idFromName(roomId);\n      const stub = c.env.BROADCAST_MESSAGE.get(id);\n\n      const baseURL = new URL(\"/\", c.req.url);\n      const client = hc\u003cBroadcastMessageAppType\u003e(baseURL.toString(), {\n        fetch: stub.fetch.bind(stub),\n      });\n\n      const res = await client.rooms[\":roomId\"].$get(\n        { query: { uid }, param: { roomId } },\n        { init: { headers: c.req.raw.headers } },\n      );\n\n      return new Response(null, {\n        webSocket: res.webSocket,\n        status: res.status,\n        headers: res.headers,\n        statusText: res.statusText,\n      });\n    },\n  )\n  .post(\n    \"/rooms/:roomId/broadcast\",\n    zValidator(\"json\", z.object({ message: z.string() })),\n    async (c) =\u003e {\n      const roomId = c.req.param(\"roomId\");\n      const id = c.env.BROADCAST_MESSAGE.idFromName(roomId);\n      const stub = c.env.BROADCAST_MESSAGE.get(id);\n\n      await stub.broadcast(c.req.valid(\"json\").message);\n      return c.json(null, 200);\n    },\n  );\n\nexport { BroadcastMessage };\n```\n\n## Client-Side Keep-Alive with `pingWebSocket`\n\nWhen the `autoClose` feature is enabled on the server side, the server automatically closes idle connections after a specified timeout. To ensure that the client connection remains active, you can use the `pingWebSocket` function on the client side to send periodic ping messages.\n\n### `pingWebSocket` Function\n\nThe `pingWebSocket` function sends a ping message to the server at regular intervals. This keeps the connection alive by resetting the idle timeout on the server side.\n\n#### Usage\n\n```typescript\nimport { pingWebSocket } from \"durabcast/helpers/client\";\n\nconst ws = new WebSocket(\"wss://your-server.com/rooms/room123\");\n\n// Start sending ping messages every 30 seconds\nconst unsubscribe = pingWebSocket(ws, { interval: 30000, ping: \"ping\" });\n\n// To stop sending pings, call the unsubscribe function\n// unsubscribe();\n```\n\n#### Parameters\n\n- **`ws`**: The WebSocket instance to send ping messages through.\n- **`options`** (optional): An object to configure the ping behavior.\n  - **`interval`**: The interval (in milliseconds) at which to send ping messages. Defaults to `10000` (10 seconds).\n  - **`ping`**: The ping message to send. Defaults to `'ping'`.\n\n### Benefits\n\n- **Keeps Connection Alive**: Ensures that the server recognizes the connection as active.\n- **Prevents Unintentional Disconnections**: Avoids the connection being closed by the server's auto-close mechanism due to inactivity.\n- **Configurable**: Allows customization of the ping interval and message.\n\n### Example\n\n```typescript\nimport { pingWebSocket } from \"durabcast/helpers/client\";\n\nconst ws = new WebSocket(\"wss://your-server.com/rooms/room123\");\n\nws.onopen = () =\u003e {\n  // Start sending pings every 30 seconds\n  const unsubscribe = pingWebSocket(ws, {\n    interval: 30000,\n    ping: \"keep-alive\",\n  });\n\n  // Handle incoming messages\n  ws.onmessage = (event) =\u003e {\n    console.log(\"Received:\", event.data);\n  };\n\n  // Optionally, stop sending pings when needed\n  // unsubscribe();\n};\n```\n\n## Advanced Usage\n\n### Extending the `BroadcastMessage` Class\n\nYou can extend the `BroadcastMessage` class to customize the behavior of your WebSocket connections.\n\n```ts\nimport { BroadcastMessage, type BroadcastMessageOptions } from \"durabcast\";\n\nclass CustomBroadcastMessage extends BroadcastMessage {\n  protected options: BroadcastMessageOptions = {\n    interval: 30000, // Check every 30 seconds\n    timeout: 60000, // Close connection if idle for 60 seconds\n    autoClose: true, // Enable auto-close\n    requestResponsePair: {\n      request: \"ping\",\n      response: \"pong\",\n    },\n  };\n\n  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {\n    // Broadcast message to other clients\n    this.broadcast(message, {\n      excludes: [ws],\n    });\n  }\n}\n\n// Use CustomBroadcastMessage in your Durable Object binding\n```\n\n### Combining with `pingWebSocket`\n\nWhen `autoClose` is enabled, it's important to ensure that the client sends periodic messages to keep the connection alive. By using `pingWebSocket` on the client side, you can automatically send these keep-alive messages.\n\n#### Server-Side Configuration\n\n```ts\nclass CustomBroadcastMessage extends BroadcastMessage {\n  protected options: BroadcastMessageOptions = {\n    autoClose: true, // Enable auto-close\n    interval: 30000, // Check every 30 seconds\n    timeout: 60000, // Close if idle for 60 seconds\n    requestResponsePair: {\n      request: \"ping\",\n      response: \"pong\",\n    },\n  };\n\n  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {\n    // Handle ping-pong messages internally\n    if (message === this.REQUEST_RESPONSE_PAIR.request) {\n      ws.send(this.REQUEST_RESPONSE_PAIR.response);\n      return;\n    }\n\n    // Broadcast other messages\n    this.broadcast(message, { excludes: [ws] });\n  }\n}\n```\n\n#### Client-Side Usage\n\n```typescript\nimport { pingWebSocket } from \"durabcast/helpers/client\";\n\nconst ws = new WebSocket(\"wss://your-server.com/rooms/room123\");\n\nws.onopen = () =\u003e {\n  // Start sending pings to keep the connection alive\n  const unsubscribe = pingWebSocket(ws, {\n    interval: 30000, // Every 30 seconds\n    ping: \"ping\", // Must match server's expected request\n  });\n\n  ws.onmessage = (event) =\u003e {\n    // Handle incoming messages\n    console.log(\"Received:\", event.data);\n  };\n};\n```\n\n### Why Use `pingWebSocket` with `autoClose`\n\n- **Seamless Integration**: `pingWebSocket` is designed to work with the server's `autoClose` feature, ensuring connections remain active as needed.\n- **Resource Optimization**: By automatically closing idle connections, the server conserves resources, and `pingWebSocket` ensures that active clients are not disconnected.\n- **Consistency**: Using standardized ping messages simplifies the client-server communication protocol.\n\n## API\n\n### Connection Monitoring and Auto-Close\n\nThe library monitors connections and can automatically close idle ones based on the configured `interval` and `timeout`. This behavior can be turned off by setting `autoClose` to `false` in the options.\n\n#### Example\n\n```ts\nimport { BroadcastMessage, type BroadcastMessageOptions } from \"durabcast\";\n\nclass CustomBroadcastMessage extends BroadcastMessage {\n  protected options: BroadcastMessageOptions = {\n    interval: 30000, // Check every 30 seconds\n    timeout: 60000, // Close connection if idle for 60 seconds\n    autoClose: true, // Enable auto-close\n    requestResponsePair: {\n      request: \"ping\",\n      response: \"pong\",\n    },\n  };\n\n  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {\n    // Handle ping-pong messages internally\n    if (message === this.REQUEST_RESPONSE_PAIR.request) {\n      ws.send(this.REQUEST_RESPONSE_PAIR.response);\n      return;\n    }\n\n    // Broadcast other messages\n    this.broadcast(message, {\n      excludes: [ws],\n    });\n  }\n}\n\n// In your Durable Object binding, use CustomBroadcastMessage\nexport { CustomBroadcastMessage as BroadcastMessage };\n```\n\n### Message Broadcasting\n\nMessages can be broadcasted to other connected clients. You can override the `webSocketMessage` method to customize how messages are handled.\n\n```ts\nclass CustomBroadcastMessage extends BroadcastMessage {\n  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {\n    // Custom message handling logic\n    this.broadcast(message, {\n      excludes: [ws],\n    });\n  }\n}\n```\n\n### Connection Alive Check\n\nThe `isAliveSocket` method checks if a connection is still alive.\n\n```ts\nconst isAlive = this.isAliveSocket(ws);\nif (!isAlive) {\n  ws.close();\n}\n```\n\n## Extending the `BroadcastMessage` Class\n\nWhen extending the `BroadcastMessage` class, you can access and modify the `options` field to customize the behavior of your WebSocket connections.\n\n### `options`\n\nThe `options` field allows you to configure the behavior of your WebSocket connections. It is a protected field, meaning it can be accessed and modified within any class that extends `BroadcastMessage`.\n\n#### Fields\n\n- **`interval`**: The interval (in milliseconds) at which to check for idle connections.\n- **`timeout`**: The timeout (in milliseconds) after which idle connections are closed.\n- **`autoClose`**: A boolean indicating whether to automatically close idle connections. Set to `false` to opt out of this behavior.\n- **`requestResponsePair`**: An object containing `request` and `response` strings used for ping-pong style connection checks.\n\n### Protected Methods and Fields\n\nThese protected methods and fields are available within any class that extends `BroadcastMessage`:\n\n- **`AUTO_CLOSE`**: Returns the value of `options.autoClose`, defaulting to `true` if not set.\n- **`INTERVAL`**: Returns the value of `options.interval`, defaulting to `30000` (30 seconds) if not set.\n- **`TIMEOUT`**: Returns the value of `options.timeout`, defaulting to `60000` (60 seconds) if not set.\n- **`REQUEST_RESPONSE_PAIR`**: Returns a `WebSocketRequestResponsePair` object using the `options.requestResponsePair` values, defaulting to `'ping'` and `'pong'` if not set.\n- **`sessions`**: A set of active WebSocket sessions.\n\n### Example\n\n```ts\nclass CustomBroadcastMessage extends BroadcastMessage {\n  protected options: BroadcastMessageOptions = {\n    interval: 30000, // Check every 30 seconds\n    timeout: 60000, // Close connection if idle for 60 seconds\n    autoClose: true, // Enable auto-close\n    requestResponsePair: {\n      request: \"ping\",\n      response: \"pong\",\n    },\n  };\n\n  protected get AUTO_CLOSE() {\n    return this.options.autoClose ?? true;\n  }\n\n  protected get INTERVAL() {\n    return this.options.interval ?? 30000;\n  }\n\n  protected get TIMEOUT() {\n    return this.options.timeout ?? 60000;\n  }\n\n  protected get REQUEST_RESPONSE_PAIR() {\n    return new WebSocketRequestResponsePair(\n      this.options.requestResponsePair?.request ?? \"ping\",\n      this.options.requestResponsePair?.response ?? \"pong\",\n    );\n  }\n\n  protected sessions = new Set\u003cWebSocket\u003e();\n\n  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {\n    // Handle ping-pong messages\n    if (message === this.REQUEST_RESPONSE_PAIR.request) {\n      ws.send(this.REQUEST_RESPONSE_PAIR.response);\n      return;\n    }\n\n    // Custom message handling logic\n    this.broadcast(message, {\n      excludes: [ws],\n    });\n  }\n}\n```\n\n## License\n\nMIT License. See [LICENSE](./LICENSE) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnapolab%2Fdurabcast","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnapolab%2Fdurabcast","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnapolab%2Fdurabcast/lists"}