npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

grubba-rpc

v0.13.5

Published

RPC e2e safe

Downloads

862

Readme

Meteor-RPC

What is this package?

Inspired on zodern:relay and on tRPC

This package provides functions for building E2E type-safe RPCs.

How to download it?

meteor npm i grubba-rpc @tanstack/react-query zod

install react query into your project, following their quick start guide

How to use it?

Firstly, you need to create a module, then you can add methods, publications, and subscriptions to it.

Then you need to build the module and use it in the client as a type.

createModule

subModule without a namespace: createModule() is used to create the main server module, the one that will be exported to be used in the client.

subModule with a namespace: createModule("namespace") is used to create a submodule that will be added to the main module.

Remember to use build at the end of module creation to ensure that the module is going to be created.

for example:

// server/main.ts
import { createModule } from "grubba-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";

const Chat = createModule("chat")
  .addMethod("createChat", z.void(), async () => {
    return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
  })
  .buildSubmodule();

const server = createModule() // server has no namespace
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .addSubmodule(Chat)
  .build();

export type Server = typeof server;

// client.ts
import { createClient } from "grubba-rpc";

const api = createClient<Server>();
const bar: "bar" = await api.bar("some string");
//   ?^ 'bar'
const newChatId = await api.chat.createChat(); // with intellisense

module.addMethod

addMethod(name: string, schema: ZodSchema, handler: (args: ZodTypeInput<ZodSchema>) => T, config?: Config<ZodTypeInput<ZodSchema>, T>)

This is the equivalent of Meteor.methods but with types and runtime validation.

// server/main.ts
import { createModule } from "grubba-rpc";
import { z } from "zod";

const server = createModule();

server.addMethod("foo", z.string(), (arg) => "foo" as const);

server.build();

// is the same as

import { Meteor } from "meteor/meteor";
import { z } from "zod";

Meteor.methods({
  foo(arg: string) {
    z.string().parse(arg);
    return "foo";
  },
});

module.addPublication

addPublication(name: string, schema: ZodSchema, handler: (args: ZodTypeInput<ZodSchema>) => Cursor<any, any>)

This is the equivalent of Meteor.publish but with types and runtime validation.

// server/main.ts
import { createModule } from "grubba-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";

const server = createModule();

server.addPublication("chatRooms", z.void(), () => {
  return ChatCollection.find();
});

server.build();

// is the same as
import { Meteor } from "meteor/meteor";
import { ChatCollection } from "/imports/api/chat";

Meteor.publish("chatRooms", function () {
  return ChatCollection.find();
});

module.addSubmodule

This is used to add a submodule to the main module, adding namespaces for your methods and publications and also making it easier to organize your code.

Remember to use submodule.buildSubmodule when creating a submodule

// module/chat.ts
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "grubba-rpc";

export const chatModule = createModule("chat")
  .addMethod("createChat", z.void(), async () => {
    return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
  })
  .buildSubmodule(); // <-- this is important so that this module can be added as a submodule

// server/main.ts
import { createModule } from "grubba-rpc";
import { chatModule } from "./module/chat";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .addSubmodule(chatModule)
  .build();

server.chat; // <-- this is the namespace for the chat module

module.addMiddlewares

addMiddlewares(middlewares: Middleware[])

Type Middleware = (raw: unknown, parsed: unknown) => void;

This is used to add middlewares to the module, it can be used to add side effects logic to the methods and publications, ideal for logging, rate limiting, etc.

The middleware ordering is last in first out. Check the example below:

// module/chat.ts
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "grubba-rpc";

export const chatModule = createModule("chat")
  .addMiddlewares([
    (raw, parsed) => {
      console.log("run first");
    },
  ])
  .addMethod("createChat", z.void(), async () => {
    return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
  })
  .buildSubmodule();

// server/main.ts
import { createModule } from "grubba-rpc";
import { chatModule } from "./module/chat";

const server = createModule()
  .addMiddlewares([
    (raw, parsed) => {
      console.log("run second");
    },
  ])
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .addSubmodule(chatModule)
  .build();

React focused API

Using in the client

When using in the client you have to use the createModule and build methods to create a module that will be used in the client and be sure that you are exporting the type of the module

You should only create one client in your application

You can have something like api.ts that will export the client and the type of the client

// server/main.ts
import { createModule } from "grubba-rpc";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

await app.bar("str"); // it will return "bar"

method.useMutation

It uses the useMutation from react-query to create a mutation that will call the method

// server/main.ts
import { createModule } from "grubba-rpc";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

export const Component = () => {
  const { mutate, isLoading, isError, error, data } = app.bar.useMutation();

  return (
    <button
      onClick={() => {
        mutation.mutate("str"); // it has intellisense
      }}
    >
      Click me
    </button>
  );
};

method.useQuery

It uses the useQuery from react-query to create a query that will call the method, it uses suspense to handle loading states

// server/main.ts
import { createModule } from "grubba-rpc";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

export const Component = () => {
  const { data } = app.bar.useQuery("str"); // will trigger suspense

  return <div>{data}</div>;
};

publication.useSubscription

Subscriptions on the client have useSubscription method that can be used as a hook to subscribe to a publication. It uses suspense to handle loading states

// server/main.ts
import { createModule } from "grubba-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";

const server = createModule()
  .addPublication("chatRooms", z.void(), () => {
    return ChatCollection.find();
  })
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

export const Component = () => {
  const { data: rooms, collection: chatCollection } =
    api.chatRooms.usePublication(); // it will trigger suspense and rooms is reactive

  return <div>{data}</div>;
};

Examples

Currently we have this chat-app that uses this package to create a chat app

it includes: methods, publications, and subscriptions

Advanced usage

you can take advantage of the hooks to add custom logic to your methods, checking the raw and parsed data, and the result of the method, you can add more complex validations.

server.addMethod("name", z.any(), () => "str", {
  hooks: {
    onBeforeResolve: [
      (raw, parsed) => {
        console.log("before resolve", raw, parsed);
      },
    ],
    onAfterResolve: [
      (raw, parsed, result) => {
        console.log("after resolve", raw, parsed, result);
      },
    ],
    onErrorResolve: [
      (err, raw, parsed) => {
        console.log("error resolve", err, raw, parsed);
      },
    ],
  },
});

or

// server.ts
server.name.addBeforeResolveHook((raw, parsed) => {
  console.log("before resolve", raw, parsed);
});

server.name.addAfterResolveHook((raw, parsed, result) => {
  console.log("after resolve", raw, parsed, result);
});

server.name.addErrorResolveHook((err, raw, parsed) => {
  console.log("error resolve", err, raw, parsed);
});

server = server.build();