{"id":24305907,"url":"https://github.com/makay11/seamlessrpc","last_synced_at":"2025-03-06T16:51:47.578Z","repository":{"id":272794865,"uuid":"914521791","full_name":"Makay11/SeamlessRPC","owner":"Makay11","description":"An RPC library for quick development of seamless full-stack applications.","archived":false,"fork":false,"pushed_at":"2025-01-16T16:39:04.000Z","size":102,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-19T02:48:59.509Z","etag":null,"topics":["real-time","rpc","server-sent-events","sse","subscriptions","typescript","vite"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/seamlessrpc","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Makay11.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-09T19:00:07.000Z","updated_at":"2025-01-28T01:56:31.000Z","dependencies_parsed_at":"2025-01-16T17:56:32.295Z","dependency_job_id":"7ff293b0-cee6-4314-940c-daa153cf3ca6","html_url":"https://github.com/Makay11/SeamlessRPC","commit_stats":null,"previous_names":["makay11/seamlessrpc"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Makay11%2FSeamlessRPC","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Makay11%2FSeamlessRPC/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Makay11%2FSeamlessRPC/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Makay11%2FSeamlessRPC/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Makay11","download_url":"https://codeload.github.com/Makay11/SeamlessRPC/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242250842,"owners_count":20096895,"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":["real-time","rpc","server-sent-events","sse","subscriptions","typescript","vite"],"created_at":"2025-01-17T02:18:18.495Z","updated_at":"2025-03-06T16:51:47.551Z","avatar_url":"https://github.com/Makay11.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🌐 SeamlessRPC\n\nAn RPC library for quick development of seamless full-stack applications.\n\nPowered by a [Vite](https://vitejs.dev/) plugin and inspired by [Telefunc](https://telefunc.com/), [tRPC](https://trpc.io/) and other similar libraries.\n\nPreviously known as [@makay/rpc](https://github.com/Makay11/rpc).\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n[✨ Features](#-features)\n[🔧 Installation and setup](#-installation-and-setup)\n[🚀 Usage](#-usage)\n\n[📝 Input validation](#-input-validation)\n[🚨 Errors](#-errors)\n[📦 Async server state](#-async-server-state)\n[👍 Results](#-results)\n[📡 Subscriptions](#-subscriptions)\n\n[🔌 Adapters](#-adapters)\n[\u003cimg src=\"icons/logos--hono.svg\" alt=\"\" height=\"14\"\u003e Hono](#-hono)\n[\u003cimg src=\"icons/logos--vue.svg\" alt=\"\" height=\"14\"\u003e Vue](#-vue)\n[\u003cimg src=\"icons/logos--zod.svg\" alt=\"\" height=\"14\"\u003e Zod](#-zod)\n\n[🧑🏻‍💻 Contributing](#-contributing)\n[📄 License](#-license)\n\n\u003c/div\u003e\n\n---\n\n## ✨ Features:\n\n- 🎉 End-to-end TypeScript\n- 🚫 Zero boilerplate\n- 📡 Optional [server-sent events](https://en.wikipedia.org/wiki/Server-sent_events) support for real-time [subscriptions](#-subscriptions)\n- 🪶 Extremely small client bundle size addition\n- 🔗 Directly import and call tailored server functions from client code\n- 📄 Co-locate client and server files (or don't 🤷)\n- 📦 Front-end and back-end framework agnostic\n- 📦 Validation library agnostic\n- 🚫 Low server overhead with no implicit run-time validations\n- 🧰 Includes utilities for [async server state](https://github.com/Makay11/SeamlessRPC/blob/main/lib/src/server/state.ts) and [results](https://github.com/Makay11/SeamlessRPC/blob/main/lib/src/result.ts)\n- 🪝 Use the [composables](https://vuejs.org/guide/reusability/composables)/[hooks](https://react.dev/reference/react/hooks) pattern in server code\n- 🔌 Includes adapters for popular libraries like [Hono](https://hono.dev/), [Vue](https://vuejs.org/) and [Zod](https://zod.dev/)\n\n## 🔧 Installation and setup\n\n1. Install a single package:\n\n   ```sh\n   npm i seamlessrpc\n   ```\n\n   ```sh\n   yarn add seamlessrpc\n   ```\n\n   ```sh\n   pnpm add seamlessrpc\n   ```\n\n   ```sh\n   bun add seamlessrpc\n   ```\n\n   Everything is included out-of-the-box!\n\n2. Set up the Vite plugin:\n\n   ```ts\n   // vite.config.ts\n   import { rpc } from \"seamlessrpc/vite\"\n   import { defineConfig } from \"vite\"\n\n   export default defineConfig({\n     plugins: [\n       rpc({\n         url: \"http://localhost:3000/rpc\",\n         credentials: \"include\",\n       }),\n     ],\n   })\n   ```\n\n   You can run both `vite` to start a dev server or `vite build` to build for production.\n\n3. Set up the RPC server (example using the included [Hono](https://hono.dev/) adapter):\n\n   ```ts\n   // src/server.ts\n   import { serve } from \"@hono/node-server\"\n   import { Hono } from \"hono\"\n   import { cors } from \"hono/cors\"\n   import { createRpc } from \"seamlessrpc/hono\"\n\n   const app = new Hono()\n\n   app.use(\n     cors({\n       origin: \"http://localhost:5173\",\n       credentials: true,\n     }),\n   )\n\n   const rpc = await createRpc()\n\n   app.post(\"/rpc/:id{.+}\", (ctx) =\u003e {\n     return rpc(ctx, ctx.req.param(\"id\"))\n   })\n\n   serve(\n     {\n       fetch: app.fetch,\n       port: 3000,\n     },\n     (info) =\u003e {\n       console.log(`Server is running on http://localhost:${info.port}`)\n     },\n   )\n   ```\n\n   You can run the above file with `npx tsx src/server.ts`.\n\n   You can also run `npx tsx watch src/server.ts` to auto-reload during development.\n\n## 🚀 Usage\n\nCreate client and server files and seamlessly import server types and functions from client code with full TypeScript support!\n\n```ts\n// src/components/Todos.ts\nimport { createTodo, getTodos, type Todo } from \"./Todos.server\"\n\nlet todos: Todo[] = []\n\nasync function main() {\n  todos = await getTodos()\n\n  console.log(todos)\n\n  const newTodo = await createTodo(\"New Todo\")\n\n  console.log(newTodo)\n}\n\nmain()\n```\n\n```ts\n// src/components/Todos.server.ts\nexport type Todo = {\n  id: string\n  text: string\n}\n\nconst todos: Todo[] = []\n\nexport async function getTodos() {\n  return todos\n}\n\nexport async function createTodo(text: string) {\n  // TODO validate text\n\n  const todo = {\n    id: crypto.randomUUID(),\n    text,\n  }\n\n  todos.push(todo)\n\n  return todo\n}\n```\n\nServe the above `src/components/Todos.ts` through Vite and you should see the array of todos printed to your browser console. Reload the page a bunch of times and you should see the array grow since the state is persisted in the server!\n\nIn a real scenario you would store your data in a database rather than in the server memory, of course. The snippets above are merely illustrative.\n\n## 📝 Input validation\n\nThere is no implicit run-time validation of inputs in the server. In the example above, the function `createTodo` expects a single string argument. However, if your server is exposed publicly, bad actors or misconfigured clients might send something unexpected which can cause undefined behavior in you program.\n\nTherefore, it is ⚠️ **extremely important** ⚠️ that you validate the inputs of all exposed function.\n\nHere's a basic example using the included [Zod](https://zod.dev/) adapter:\n\n```ts\nimport { z, zv } from \"seamlessrpc/zod\"\n\nconst TextSchema = z.string().min(1).max(256)\n\ntype Text = z.output\u003ctypeof TextSchema\u003e\n\nexport async function createTodo(text: Text) {\n  zv(text, TextSchema)\n\n  // `text` is now safe to use\n}\n```\n\nYou can use any validation library or even your own custom code to validate your inputs since SeamlessRPC is completely agnostic. Just make sure to throw an instance of the included `ValidationError` so that the server responds with a `400 Bad Request` instead of the default `500 Internal Server Error`.\n\n```ts\nimport { ValidationError } from \"seamlessrpc/server\"\n\nexport async function createTodo(text: string) {\n  if (typeof text !== \"string\" || text.length \u003c 1 || text.length \u003e 256) {\n    throw new ValidationError(\"Invalid text\")\n  }\n\n  // `text` is now safe to use\n}\n```\n\nIf you are worried about forgetting to validate your inputs within the function, you can write a separate function signature with the expected types and then use `unknown` in the actual function implementation. The downside is that you have to explicitly provide a return type rather than letting it be inferred.\n\n```ts\nexport async function createTodo(text: string): Promise\u003cstring\u003e\n\nexport async function createTodo(text: unknown) {\n  if (typeof text !== \"string\" || text.length \u003c 1 || text.length \u003e 256) {\n    throw new ValidationError(\"Invalid text\")\n  }\n\n  // `text` is now safe to use\n}\n```\n\n## 🚨 Errors\n\nSeamlessRPC includes the following error classes:\n\n- `RpcError`: The base error class for all SeamlessRPC errors.\n- `InvalidRequestBodyError`: Thrown when the request body is invalid or cannot be parsed.\n- `ValidationError`: Thrown when the input validation fails.\n- `UnauthorizedError`: Thrown when the request is unauthorized.\n- `ForbiddenError`: Thrown when the request is forbidden.\n- `ProcedureNotFoundError`: Thrown when the requested procedure is not found.\n\nYou can throw any error in your functions:\n\n```typescript\n// src/components/Todos.server.ts\nimport {\n  ValidationError,\n  UnauthorizedError,\n  ForbiddenError,\n} from \"seamlessrpc/server\"\n\nexport async function createTodo(text: string) {\n  // if text is invalid\n  throw new ValidationError(\"Invalid text\")\n\n  // if user is not authenticated\n  throw new UnauthorizedError()\n\n  // if user is not allowed to perform an action\n  throw new ForbiddenError()\n\n  // custom error\n  throw new Error(\"Something went wrong\")\n}\n```\n\nThe included [Hono](https://hono.dev/) adapter maps errors to HTTP status codes in the following manner, by default:\n\n- `RpcError` -\u003e `500 Internal Server Error`\n- `InvalidRequestBodyError` -\u003e `400 Bad Request`\n- `ValidationError` -\u003e `400 Bad Request`\n- `UnauthorizedError` -\u003e `401 Unauthorized`\n- `ForbiddenError` -\u003e `403 Forbidden`\n- `ProcedureNotFoundError` -\u003e `404 Not Found`\n- Custom errors -\u003e `500 Internal Server Error`\n\nHowever, it also allows you to provide a custom error handler to handle errors in a different way. This is covered in the [Hono section](#-hono) below.\n\n## 📦 Async server state\n\nSeamlessRPC provides a way to store temporary server state tied to a request. The state is stored within the server process using [AsyncLocalStorage](https://nodejs.org/docs/latest-v22.x/api/async_context.html#class-asynclocalstorage).\n\nThe state can be accessed from any function in a way that resembles the [composables](https://vuejs.org/guide/reusability/composables)/[hooks](https://react.dev/reference/react/hooks) pattern.\n\n```typescript\n// src/components/Example.server.ts\nimport { defineState } from \"seamlessrpc/server\"\n\nexport type User = {\n  id: number\n  name: string\n}\n\nconst { createState, replaceState, clearState, useState, useStateOrThrow } =\n  defineState\u003cUser\u003e()\n\nexport async function example() {\n  // throws if state has already been created\n  let user = createState({\n    id: 1,\n    name: \"John Doe\",\n  })\n\n  // creates or replaces existing state\n  user = replaceState({\n    id: 2,\n    name: \"Jane Doe\",\n  })\n\n  // clears state if it exists\n  clearState()\n\n  // returns undefined if state has not been created\n  const maybeUser = useState()\n\n  // throws if state has not been created\n  user = useStateOrThrow()\n}\n```\n\nTake a look at [sandbox/src/server/auth.ts](https://github.com/Makay11/SeamlessRPC/blob/main/sandbox/src/server/auth.ts) and [sandbox/src/components/OnlineChat.server.ts](https://github.com/Makay11/SeamlessRPC/blob/main/sandbox/src/components/OnlineChat.server.ts) for a full example of using async server state to implement basic authentication.\n\n## 👍 Results\n\nResults allow you to send fully typed serializable success or failure values back to the client.\n\nYou can create results using the `ok` and `err` functions.\n\n```typescript\nimport { ok, err } from \"seamlessrpc/result\"\n\nconst successResult = ok(\"success\")\n// Ok\u003cstring\u003e -\u003e { ok: true, value: \"success\" }\n\nconst errorResult = err(\"failure\")\n// Err\u003cstring\u003e -\u003e { ok: false, error: \"failure\" }\n```\n\nFor stronger typing you can use the `okConst` and `errConst` convenience functions.\n\n```typescript\nimport { okConst, errConst } from \"seamlessrpc/result\"\n\n// same as `ok(\"success\" as const)`\nconst constSuccessResult = okConst(\"success\")\n// Ok\u003c\"success\"\u003e -\u003e { ok: true, value: \"success\" }\n\n// same as `err(\"failure\" as const)`\nconst constErrorResult = errConst(\"failure\")\n// Err\u003c\"failure\"\u003e -\u003e { ok: false, error: \"failure\" }\n```\n\nHere's a full example:\n\n```typescript\nimport { ok, errConst } from \"seamlessrpc/result\"\n\nexport async function example() {\n  if (someCondition) {\n    return errConst(\"some_error\")\n  }\n\n  return ok(\"success\")\n}\n\nconst result = await example()\n// Err\u003c\"some_error\"\u003e | Ok\u003cstring\u003e\n\nif (result.ok) {\n  // result: Ok\u003cstring\u003e\n  console.log(result.value) // \"success\"\n} else {\n  // result: Err\u003c\"some_error\"\u003e\n  console.error(result.error) // \"some_error\"\n}\n```\n\nTake a look at [lib/src/result.ts](https://github.com/Makay11/SeamlessRPC/blob/main/lib/src/result.ts) for all the type definitions, including some helper types not mentioned here.\n\n## 📡 Subscriptions\n\nSubscriptions allow clients to receive real-time updates from the server via [server-sent events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).\n\nClient support needs to be explicitly enabled because it increases the client bundle size by a small amount.\n\n```typescript\n// vite.config.ts\nimport { rpc } from \"seamlessrpc/vite\"\n\nexport default defineConfig({\n  plugins: [\n    rpc({\n      sse: true,\n    }),\n  ],\n})\n```\n\nThen you can just return a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) from your exposed server functions. These functions can still receive inputs and throw errors as usual. For convenience, SeamlessRPC provides a helper function `eventStream` that makes it easier to set up and clean up an event stream.\n\n```typescript\n// src/components/OnlineChat.server.ts\nimport { EventEmitter } from \"node:events\"\n\nimport { eventStream } from \"seamlessrpc/server\"\n\nexport type Message = {\n  id: string\n  topic: string\n  text: string\n}\n\n// example event source\nconst events = new EventEmitter\u003c{\n  MESSAGE_CREATED: [message: Message]\n}\u003e()\n\nsetInterval(() =\u003e {\n  events.emit(\"MESSAGE_CREATED\", {\n    topic: \"general\",\n    text: \"Hello, world!\",\n  })\n}, 1000)\n\nexport async function useMessageCreatedEvents(topic: string) {\n  // TODO validate topic\n  // TODO check user auth\n\n  return eventStream\u003cMessage\u003e(({ enqueue }) =\u003e {\n    console.log(`User subscribed`)\n    events.on(\"MESSAGE_CREATED\", onMessage)\n\n    function onMessage(message: Message) {\n      if (message.topic === topic) {\n        enqueue(message)\n      }\n    }\n\n    // return a cleanup function\n    return () =\u003e {\n      console.log(`User unsubscribed`)\n      events.off(\"MESSAGE_CREATED\", onMessage)\n    }\n  })\n}\n```\n\nIn your client code you can just read the [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) and handle the events as they come in. If you are using [Vue](https://vuejs.org), SeamlessRPC provides a `useSubscription` helper function; check the [Vue section](#-vue) below.\n\nTake a look at [sandbox/src/components/OnlineChat.vue](https://github.com/Makay11/SeamlessRPC/blob/main/sandbox/src/components/OnlineChat.vue) and [sandbox/src/components/OnlineChat.server.ts](https://github.com/Makay11/SeamlessRPC/blob/main/sandbox/src/components/OnlineChat.server.ts) for a more advanced example.\n\n## 🔌 Adapters\n\n### \u003cimg src=\"icons/logos--hono.svg\" alt=\"\" height=\"18\"\u003e Hono\n\nThe Hono adapter allows you to use SeamlessRPC with a [Hono](https://honojs.dev) back-end.\n\n```typescript\n// src/server.ts\nimport { serve } from \"@hono/node-server\"\nimport { Hono } from \"hono\"\nimport { createRpc } from \"seamlessrpc/hono\"\n\nconst app = new Hono()\n\nconst rpc = await createRpc()\n\napp.post(\"/rpc/:id{.+}\", (ctx) =\u003e {\n  return rpc(ctx, ctx.req.param(\"id\"))\n})\n\nserve(\n  {\n    fetch: app.fetch,\n    port: 3000,\n  },\n  (info) =\u003e {\n    console.log(`Server is running on http://localhost:${info.port}`)\n  },\n)\n```\n\nThe `createRpc` function accepts an optional object with the following optional properties:\n\n- `onRequest`: an async function that is called before each RPC request\n- `onError`: an async function that is called when an error occurs during an RPC request\n- `files`: an object with the following optional properties:\n  - `rootDir`: the root directory of the RPC files\n  - `include`: file patterns to include\n  - `exclude`: file patterns to exclude\n\n```typescript\n// src/server.ts\nimport { createRpc } from \"seamlessrpc/hono\"\nimport { RpcError, getHttpStatusCode } from \"seamlessrpc/server\"\n\nconst rpc = await createRpc({\n  async onRequest(ctx) {\n    // do something before each request\n    // like initializing async server state\n  },\n\n  async onError(ctx, error) {\n    // do something when an error occurs\n\n    // log the error\n    console.error(error)\n\n    // keep default error handling behavior\n    if (error instanceof RpcError) {\n      return ctx.json(error, getHttpStatusCode(error))\n    } else {\n      throw error\n    }\n\n    // or send custom response\n    return ctx.json(\"Something went wrong\", 500)\n  },\n\n  files: {\n    rootDir: \"src\",\n    include: [\"./**/*.server.ts\"],\n    exclude: [\"./**/*.server.test.ts\"],\n  },\n})\n```\n\nCheck the [Errors section](#-errors) above for more information regarding the default error handling behavior.\n\n### \u003cimg src=\"icons/logos--vue.svg\" alt=\"\" height=\"18\"\u003e Vue\n\nSeamlessRPC includes a `useSubscription` helper function that makes it easier to handle subscriptions in [Vue](https://vuejs.org) applications. The function takes an object with the following properties:\n\n- `source`: an async function that resolves with a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)\n- `onData`: a callback function that will be called whenever new data is received\n- `onClose`: an optional callback function that will be called when the subscription is closed\n- `onError`: an optional callback function that will be called if the subscription is closed with an error\n\n```typescript\n// script setup (or wrapped in a composable)\nimport { useSubscription } from \"seamlessrpc/vue\"\n\nimport { useMessageCreatedEvents } from \"./OnlineChat.server\"\n\nconst { isSubscribed, isSubscribing, subscribe, unsubscribe } = useSubscription(\n  {\n    source: async () =\u003e useMessageCreatedEvents(\"general\"), // topic: \"general\"\n    onData(message) {\n      console.log(message)\n    },\n    onClose() {\n      console.log(\"closed\")\n    },\n    onError(error) {\n      console.error(error)\n    },\n  },\n)\n\nonMounted(() =\u003e {\n  subscribe().catch(console.error)\n})\n\nonBeforeUnmount(() =\u003e {\n  unsubscribe().catch(console.error)\n})\n```\n\nTake a look at [sandbox/src/components/OnlineChat.vue](https://github.com/Makay11/SeamlessRPC/blob/main/sandbox/src/components/OnlineChat.vue) for a more advanced example.\n\n### \u003cimg src=\"icons/logos--zod.svg\" alt=\"\" height=\"18\"\u003e Zod\n\nThe Zod adapter allows you to use [Zod](https://zod.dev/) for input validation.\n\n```typescript\n// src/components/Todos.server.ts\nimport { z, zv } from \"seamlessrpc/zod\"\n\nconst TextSchema = z.string().min(1).max(256)\n\ntype Text = z.output\u003ctypeof TextSchema\u003e\n\nexport async function createTodo(text: Text) {\n  zv(text, TextSchema)\n\n  // `text` is now safe to use\n}\n```\n\nCheck the [Input validation section](#-input-validation) above for more information.\n\n## 🧑🏻‍💻 Contributing\n\nContributions, issues, suggestions, ideas and discussions are all welcome!\n\nThis is a very young library and a lot can still change.\n\n## 📄 License\n\n[MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmakay11%2Fseamlessrpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmakay11%2Fseamlessrpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmakay11%2Fseamlessrpc/lists"}