GithubHelp home page GithubHelp logo

astahmer / partyrpc Goto Github PK

View Code? Open in Web Editor NEW
29.0 1.0 3.0 265 KB

Partykit + RPC. Move Fast (and Break Everything). Everything is better with typesafety.

TypeScript 100.00%
partykit rpc typesafe websocket

partyrpc's Introduction

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).

  • usePartyMessage is 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 useEvent hook.
  • usePartyMessage doesn'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 use typeschema at 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 a data property, but that seems like a lot of extra typing.

partyrpc's People

Contributors

astahmer avatar ayoubqrt avatar luke-rucker avatar

Stargazers

İsmail Codar avatar naporitan avatar Troy Poulter avatar J. Araujo avatar Anthony Corletti avatar Martin Ledoux avatar Igor Bedesqui avatar Junglei Kim avatar Brad Pillow avatar Matthew Ary avatar Franklin H avatar Nazar avatar  avatar Anthony Powell avatar Nikita avatar Kaiyu Hsu avatar Carlo avatar Brandon (opti) avatar Peter Ferguson avatar Shun Kakinoki avatar  avatar Paweł Błaszczyk avatar Ryota Murakami avatar jack avatar Saurabh Prakash avatar Bhanu Teja Pachipulusu avatar  avatar Guillermo Antony Cava Nuñez avatar  avatar

Watchers

 avatar

partyrpc's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.