{"id":19522283,"url":"https://github.com/anycable/anycable-serverless-js","last_synced_at":"2025-04-26T09:32:07.251Z","repository":{"id":196168886,"uuid":"694933301","full_name":"anycable/anycable-serverless-js","owner":"anycable","description":"AnyCable channels API for serverless","archived":false,"fork":false,"pushed_at":"2024-12-09T19:03:17.000Z","size":121,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-04T10:36:35.625Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anycable.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"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":"2023-09-22T02:00:10.000Z","updated_at":"2025-03-26T18:42:56.000Z","dependencies_parsed_at":"2024-11-11T00:38:08.690Z","dependency_job_id":"2008fdc2-ec5e-4204-824f-8cd731d27986","html_url":"https://github.com/anycable/anycable-serverless-js","commit_stats":null,"previous_names":["anycable/anycable-serverless-js"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-serverless-js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-serverless-js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-serverless-js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-serverless-js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anycable","download_url":"https://codeload.github.com/anycable/anycable-serverless-js/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250967242,"owners_count":21515565,"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":[],"created_at":"2024-11-11T00:38:05.460Z","updated_at":"2025-04-26T09:32:07.240Z","avatar_url":"https://github.com/anycable.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm version](https://badge.fury.io/js/@anycable%2Fserverless-js.svg)](https://badge.fury.io/js/@anycable%2Fserverless-js)\n\n# AnyCable Serverless\n\nThis package provides modules to implement [AnyCable](https://anycable.io) backend APIs to be executed in serverless Node.js environments. (Works with serverful apps, too, of course.)\n\n\u003e See our [demo application](https://github.com/anycable/vercel-anycable-demo) for a working example.\n\n## Architecture and components\n\nThis package provides functionality to work with AnyCable server from a JS/TS backend applications and includes support for the following features:\n\n- [JWT authentication](#using-anycable-jwt)\n- [Signed streams](#signed-streams)\n- [Broadcasting](#broadcasting)\n\nThe package also comes with HTTP handlers to handle [AnyCable RPC-over-HTTP](https://docs.anycable.io/anycable-go/rpc) requests and provides **channels** and **application** abstractions to describe real-time features of your application.\n\n## Usage\n\nInstall the `@anycable/serverless-js` package using your tool of choice, e.g.:\n\n```sh\nnpm install @anycable/serverless-js\n```\n\n### Using AnyCable JWT\n\n[AnyCable JWT](https://docs.anycable.io/anycable-go/jwt_identification) is a recommended way to authenticate your clients. To get started, you can use the identificator object and generate auth tokens with it:\n\n```js\nimport { identificator } from \"@anycable/serverless-js\";\n\nconst jwtSecret = \"very-secret\";\nconst jwtTTL = \"1h\";\n\nexport const identifier = identificator(jwtSecret, jwtTTL);\n\n// Then, somewhere in your code, generate a token and provide it to the client\nconst userId = authenticatedUser.id;\nconst token = await identifier.generateToken({ userId });\n\nconst cableURL = `${CABLE_URL}?jid=${token}`\n```\n\nYou can pass any identification information to the token. It can be later used in _channels_ (see below).\n\n### Signed streams\n\nYou can create a _signer_ instance to generate signed streams names and use them with [AnyCable pub/sub](https://docs.anycable.io/anycable-go/signed_streams):\n\n```js\nimport { signer } from \"@anycable/serverless-js\";\n\nconst streamsSecret = process.env.ANYCABLE_STREAMS_SECRET;\n\nconst sign = signer(secret);\n\nconst signedStreamName = sign(\"room/13\");\n```\n\nThen, you can use the generated stream name with your client (using [AnyCable JS client SDK](https://github.com/anycable/anycable-client)):\n\n```js\nimport { createCable } from \"@anycable/web\";\n\nconst cable = createCable(WEBSOCKET_URL);\nconst stream = await fetchStreamForRoom(\"13\");\n\nconst channel = cable.streamFromSigned(stream);\nchannel.on(\"message\", (msg) =\u003e {\n  // handle notification\n})\n```\n\n### Broadcasting\n\nTo **broadcast** messages to connected clients, you must use a broadcaster instance:\n\n```js\nimport { broadcaster } from \"@anycable/serverless-js\";\n\n// Broadcasting configuration\nconst broadcastURL =\n  process.env.ANYCABLE_BROADCAST_URL || \"http://127.0.0.1:8090/_broadcast\";\nconst broadcastToken = process.env.ANYCABLE_HTTP_BROADCAST_SECRET || \"\";\n\n// Create a broadcasting function to send broadcast messages via HTTP API\nexport const broadcastTo = broadcaster(broadcastURL, broadcastToken);\n```\n\nCurrently, this package only supports broadcasting over HTTP. However, AnyCable provides different [broadcasting adapters](https://docs.anycable.io/anycable-go/broadcasting) (e.g., Redis, NATS, etc.) that you can integrate yourself.\n\n### Using channels (AnyCable RPC)\n\nAn **application** instance is responsible for handling the connection lifecycle and dispatching messages to the appropriate channels.\n\n```js\n// api/cable.ts\nimport {\n  Application,\n  ConnectionHandle,\n  broadcaster,\n} from \"@anycable/serverless-js\";\n\n// Some custom authentication logic\nimport { verifyToken } from \"./auth\";\n\n// The identifiers  type describe connection identifiers—e.g., user ID, username, etc.\nexport type CableIdentifiers = {\n  userId: string;\n};\n\n// Application instance handles connection lifecycle events\nclass CableApplication extends Application\u003cCableIdentifiers\u003e {\n  // IMPORTANT: When using AnyCable JWT, you don't need to define\n  // the connect() callback, authentication doesn't hit your server\n  async connect(handle: ConnectionHandle\u003cCableIdentifiers\u003e) {\n    const url = handle.env.url;\n    const params = new URL(url).searchParams;\n\n    if (params.has(\"token\")) {\n      const payload = await verifyToken(params.get(\"token\")!);\n\n      if (payload) {\n        const { userId } = payload;\n\n        handle.identifiedBy({ userId });\n      }\n      return;\n    }\n\n    // Reject connection if not authenticated\n    handle.reject();\n  }\n\n  async disconnect(handle: ConnectionHandle\u003cCableIdentifiers\u003e) {\n    // Here you can perform any cleanup work\n    console.log(`User ${handle.identifiers!.userId} disconnected`);\n  }\n}\n\n// Create and instance of the class to use in HTTP handlers (see the next section)\nconst app = new CableApplication();\n\n// Register channels (see below)\n\nexport default app;\n```\n\n**Channels** instances reflect particular features (e.g, chat room, notifications, etc.) and are responsible for handling incoming commands and subscription lifecycle events:\n\n```js\nimport { Channel, ChannelHandle } from \"@anycable/serverless-js\";\n// We re-using the identifiers type from the cable application\nimport type { CableIdentifiers } from \"../cable\";\n\n// Define the channel params (used by the client according to Action Cable protocol)\ntype ChatChannelParams = {\n  roomId: string;\n};\n\nexport type ChatMessage = {\n  id: string;\n  username: string;\n  body: string;\n  createdAt: string;\n};\n\nexport default class ChatChannel\n  extends Channel\u003cCableIdentifiers, ChatChannelParams, ChatMessage\u003e\n{\n  // The `subscribed` method is called when the client subscribes to the channel\n  // You can use it to authorize the subscription and setup streaming\n  async subscribed(\n    handle: ChannelHandle\u003cCableIdentifiers\u003e,\n    params: ChatChannelParams | null,\n  ) {\n    if (!params) {\n      handle.reject();\n      return;\n    }\n\n    if (!params.roomId) {\n      handle.reject();\n      return;\n    }\n\n    handle.streamFrom(`room:${params.roomId}`);\n  }\n\n  // This method is called by the client\n  async sendMessage(\n    handle: ChannelHandle\u003cCableIdentifiers\u003e,\n    params: ChatChannelParams,\n    data: SentMessage,\n  ) {\n    const { body } = data;\n\n    if (!body) {\n      throw new Error(\"Body is required\");\n    }\n\n    console.log(\n      `User ${handle.identifiers!.username} sent message: ${data.body}`,\n    );\n\n    const message: ChatMessage = {\n      id: Math.random().toString(36).substr(2, 9),\n      username: handle.identifiers!.username,\n      body,\n      createdAt: new Date().toISOString(),\n    };\n\n    // Broadcast the message to all subscribers (see below)\n    await broadcastTo(`room:${params.roomId}`, message);\n  }\n}\n\n// You MUST register a channel instance within the application\n// The client MUST use the provided identifier to subscribe to the channel.\napp.registerChannel(\"chat\", new ChatChannel());\n```\n\n### HTTP handlers\n\nTo glue our HTTP layer with the channels, we need to configure HTTP handlers. Below you can find an examples for popular serverless platforms.\n\n#### Vercel\n\nDefine [Vercel](https://vercel.com) serverless functions as follows:\n\n```js\n// api/anycable/connect/route.ts\nimport { NextResponse } from \"next/server\";\nimport { connectHandler, Status } from \"@anycable/serverless-js\";\nimport app from \"../../cable\";\n\nexport async function POST(request: Request) {\n  try {\n    const response = await connectHandler(request, app);\n    return NextResponse.json(response, {\n      status: 200,\n    });\n  } catch (e) {\n    console.error(e);\n    return NextResponse.json({\n      status: Status.ERROR,\n      error_msg: \"Server error\",\n    });\n  }\n}\n\n// api/anycable/command/route.ts\nimport { NextResponse } from \"next/server\";\nimport { commandHandler, Status } from \"@anycable/serverless-js\";\nimport app from \"../../cable\";\n\nexport async function POST(request: Request) {\n  try {\n    const response = await commandHandler(request, app);\n    return NextResponse.json(response, {\n      status: 200,\n    });\n  } catch (e) {\n    console.error(e);\n    return NextResponse.json({\n      status: Status.ERROR,\n      error_msg: \"Server error\",\n    });\n  }\n}\n\n// api/anycable/disconnect/route.ts\nimport { NextResponse } from \"next/server\";\nimport { disconnectHandler, Status } from \"@anycable/serverless-js\";\nimport app from \"../../cable\";\n\nexport async function POST(request: Request) {\n  try {\n    const response = await disconnectHandler(request, app);\n    return NextResponse.json(response, {\n      status: 200,\n    });\n  } catch (e) {\n    console.error(e);\n    return NextResponse.json({\n      status: Status.ERROR,\n      error_msg: \"Server error\",\n    });\n  }\n}\n```\n\nYou can also avoid repeatition by using a universal handler and a bit of configuration:\n\n```js\n// next.config.js\nconst nextConfig = {\n  // ...\n  rewrites: async () =\u003e {\n    return [\n      {\n        source: \"/api/anycable/:path*\",\n        destination: \"/api/anycable\",\n      },\n    ];\n  },\n};\n\n// ...\n```\n\nAnd then you can use the following handler:\n\n```js\n// api/anycable/route.ts\nimport { NextResponse } from \"next/server\";\nimport { handler, Status } from \"@anycable/serverless-js\";\nimport app from \"../../cable\";\n\nexport async function POST(request: Request) {\n  try {\n    const response = await handler(request, app);\n    return NextResponse.json(response, {\n      status: 200,\n    });\n  } catch (e) {\n    console.error(e);\n    return NextResponse.json({\n      status: Status.ERROR,\n      error_msg: \"Server error\",\n    });\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanycable%2Fanycable-serverless-js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanycable%2Fanycable-serverless-js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanycable%2Fanycable-serverless-js/lists"}