{"id":18563304,"url":"https://github.com/compulim/message-port-rpc","last_synced_at":"2026-06-13T06:01:22.329Z","repository":{"id":154124437,"uuid":"631795189","full_name":"compulim/message-port-rpc","owner":"compulim","description":"Turns a MessagePort into an remote procedure call (RPC) stub","archived":false,"fork":false,"pushed_at":"2026-06-11T09:52:44.000Z","size":521,"stargazers_count":0,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-11T10:04:03.242Z","etag":null,"topics":["messageport","rpc"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/message-port-rpc/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/compulim.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-04-24T04:30:40.000Z","updated_at":"2026-06-11T09:51:43.000Z","dependencies_parsed_at":"2026-06-11T10:00:47.660Z","dependency_job_id":null,"html_url":"https://github.com/compulim/message-port-rpc","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/compulim/message-port-rpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fmessage-port-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fmessage-port-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fmessage-port-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fmessage-port-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/compulim","download_url":"https://codeload.github.com/compulim/message-port-rpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fmessage-port-rpc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34273788,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["messageport","rpc"],"created_at":"2024-11-06T22:12:19.118Z","updated_at":"2026-06-13T06:01:21.434Z","avatar_url":"https://github.com/compulim.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `message-port-rpc`\n\nTurns a [`MessagePort`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort) into an remote procedure call (RPC) stub.\n\n## Background\n\nModern web apps often need to deal with multiple JavaScript workers or VMs. The communication channel is often [`MessagePort`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort).\n\nBy converting a dedicated `MessagePort` into an RPC stub, we can easily offload a Promise function to a different thread.\n\n## How to use\n\nMake sure the pair of `MessagePort` used for RPC is dedicated and not started. No other RPC, listeners, or posters should be using the same pair.\n\nIt is highly recommended to create a new [`MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel) and convert them into RPC stub.\n\n### Server stub\n\n```ts\nimport { messagePortRPC } from 'message-port-rpc';\n\nmessagePortRPC(port1, (x, y) =\u003e x + y);\n```\n\n### Client stub\n\n```ts\nimport { messagePortRPC } from 'message-port-rpc';\n\nconst rpc = messagePortRPC(port2);\n\nawait rpc(1, 2); // 3.\n```\n\n## Full Web Worker example\n\nOne of the advantage of Web Worker is to offload computation-intensive functions.\n\n### On main thread (client stub)\n\nCreates a new pair of `MessagePort`, pass one of the port to the worker thread, then create a RPC stub on another port.\n\n```ts\nimport { messagePortRPC } from 'message-port-rpc';\n\n// TypeScript: define the function type.\ntype Fn = (x: number, y: number) =\u003e Promise\u003cnumber\u003e;\n\n// Loads a Web Worker.\nconst worker = new Worker('./static/worker/js/main.js');\n\n// Creates a new pair of `MessagePort` dedicated for RPC.\nconst { port1, port2 } = new MessageChannel();\n\n// Sends the dedicated port to the worker.\nworker.postMessage(undefined, [port2]);\n\n// Creates a function stub.\nconst callFunction = messagePortRPC\u003cFn\u003e(port1);\n\n// Calls the function stub.\nconst result: number = await callFunction(1, 2);\n```\n\n### On worker thread (server stub)\n\nReceives the `MessagePort` and registers an RPC function on the port.\n\n```ts\nimport { messagePortRPC } from 'message-port-rpc';\n\n// TypeScript: define the function type.\ntype Fn = (x: number, y: number) =\u003e Promise\u003cnumber\u003e;\n\n// Receives the port dedicated for RPC.\naddEventListener('message', ({ ports }) =\u003e {\n  // Registers an RPC function on the received `MessagePort`.\n  messagePortRPC\u003cFn\u003e(ports[0], (x, y) =\u003e Promise.resolve(x + y));\n});\n```\n\nIf the worker takes time to start, it is okay, no invocations would be lost. Thanks to `MessagePort`, all messages will be queued internally until the other side signals ready to receive.\n\n## Aborting the call\n\nClient can abort an invocation sooner by passing an `AbortSignal` via the `withOptions` function. An `AbortSignal` will be passed to the remote function inside `this` context.\n\nIn the following example, we assume the client is remotely invoking a `fetch()` function, which supports `AbortSignal` natively.\n\n### Server stub\n\nThe following code snippet will use the `AbortSignal` to abort the `fetch()` call.\n\n```ts\nmessagePortRPC(ports[0], async url =\u003e {\n  // During an RPC call, the `AbortSignal` is passed in the `this` context.\n  const res = await fetch(url, { signal: this.signal });\n\n  // ...\n});\n```\n\n### Client stub\n\nThe following code snippet will call the stub with additional options to pass an `AbortSignal`.\n\n```ts\nconst abortController = new AbortController();\nconst remoteFetch = messagePortRPC(port);\n\n// Calls the stub with arguments in array, and options.\nconst fetchPromise = remoteFetch.withOptions({ signal: abortController.signal })('https://github.com');\n\n// Aborts the ongoing call.\nabortController.abort();\n\n// The promise will reject locally.\nfetchPromise.catch(error =\u003e {});\n```\n\nNotes: despite the `AbortSignal` is passed to `fetch()`, when aborted, the rejection will be done locally regardless of the result of the `fetch()` call.\n\n## Generators and iterators\n\nGenerators and iterators are supported. This helps iterating large set of data without sending it over.\n\n`Generator` and `Iterator` are automatically converted to `AsyncGenerator` and `AsyncIterator` respectively.\n\n```ts\nconst { port1, port2 } = new MessageChannel();\nconst iterateValues = (): Iterator\u003cnumber\u003e =\u003e [1, 2, 3].values();\n\nforGenerator\u003cFn\u003e(port2, iterateValues);\n\niterateValuesRemote = forGenerator\u003cFn\u003e(port1);\n\nfor await (const value of iterateValuesRemote()) {\n  console.log(value); // Will print 1, 2, 3.\n}\n```\n\nAfter the generator/iterator is exhausted (server pass `{ done: true }` to the client), both server and client will close associated `MessagePort`.\n\n[Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) is supported. To close `MessagePort` prematurely, use the `using` keyword or call `await generator[Symbol.asyncDispose]()`.\n\n## API\n\nThe following is simplified version of the API. Please refer to our published typings for the full version.\n\n```ts\nfunction messagePortRPC\u003cT extends (...args: unknown[]) =\u003e Promise\u003cunknown\u003e\u003e(\n  port: MessagePort,\n  fn?: (this: { signal: AbortSignal }, ...args: Parameters\u003cT\u003e) =\u003e ReturnType\u003cT\u003e\n): {\n  (...args: Parameters\u003cT\u003e): Promise\u003cReturnType\u003cT\u003e\u003e;\n\n  withOptions: (init: { signal?: AbortSignal }) =\u003e (...args: Parameters\u003cT\u003e): Promise\u003cReturnType\u003cT\u003e\u003e;\n};\n\nfunction forGenerator\u003c\n  T extends (...args: unknown[]) =\u003e Promise\u003cGenerator\u003cTYield, TReturn, TNext\u003e\u003e,\n  TYield = unknown,\n  TReturn = any,\n  TNext = unknown\n\u003e(\n  port: MessagePort,\n  fn?: t\n): {\n  (...args: Parameters\u003cT\u003e): AsyncGenerator\u003cTYield, TReturn, TNext\u003e;\n\n  withOptions: (init: { signal?: AbortSignal }) =\u003e (...args: Parameters\u003cT\u003e): AsyncGenerator\u003cTYield, TReturn, TNext\u003e;\n};\n```\n\n## Behaviors\n\n### Why use a dedicated `MessagePort`?\n\nInstead of multiplexing multiple calls into a single `MessagePort`, a dedicated `MessagePort` simplifies the code, easier to secure and audit the channel, and eliminates crosstalk.\n\nInternally, for every RPC call, we create a new pair of `MessagePort`. The result of the call is passed through the `MessagePort`. After the call is resolved/rejected/aborted, the `MessagePort` will be shutdown.\n\nWith a new pair of `MessagePort`, messages are queued until the event listener call [`MessagePort.start()`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/start). In other words, with dedicated `MessagePort`, calls are less likely to get lost due to false-start.\n\n### What can be passed as arguments and return value?\n\nAll arguments and return value will be sent over the `MessagePort`. The `MessagePort` implementation should send the value using the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). You should not pass `function` or `class` as an argument or return value.\n\n[Transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) in call arguments and return values are automatically populated into the `transfer` argument of `MessagePort.postMessage` function call.\n\n### Will it pass the `this` context?\n\nNo, because the `this` context is commonly a class object or [`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis). [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) probably will not work for most `this`.\n\nIf you need to pass `this`, please pass it as an argument.\n\n### Can I use it with `\u003ciframe\u003e`?\n\nYes, you can use it with `\u003ciframe\u003e`.\n\nHowever, despite the communication channel in `\u003ciframe\u003e` is very similar to `MessagePort` and supports [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), it is not `MessagePort`.\n\nYou will need to create a new `MessageChannel` and use [`HTMLIFrameElement.contentWindow.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to send one of the `MessagePort` to the `\u003ciframe\u003e` content window. Then, you can convert the `MessagePort` into RPC with this package.\n\n### Why hosting a single function vs. multiple functions?\n\nWe think a single function is much simpler, less responsibility, and more flexible approach.\n\nTo create a pool of stubs, you should create multiple `MessagePort`, one for each stub. Then, send it through an initializer stub. The receiver side receiving these ports should set up stubs for each of the port, registering their respective subroutine.\n\n### Can I call from the other side too?\n\nYes, our implementation supports bidirectional and asymmetrical calls over a single pair of `MessagePort`.\n\nYou can register different functions on both sides and call from the other side.\n\n```ts\n// On main thread:\n// - a power function is hosted on the port;\n// - the return value is the stub of the worker, which is a sum function.\nconst sum = messagePortRPC(port1, (x ** y) =\u003e x ** y);\n\nawait sum(1, 2); // 1 + 2 = 3\n```\n\n```ts\n// On worker thread:\n// - a sum function is hosted on the port;\n// - the return value is the stub of the main thread, which is a power function.\naddEventListener('message', ({ ports }) =\u003e {\n  const power = messagePortRPC(ports[0], (x + y) =\u003e x + y);\n\n  await power(3, 4); // 3 ** 4 = 81\n});\n```\n\n### Do I need to sequence the calls myself?\n\nNo, you do not need to wait for a call to return before making another call.\n\nInternally, all calls are isolated by their own pair of `MessagePort` and processed asynchronously.\n\n### Can I send `Error` object?\n\nYes, thanks to the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), you can send objects of `Error` class.\n\nHowever, there are slight differences in the error object received.\n\n```ts\nconst obj = await stub();\n\nobj instanceof Error; // False. Error object from SCA has a different prototype.\nObject.prototype.toString.call(obj) === '[object Error]'; // True.\n```\n\nAlternatively, you can recreate the error object.\n\n### Can I provide my own marshalling function?\n\nNo, we do not support marshalling function.\n\nAlternatively, you can channel `MessagePort` to a pair of marshal and unmarshal functions. Make sure you implement both marshal and unmarshal functions on both sides of the port.\n\n### Can I offload a Redux store or `useReducer` to a Web Worker?\n\nYes, you can offload them to a Web Worker. Some notes to take:\n\n- action and state must be serializable through [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)\n  - no classes, functions, DOM elements, no thunk, etc.\n- middleware must not contains code that does not work in worker\n  - no DOM access, etc.\n\nYou can look at sample [`useBindReducer`](https://github.com/compulim/message-port-rpc/tree/main/packages/pages/src/app/useBindReducer.ts) and [`useReducerSource`](https://github.com/compulim/message-port-rpc/tree/main/packages/pages/src/iframe/useReducerSource.ts) to see how it work.\n\nWe will eventually made these React hooks available. Stay tuned.\n\n### How could I stop the stub from listening to a port?\n\nTo stop the stub, you should close the port by calling [`MessagePort.close()`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/close).\n\nThe port for the stub must be dedicated and not to be reused. When you want to stop the stub from listening to a port, you should simply close the port.\n\n### Why don't you create `MessagePort` for me?\n\nWe understood there are hassles to create `MessagePort` yourself.\n\nWe spent a lot of time experimenting with different options and landed on this design for several reasons:\n\n- you own the resources and control the lifetime of the resources, less likely to resources leak\n- you do not need to create the stub before sending the port to the other side\n- you can control which side creates the ports and do not need to pipe them yourself\n- you can build marshal/unmarshal function without too much piping\n- you can build a `MessagePort`-like custom channel without extra piping\n\nThere are downsides:\n\n- you forget to dedicate the `MessagePort` to a stub\n- you need to write one more line of code\n\nAt the end of the day, we think channel customization outweighted the disadvantages and made a bet on this design.\n\n### Can I use `BroadcastChannel` to listen to many client stubs at once?\n\nNo, you cannot use [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).\n\n`BroadcastChannel` does not support sending `MessagePort` and other [transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects), which is critical to the operation of the stub.\n\n### Why should I use this implementation of RPC?\n\nWe are professional developers. Our philosophy makes this package easy to use.\n\n- Standards: we use [`MessagePort`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort) and [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) as-is\n- Airtight: we wrap everything in their own `MessagePort`, no multiplexing = no crosstalks\n- Small scope: one `MessagePort` host one function only, more flexibility on building style\n- Simple: you almost know how to write this package\n- Maintainability: we relies heavily on tooling and automation to maintain this package\n\n## Contributions\n\nLike us? [Star](https://github.com/compulim/message-port-rpc/stargazers) us.\n\nWant to make it better? [File](https://github.com/compulim/message-port-rpc/issues) us an issue.\n\nDon't like something you see? [Submit](https://github.com/compulim/message-port-rpc/pulls) a pull request.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompulim%2Fmessage-port-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcompulim%2Fmessage-port-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompulim%2Fmessage-port-rpc/lists"}