├── .npmrc ├── .editorconfig ├── .eslintignore ├── .media ├── ipc.png ├── logo.png └── type-infer.png ├── .eslintrc.js ├── .gitignore ├── tsconfig.json ├── tsconfig.examples.json ├── examples ├── child-process │ ├── port.ts │ ├── child.ts │ └── parent.ts ├── web-socket │ ├── port.ts │ ├── client.ts │ ├── server.ts │ ├── server-legcy.ts │ └── index.html └── child-process-rpc │ ├── port.ts │ ├── child.ts │ └── parent.ts ├── .github └── workflows │ └── test.yml ├── tsconfig.src.json ├── LICENSE ├── .vscode └── settings.json ├── package.json ├── CHANGELOG.md ├── .archived └── protootype.ts ├── __tests__ ├── index.test.ts └── rpc.test.ts ├── README.md └── src └── index.ts /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | max_line_length = 120 2 | indent_style = space 3 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __e2e__ 3 | __fixtures__ 4 | lib 5 | es 6 | dist 7 | -------------------------------------------------------------------------------- /.media/ipc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-infra-dev/unport/HEAD/.media/ipc.png -------------------------------------------------------------------------------- /.media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-infra-dev/unport/HEAD/.media/logo.png -------------------------------------------------------------------------------- /.media/type-infer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-infra-dev/unport/HEAD/.media/type-infer.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-typescript-library', 3 | rules: { 4 | camelcase: 'off', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | esm 4 | .DS_Store 5 | examples-compiled 6 | *.tsbuildinfo 7 | tsconfig.examples.tsbuildinfo 8 | tsconfig.tsbuildinfo 9 | coverage -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "unport": [ 5 | "./src/index.ts" 6 | ] 7 | } 8 | }, 9 | "references": [ 10 | { 11 | "path": "./tsconfig.src.json" 12 | }, 13 | { 14 | "path": "./tsconfig.examples.json" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "rootDir": "examples", 5 | "outDir": "./examples-compiled", 6 | }, 7 | "include": [ 8 | "examples", 9 | ], 10 | "exclude": [ 11 | "node_modules", 12 | ], 13 | "references": [ 14 | { 15 | "path": "./tsconfig.src.json" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /examples/child-process/port.ts: -------------------------------------------------------------------------------- 1 | import { Unport } from '../../src'; 2 | 3 | export type Definition = { 4 | parent2child: { 5 | syn: { 6 | pid: string; 7 | }; 8 | body: { 9 | name: string; 10 | path: string; 11 | } 12 | }; 13 | child2parent: { 14 | ack: { 15 | pid: string; 16 | }; 17 | }; 18 | }; 19 | 20 | export type ChildPort = Unport; 21 | export type ParentPort = Unport; 22 | -------------------------------------------------------------------------------- /examples/web-socket/port.ts: -------------------------------------------------------------------------------- 1 | import { Unport } from '../../src'; 2 | 3 | export type Definition = { 4 | server2client: { 5 | ack: { 6 | pid: string; 7 | }; 8 | }; 9 | client2server: { 10 | syn: { 11 | pid: string; 12 | }; 13 | body: { 14 | name: string; 15 | path: string; 16 | } 17 | }; 18 | }; 19 | 20 | export type ClientPort = Unport; 21 | export type ServerPort = Unport; 22 | -------------------------------------------------------------------------------- /examples/child-process-rpc/port.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Unport } from '../../lib'; 3 | 4 | export type Definition = { 5 | parent2child: { 6 | syn: { 7 | pid: string; 8 | }; 9 | getParentInfo__callback: { 10 | parentId: string; 11 | from: string; 12 | }; 13 | getChildInfo: { 14 | name: string; 15 | } 16 | }; 17 | child2parent: { 18 | getParentInfo: { 19 | user: string; 20 | }; 21 | getChildInfo__callback: { 22 | childId: string; 23 | }; 24 | ack: { 25 | pid: string; 26 | }; 27 | }; 28 | }; 29 | 30 | export type ChildPort = Unport; 31 | export type ParentPort = Unport; 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # https://pnpm.io/zh/5.x/continuous-integration 2 | 3 | name: pnpm Example Workflow 4 | on: 5 | push: 6 | jobs: 7 | build: 8 | runs-on: ubuntu-20.04 9 | strategy: 10 | matrix: 11 | node-version: [16,18] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Cache .pnpm-store 19 | uses: actions/cache@v1 20 | with: 21 | path: ~/.pnpm-store 22 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 23 | - name: Install pnpm 24 | run: npm install -g pnpm@8.7.0 25 | - name: pnpm Build 26 | run: pnpm install 27 | - name: test 28 | run: pnpm run cov -------------------------------------------------------------------------------- /examples/child-process/child.ts: -------------------------------------------------------------------------------- 1 | import { Unport, ChannelMessage } from '../../src'; 2 | import { ChildPort } from './port'; 3 | 4 | // 1. Initialize a port 5 | const childPort: ChildPort = new Unport(); 6 | 7 | // 2. Implement a Channel based on underlying IPC capabilities 8 | childPort.implementChannel({ 9 | send(message) { 10 | process.send && process.send(message); 11 | }, 12 | accept(pipe) { 13 | process.on('message', (message: ChannelMessage) => { 14 | pipe(message); 15 | }); 16 | }, 17 | }); 18 | 19 | // 3. You get a complete typed Port with a unified interface 🤩 20 | childPort.onMessage('syn', payload => { 21 | console.log('[child] [syn]', payload.pid); 22 | childPort.postMessage('ack', { pid: 'child' }); 23 | }); 24 | childPort.onMessage('body', payload => { 25 | console.log('[child] [body]', JSON.stringify(payload)); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/web-socket/client.ts: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | import { Unport } from '../../src'; 3 | import { ClientPort } from './port'; 4 | 5 | // 1. Initialize a port 6 | const clientPort: ClientPort = new Unport(); 7 | 8 | // 2. Implement a Channel based on underlying IPC capabilities 9 | const socket = io('http://localhost:10101/'); 10 | socket.on('connect', () => { 11 | clientPort.implementChannel(() => ({ 12 | send(message) { 13 | socket.emit('message', message); 14 | }, 15 | accept(pipe) { 16 | socket.on('message', pipe); 17 | }, 18 | })); 19 | }); 20 | 21 | // 3. You get a complete typed Port with a unified interface 🤩 22 | clientPort.postMessage('syn', { pid: 'parent' }); 23 | clientPort.onMessage('ack', payload => { 24 | console.log('[parent] [ack]', payload.pid); 25 | clientPort.postMessage('body', { 26 | name: 'index', 27 | path: ' /', 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/child-process/parent.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { fork } from 'child_process'; 3 | import { Unport, ChannelMessage } from '../../src'; 4 | import { ParentPort } from './port'; 5 | 6 | // 1. Initialize a port 7 | const parentPort: ParentPort = new Unport(); 8 | 9 | // 2. Implement a Channel based on underlying IPC capabilities 10 | const childProcess = fork(join(__dirname, './child.js')); 11 | parentPort.implementChannel({ 12 | send(message) { 13 | childProcess.send(message); 14 | }, 15 | accept(pipe) { 16 | childProcess.on('message', (message: ChannelMessage) => { 17 | pipe(message); 18 | }); 19 | }, 20 | }); 21 | 22 | // 3. You get a complete typed Port with a unified interface 🤩 23 | parentPort.postMessage('syn', { pid: 'parent' }); 24 | parentPort.onMessage('ack', payload => { 25 | console.log('[parent] [ack]', payload.pid); 26 | parentPort.postMessage('body', { 27 | name: 'index', 28 | path: ' /', 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/child-process-rpc/child.ts: -------------------------------------------------------------------------------- 1 | import { Unport, Unrpc, ChannelMessage } from '../../lib'; 2 | import { ChildPort } from './port'; 3 | 4 | // 1. Initialize a port 5 | const childPort: ChildPort = new Unport(); 6 | 7 | // 2. Implement a Channel based on underlying IPC capabilities 8 | childPort.implementChannel({ 9 | send(message) { 10 | process.send && process.send(message); 11 | }, 12 | accept(pipe) { 13 | process.on('message', (message: ChannelMessage) => { 14 | pipe(message); 15 | }); 16 | }, 17 | }); 18 | 19 | // 3. Initialize a rpc client 20 | const childRpcClient = new Unrpc(childPort); 21 | childRpcClient.implement('getChildInfo', request => ({ 22 | childId: 'child_123', 23 | })); 24 | childRpcClient.port.onMessage('syn', async payload => { 25 | console.log('[child] [event] [syn] [result]', payload); 26 | const response = await childRpcClient.call('getParentInfo', { user: 'child' }); 27 | console.log('[child] [rpc] [getInfo] [response]', response); 28 | childPort.postMessage('ack', { pid: 'child' }); 29 | }); 30 | -------------------------------------------------------------------------------- /tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictFunctionTypes": true, 5 | "incremental": true, 6 | "noUncheckedIndexedAccess": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "allowJs": false, 11 | "alwaysStrict": true, 12 | "skipLibCheck": true, 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "noEmitOnError": false, 16 | "noImplicitThis": true, 17 | "noImplicitAny": true, 18 | "rootDir": "src", 19 | "outDir": "./lib", 20 | "sourceMap": true, 21 | "target": "ES2019", 22 | "typeRoots": [ 23 | "node_modules/@types", 24 | "types" 25 | ], 26 | "declaration": true, 27 | "declarationMap": true, 28 | "resolveJsonModule": true, 29 | "downlevelIteration": true, 30 | "lib": [ 31 | "es5", 32 | "es6" 33 | ], 34 | "composite": true 35 | }, 36 | "include": [ 37 | "src", 38 | "types", 39 | "*.d.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) ULIVZ (https://github.com/ulivz) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact", 7 | "html", 8 | "markdown" 9 | ], 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit" 12 | }, 13 | "search.exclude": { 14 | "**/.git": true, 15 | "**/.svn": true, 16 | "**/.hg": true, 17 | "**/CVS": true, 18 | "**/.DS_Store": true, 19 | "docs/docs/out/**": true, 20 | "packages/*/lib/**": true 21 | }, 22 | "typescript.tsdk": "node_modules/typescript/lib", 23 | "[typescript]": { 24 | "editor.defaultFormatter": "vscode.typescript-language-features" 25 | }, 26 | "[typescriptreact]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | }, 29 | "[javascript]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | }, 32 | "[javascriptreact]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "[markdown]": { 36 | "editor.quickSuggestions": { 37 | "other": true, 38 | "comments": true, 39 | "strings": true 40 | } 41 | }, 42 | "eslint.trace.server": "verbose", 43 | "liveServer.settings.port": 5501, 44 | "js/ts.implicitProjectConfig.strictNullChecks": false, 45 | "cSpell.words": [ 46 | "camelcase", 47 | "insx", 48 | "Unport", 49 | "Unrpc" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /examples/web-socket/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { Server as SocketServer, Socket } from 'socket.io'; 3 | import { ServerPort } from './port'; 4 | import { Unport } from '../../src'; 5 | 6 | // 1. Initialize a port 7 | const serverPort: ServerPort = new Unport(); 8 | 9 | // 2. Implement a Channel based on underlying IPC capabilities 10 | const server = createServer(); 11 | const io = new SocketServer(server, { 12 | cors: { 13 | origin: '*', 14 | methods: ['GET', 'POST'], 15 | }, 16 | }); 17 | const sockets: Record = {}; 18 | const channel = serverPort.implementChannel({ 19 | send: message => { 20 | Object.values(sockets).forEach(socket => { 21 | socket.emit('message', message); 22 | }); 23 | }, 24 | }); 25 | io.on('connection', (socket: Socket) => { 26 | sockets[socket.id] = socket; 27 | socket.on('disconnect', () => { 28 | delete sockets[socket.id]; 29 | }); 30 | socket.on('message', message => channel.pipe(message)); 31 | }); 32 | 33 | server.listen(10101); 34 | 35 | // 3. You get a complete typed Port with a unified interface 🤩 36 | serverPort.onMessage('syn', payload => { 37 | console.log('[child] [syn]', payload.pid); 38 | serverPort.postMessage('ack', { pid: 'child' }); 39 | }); 40 | serverPort.onMessage('body', payload => { 41 | console.log('[child] [body]', JSON.stringify(payload)); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /examples/child-process-rpc/parent.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { fork } from 'child_process'; 3 | import { Unport, Unrpc, ChannelMessage } from '../../lib'; 4 | import { ParentPort } from './port'; 5 | 6 | // 1. Initialize a port 7 | const parentPort: ParentPort = new Unport(); 8 | 9 | // 2. Implement a Channel based on underlying IPC capabilities 10 | const childProcess = fork(join(__dirname, './child.js')); 11 | parentPort.implementChannel({ 12 | send(message) { 13 | childProcess.send(message); 14 | }, 15 | accept(pipe) { 16 | childProcess.on('message', (message: ChannelMessage) => { 17 | pipe(message); 18 | }); 19 | }, 20 | destroy() { 21 | childProcess.removeAllListeners('message'); 22 | childProcess.kill(); 23 | }, 24 | }); 25 | 26 | // 3. Initialize a rpc client from port. 27 | const parentRpcClient = new Unrpc(parentPort); 28 | 29 | parentRpcClient.implement('getParentInfo', request => ({ 30 | from: request.user, 31 | parentId: 'parent123', 32 | })); 33 | parentRpcClient.port.postMessage('syn', { pid: 'parent' }); 34 | parentRpcClient.port.onMessage('ack', async payload => { 35 | console.log('[parent] [event] [ack] [result]', payload); 36 | const response = await parentRpcClient.call('getChildInfo', { 37 | name: 'parent', 38 | }); 39 | console.log('[parent] [rpc] [getChildInfo] [response]', response); 40 | setTimeout(() => { 41 | console.log('destroy'); 42 | parentPort.destroy(); 43 | }, 1000); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/web-socket/server-legcy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file using a old implementation for `one-to-many` scenario, 3 | * which could be refactored by `.pipe()`, 4 | * 5 | * @see https://github.com/web-infra-dev/unport/pull/2 6 | */ 7 | import { createServer } from 'http'; 8 | import { Server as SocketServer, Socket } from 'socket.io'; 9 | import { ServerPort } from './port'; 10 | import { Unport } from '../../src'; 11 | 12 | // 1. Initialize a port map 13 | const socketPorts: Map = new Map(); 14 | 15 | // 2. Implement a Channel based on underlying IPC capabilities 16 | const server = createServer(); 17 | const io = new SocketServer(server, { 18 | cors: { 19 | origin: '*', 20 | methods: ['GET', 'POST'], 21 | }, 22 | }); 23 | 24 | io.on('connection', (socket: Socket) => { 25 | // One connection, one port instance 26 | const socketPort: ServerPort = new Unport(); 27 | socketPort.implementChannel({ 28 | send: message => { 29 | socket.emit('message', message); 30 | }, 31 | accept(pipe) { 32 | socket.on('message', message => { 33 | pipe(message); 34 | }); 35 | }, 36 | }); 37 | 38 | socketPorts.set(socket.id, socketPort); 39 | 40 | socket.on('disconnect', () => { 41 | socketPorts.delete(socket.id); 42 | }); 43 | }); 44 | 45 | server.listen(10101); 46 | 47 | // You'll get many ports so that you need to retrieve it before using it. 48 | const serverPort = socketPorts.get('' /* id */); 49 | if (serverPort) { 50 | serverPort.onMessage('syn', payload => { 51 | console.log('[child] [syn]', payload.pid); 52 | serverPort.postMessage('ack', { pid: 'child' }); 53 | }); 54 | serverPort.onMessage('body', payload => { 55 | console.log('[child] [body]', JSON.stringify(payload)); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unport", 3 | "description": "Unport - a Universal Port with strict type inference capability for cross-JSContext communication.", 4 | "version": "0.7.1", 5 | "main": "lib/index.js", 6 | "module": "esm/index.js", 7 | "typings": "esm/index.d.ts", 8 | "publishConfig": { 9 | "registry": "https://registry.npmjs.org", 10 | "access": "public" 11 | }, 12 | "keywords": [ 13 | "typescript", 14 | "lib" 15 | ], 16 | "scripts": { 17 | "bootstrap": "pnpm i --prefer-offline", 18 | "clean": "rm -rf cjs esm", 19 | "dev": "run-p dev:cjs dev:esm dev:example", 20 | "build": "run-s build:cjs build:esm build:example", 21 | "dev:cjs": "npm run build:cjs -- --watch", 22 | "dev:esm": "npm run build:esm -- --watch", 23 | "build:cjs": "tsc -p tsconfig.src.json --module commonjs --outDir lib", 24 | "build:esm": "tsc -p tsconfig.src.json --module ES2015 --outDir esm", 25 | "dev:example": "tsc -p tsconfig.examples.json --watch", 26 | "build:example": "tsc -p tsconfig.examples.json", 27 | "prepublishOnly": "npm run build", 28 | "lint": "eslint -c .eslintrc.js src --ext .js,.jsx,.ts,.tsx", 29 | "lint:fix": "npm run lint -- --fix", 30 | "test:watch": "vitest", 31 | "test": "vitest run", 32 | "cov": "vitest run --coverage", 33 | "release": "quick-publish" 34 | }, 35 | "engines": { 36 | "node": ">=16", 37 | "pnpm": "9.9.0" 38 | }, 39 | "packageManager": "pnpm@9.9.0", 40 | "devDependencies": { 41 | "@types/node": "18.7.6", 42 | "@vitest/coverage-v8": "^0.34.6", 43 | "eslint": "7", 44 | "eslint-config-typescript-library": "0.2.4", 45 | "npm-run-all": "4.1.5", 46 | "prettier": "2.7.1", 47 | "quick-publish": "0.6.0", 48 | "tsx": "^4.1.2", 49 | "typescript": "4.7.4", 50 | "vitest": "^0.34.6", 51 | "socket.io": "^4.7.2", 52 | "socket.io-client": "^4.5.1" 53 | }, 54 | "files": [ 55 | "bin", 56 | "esm", 57 | "!esm/*.tsbuildinfo", 58 | "!esm/*.map", 59 | "lib", 60 | "!lib/*.tsbuildinfo", 61 | "!lib/*.map", 62 | "types", 63 | "*.d.ts" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.7.1](https://github.com/ulivz/unport/compare/v0.6.0...v0.7.1) (2025-03-09) 2 | 3 | 4 | ### Features 5 | 6 | * add offListener method ([91ee549](https://github.com/ulivz/unport/commit/91ee5499ec69f3368b7adc447bdcd2e663bccf1c)) 7 | 8 | 9 | 10 | # [0.6.0](https://github.com/ulivz/unport/compare/v0.5.0...v0.6.0) (2024-06-04) 11 | 12 | 13 | ### Features 14 | 15 | * typed rpc ([#3](https://github.com/ulivz/unport/issues/3)) ([baa5645](https://github.com/ulivz/unport/commit/baa5645c96a7aade2166ae7e99252b49e3f0c03a)) 16 | 17 | 18 | 19 | # [0.5.0](https://github.com/ulivz/unport/compare/v0.3.1...v0.5.0) (2023-11-17) 20 | 21 | To streamline usage, we have implemented a significant update: the basic concept `UnportChannel` has been renamed to `Channel`, see [3e2bcc7](https://github.com/web-infra-dev/unport/commit/3e2bcc73bb97e7d46b7c7f79a1b9481c98157bdc). 22 | 23 | # [0.4.0](https://github.com/ulivz/unport/compare/v0.3.1...v0.4.0) (2023-11-17) 24 | 25 | 26 | To support `one-to-many` scenarios and avoid creating multiple Unport instances, we have introduced the [Channel.pipe](https://github.com/web-infra-dev/unport#pipe) method. This allows users to manually send messages through the intermediary pipeline, enhancing efficiency and flexibility, see [b8ef448](https://github.com/web-infra-dev/unport/commit/b8ef4482088e994eef37823a6991a67a93c5c77c). 27 | 28 | 29 | 30 | ## [0.3.1](https://github.com/ulivz/unport/compare/v0.3.0...v0.3.1) (2023-11-17) 31 | 32 | This is a patch release where we have removed some unnecessary logs 33 | 34 | 35 | # [0.3.0](https://github.com/ulivz/unport/compare/v0.2.0...v0.3.0) (2023-11-17) 36 | 37 | 38 | Added support for watching messages multiple times and provided users with the ability to destroy a port ([#1](https://github.com/ulivz/unport/issues/1)) ([a179e61](https://github.com/ulivz/unport/commit/a179e616983004f04e40ae9b85ea73cbe81d9083)) 39 | 40 | 41 | # [0.2.0](https://github.com/ulivz/unport/compare/v0.1.0...v0.2.0) (2023-11-17) 42 | 43 | Renamed the exported `UnPort` to `Unport` for consistency and ease of use, see [b1b8b56](b1b8b5694043f1bccbe3f86b78b20351988c0d4f). 44 | 45 | 46 | # [0.1.0](https://github.com/ulivz/unport/compare/93c89d960e8dab105e5e1b46df2b2179bdb1c945...v0.1.0) (2023-11-16) 47 | 48 | This is the inaugural release of Unport. It encapsulates the core concept of Unport: `TypedPort = f(types, channel)`. We have successfully implemented the fundamental functionalities. 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/web-socket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket-IO client 5 | 6 | 7 | 8 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /.archived/protootype.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | /* eslint-disable quotes */ 3 | 4 | /** 5 | * Multidirectional Typed Port 6 | */ 7 | export namespace MultidirectionalTypedPort { 8 | /** 9 | * Format: 2 10 | */ 11 | export type Channel = { 12 | server2client: { 13 | foo: string; 14 | bar: { 15 | a: string; 16 | b: string; 17 | }; 18 | }; 19 | client2server: { 20 | baz: string; 21 | qux: { 22 | c: string; 23 | d: string; 24 | }; 25 | }; 26 | }; 27 | 28 | export type EnsureString = T extends string ? T : never; 29 | export type Direction = keyof Channel; 30 | 31 | /** 32 | * Infer the port 33 | */ 34 | type GetPorts = { 35 | [k in keyof T]: k extends `${infer A}2${infer B}` ? A | B : k; 36 | }[keyof T]; 37 | 38 | /** 39 | * Current port: "server" | "client" 40 | */ 41 | export type Ports = GetPorts; 42 | 43 | export type ReverseDirection< 44 | T extends Direction, 45 | Sep extends string = "2" 46 | > = T extends `${infer A}${Sep}${infer B}` ? `${B}${Sep}${A}` : T; 47 | 48 | type Payload = Channel[D][U]; 49 | 50 | export type Callback< 51 | T extends unknown[] = [], 52 | U = unknown 53 | > = (...args: T) => U; 54 | 55 | export interface Port { 56 | postMessage(t: T, p: Payload): void; 57 | onMessage]>( 58 | t: T, 59 | handler: Callback<[Payload, T>]> 60 | ): void; 61 | } 62 | } 63 | 64 | declare const client2serverPort: MultidirectionalTypedPort.Port<"client2server">; 65 | client2serverPort.postMessage("qux", { c: "a", d: "d" }); 66 | client2serverPort.postMessage("baz", "x"); 67 | client2serverPort.onMessage("foo", message => { 68 | // => string 69 | }); 70 | client2serverPort.onMessage("bar", message => { 71 | // => { a: string; b: string; } 72 | }); 73 | 74 | /** 75 | * Port Adapter. 76 | */ 77 | export interface Message { 78 | t: string | number | symbol; 79 | p: any; 80 | } 81 | 82 | export interface PortAdaptor { 83 | postMessage(message: Message): void; 84 | onmessage?: (message: Message) => void; 85 | } 86 | 87 | export function buildPort( 88 | adaptor: PortAdaptor, 89 | ): MultidirectionalTypedPort.Port { 90 | // eslint-disable-next-line @typescript-eslint/ban-types 91 | const handlers: Record = {}; 92 | 93 | adaptor.onmessage = function (msg: Message) { 94 | const { t, p } = msg; 95 | const handler = handlers[t]; 96 | if (handler) { 97 | handler(p); 98 | } 99 | }; 100 | 101 | const port: MultidirectionalTypedPort.Port = { 102 | postMessage(t, p) { 103 | adaptor.postMessage({ t, p }); 104 | }, 105 | onMessage(t, handler) { 106 | handlers[t] = handler; 107 | }, 108 | }; 109 | 110 | return port; 111 | } 112 | 113 | /** 114 | * Examples 115 | */ 116 | 117 | /** 118 | * ChildProcess (Parent) 119 | */ 120 | declare const childProcess: import("child_process").ChildProcess; 121 | const parentPortAdaptor: PortAdaptor = { 122 | postMessage: message => { 123 | childProcess.send(message); 124 | }, 125 | }; 126 | 127 | childProcess.on("message", (message: Message) => { 128 | parentPortAdaptor.onmessage && parentPortAdaptor.onmessage(message); 129 | }); 130 | 131 | declare function getPort( 132 | d: PortAdaptor 133 | ): MultidirectionalTypedPort.Port; 134 | 135 | const parentPort = getPort<"server2client">(parentPortAdaptor); 136 | parentPort.postMessage("bar", { a: "a", b: "b" }); 137 | 138 | /** 139 | * ChildProcess (Child) 140 | */ 141 | const childPortAdaptor: PortAdaptor = { 142 | postMessage: message => { 143 | process.send && process.send(message); 144 | }, 145 | }; 146 | 147 | process.on("message", (message: Message) => { 148 | childPortAdaptor.onmessage && childPortAdaptor.onmessage(message); 149 | }); 150 | 151 | const childPort = getPort<"client2server">(childPortAdaptor); 152 | childPort.postMessage("qux", { c: "c", d: "d" }); 153 | childPort.onMessage("bar", p => { 154 | console.log(p.a); 155 | }); 156 | -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, beforeEach, vi, expect } from 'vitest'; 2 | import { Unport, Channel } from '../src'; 3 | 4 | export type Definition = { 5 | parent2child: { 6 | syn: { 7 | pid: string; 8 | }; 9 | body: { 10 | name: string; 11 | path: string; 12 | }; 13 | }; 14 | child2parent: { 15 | ack: { 16 | pid: string; 17 | }; 18 | }; 19 | }; 20 | 21 | export type ChildPort = Unport; 22 | export type ParentPort = Unport; 23 | 24 | describe('Unport', () => { 25 | let mockChannel: Channel; 26 | let unPort: ParentPort; 27 | 28 | beforeEach(() => { 29 | mockChannel = { 30 | send: vi.fn(), 31 | accept: vi.fn(), 32 | }; 33 | 34 | unPort = new Unport(); 35 | }); 36 | 37 | test('should implement channel correctly when provided channel as object', () => { 38 | unPort.implementChannel(mockChannel); 39 | expect(mockChannel.accept).toHaveBeenCalledOnce(); 40 | expect(unPort.channel).toBe(mockChannel); 41 | }); 42 | 43 | test('should implement channel correctly when provided channel as function returning object', () => { 44 | const mockChannelFn = () => mockChannel; 45 | unPort.implementChannel(mockChannelFn); 46 | expect(unPort.channel).toBe(mockChannel); 47 | }); 48 | 49 | test('should throw error when provided invalid channel object', () => { 50 | const invalidChannel = {}; // Does not contain required 'send' method 51 | // @ts-expect-error we mocked error here 52 | expect(() => unPort.implementChannel(invalidChannel)).toThrowError( 53 | '[1] invalid channel implementation', 54 | ); 55 | }); 56 | 57 | test('should throw error when provided invalid channel function', () => { 58 | const invalidChannelFn = () => ({} as Channel); // Does not contain required 'send' method 59 | expect(() => unPort.implementChannel(invalidChannelFn)).toThrowError( 60 | '[1] invalid channel implementation', 61 | ); 62 | }); 63 | 64 | test('should correctly call channel send method', () => { 65 | unPort.implementChannel(mockChannel); 66 | unPort.postMessage('syn', { pid: '1' }); 67 | expect(mockChannel.send).toHaveBeenCalledWith({ 68 | t: 'syn', 69 | p: { pid: '1' }, 70 | _$: 'un', 71 | }); 72 | }); 73 | 74 | test('should correctly call registered message handler', () => { 75 | const channel = unPort.implementChannel(mockChannel); 76 | const messageHandler = vi.fn(); 77 | 78 | unPort.onMessage('ack', messageHandler); 79 | channel.pipe({ t: 'ack', p: { pid: 'child' }, _$: 'un' }); 80 | 81 | expect(messageHandler).toHaveBeenCalledWith({ pid: 'child' }); 82 | }); 83 | 84 | test('should not call message handler when _$ is not "un"', () => { 85 | const channel = unPort.implementChannel(mockChannel); 86 | const messageHandler = vi.fn(); 87 | 88 | unPort.onMessage('ack', messageHandler); 89 | // @ts-expect-error we mocked error here 90 | channel.pipe({ t: 'ack', p: { pid: 'child' }, _$: 'other' }); 91 | 92 | expect(messageHandler).not.toHaveBeenCalledWith(); 93 | }); 94 | 95 | test('should ignore unknown messages', () => { 96 | const channel = unPort.implementChannel(mockChannel); 97 | const messageHandler = vi.fn(); 98 | 99 | unPort.onMessage('ack', messageHandler); 100 | // @ts-expect-error we mocked error here 101 | channel.pipe('unknown'); 102 | 103 | expect(messageHandler).not.toHaveBeenCalledWith(); 104 | }); 105 | 106 | test('should correctly handle multiple message handlers', () => { 107 | const channel = unPort.implementChannel(mockChannel); 108 | const messageHandler1 = vi.fn(); 109 | const messageHandler2 = vi.fn(); 110 | const messageHandler3 = vi.fn(); 111 | 112 | unPort.onMessage('ack', messageHandler1); 113 | unPort.onMessage('ack', messageHandler2); 114 | unPort.onMessage('ack', messageHandler3); 115 | channel.pipe({ t: 'ack', p: { pid: 'child' }, _$: 'un' }); 116 | 117 | expect(messageHandler1).toHaveBeenCalledWith({ pid: 'child' }); 118 | expect(messageHandler2).toHaveBeenCalledWith({ pid: 'child' }); 119 | expect(messageHandler3).toHaveBeenCalledWith({ pid: 'child' }); 120 | }); 121 | 122 | test('should handle port destruction correctly - postMessage', () => { 123 | const channel = unPort.implementChannel(mockChannel); 124 | unPort.postMessage('syn', { pid: '1' }); 125 | expect(mockChannel.send).toHaveBeenCalledTimes(1); 126 | 127 | unPort.destroy(); 128 | 129 | // When the port has been destroyed, it should no longer accept messages 130 | expect(() => unPort.postMessage('syn', { pid: '1' })).toThrowError( 131 | '[2] Port is not implemented or has been destroyed', 132 | ); 133 | expect(mockChannel.send).toHaveBeenCalledTimes(1); 134 | }); 135 | 136 | test('should handle port destruction correctly - onMessage', () => { 137 | const channel = unPort.implementChannel(mockChannel); 138 | const messageHandler = vi.fn(); 139 | 140 | // Handlers registered after destroy should not get called 141 | unPort.onMessage('ack', messageHandler); 142 | channel.pipe({ t: 'ack', p: { pid: 'child' }, _$: 'un' }); 143 | expect(messageHandler).toBeCalledTimes(1); 144 | 145 | unPort.destroy(); 146 | 147 | channel.send({ t: 'ack', p: { pid: 'child' }, _$: 'un' }); 148 | expect(messageHandler).toBeCalledTimes(1); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /__tests__/rpc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { ChannelMessage, Unport, Unrpc, UnrpcExecutionErrorError, UnrpcNotImplementationError } from '../src'; 3 | 4 | export type Definition = { 5 | parent2child: { 6 | syn: { 7 | pid: string; 8 | }; 9 | getInfo__callback: { 10 | user: string; 11 | }; 12 | getChildInfo: { 13 | name: string; 14 | } 15 | }; 16 | child2parent: { 17 | getInfo: { 18 | id: string; 19 | }; 20 | getChildInfo__callback: { 21 | clientKey: string; 22 | }; 23 | ack: { 24 | pid: string; 25 | }; 26 | }; 27 | }; 28 | 29 | describe('Unrpc', () => { 30 | let childPort: Unport; 31 | let parentPort: Unport; 32 | let child: Unrpc; 33 | let parent: Unrpc; 34 | 35 | beforeEach(() => { 36 | const messageChannel = new MessageChannel(); 37 | if (childPort) childPort.destroy(); 38 | childPort = new Unport(); 39 | childPort.implementChannel({ 40 | send(message) { 41 | messageChannel.port1.postMessage(message); 42 | }, 43 | accept(pipe) { 44 | messageChannel.port1.onmessage = (message: MessageEvent) => pipe(message.data); 45 | }, 46 | destroy() { 47 | messageChannel.port1.close(); 48 | }, 49 | }); 50 | child = new Unrpc(childPort); 51 | 52 | parentPort = new Unport(); 53 | parentPort.implementChannel({ 54 | send(message) { 55 | console.log(message); 56 | messageChannel.port2.postMessage(message); 57 | }, 58 | accept(pipe) { 59 | messageChannel.port2.onmessage = (message: MessageEvent) => pipe(message.data); 60 | }, 61 | destroy() { 62 | messageChannel.port2.close(); 63 | }, 64 | }); 65 | 66 | parent = new Unrpc(parentPort); 67 | }); 68 | 69 | it('implemented method - asynchronous implementation', async () => { 70 | parent.implement('getInfo', async ({ id }) => ({ user: id })); 71 | const response = child.call('getInfo', { id: 'name' }); 72 | expect(response).resolves.toMatchObject({ user: 'name' }); 73 | }); 74 | 75 | it('implemented method - synchronous implementation', async () => { 76 | parent.implement('getInfo', ({ id }) => ({ user: id })); 77 | const response = child.call('getInfo', { id: 'name' }); 78 | expect(response).resolves.toMatchObject({ user: 'name' }); 79 | }); 80 | 81 | it('Error: UnrpcNotImplementationError', async () => { 82 | expect(child.call('getInfo', { id: 'name' })).rejects.toMatchObject( 83 | new UnrpcNotImplementationError('Method getInfo is not implemented'), 84 | ); 85 | }); 86 | 87 | it('Error: UnrpcExecutionErrorError - script error - asynchronous implementation', async () => { 88 | parent.implement('getInfo', async () => { 89 | // @ts-expect-error mock execution error here. 90 | const result = foo; 91 | return result; 92 | }); 93 | expect(child.call('getInfo', { id: 'name' })).rejects.toMatchObject( 94 | new UnrpcExecutionErrorError('foo is not defined'), 95 | ); 96 | }); 97 | 98 | it('Error: UnrpcExecutionErrorError - script error - synchronous implementation', async () => { 99 | parent.implement('getInfo', () => { 100 | // @ts-expect-error mock execution error here. 101 | const result = foo; 102 | return result; 103 | }); 104 | expect(child.call('getInfo', { id: 'name' })).rejects.toMatchObject( 105 | new UnrpcExecutionErrorError('foo is not defined'), 106 | ); 107 | }); 108 | 109 | it('Error: UnrpcExecutionErrorError - user throws error', async () => { 110 | parent.implement('getInfo', () => { 111 | throw new Error('mock error'); 112 | }); 113 | expect(child.call('getInfo', { id: 'name' })).rejects.toMatchObject( 114 | new UnrpcExecutionErrorError('mock error'), 115 | ); 116 | }); 117 | 118 | it('complicated case', async () => { 119 | parent.implement('getInfo', async ({ id }) => ({ user: id })); 120 | child.implement('getChildInfo', async ({ name }) => ({ clientKey: name })); 121 | 122 | let finishHandshake: (value?: unknown) => void; 123 | const handshakePromise = new Promise(resolve => { 124 | finishHandshake = resolve; 125 | }); 126 | 127 | /** 128 | * Simulates a handshake 129 | */ 130 | parent.port.postMessage('syn', { pid: 'parent' }); 131 | parent.port.onMessage('ack', async payload => { 132 | expect(payload.pid).toBe('child'); 133 | finishHandshake(); 134 | }); 135 | child.port.onMessage('syn', async payload => { 136 | expect(payload.pid).toBe('parent'); 137 | child.port.postMessage('ack', { pid: 'child' }); 138 | }); 139 | 140 | /** 141 | * Wait handshake finished 142 | */ 143 | await handshakePromise; 144 | 145 | const [response1, response2] = await Promise.all([ 146 | child.call('getInfo', { id: 'child' }), 147 | parent.call('getChildInfo', { name: 'parent' }), 148 | ]); 149 | expect(response1).toMatchObject({ user: 'child' }); 150 | expect(response2).toMatchObject({ clientKey: 'parent' }); 151 | }); 152 | 153 | it('removeMessageListener - remove specific callback', () => { 154 | const callback = vi.fn(); 155 | parent.port.onMessage('getInfo', callback); 156 | parent.port.removeMessageListener('getInfo', callback); 157 | child.port.postMessage('getInfo', { 158 | id: 'child', 159 | }); 160 | expect(callback).not.toHaveBeenCalled(); 161 | }); 162 | 163 | it('removeMessageListener - remove all callbacks for an event', () => { 164 | const callback1 = vi.fn(); 165 | const callback2 = vi.fn(); 166 | parent.port.onMessage('getInfo', callback1); 167 | parent.port.onMessage('getInfo', callback2); 168 | parent.port.removeMessageListener('getInfo'); 169 | child.port.postMessage('getInfo', { 170 | id: 'child', 171 | }); 172 | expect(callback1).not.toHaveBeenCalled(); 173 | expect(callback2).not.toHaveBeenCalled(); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Unport Logo
3 | Unport Logo 4 |

