Partyrpc
Partykit + RPC. Move Fast (and Break Everything). Everything is better with typesafety.
Install / Use
/learn @astahmer/PartyrpcREADME
partyrpc
Partykit + RPC = PartyRPC Move Fast (and Break Everything). Everything is better with typesafety.
Install
pnpm i partyrpc
Usage
Terminology
- Events: what WS messages your party server expects to receive from clients
- Responses: what WS messages your party server sends back to clients
WS Events
Define your (safe) party events and responses:
// src/safe-party.ts
import * as v from "valibot";
import { createPartyRpc } from "partyrpc/server";
type UContext = { counter: number };
type PongResponse = { type: "pong"; size: number };
type LatencyResponse = { type: "latency"; id: string };
type CounterResponse = { type: "counter"; counter: number };
type PartyResponses = PongResponse | LatencyResponse | CounterResponse;
const party = createPartyRpc<PartyResponses, UContext>();
export const safeParty = party.events({
ping: {
schema: v.never(),
onMessage(message, ws, room, ctx) {
party.send(ws, { type: "pong", size: room.connections.size });
},
},
latency: {
schema: v.object({ id: v.string() }),
onMessage(message, ws, room, ctx) {
party.send(ws, { type: "latency", id: message.id });
},
},
"add-to-counter": {
schema: v.object({ amount: v.number() }),
onMessage(message, ws, room, ctx) {
ctx.counter += message.amount;
party.send(ws, { type: "counter", counter: ctx.counter });
},
},
});
export type SafePartyEvents = typeof safeParty.events;
export type SafePartyResponses = typeof safeParty.responses;
Bind it to your party server:
// src/server.ts
import * as Party from "partykit/server";
import { safeParty } from "./safe-party";
// optional context
const ctx = { counter: 0 };
export default class Server implements Party.Server {
constructor(readonly party: Party.Party) {}
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
conn.addEventListener("message", (evt) => {
safeParty.onMessage(evt.data, conn, this.party, userCtx);
});
}
}
Server satisfies Party.Worker;
Finally, create your party client:
// src/client.ts
import PartySocket from "partysocket";
import { createPartyClient } from "partyrpc/client";
import { SafePartyEvents, SafePartyResponses } from "./safe-party";
const partySocket = new PartySocket({
host: PARTYKIT_HOST,
room: "some-room",
});
const client = createPartyClient<SafePartyEvents, SafePartyResponses>(partySocket, { debug: true });
Subscribe to typesafe responses:
// src/clients.ts
client.on("latency", (msg) => {
// msg is typed as LatencyResponse, defined above as { type: "latency"; id: string }
});
client.on("pong", (msg) => {
console.log("got pong", msg.size);
// msg is typed as PongResponse, defined above as { type: "pong"; size: number }
});
client.on("counter", (msg) => {
// msg is typed as CounterResponse, defined above as { type: "counter"; counter: number }
});
Send typesafe events:
// src/clients.ts
client.send({ type: "ping" }); // ✅
client.send({ type: "ping", id: "foo" }); // ❌ error, 'id' does not exist in type '{ type: "ping"; }'.
client.send({ type: "add-to-counter", amount: 3 }); // ✅
client.send({ type: "add-to-counter" }); // ❌ error, 'amount' is declared here.
You can also hook to typesafe events (only react atm).
usePartyMessageis a hook that will trigger your callback whenever a message of a given type is received.- that callback will always have the latest state of your component, thanks to a
useEventhook. usePartyMessagedoesn't add any event listener to the socket, it really just hooks into the client's message handler
// src/clients.ts
import { createPartyHooks } from "partyrpc/react";
const { usePartyMessage, useSocketEvent } = createPartyHooks(client);
function App() {
const [count, setCount] = useState(0);
usePartyMessage("counter", (msg) => {
console.log("received counter", msg);
// msg is typed as CounterResponse, defined above as { type: "counter"; counter: number }
console.log({ count });
// count is always up to date, thanks to a useEvent hook
});
useSocketEvent("open", () => {
console.log("socket opened");
});
useSocketEvent("close", () => {
console.log("socket closed");
});
// ...
}
Fetch requests
You can also use partyrpc to define typesafe endpoints on your PartyKit server.
// src/safe-party.ts
import * as v from "valibot";
import { createPartyRpc } from "partyrpc/server";
type UContext = { counter: number };
const party = createPartyRpc<PartyResponses, UContext>();
export const router = party.endpoints([
party.route({
method: "get",
path: "/api/counter",
response: v.object({ counter: v.number() }),
handler(_req, _lobby, _ctx, userCtx) {
return { counter: userCtx.counter };
},
}),
party.route({
method: "post",
path: "/api/counter",
parameters: {
body: v.object({ amount: v.number() }),
},
response: v.object({ counter: v.number(), added: v.number() }),
handler(req, lobby, ctx, userCtx) {
req.params;
// ^? typed as { body: { amount: number } }
userCtx.counter += req.params.body.amount;
// ^? typed as { counter: number }
return { counter: userCtx.counter, added: req.params.body.amount };
},
}),
]);
and later used them with your own fetcher instance:
// src/client.ts
import { ofetch } from "ofetch";
import { createPartyClient } from "partyrpc/client";
import { SafePartyEvents, SafePartyResponses } from "./safe-party";
const api = createApiClient(router.endpoints, (method, url, params) =>
ofetch(url, {
method,
body: params?.body as any,
headers: params?.header as any,
query: params?.query as any,
}),
).setBaseUrl("http://127.0.0.1:1999");
// ...
api.post("/api/counter", { body: { amount: 4 } }).then((res) => {
res;
// ^? typed as { counter: number; added: number; }
return console.log(res);
});
Caveats
- Currently only compatible with
valibot, ideally it'll usetypeschemaat some point to allow you to use your preferred validation library. - Currently only allow events and responses that match a
{ type: string }shape, ala xstate. Not sure if that will change. Maybe data will end up being wrapped in adataproperty, but that seems like a lot of extra typing.
Related Skills
canvas
345.4kCanvas Skill Display HTML content on connected OpenClaw nodes (Mac app, iOS, Android). Overview The canvas tool lets you present web content on any connected node's canvas view. Great for: -
node-connect
345.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
104.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
