{"id":18583016,"url":"https://github.com/astahmer/partyrpc","last_synced_at":"2025-10-18T01:02:17.074Z","repository":{"id":188742410,"uuid":"679335301","full_name":"astahmer/partyrpc","owner":"astahmer","description":"Partykit + RPC. Move Fast (and Break Everything). Everything is better with typesafety.","archived":false,"fork":false,"pushed_at":"2023-12-13T20:10:43.000Z","size":265,"stargazers_count":29,"open_issues_count":2,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-05-02T01:13:16.685Z","etag":null,"topics":["partykit","rpc","typesafe","websocket"],"latest_commit_sha":null,"homepage":"","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/astahmer.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}},"created_at":"2023-08-16T15:57:01.000Z","updated_at":"2024-04-29T13:08:29.000Z","dependencies_parsed_at":"2023-12-12T15:57:25.619Z","dependency_job_id":null,"html_url":"https://github.com/astahmer/partyrpc","commit_stats":null,"previous_names":["astahmer/partyrpc"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Fpartyrpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Fpartyrpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Fpartyrpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Fpartyrpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/astahmer","download_url":"https://codeload.github.com/astahmer/partyrpc/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248208689,"owners_count":21065205,"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":["partykit","rpc","typesafe","websocket"],"created_at":"2024-11-07T00:18:57.841Z","updated_at":"2025-10-18T01:02:12.025Z","avatar_url":"https://github.com/astahmer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# partyrpc\n\n[Partykit](https://partykit.io/) + [RPC](https://trpc.io/) = PartyRPC Move Fast (and Break Everything). Everything is\nbetter with typesafety.\n\n## Install\n\n`pnpm i partyrpc`\n\n## Usage\n\n### Terminology\n\n- Events: what WS messages your party server expects to receive from clients\n- Responses: what WS messages your party server sends back to clients\n\n### WS Events\n\nDefine your (safe) party events and responses:\n\n```ts\n// src/safe-party.ts\nimport * as v from \"valibot\";\nimport { createPartyRpc } from \"partyrpc/server\";\n\ntype UContext = { counter: number };\ntype PongResponse = { type: \"pong\"; size: number };\ntype LatencyResponse = { type: \"latency\"; id: string };\ntype CounterResponse = { type: \"counter\"; counter: number };\n\ntype PartyResponses = PongResponse | LatencyResponse | CounterResponse;\n\nconst party = createPartyRpc\u003cPartyResponses, UContext\u003e();\n\nexport const safeParty = party.events({\n  ping: {\n    schema: v.never(),\n    onMessage(message, ws, room, ctx) {\n      party.send(ws, { type: \"pong\", size: room.connections.size });\n    },\n  },\n  latency: {\n    schema: v.object({ id: v.string() }),\n    onMessage(message, ws, room, ctx) {\n      party.send(ws, { type: \"latency\", id: message.id });\n    },\n  },\n  \"add-to-counter\": {\n    schema: v.object({ amount: v.number() }),\n    onMessage(message, ws, room, ctx) {\n      ctx.counter += message.amount;\n      party.send(ws, { type: \"counter\", counter: ctx.counter });\n    },\n  },\n});\n\nexport type SafePartyEvents = typeof safeParty.events;\nexport type SafePartyResponses = typeof safeParty.responses;\n```\n\nBind it to your party server:\n\n```ts\n// src/server.ts\nimport * as Party from \"partykit/server\";\nimport { safeParty } from \"./safe-party\";\n\n// optional context\nconst ctx = { counter: 0 };\n\nexport default class Server implements Party.Server {\n  constructor(readonly party: Party.Party) {}\n\n  onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {\n    conn.addEventListener(\"message\", (evt) =\u003e {\n      safeParty.onMessage(evt.data, conn, this.party, userCtx);\n    });\n  }\n}\n\nServer satisfies Party.Worker;\n```\n\nFinally, create your party client:\n\n```ts\n// src/client.ts\nimport PartySocket from \"partysocket\";\nimport { createPartyClient } from \"partyrpc/client\";\nimport { SafePartyEvents, SafePartyResponses } from \"./safe-party\";\n\nconst partySocket = new PartySocket({\n  host: PARTYKIT_HOST,\n  room: \"some-room\",\n});\nconst client = createPartyClient\u003cSafePartyEvents, SafePartyResponses\u003e(partySocket, { debug: true });\n```\n\nSubscribe to typesafe responses:\n\n```ts\n// src/clients.ts\n\nclient.on(\"latency\", (msg) =\u003e {\n  // msg is typed as LatencyResponse, defined above as { type: \"latency\"; id: string }\n});\n\nclient.on(\"pong\", (msg) =\u003e {\n  console.log(\"got pong\", msg.size);\n  // msg is typed as PongResponse, defined above as { type: \"pong\"; size: number }\n});\n\nclient.on(\"counter\", (msg) =\u003e {\n  // msg is typed as CounterResponse, defined above as { type: \"counter\"; counter: number }\n});\n```\n\nSend typesafe events:\n\n```ts\n// src/clients.ts\n\nclient.send({ type: \"ping\" }); // ✅\nclient.send({ type: \"ping\", id: \"foo\" }); // ❌ error, 'id' does not exist in type '{ type: \"ping\"; }'.\n\nclient.send({ type: \"add-to-counter\", amount: 3 }); // ✅\nclient.send({ type: \"add-to-counter\" }); // ❌ error, 'amount' is declared here.\n```\n\nYou can also hook to typesafe events (only react atm).\n\n- `usePartyMessage` is a hook that will trigger your callback whenever a message of a given type is received.\n- that callback will always have the latest state of your component, thanks to a\n  [`useEvent`](https://github.com/scottrippey/react-use-event-hook) hook.\n- `usePartyMessage` doesn't add any event listener to the socket, it really just hooks into the client's message handler\n\n```ts\n// src/clients.ts\nimport { createPartyHooks } from \"partyrpc/react\";\nconst { usePartyMessage, useSocketEvent } = createPartyHooks(client);\n\nfunction App() {\n  const [count, setCount] = useState(0);\n\n  usePartyMessage(\"counter\", (msg) =\u003e {\n    console.log(\"received counter\", msg);\n    // msg is typed as CounterResponse, defined above as { type: \"counter\"; counter: number }\n\n    console.log({ count });\n    // count is always up to date, thanks to a useEvent hook\n  });\n\n  useSocketEvent(\"open\", () =\u003e {\n    console.log(\"socket opened\");\n  });\n\n  useSocketEvent(\"close\", () =\u003e {\n    console.log(\"socket closed\");\n  });\n\n  // ...\n}\n```\n\n### Fetch requests\n\nYou can also use `partyrpc` to define typesafe endpoints on your PartyKit server.\n\n```ts\n// src/safe-party.ts\nimport * as v from \"valibot\";\nimport { createPartyRpc } from \"partyrpc/server\";\n\ntype UContext = { counter: number };\n\nconst party = createPartyRpc\u003cPartyResponses, UContext\u003e();\nexport const router = party.endpoints([\n  party.route({\n    method: \"get\",\n    path: \"/api/counter\",\n    response: v.object({ counter: v.number() }),\n    handler(_req, _lobby, _ctx, userCtx) {\n      return { counter: userCtx.counter };\n    },\n  }),\n  party.route({\n    method: \"post\",\n    path: \"/api/counter\",\n    parameters: {\n      body: v.object({ amount: v.number() }),\n    },\n    response: v.object({ counter: v.number(), added: v.number() }),\n    handler(req, lobby, ctx, userCtx) {\n      req.params;\n      //   ^? typed as { body: { amount: number } }\n\n      userCtx.counter += req.params.body.amount;\n      // ^? typed as { counter: number }\n\n      return { counter: userCtx.counter, added: req.params.body.amount };\n    },\n  }),\n]);\n```\n\nand later used them with your own fetcher instance:\n\n```ts\n// src/client.ts\nimport { ofetch } from \"ofetch\";\nimport { createPartyClient } from \"partyrpc/client\";\nimport { SafePartyEvents, SafePartyResponses } from \"./safe-party\";\n\nconst api = createApiClient(router.endpoints, (method, url, params) =\u003e\n  ofetch(url, {\n    method,\n    body: params?.body as any,\n    headers: params?.header as any,\n    query: params?.query as any,\n  }),\n).setBaseUrl(\"http://127.0.0.1:1999\");\n\n// ...\n\napi.post(\"/api/counter\", { body: { amount: 4 } }).then((res) =\u003e {\n  res;\n  // ^? typed as { counter: number; added: number; }\n  return console.log(res);\n});\n```\n\n## Caveats\n\n- Currently only compatible with `valibot`, ideally it'll use [`typeschema`](https://github.com/decs/typeschema) at some\n  point to allow you to use your preferred validation library.\n- Currently only allow events and responses that match a `{ type: string }` shape, ala\n  [xstate](https://github.com/statelyai/xstate). Not sure if that will change. Maybe data will end up being wrapped in a\n  `data` property, but that seems like a lot of extra typing.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fastahmer%2Fpartyrpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fastahmer%2Fpartyrpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fastahmer%2Fpartyrpc/lists"}