WSRPC

As duas pontas chamam.

O WSRPC torna um único WebSocket simétrico. O navegador chama seus handlers, e um handler chama o navegador e espera a resposta.

navegador servidor conectando

Esta página

    A conexão

      Cada linha aqui foi desenhada por uma chamada do servidor nesta página. Esta página usa o cliente JavaScript que acompanha o pacote, e estes quadros são os mesmos que um servidor em Python troca.

      A mesma chamada de qualquer ponta

      Instale o pacote e escreva o handler. Cada aba constrói o mesmo chat que funciona acima.

      pip install wsrpc-aiohttp

      Uma classe de rota atende uma conexão. O handler desenha sua linha na página e depois responde com uma citação.

      server.py
      import random
      
      from aiohttp import web
      from wsrpc_aiohttp import STATIC_DIR, Route, WebSocketAsync, decorators
      
      QUOTES = [
          ("HAL 9000", "I'm sorry, Dave. I'm afraid I can't do that."),
          ("The Oracle", "There is no spoon."),
          ("T-800", "I'll be back."),
      ]
      
      
      class Socket(WebSocketAsync):
          pass
      
      
      class Chat(Route[Socket]):
          @decorators.proxy
          async def send(self, *, text: str) -> dict:
              # Draw the line in the page that called, and wait for its answer.
              await self.socket.proxy.chat.append(text=text, mine=True)
      
              sender, reply = random.choice(QUOTES)
              await self.socket.proxy.chat.append(sender=sender, text=reply)
              return {"delivered": True}
      
      
      def create_app() -> web.Application:
          Socket.add_route("chat", Chat)
          app = web.Application()
          app.router.add_get("/ws/", Socket)
          app.router.add_static("/js/", STATIC_DIR)
          return app
      
      
      if __name__ == "__main__":
          web.run_app(create_app(), host="127.0.0.1", port=8000)
      
      pip install 'wsrpc-asgi[fastapi]'

      A mesma classe de rota, montada em um endpoint WebSocket do FastAPI.

      app.py
      import random
      
      from fastapi import FastAPI, WebSocket
      from wsrpc_asgi import Route, WebSocketAsync, decorators
      
      QUOTES = [
          ("HAL 9000", "I'm sorry, Dave. I'm afraid I can't do that."),
          ("The Oracle", "There is no spoon."),
          ("T-800", "I'll be back."),
      ]
      
      
      class RPC(WebSocketAsync):
          pass
      
      
      class Chat(Route[RPC]):
          @decorators.proxy
          async def send(self, *, text: str) -> dict:
              # Draw the line in the page that called, and wait for its answer.
              await self.socket.proxy.chat.append(text=text, mine=True)
      
              sender, reply = random.choice(QUOTES)
              await self.socket.proxy.chat.append(sender=sender, text=reply)
              return {"delivered": True}
      
      
      RPC.add_route("chat", Chat)
      app = FastAPI()
      
      
      @app.websocket("/ws/")
      async def rpc(websocket: WebSocket) -> None:
          await RPC.serve(websocket)
      
      go get github.com/wsrpc/wsrpc-go

      Um nome de rota com ponto, e uma chamada tipada do handler de volta para a página.

      main.go
      package main
      
      import (
      	"context"
      	"log"
      	"math/rand/v2"
      	"net/http"
      
      	wsrpc "github.com/wsrpc/wsrpc-go"
      	"github.com/wsrpc/wsrpc-go/ws"
      )
      
      type Line struct {
      	Sender string `json:"sender,omitempty"`
      	Text   string `json:"text"`
      	Mine   bool   `json:"mine,omitempty"`
      }
      
      type Sent struct {
      	Text string `json:"text"`
      }
      
      type Delivered struct {
      	Delivered bool `json:"delivered"`
      }
      
      type Seen struct {
      	Seen bool `json:"seen"`
      }
      
      var quotes = []Line{
      	{Sender: "HAL 9000", Text: "I'm sorry, Dave. I'm afraid I can't do that."},
      	{Sender: "The Oracle", Text: "There is no spoon."},
      	{Sender: "T-800", Text: "I'll be back."},
      }
      
      type Chat struct{}
      
      // Draw the line in the page that called, and wait for its answer.
      func (Chat) Send(ctx context.Context, p Sent) (Delivered, error) {
      	peer, _ := wsrpc.FromContext(ctx)
      
      	if _, err := peer.Call[Seen](ctx, "chat.append", Line{Text: p.Text, Mine: true}); err != nil {
      		return Delivered{}, err
      	}
      
      	reply := quotes[rand.IntN(len(quotes))]
      	if _, err := peer.Call[Seen](ctx, "chat.append", reply); err != nil {
      		return Delivered{}, err
      	}
      
      	return Delivered{Delivered: true}, nil
      }
      
      func main() {
      	routes := new(wsrpc.Router)
      	if err := routes.Handle("chat.send", Chat{}.Send); err != nil {
      		log.Fatal(err)
      	}
      
      	rpc, err := ws.NewServer(ws.ServerOptions{Peer: wsrpc.Options{Routes: routes}})
      	if err != nil {
      		log.Fatal(err)
      	}
      
      	mux := http.NewServeMux()
      	mux.Handle("/ws/", rpc)
      	log.Fatal(http.ListenAndServe("127.0.0.1:8000", mux))
      }
      
      pip install wsrpc-aiohttp

      O cliente registra sua rota antes de conectar, para que o servidor possa chamá-la.

      client.py
      import asyncio
      
      from wsrpc_aiohttp import Route, WSRPCClient, decorators
      
      
      class Chat(Route):
          @decorators.proxy
          def append(self, *, text: str, sender: str = "you", mine: bool = False) -> dict:
              # The server calls this method. Draw the line and confirm it.
              print(f"{sender}: {text}")
              return {"seen": True}
      
      
      async def main() -> None:
          client = WSRPCClient("http://127.0.0.1:8000/ws/")
      
          # Register the route before the connection opens.
          client.add_route("chat", Chat)
      
          async with client:
              print(await client.proxy.chat.send(text="Hello"))
              await asyncio.sleep(1)
      
      
      if __name__ == "__main__":
          asyncio.run(main())
      
      npm install @wsrpc/client

      A página registra a rota que o servidor chama e desenha cada linha que recebe.

      chat.ts
      import WSRPC from "@wsrpc/client";
      
      interface Line {
        sender?: string;
        text: string;
        mine?: boolean;
      }
      
      const chat = document.getElementById("chat") as HTMLElement;
      const rpc = new WSRPC("/ws/");
      
      // The server calls this route. The page draws what it gets.
      rpc.addRoute("chat.append", (line: Line) => {
        const row = document.createElement("p");
        row.textContent = `${line.sender ?? "you"}: ${line.text}`;
        chat.append(row);
        return { seen: true };
      });
      
      rpc.connect();
      
      export async function say(text: string): Promise<void> {
        await rpc.call<{ delivered: boolean }>("chat.send", { text });
      }
      
      wsrpc-client.d.ts
      // The package ships wsrpc.d.ts. Map it onto the module name once.
      declare module "@wsrpc/client" {
        export default class WSRPC {
          constructor(url: string, reconnectTimeout?: number);
          connect(): void;
          destroy(): void;
          state(): "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED";
          call<T>(method: string, params?: object): Promise<T>;
          addRoute(
            route: string,
            handler: (params: never) => unknown,
            isAsync?: boolean,
          ): void;
        }
      }
      

      O script incluído adiciona um global WSRPC. Sem etapa de build e sem bundler.

      index.html
      <form id="say"><input name="text" autocomplete="off"></form>
      <div id="chat"></div>
      
      <script src="/js/wsrpc.min.js"></script>
      <script>
        const rpc = new WSRPC("/ws/");
      
        // The server calls this route. The page draws what it gets.
        rpc.addRoute("chat.append", function (line) {
          const row = document.createElement("p");
          row.textContent = (line.sender || "you") + ": " + line.text;
          document.getElementById("chat").append(row);
          return {seen: true};
        });
      
        rpc.connect();
      
        document.getElementById("say").addEventListener("submit", function (event) {
          event.preventDefault();
          rpc.proxy.chat.send({text: event.target.text.value});
          event.target.reset();
        });
      </script>
      

      O que a conexão oferece

      Chamadas nos dois sentidos

      Um handler chama uma rota que o navegador registrou e espera o resultado no mesmo socket.

      Rotas ou funções simples

      Exponha uma função, ou uma classe de rota cuja instância vive o tempo da conexão que atende.

      Muitas chamadas ao mesmo tempo

      Cada chamada carrega o seu id, então os resultados voltam a quem chamou, em qualquer ordem.

      Erros remotos sobem localmente

      Uma falha do outro lado chega com tipo e mensagem, e sobe onde você fez a chamada.

      Sinais do ciclo de vida

      Acompanhe conexão, desconexão e cada chamada sem envolver nenhum handler.

      Código bloqueante não prende o loop

      Handlers assíncronos rodam no loop de eventos. Handlers síncronos rodam em um pool de threads.

      Como uma chamada trafega

      Cada quadro é um objeto JSON. A chamada carrega id, método e parâmetros; a resposta volta com o mesmo id. As duas pontas enviam chamadas, então um handler pode perguntar ao navegador enquanto atende uma.

      navegador servidor {"id": 3, "method": "chat.send", "params": {"text": "hi"}} {"id": 3, "result": {"delivered": true}}
      O navegador chama o servidor.
      navegador servidor handler {"id": 5, "method": "chat.send", "params": {"text": "hi"}} {"id": 2, "method": "chat.append", "params": {"text": "hi"}} {"id": 2, "result": {"seen": true}} {"id": 5, "result": {"delivered": true}}
      O servidor chama o navegador dentro da chamada que está atendendo.

      Onde funciona

      Cada framework aqui tem um exemplo executável e um teste no seu repositório.

      Servidor aiohttp

      wsrpc-aiohttp

      • aiohttp

      Servidores ASGI

      wsrpc-asgi

      • FastAPI
      • Starlette
      • Django
      • Channels
      • Litestar
      • Quart
      • Sanic
      • Falcon
      • BlackSheep
      • ASGI

      Servidores Node.js

      @wsrpc/server

      • node:http
      • Express
      • Fastify

      Servidores Go

      github.com/wsrpc/wsrpc-go

      • net/http

      Clientes

      @wsrpc/client

      • Browsers
      • Node.js
      • Cloudflare Workers

      Qual pacote de Python?

      Duas implementações, um protocolo. Um par em Python, um par no navegador e um servidor combinam livremente.

      wsrpc-aiohttpwsrpc-asgi
      Servidor handler web do aiohttp rota ASGI, middleware ou aplicação independente
      Instalação pip install wsrpc-aiohttp pip install 'wsrpc-asgi[fastapi,client]'
      Cliente Python sobre aiohttp sobre websockets
      Cliente JavaScript incluído o mesmo cliente
      Codec JSON loads e dumps são substituíveis igual