5 | 6 |
7 | 8 | [![NPM version][npm-badge]][npm-url] 9 | 10 |
11 | 12 | ## 🛰️ What's Unport? 13 | 14 | **Unport is a fully type-inferred IPC (Inter-Process Communication) library**. It ensures robust and reliable cross-context communication with strict type checking, enhancing the predictability and stability of your application. 15 | 16 | ```math 17 | Port = f(types, channel) 18 | ``` 19 | 20 | Unport is designed to simplify the complexity revolving around various JSContext environments. These environments encompass a wide range of technologies, including [Node.js](https://nodejs.org/), [ChildProcess](https://nodejs.org/api/child_process.html), [Webview](https://en.wikipedia.org/wiki/WebView), [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), [worker_threads](https://nodejs.org/api/worker_threads.html), [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe), [MessageChannel](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel), [ServiceWorker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), and much more. 21 | 22 | Each of these JSContexts exhibits distinct methods of communicating with the external world. Still, the lack of defined types can make handling the code for complex projects an arduous task. In the context of intricate and large-scale projects, it's often challenging to track the message's trajectory and comprehend the fields that the recipient necessitates. 23 | 24 | - [🛰️ What's Unport?](#️-whats-unport) 25 | - [💡 Features](#-features) 26 | - [🛠️ Install](#️-install) 27 | - [⚡️ Quick Start](#️-quick-start) 28 | - [📖 Basic Concepts](#-basic-concepts) 29 | - [MessageDefinition](#messagedefinition) 30 | - [Channel](#channel) 31 | - [📚 API Reference](#-api-reference) 32 | - [Unport](#unport) 33 | - [.implementChannel()](#implementchannel) 34 | - [.postMessage()](#postmessage) 35 | - [.onMessage()](#onmessage) 36 | - [Channel](#channel-1) 37 | - [.pipe()](#pipe) 38 | - [ChannelMessage](#channelmessage) 39 | - [Unrpc (Experimental)](#unrpc-experimental) 40 | - [🤝 Contributing](#-contributing) 41 | - [🤝 Credits](#-credits) 42 | - [LICENSE](#license) 43 | 44 | ## 💡 Features 45 | 46 | 1. Provides a unified Port paradigm. You only need to define the message types ([MessageDefinition](#messagedefinition)) and Intermediate communication channel ([Channel](#channel)) that different JSContexts need to pass, and you will get a unified type of Port: 47 | 2. 100% type inference. Users only need to maintain the message types between JSContexts, and leave the rest to unport. 48 | 3. Lightweight size and succinct API. 49 | 50 | ![IPC](https://github.com/ulivz/unport/blob/main/.media/ipc.png?raw=true) 51 | 52 | ## 🛠️ Install 53 | 54 | ```bash 55 | npm i unport -S 56 | ``` 57 | 58 | ## ⚡️ Quick Start 59 | 60 | Let's take ChildProcess as an example to implement a process of sending messages after a parent-child process is connected: 61 | 62 | 1. Define Message Definition: 63 | 64 | ```ts 65 | import { Unport } from "unport"; 66 | 67 | export type Definition = { 68 | parent2child: { 69 | syn: { 70 | pid: string; 71 | }; 72 | body: { 73 | name: string; 74 | path: string; 75 | }; 76 | }; 77 | child2parent: { 78 | ack: { 79 | pid: string; 80 | }; 81 | }; 82 | }; 83 | 84 | export type ChildPort = Unport; 85 | export type ParentPort = Unport; 86 | ``` 87 | 88 | 2. Parent process implementation: 89 | 90 | ```ts 91 | // parent.ts 92 | import { join } from "path"; 93 | import { fork } from "child_process"; 94 | import { Unport, ChannelMessage } from "unport"; 95 | import { ParentPort } from "./port"; 96 | 97 | // 1. Initialize a port 98 | const parentPort: ParentPort = new Unport(); 99 | 100 | // 2. Implement a Channel based on underlying IPC capabilities 101 | const childProcess = fork(join(__dirname, "./child.js")); 102 | parentPort.implementChannel({ 103 | send(message) { 104 | childProcess.send(message); 105 | }, 106 | accept(pipe) { 107 | childProcess.on("message", (message: ChannelMessage) => { 108 | pipe(message); 109 | }); 110 | }, 111 | }); 112 | 113 | // 3. You get a complete typed Port with a unified interface 🤩 114 | parentPort.postMessage("syn", { pid: "parent" }); 115 | parentPort.onMessage("ack", (payload) => { 116 | console.log("[parent] [ack]", payload.pid); 117 | parentPort.postMessage("body", { 118 | name: "index", 119 | path: " /", 120 | }); 121 | }); 122 | 123 | // 4. If you want to remove some listeners 124 | const handleAck = (payload) => { 125 | console.log("[parent] [syn]"); 126 | }; 127 | parentPort.onMessage("ack", handleAck); 128 | parentPort.removeMessageListener("ack", handleAck); 129 | // Note: if the second param of `removeMessageListener` is omitted, all listeners will be removed. 130 | parentPort.removeMessageList("ack"); 131 | ``` 132 | 133 | 3. Child process implementation: 134 | 135 | ```ts 136 | // child.ts 137 | import { Unport, ChannelMessage } from "unport"; 138 | import { ChildPort } from "./port"; 139 | 140 | // 1. Initialize a port 141 | const childPort: ChildPort = new Unport(); 142 | 143 | // 2. Implement a Channel based on underlying IPC capabilities 144 | childPort.implementChannel({ 145 | send(message) { 146 | process.send && process.send(message); 147 | }, 148 | accept(pipe) { 149 | process.on("message", (message: ChannelMessage) => { 150 | pipe(message); 151 | }); 152 | }, 153 | }); 154 | 155 | // 3. You get a complete typed Port with a unified interface 🤩 156 | childPort.onMessage("syn", (payload) => { 157 | console.log("[child] [syn]", payload.pid); 158 | childPort.postMessage("ack", { pid: "child" }); 159 | }); 160 | 161 | childPort.onMessage("body", (payload) => { 162 | console.log("[child] [body]", JSON.stringify(payload)); 163 | }); 164 | 165 | // 4. If you want to remove some listeners by `removeMessageList` 166 | const handleSyn = (payload) => { 167 | console.log("[child] [syn]"); 168 | }; 169 | childPort.onMessage("syn", handleSyn); 170 | childPort.removeMessageListener("syn", handleSyn); 171 | // Note: if the second param of `removeMessageListener` is omitted, all listeners will be removed. 172 | childPort.removeMessageList("syn"); 173 | ``` 174 | 175 | ## 📖 Basic Concepts 176 | 177 | ### MessageDefinition 178 | 179 | In Unport, a `MessageDefinition` is a crucial concept that defines the structure of the messages that can be sent and received through a `Channel`. It provides a clear and consistent way to specify the data that can be communicated between different JSContexts 180 | 181 | A `MessageDefinition` is an object where each key represents a type of message that can be sent or received, and the value is an object that defines the structure of the message. 182 | 183 | Here is an example of a `MessageDefinition`: 184 | 185 | ```ts 186 | export type Definition = { 187 | parent2child: { 188 | syn: { 189 | pid: string; 190 | }; 191 | body: { 192 | name: string; 193 | path: string; 194 | }; 195 | }; 196 | child2parent: { 197 | ack: { 198 | pid: string; 199 | }; 200 | }; 201 | }; 202 | ``` 203 | 204 | In this example, the `MessageDefinition` defines two types of messages that can be sent from the parent to the child (`syn` and `body`), and one type of message that can be sent from the child to the parent (`ack`). Each message type has its own structure, defined by an object with keys representing message types and values representing their message types. 205 | 206 | By using a `MessageDefinition`, you can ensure that the messages sent and received through a `Channel` are consistent and predictable, making your code easier to understand and maintain. 207 | 208 | ### Channel 209 | 210 | In Unport, a `Channel` is a fundamental concept that represents a Intermediate communication pathway between different JavaScript contexts. It provides a unified interface for sending and receiving messages across different environments. 211 | 212 | A `Channel` is implemented using two primary methods: 213 | 214 | - `send(message)`: This method is used to send a message through the channel. The `message` parameter is the data you want to send. 215 | 216 | - `accept(pipe)`: This method is used to accept incoming messages from the channel. The `pipe` parameter is a function that takes a message as its argument. 217 | 218 | Here is an example of how to implement a `Channel`: 219 | 220 | ```ts 221 | parentPort.implementChannel({ 222 | send(message) { 223 | childProcess.send(message); 224 | }, 225 | accept(pipe) { 226 | childProcess.on("message", (message: ChannelMessage) => { 227 | pipe(message); 228 | }); 229 | }, 230 | }); 231 | ``` 232 | 233 | In this example, the `send` method is implemented using the `send` method of a child process, and the `accept` method is implemented using the `on` method of the child process to listen for 'message' events. 234 | 235 | By abstracting the details of the underlying communication mechanism, Unport allows you to focus on the logic of your application, rather than the specifics of inter-context communication. 236 | 237 | ## 📚 API Reference 238 | 239 | ### Unport 240 | 241 | The `Unport` class is used to create a new port. 242 | 243 | ```ts 244 | import { Unport } from "unport"; 245 | ``` 246 | 247 | #### .implementChannel() 248 | 249 | This method is used to implement a universal port based on underlying IPC capabilities. 250 | 251 | ```ts 252 | parentPort.implementChannel({ 253 | send(message) { 254 | childProcess.send(message); 255 | }, 256 | accept(pipe) { 257 | childProcess.on("message", (message: ChannelMessage) => { 258 | pipe(message); 259 | }); 260 | }, 261 | }); 262 | ``` 263 | 264 | #### .postMessage() 265 | 266 | This method is used to post a message. 267 | 268 | ```ts 269 | parentPort.postMessage("syn", { pid: "parent" }); 270 | ``` 271 | 272 | #### .onMessage() 273 | 274 | This method is used to listen for a message. 275 | 276 | ```ts 277 | parentPort.onMessage("ack", (payload) => { 278 | console.log("[parent] [ack]", payload.pid); 279 | parentPort.postMessage("body", { 280 | name: "index", 281 | path: " /", 282 | }); 283 | }); 284 | ``` 285 | 286 | ### Channel 287 | 288 | When you invoke the [.implementChannel()](#implementchannel) to implement an intermediary pipeline, you will receive a `Channel` instance. This instance comes with several useful methods that enhance the functionality and usability of the pipeline. 289 | 290 | #### .pipe() 291 | 292 | - Type: `(message: ChannelMessage) => void` 293 | 294 | The `pipe` method is used to manually handle incoming messages. It's often used in Server with `one-to-many` connections, e.g. Web Socket. 295 | 296 | Example: 297 | 298 | ```ts 299 | const channel = port.implementChannel({ 300 | send: (message) => { 301 | // send message to the other end of the channel 302 | }, 303 | }); 304 | 305 | // when a message is received 306 | channel.pipe(message); 307 | ``` 308 | 309 | See our [Web Socket](./examples/web-socket/) example to check more details. 310 | 311 | ### ChannelMessage 312 | 313 | The `ChannelMessage` type is used for the message in the `onMessage` method. 314 | 315 | ```ts 316 | import { ChannelMessage } from "unport"; 317 | ``` 318 | 319 | ### Unrpc (Experimental) 320 | 321 | Starting with the 0.6.0 release, we are experimentally introducing support for Typed [RPC (Remote Procedure Call)](https://en.wikipedia.org/wiki/Remote_procedure_call). 322 | 323 | When dealing with a single Port that requires RPC definition, we encounter a problem related to the programming paradigm. It's necessary to define `Request` and `Response` messages such as: 324 | 325 | ```ts 326 | export type IpcDefinition = { 327 | a2b: { 328 | callFoo: { 329 | input: string; 330 | }; 331 | }; 332 | b2a: { 333 | callFooCallback: { 334 | result: string; 335 | }; 336 | }; 337 | }; 338 | ``` 339 | 340 | In the case where an RPC call needs to be encapsulated, the API might look like this: 341 | 342 | ```ts 343 | function rpcCall(request: { input: string }): Promise<{ result: string }>; 344 | ``` 345 | 346 | Consequently, to associate a callback function, it becomes a requirement to include a `CallbackId` at the **application layer** for every RPC method: 347 | 348 | ```diff 349 | export type IpcDefinition = { 350 | a2b: { 351 | callFoo: { 352 | input: string; 353 | + callbackId: string; 354 | }; 355 | }; 356 | b2a: { 357 | callFooCallback: { 358 | result: string; 359 | + callbackId: string; 360 | }; 361 | }; 362 | }; 363 | ``` 364 | 365 | `Unrpc` is provided to address this issue, enabling support for Typed RPC starting from the **protocol layer**: 366 | 367 | ```ts 368 | // "parentPort" is a Port defined based on Unport in the previous example. 369 | const parent = new Unrpc(parentPort); 370 | 371 | // Implementing an RPC method. 372 | parent.implement("getParentInfo", (request) => ({ 373 | id: "parent", 374 | from: request.user, 375 | })); 376 | ``` 377 | 378 | The implementation on the `child` side is as follows: 379 | 380 | ```ts 381 | // "parentPort" is a Port also defined based on Unport. 382 | const child = new Unrpc(childPort); 383 | const response = await child.call("getParentInfo", { user: "child" }); // => { id: "parent", from: "child" } 384 | ``` 385 | 386 | The types are defined as such: 387 | 388 | ```ts 389 | import { Unport } from "unport"; 390 | 391 | export type Definition = { 392 | parent2child: { 393 | getParentInfo__callback: { 394 | content: string; 395 | }; 396 | }; 397 | child2parent: { 398 | getParentInfo: { 399 | user: string; 400 | }; 401 | }; 402 | }; 403 | 404 | export type ChildPort = Unport; 405 | export type ParentPort = Unport; 406 | ``` 407 | 408 | In comparison to Unport, the only new concept to grasp is that the RPC response message key must end with `__callback`. Other than that, no additional changes are necessary! `Unrpc` also offers comprehensive type inference based on this convention; for instance, you won't be able to implement an RPC method that is meant to serve as a response. 409 | 410 | > [!NOTE] 411 | > You can find the full code example here: [child-process-rpc](https://github.com/web-infra-dev/unport/tree/main/examples/child-process-rpc). 412 | 413 | ## 🤝 Contributing 414 | 415 | Contributions, issues and feature requests are welcome! 416 | 417 | Here are some ways you can contribute: 418 | 419 | 1. 🐛 Submit a [Bug Report](https://github.com/ulivz/unport/issues) if you found something isn't working correctly. 420 | 2. 🆕 Suggest a new [Feature Request](https://github.com/ulivz/unport/issues) if you'd like to see new functionality added. 421 | 3. 📖 Improve documentation or write tutorials to help other users. 422 | 4. 🌐 Translate the documentation to other languages. 423 | 5. 💻 Contribute code changes by [Forking the Repository](https://github.com/ulivz/unport/fork), making changes, and submitting a Pull Request. 424 | 425 | ## 🤝 Credits 426 | 427 | The birth of this project is inseparable from the complex IPC problems we encountered when working in large companies. The previous name of this project was `Multidirectional Typed Port`, and we would like to thank [ahaoboy](https://github.com/ahaoboy) for his previous ideas on this matter. 428 | 429 | ## LICENSE 430 | 431 | MIT License © [ULIVZ](https://github.com/ulivz) 432 | 433 | [npm-badge]: https://img.shields.io/npm/v/unport.svg?style=flat 434 | [npm-url]: https://www.npmjs.com/package/unport 435 | [ci-badge]: https://github.com/ulivz/unport/actions/workflows/ci.yml/badge.svg?event=push&branch=main 436 | [ci-url]: https://github.com/ulivz/unport/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain 437 | [code-coverage-badge]: https://codecov.io/github/ulivz/unport/branch/main/graph/badge.svg 438 | [code-coverage-url]: https://codecov.io/gh/ulivz/unport 439 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | /** 3 | * @license 4 | * Copyright (c) ULIVZ. All Rights Reserved. 5 | */ 6 | 7 | /** 8 | * A template literal type that is used to describe the direction of message transmission. 9 | * 10 | * It uses the form `${foo}2${bar}`, where "foo" and "bar" can be any string, indicating 11 | * where the message comes from (foo), and where it is going to (bar). 12 | * 13 | * For example, possible values could be "server2client" or "client2server", which indicate 14 | * that the message is transmitted from the server to the client, or from the client to the 15 | * server, respectively. 16 | */ 17 | export type MessageDirectionDescriptor = `${string}2${string}`; 18 | /** 19 | * This type is designed for handling messages within a singular JavaScript context. 20 | * 21 | * In this `Record` type, we have: 22 | * - Keys: These are of type `string` used to define the unique message names. 23 | * - Values: These are of `any` type and they represent the payload or body of the messages. 24 | * 25 | * For example, a message can be defined as: 26 | * ```ts 27 | * { 28 | * "messageName": { 29 | * property1: "value1", 30 | * property2: "value2" 31 | * } 32 | * } 33 | * ``` 34 | */ 35 | export type MessageDefinition4SingleJSContext = Record; 36 | /** 37 | * The exported type `MessageDefinition` represents a TypeScript `Record` type which aims 38 | * to describe all message definitions between different JSContexts 39 | * 40 | * In this `Record` type, the composition is as follows: 41 | * 42 | * - Keys: These keys are represented by the @type {MessageDirectionDescriptor}, which describes 43 | * the direction of a message. 44 | * - Values: The values are of @type {MessageDefinition4SingleJSContext} 45 | * 46 | * For example, a message definition could look like this: 47 | * 48 | * ```ts 49 | * { 50 | * "server2client": { 51 | * "messageName": { 52 | * property1: "value1", 53 | * property2: "value2" 54 | * } 55 | * } 56 | * } 57 | */ 58 | export type MessageDefinition = Record; 59 | /** 60 | * `Direction` type is a utility type to extract the direction field in a @type {MessageDefinition}. 61 | * It narrows down the index signature of MessageDefinition to exclusively the direction field. 62 | */ 63 | export type Direction = keyof T; 64 | /** 65 | * `InferPorts` type is a utility to infer the list of port names, given a @type {MessageDefinition}. 66 | * 67 | * It inspects the direction fields in the definition and extracts the unique port names 68 | * present in them. 69 | * 70 | * For example, with following @type {MessageDefinition}: { "server2client": { ... } } 71 | * The inferred ports will be 'server' | 'client' 72 | */ 73 | export type InferPorts = { 74 | [k in Direction]: k extends `${infer A}2${infer B}` ? A | B : k; 75 | }[keyof T]; 76 | /** 77 | * `InferDirectionByPort` type is a utility to determine the direction of message transmission 78 | * based on the port name. 79 | * 80 | * For example, with following @type {MessageDefinition}: { "server2client": { ... } } 81 | * and port "client", the inferred direction will be 'client2server' 82 | */ 83 | type InferDirectionByPort> = { 84 | [k in Direction]: k extends `${infer A}2${infer B}` 85 | ? A extends U 86 | ? `${A}2${B}` 87 | : B extends U 88 | ? `${B}2${A}` 89 | : k 90 | : k; 91 | }[keyof T]; 92 | /** 93 | * Reverse direction 94 | */ 95 | type ReverseDirection< 96 | U extends MessageDefinition, 97 | T extends Direction, 98 | Sep extends string = '2', 99 | > = T extends `${infer A}${Sep}${infer B}` ? `${B}${Sep}${A}` : T; 100 | /** 101 | * `Payload` type is a utility to extract the payload type of a specific message, given its 102 | * direction and the name. 103 | */ 104 | export type Payload, U extends keyof T[D]> = T[D][U]; 105 | /** 106 | * `Callback` is a type representing a generic function 107 | */ 108 | type Callback = (...args: T) => U; 109 | /** 110 | * A base interface used to describe a Message Port 111 | */ 112 | interface Port> { 113 | // eslint-disable-next-line no-use-before-define 114 | postMessage(t: U, p?: Payload, extra?: Pick): void; 115 | onMessage]>( 116 | t: U, 117 | handler: Callback<[Payload, U>]>, 118 | ): void; 119 | removeMessageListener]>( 120 | t: U, 121 | handler?: Callback<[Payload, U>]>, 122 | ): void; 123 | } 124 | 125 | export type EnsureString = T extends string ? T : never; 126 | export type CallbackSuffix = '__callback'; 127 | 128 | /** 129 | * A generic type used to infer the return value type of an Rpc call. For example, when you call 130 | * "foo" on one end of the port, the return value is of the type defined by "foo__callback" on 131 | * the other end. 132 | */ 133 | export type CallbackPayload< 134 | T extends MessageDefinition, 135 | D extends Direction, 136 | U extends keyof T[D], 137 | S extends EnsureString = EnsureString 138 | > = 139 | `${S}${CallbackSuffix}` extends keyof T[ReverseDirection] 140 | ? Payload, `${S}${CallbackSuffix}`> : unknown; 141 | 142 | /** 143 | * We filtered the messages, only the message without {@type {CallbackSuffix}} is defined rpc method. 144 | */ 145 | export type RpcMethod, U extends keyof T[D]> 146 | = U extends `${infer A}${CallbackSuffix}` ? never : U; 147 | 148 | /** 149 | * A base interface used to describe a Rpc client instance. 150 | */ 151 | export interface Rpc, > { 152 | call(t: RpcMethod, p: Payload): Promise>; 153 | implement]>( 154 | t: RpcMethod, R>, 155 | handler: Callback< 156 | [Payload, R>], 157 | CallbackPayload, R> | Promise, R>> 158 | >, 159 | ): void; 160 | } 161 | 162 | // eslint-disable-next-line no-shadow 163 | export const enum ChannelMessageErrorCode { 164 | NotImplemented = 'NOT_IMPLEMENTED', 165 | ExecutionError = 'EXECUTION_ERROR', 166 | } 167 | 168 | /** 169 | * Different messages or methods define different Responses, so it is an any. 170 | */ 171 | export type Result = any; 172 | 173 | /** 174 | * `ChannelMessage` interface defines the structure of a message that can be sent 175 | * or received through an `Channel`. 176 | * 177 | * It contains a `t` field for the name of the message, and a `p` field for the payload 178 | * of the message, `d` for the message id. 179 | */ 180 | export interface ChannelMessage { 181 | _$: 'un'; 182 | t: string | number | symbol; /* message key */ 183 | p?: Result; /* message payload */ 184 | d?: number; /* message id */ 185 | e?: string; /* error message */ 186 | c?: ChannelMessageErrorCode; /* error code */ 187 | } 188 | 189 | /** 190 | * `Channel` interface specifies the methods that a valid unport channel should have. 191 | * 192 | * The `send` method takes a message conforming to @type {ChannelMessage} interface and sends 193 | * it through the channel. The `accept` method sets a handler function that will be triggered 194 | * whenever a message arrives at the channel. 195 | */ 196 | export interface Channel { 197 | send(message: ChannelMessage): void; 198 | accept?(pipe: (message: ChannelMessage) => unknown): void; 199 | destroy?(): void; 200 | pipe?(message: ChannelMessage): unknown; 201 | } 202 | 203 | export interface EnhancedChannel extends Channel { 204 | pipe(message: ChannelMessage): unknown; 205 | } 206 | 207 | /** 208 | * Expose Unport class 209 | */ 210 | export class Unport< 211 | T extends MessageDefinition, 212 | U extends InferPorts 213 | > implements Port> { 214 | private handlers: Record[]> = {}; 215 | 216 | public channel?: EnhancedChannel; 217 | 218 | public channelReceiveMessageListener?: (message: ChannelMessage) => unknown; 219 | 220 | public setChannelReceiveMessageListener(listener: (message: ChannelMessage) => unknown) { 221 | if (typeof listener === 'function') { 222 | this.channelReceiveMessageListener = listener; 223 | } 224 | } 225 | 226 | public implementChannel(channel: Channel | (() => Channel)): EnhancedChannel { 227 | // @ts-expect-error We will assign it immediately 228 | this.channel = typeof channel === 'function' ? channel() : channel; 229 | if (typeof this.channel === 'object' && typeof this.channel.send === 'function') { 230 | this.channel.pipe = (message: ChannelMessage) => { 231 | if (typeof this.channelReceiveMessageListener === 'function') { 232 | this.channelReceiveMessageListener(message); 233 | } 234 | if (typeof message === 'object' && message._$ === 'un') { 235 | const { t, p } = message; 236 | const handler = this.handlers[t]; 237 | if (handler) { 238 | handler.forEach(fn => fn(p)); 239 | } 240 | } 241 | }; 242 | if (typeof this.channel.accept === 'function') { 243 | this.channel.accept(message => this.channel && this.channel.pipe?.(message)); 244 | } 245 | } else { 246 | throw new Error('[1] invalid channel implementation'); 247 | } 248 | return this.channel; 249 | } 250 | 251 | public postMessage: Port>['postMessage'] = (t, p, extra) => { 252 | if (!this.channel) { 253 | throw new Error('[2] Port is not implemented or has been destroyed'); 254 | } 255 | this.channel.send({ ...(extra || {}), t, p, _$: 'un' }); 256 | }; 257 | 258 | public onMessage: Port>['onMessage'] = (t, handler) => { 259 | if (!this.handlers[t]) { 260 | this.handlers[t] = []; 261 | } 262 | this.handlers[t].push(handler); 263 | }; 264 | 265 | public removeMessageListener: Port>['removeMessageListener'] = (t, handler) => { 266 | if (!this.handlers[t]) { 267 | return; 268 | } 269 | if (handler) { 270 | this.handlers[t] = this.handlers[t].filter(h => h !== handler); 271 | if (this.handlers[t].length === 0) { 272 | delete this.handlers[t]; 273 | } 274 | } else { 275 | delete this.handlers[t]; 276 | } 277 | }; 278 | 279 | public destroy() { 280 | this.handlers = {}; 281 | this.channel?.destroy && this.channel.destroy(); 282 | delete this.channel; 283 | } 284 | } 285 | 286 | const CALLBACK_SUFFIX: CallbackSuffix = '__callback'; 287 | 288 | export class UnrpcNotImplementationError extends Error { 289 | constructor(message?: string) { 290 | super(message); 291 | this.name = ChannelMessageErrorCode.NotImplemented; 292 | } 293 | } 294 | 295 | export class UnrpcExecutionErrorError extends Error { 296 | constructor(message?: string) { 297 | super(message); 298 | this.name = ChannelMessageErrorCode.ExecutionError; 299 | } 300 | } 301 | 302 | /** 303 | * Check if the given object is a Promise or PromiseLike. 304 | * 305 | * @param value - The object to check. 306 | * @returns True if the object is a Promise or PromiseLike, otherwise false. 307 | */ 308 | function isPromise(value: any): value is Promise { 309 | // Check if the value is an object and not null, then check if it has a 'then' function 310 | return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function'; 311 | } 312 | 313 | /** 314 | * Expose Unrpc class 315 | */ 316 | export class Unrpc> implements Rpc> { 317 | private callbackMap = new Map, Callback<[any]>]>(); 318 | 319 | private currentCallbackId = 0; 320 | 321 | private implementations = new Map>(); 322 | 323 | constructor(public readonly port: Unport) { 324 | /** 325 | * The implementation of Rpc is based on the message protocol layer {@type {ChannelMessage}} at {@type {Unport}}. 326 | */ 327 | this.port.setChannelReceiveMessageListener(message => { 328 | if (typeof message === 'object' && message._$ === 'un') { 329 | const { t, p, d } = message; 330 | const messageKey = String(t); 331 | /** 332 | * If a message contains "d" field, it is considered a message sent by Rpc. 333 | * Therefore, messages sent directly by {@type {Unport#postMessage}} will not be affected in any way. 334 | */ 335 | if (typeof d === 'number') { 336 | /** 337 | * If a message ends with {@type {CALLBACK_SUFFIX}}, it is considered a Response message of Rpc 338 | */ 339 | if (messageKey.endsWith(CALLBACK_SUFFIX)) { 340 | const callbackTuple = this.callbackMap.get(d); 341 | if (callbackTuple) { 342 | const [resolve, reject] = callbackTuple; 343 | if (message.c) { 344 | switch (message.c) { 345 | case ChannelMessageErrorCode.NotImplemented: 346 | reject(new UnrpcNotImplementationError(message.e)); break; 347 | case ChannelMessageErrorCode.ExecutionError: 348 | reject(new UnrpcExecutionErrorError(message.e)); break; 349 | default: 350 | /* c8 ignore next */ 351 | reject(new Error(message.e)); 352 | } 353 | } else { 354 | resolve(p); 355 | } 356 | } 357 | } else { 358 | /** 359 | * If a message is not a callback, it is considered a rpc request. 360 | */ 361 | const handler = this.implementations.get(t); 362 | const callbackMessageKey = `${messageKey}${CALLBACK_SUFFIX}` as keyof T[InferDirectionByPort]; 363 | if (handler) { 364 | const handleCallback = (result: Result) => { 365 | this.port.postMessage(callbackMessageKey, result, { 366 | d, 367 | }); 368 | }; 369 | const handleExecutionError = (e: Result) => { 370 | this.port.postMessage(callbackMessageKey, undefined, { 371 | d, 372 | c: ChannelMessageErrorCode.ExecutionError, 373 | e: e instanceof Error ? e.message : String(e), 374 | }); 375 | }; 376 | let result: Result; 377 | try { 378 | result = handler(p); 379 | } catch (e) { 380 | handleExecutionError(e); 381 | } 382 | if (isPromise(result)) { 383 | result.then(handleCallback).catch(handleExecutionError); 384 | } else { 385 | handleCallback(result); 386 | } 387 | } else { 388 | this.port.postMessage(callbackMessageKey, undefined, { 389 | d, 390 | c: ChannelMessageErrorCode.NotImplemented, 391 | e: `Method ${messageKey} is not implemented`, 392 | }); 393 | } 394 | } 395 | } 396 | } 397 | }); 398 | } 399 | 400 | public call: Rpc>['call'] = async (t, p) => { 401 | const callbackId = this.currentCallbackId++; 402 | const response = new Promise, typeof t>>((resolve, reject) => { 403 | this.callbackMap.set(callbackId, [resolve, reject]); 404 | }); 405 | this.port.postMessage(t, p, { d: callbackId }); 406 | return response; 407 | }; 408 | 409 | public implement: Rpc>['implement'] = (t, p) => { 410 | this.implementations.set(t, p); 411 | }; 412 | } 413 | --------------------------------------------------------------------------------