{"id":22951300,"url":"https://github.com/daniel-nagy/transporter","last_synced_at":"2025-04-05T04:08:51.271Z","repository":{"id":205307253,"uuid":"704073221","full_name":"daniel-nagy/transporter","owner":"daniel-nagy","description":"Typesafe distributed computing in TypeScript.","archived":false,"fork":false,"pushed_at":"2024-11-16T16:07:39.000Z","size":460,"stargazers_count":108,"open_issues_count":0,"forks_count":1,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-03-29T03:04:36.451Z","etag":null,"topics":["browser","client","distributed","interprocess","message","observable","proxy","pubsub","realtime","rpc","server","socket","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/daniel-nagy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-12T13:34:09.000Z","updated_at":"2025-03-24T23:12:50.000Z","dependencies_parsed_at":"2024-07-31T18:00:00.606Z","dependency_job_id":"691de6d4-1a53-4f3b-85e1-e29ff16171b2","html_url":"https://github.com/daniel-nagy/transporter","commit_stats":null,"previous_names":["daniel-nagy/transporter"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daniel-nagy%2Ftransporter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daniel-nagy%2Ftransporter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daniel-nagy%2Ftransporter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daniel-nagy%2Ftransporter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/daniel-nagy","download_url":"https://codeload.github.com/daniel-nagy/transporter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247284943,"owners_count":20913704,"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":["browser","client","distributed","interprocess","message","observable","proxy","pubsub","realtime","rpc","server","socket","typescript"],"created_at":"2024-12-14T15:07:08.205Z","updated_at":"2025-04-05T04:08:51.219Z","avatar_url":"https://github.com/daniel-nagy.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cdiv\u003e\n    \u003cimg width=\"600px\" src=\"https://github.com/daniel-nagy/transporter/assets/1622446/2d548e46-c66e-43d3-bf9e-f5b9845dbd69\"\u003e\n  \u003c/div\u003e\n  \u003cb\u003eTypesafe distributed computing in TypeScript.\u003c/b\u003e\n\u003c/div\u003e\n\n## Introduction\n\nTransporter is an RPC library for typesafe distributed computing. The Transporter API was influenced by [comlink](https://github.com/GoogleChromeLabs/comlink) and [rxjs](https://github.com/ReactiveX/rxjs).\n\nMessage passing can quickly grow in complexity, cause race conditions, and make apps difficult to maintain. Transporter eliminates the cognitive overhead associated with message passing by enabling the use of functions as a means of communication between distributed systems.\n\nFor an introduction to Transporter check out my [blog post](https://danielnagy.me/posts/Post_s2fh85ot8gqd)!\n\n### Features\n\n- 👌 Typesaftey without code generation.[^1]\n- 😍 Support for generic functions.\n- 🤩 The core API works in any JavaScript runtime.[^2][^3]\n- 😎 Easily integrates into your existing codebase.\n- 👍 No schema builders required, though you may still use them.\n- 🥹 Dependency injection.\n- 🫶 FP friendly.\n- 🤘 Memoization of remote functions.\n- 🫡 Small footprint with 0 dependencies.\n- 🚀 Configurable subprotocols.\n- 🚰 Flow control and protocol stacking using Observables.\n- 🤯 Recursive RPC for select subprotocols.\n- 🌶️ PubSub using Observables for select subprotocols.\n- 👏 Resource management.\n- 🥳 No globals.[^4]\n- 🧪 Easy to test.\n\n[^1]: Transporter is tested using the latest version of TypeScript with strict typechecking enabled.\n[^2]: Transporter works in Node, Bun, Deno, Chrome, Safari, Firefox, Edge, and React Native.\n[^3]: Hermes, a popular JavaScript runtime for React Native apps, does not support `FinalizationRegistry`. It also requires a polyfill for `crypto.randomUUID`.\n[^4]: Transporter has a global `AddressBook` that is used to ensure every server has a unique address.\n\n### Practical Usage\n\nTransporter may be used to build typesafe APIs for fullstack TypeScript applications.\n\nTransporter may be used in the browser to communicate with other browsing contexts (windows, tabs, iframes) or workers (dedicated workers, shared workers, service workers). The browser is ripe for distributed computing and parallel processing but not many developers take advantage of this because the `postMessage` API is very primitive.\n\nTransporter my be used for inter-process communication in Electron applications.\n\nTransporter may also be used in React Native apps to communicate with webviews. You could take this to the extreme and build your entire native app as a Web app that is wrapped in a React Native shell. The Web app could use Transporter to call out to the React Native app to access native APIs not available in the browser.\n\n## Getting Started\n\nTo get started using Transporter install the package from the npm registry using your preferred package manager.\n\n```\nnpm add @daniel-nagy/transporter\n```\n\nAs of beta 3 Transporter is nearing API stability but there may still be some breaking changes to the API. For API docs see the README for each package.\n\n### Packages\n\n- [core](/packages/core) - Core APIs that are designed to work in any JavaScript runtime.\n- [browser](/packages/browser) - Wrappers around the browser's messaging APIs that provide normalized interfaces and additional semantics.\n\n### The Basics\n\nLet's get up and running with Transporter. We'll create a `User` module and use Transporter to expose that module.\n\nHere's our `User` module.\n\n```ts\nconst users = [\n  { id: 0, name: \"Dan\" },\n  { id: 1, name: \"Jessica\" },\n  { id: 2, name: \"Mike\" },\n];\n\nexport async function list() {\n  return users;\n}\n\nexport async function findById(id: number) {\n  return users.find((user) =\u003e user.id === id);\n}\n```\n\nNotice that our `User` module is just a plain old JavaScript module. There's no tight coupling between Transporter and our functions. Nor does Transporter impose any semantics on our API. You can use modules, plain objects, classes, or even arrays it doesn't really matter. The only requirement is that our functions must return a `Promise`. To expose our `User` module we need to create a `ServerSession`. At a minimum, when creating a session, we must provide a `Subprotocol`. A subprotocol is necessary to provide typesaftey at the protocol level. Let's create a subprotocol and a session for our server.\n\n```ts\nimport * as Json from \"@daniel-nagy/transporter/Json\";\nimport * as Session from \"@daniel-nagy/transporter/Session\";\nimport * as Subprotocol from \"@daniel-nagy/transporter/Subprotocol\";\n\nimport * as User from \"./User\";\n\nconst Api = {\n  User,\n};\n\nexport type Api = typeof Api;\n\nconst protocol = Subprotocol.init({\n  connectionMode: Subprotocol.ConnectionMode.Connectionless,\n  dataType: Subprotocol.DataType\u003cJson.t\u003e(),\n  operationMode: Subprotocol.OperationMode.Unicast,\n  transmissionMode: Subprotocol.TransmissionMode.HalfDuplex,\n});\n\nconst session = Session.server({ protocol, provide: Api });\n```\n\nFor now don't worry about the different modes and just focus on the data type. In this case we are telling Transporter that our API only uses JSON data types. With strict type checking enabled we get a type error.\n\n```\nType 'undefined' is not assignable to type 'Json'.\n```\n\nCan you spot the problem? If you can't then don't worry because the compiler spotted it for you. We are telling Transporter that our API only uses JSON data types but the return type of `findById` could be `undefined`. To fix this we could update `findById` to always return valid JSON, for example by returning `null` if the user is not found, but since our server and client are both JavaScript runtimes it would be nice if we could allow `undefined`. Let's instead use the `SuperJson` type provided by Transporter. The `SuperJson` type is a subtype of JSON that allows many built in JavaScript types, such as `undefined`, `Date`, `RegExp`, `Map`, etc.\n\n```diff\n- import * as Json from \"@daniel-nagy/transporter/Json\";\n+ import * as SuperJson from \"@daniel-nagy/transporter/SuperJson\";\n\n-   dataType: Subprotocol.DataType\u003cJson.t\u003e(),\n+   dataType: Subprotocol.DataType\u003cSuperJson.t\u003e(),\n```\n\nWith that change the error will go away.\n\nWe just learned that Transporter provides type safety at the protocol level. It will complain if our API and subprotocol are incompatible. We also learned that there is no tight coupling between Transporter and how we build our API. We can also see that Transporter does not use a router. Instead objects can be composed to create namespaces.\n\nLet's move on now and create a client session.\n\n```typescript\nimport * as Session from \"@daniel-nagy/transporter/Session\";\nimport * as Subprotocol from \"@daniel-nagy/transporter/Subprotocol\";\nimport * as SuperJson from \"@daniel-nagy/transporter/SuperJson\";\n\nimport type { Api } from \"./Server\";\n\nconst protocol = Subprotocol.init({\n  connectionMode: Subprotocol.ConnectionMode.Connectionless,\n  dataType: Subprotocol.DataType\u003cSuperJson.t\u003e(),\n  operationMode: Subprotocol.OperationMode.Unicast,\n  transmissionMode: Subprotocol.TransmissionMode.HalfDuplex,\n});\n\nconst session = Session.client({\n  protocol,\n  resource: Session.Resource\u003cApi\u003e(),\n});\n\nconst client = session.createProxy();\n```\n\nCreating a client session is almost identical to creating a server session. Generally the client and the server will use the same subprotocol. To get a client that acts as a proxy for our API we use the `createProxy` method on the `ClientSession`.\n\nThe last thing we need to do is we need to get our server session and our client session to talk to each other. A session is both a message source and a message sink. If our server session and our client session were in the same process then we could just pipe the output of one into the input of the other to complete the circuit.\n\n```typescript\nserverSession.output.subscribe(clientSession.input);\nclientSession.output.subscribe(serverSession.input);\n```\n\nWhile using Transporter in a single process is not very useful, it is useful to understand this example because it will allow you to easily adapt Transporter for just about any transport layer. As long as you can route the messages then you will be able to get Transporter working. This makes the core Transporter API general purpose and, perhaps ironically, transport layer agnostic.\n\nLet's finish off this example by using HTTP as our Transport layer. HTTP is a text base protocol so we need to go from `SuperJson` to `string` in order to use HTTP. Let's start on the server side. I'm going to use Bun's built-in server API for this example.\n\n```ts\nimport * as Message from \"@daniel-nagy/transporter/Message\";\nimport * as Observable from \"@daniel-nagy/transporter/Observable\";\n\nBun.serve({\n  async fetch(req) {\n    using session = Session.server({ protocol, provide: Api });\n    const reply = Observable.firstValueFrom(session.output);\n    const message = SuperJson.fromJson(await req.json());\n    session.input.next(message as Message.t\u003cSuperJson.t\u003e);\n    return Response.json(SuperJson.toJson(await reply));\n  },\n  port: 3000,\n});\n```\n\nNotice I moved the creation of the session into the request handler. This is perfectly fine, each request will create a session and the session will be terminated at the end of the request handler. In this example this is accomplished using a new feature of JavaScript called explicit resource management. That's the `using` syntax you may be wondering about. If you are unable to use explicit resource management then that is ok. You can just call `session.terminate()` explicitly before returning the response.\n\nWe get the request body as JSON and then decode the message using `SuperJson.fromJson`. We then feed that massage into our session and wait for a reply. We encode the reply as text, using the reverse process, and then send the message to the client.\n\nLet's move on now to our client. For our client I am going to use JavaScript's built-in HTTP client `fetch`.\n\n```ts\nimport * as Observable from \"@daniel-nagy/transporter/Observable\";\n\nconst session = Session.client({\n  protocol,\n  resource: Session.Resource\u003cApi\u003e(),\n});\n\nconst toRequest = (message: string) =\u003e\n  new Request(\"http://localhost:3000\", {\n    body: message,\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    method: \"POST\",\n  });\n\nsession.output\n  .pipe(\n    Observable.map(SuperJson.toJson),\n    Observable.map(JSON.stringify),\n    Observable.map(toRequest),\n    Observable.flatMap(fetch),\n    Observable.flatMap((response) =\u003e response.json()),\n    Observable.map(SuperJson.fromJson)\n  )\n  .subscribe(session.input);\n\nconst client = session.createProxy();\n```\n\nWe take the output of our client session and map over it to do some protocol stacking and make our fetch request. We then, like an ouroboros, feed the response back into our client session to complete the circuit. While slightly more keystrokes than alternative libraries, this example is endlessly adaptable and customizable.\n\nWe just learned that a session is both a message source and a message sink. We also learned that the core API of Transporter is transport layer agnostic. To get Transporter working with any transport layer we just need to complete the circuit.\n\n#### What about these Modes?\n\nThe modes are used to determine if recursive RPC is enabled or not. Recursive RPC refers to including functions or proxies in function IO. This is an interesting concept because it allows state to be held between processes on the call stack. To enable recursive RPC your transport protocol must be connection-oriented and bidirectional. A transport protocol is bidirectional if its transmission mode is `Duplex` or `HalfDuplex` and its operation mode is `Unicast`.\n\nIt is important to make sure your subprotocol and your transport layer are compatible. For example, HTTP is a connectionless protocol. So even though it is bidirectional you should not use recursive RPC if you are using HTTP as your transport protocol. Fortunately, when using Transporter with WebSockets, in the browser, React Native, or in Electron apps you often can enable recursive RPC. It may be possible to use recursive RPC with HTTP streaming. If `WebTransport` becomes generally available then that would likely allow recursive RPC over HTTP. For completeness here is an example subprotocol that would enable recursive RPC.\n\n```ts\nconst protocol = Subprotocol.init({\n  connectionMode: Subprotocol.ConnectionMode.ConnectionOriented,\n  dataType: Subprotocol.DataType\u003cSuperJson.t\u003e(),\n  operationMode: Subprotocol.OperationMode.Unicast,\n  transmissionMode: Subprotocol.TransmissionMode.Duplex,\n});\n```\n\nThat concludes the introduction to Transporter but Transporter provides many more APIs for things like memoization and dependency injection. You can find API docs in the README of each package. Also check out the examples below to see how you can adapt Transporter for different use cases. If you have any questions then feel free to start a discussion on GitHub 🤘.\n\n### Examples\n\nHere are some examples to help you learn Transporter and become inspired ✨.\n\n#### Using Transporter to build End-To-End Typesafe Applications\n\n[codesandbox.io](https://codesandbox.io/p/devbox/transporter-fullstack-example-st2647)\n\nThis example uses [Prisma](https://www.prisma.io/), [React Query](https://tanstack.com/query/latest), and Transporter to build a TypeScript application. What's neat is you can use Transporter to expose your Prisma client to the FE to quickly start prototyping without doing much API work. However, when you're ready to grow your API Transporter is ready to grow with you.\n\n#### Using Transporter to Communicate with iFrames\n\n[codesandbox.io](https://codesandbox.io/p/devbox/transporter-iframe-example-tymcnh)\n\nThis example is a pretty basic todo app. However, what's not so basic is that the app is composed of 3 frames; a top frame and 2 subframes. The top frame holds the app state and renders 2 subframes. The first subframe renders a form for adding new todos. The second subframe renders a list of the current todos. The state of all 3 frames is syncroized using Transporter.\n\nThis example uses React but Transporter is framework agnostics and can work with any Framework. There is an issue with React Fast Refresh and likely HMR in general. This issue should be investigated before the release of v1.0.0.\n\n#### Using Transporter to Communicate with a Service Worker\n\n[codesandbox.io](https://codesandbox.io/p/devbox/transporter-service-worker-example-mvmmqc)\n\nThis example uses the `BrowserServer` API to communicate with a service worker. If you use Brave you will need to turn off its shield feature to allow service workers in 3rd party iframes, since Codesandbox will run the preview in a crossorigin iframe. You may need to turn off similar security features in other browsers as well, or open the preview in a top level browsing context.\n\n#### Using Transporter to Communicate with a Webview in React Native\n\n[snack.expo.dev](https://snack.expo.dev/@daniel_nagy/transporter-test)\n\nThis example renders a webview with a button to scan a barcode. When the button is tapped it will use the `BarCodeScanner` component from Expo to access the camera to scan a barcode. Because this example uses the camera you will need to run it on a real device. I just use the Expo Go app on my phone.\n\nTransporter does not currently offer any React Native specific APIs. However, I may add React Native specific APIs similar to the browser APIs. It's just that React Native can be..._time consuming_.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaniel-nagy%2Ftransporter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdaniel-nagy%2Ftransporter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaniel-nagy%2Ftransporter/lists"}