{"id":20632143,"url":"https://github.com/vladimiry/pubsub-to-rpc-api","last_synced_at":"2025-04-15T18:58:13.635Z","repository":{"id":32621449,"uuid":"138222584","full_name":"vladimiry/pubsub-to-rpc-api","owner":"vladimiry","description":"Converting IPC-like / publish-subscribe interaction model to the reactive RPC-like / request-response model","archived":false,"fork":false,"pushed_at":"2024-01-10T08:25:51.000Z","size":1266,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-22T21:29:22.335Z","etag":null,"topics":["api","pubsub","reactive","rpc","rxjs"],"latest_commit_sha":null,"homepage":"","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/vladimiry.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-06-21T21:15:53.000Z","updated_at":"2022-08-15T15:11:37.000Z","dependencies_parsed_at":"2024-10-15T04:01:15.770Z","dependency_job_id":"eb636671-b2d0-44ea-9be9-276cc9ddc7f5","html_url":"https://github.com/vladimiry/pubsub-to-rpc-api","commit_stats":{"total_commits":92,"total_committers":3,"mean_commits":"30.666666666666668","dds":"0.32608695652173914","last_synced_commit":"65fe16fbcdeaceba2b5bd112d7892ea901d1df8c"},"previous_names":["vladimiry/pubsub-to-stream-api"],"tags_count":46,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vladimiry%2Fpubsub-to-rpc-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vladimiry%2Fpubsub-to-rpc-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vladimiry%2Fpubsub-to-rpc-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vladimiry%2Fpubsub-to-rpc-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vladimiry","download_url":"https://codeload.github.com/vladimiry/pubsub-to-rpc-api/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249076541,"owners_count":21208812,"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":["api","pubsub","reactive","rpc","rxjs"],"created_at":"2024-11-16T14:15:04.282Z","updated_at":"2025-04-15T18:58:13.596Z","avatar_url":"https://github.com/vladimiry.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pubsub-to-rpc-api\n\nIs a Node.js / browser library that converts _publish-subscribe / IPC_ - like interaction model into the _request/response_ model with _provider_ and _client_ parties involved. So it's like flattening _pub/sub_ interactions into the Observables/Promises-based API. It comes with type safety out of the box, thanks to TypeScript.\n\n[![GitHub Actions CI](https://github.com/vladimiry/pubsub-to-rpc-api/workflows/GitHub%20Actions%20CI/badge.svg?branch=master)](https://github.com/vladimiry/pubsub-to-rpc-api/actions)\n\n## Getting started\n\nYour project needs `rxjs@6` to be installed, which is a peer dependency of this module.\n\nExample-related source code is located [here](src/example/readme), can be executed by running `yarn example` console command.\n\nLet's first describe API methods and create service instance ([shared/index.ts](src/example/readme/shared/index.ts)):\n```typescript\n// no need to put implementation logic here\n// but only API definition and service instance creating\n// as this file is supposed to be shared between provider and client implementations\n\nimport {ActionType, ScanService, createService} from \"lib\";\n\nconst apiDefinition = {\n    evaluateMathExpression: ActionType.Promise\u003cstring, number\u003e(),\n    httpPing: ActionType.Observable\u003cArray\u003c{\n        address?: string;\n        port?: number;\n        attempts?: number;\n        timeout?: number;\n    }\u003e, { domain: string } \u0026 ({ time: number } | { error: string })\u003e(),\n};\n\nexport const API_SERVICE = createService({\n    channel: \"some-event-name\", // event name used to communicate between the event emitters\n    apiDefinition,\n});\n\n// optionally exposing inferred API structure\nexport type ScannedApiService = ScanService\u003ctypeof API_SERVICE\u003e;\n```\n\n`ActionReturnType.Promise` and `ActionReturnType.Observable` return values used to preserve action result type in runtime so the client-side code is able to distinguish return types not knowing anything about the actual API implementation at the provider-side.\n\nAPI implementation, ie provider side ([provider/index.ts](src/example/readme/provider/index.ts)):\n```typescript\nimport tcpPing from \"tcp-ping\";\nimport {evaluate} from \"maths.ts\";\nimport {from, merge} from \"rxjs\";\nimport {promisify} from \"util\";\n\nimport {API_SERVICE, ScannedApiService} from \"../shared\";\nimport {EM_CLIENT, EM_PROVIDER} from \"../shared/event-emitters-mock\";\n\nexport const API_IMPLEMENTATION: ScannedApiService[\"ApiImpl\"] = {\n    evaluateMathExpression: async (input) =\u003e Number(String(evaluate(input))),\n    httpPing(entries) {\n        const promises = entries.map(async (entry) =\u003e {\n            const ping = await promisify(tcpPing.ping)(entry);\n            const baseResponse = {domain: ping.address};\n            const failed = typeof ping.avg === \"undefined\" || isNaN(ping.avg);\n\n            return failed\n                ? {...baseResponse, error: JSON.stringify(ping)}\n                : {...baseResponse, time: ping.avg};\n        });\n\n        return merge(\n            ...promises.map((promise) =\u003e from(promise)),\n        );\n    },\n};\n\nAPI_SERVICE.register(\n    API_IMPLEMENTATION,\n    EM_PROVIDER,\n    // 3-rd parameter is optional\n    // if not defined, then \"EM_PROVIDER\" would be used for listening and emitting\n    // but normally listening and emitting happens on different instances, so specifying separate emitting instance as 3rd parameter\n    {\n        onEventResolver: (payload) =\u003e ({payload, emitter: EM_CLIENT}),\n        // in a more real world scenario you would extract emitter from the payload, see Electron.js example:\n        // onEventResolver: ({sender}, payload) =\u003e ({payload, emitter: {emit: sender.send.bind(sender)}}),\n    },\n);\n```\n\nNow we can call the defined and implemented methods in a type-safe way ([client/index.ts](src/example/readme/client/index.ts)):\n```typescript\n// tslint:disable:no-console\n\nimport {API_SERVICE} from \"../shared\";\nimport {EM_CLIENT, EM_PROVIDER} from \"../shared/event-emitters-mock\";\n\nconst apiClient = API_SERVICE.caller({emitter: EM_PROVIDER, listener: EM_CLIENT});\nconst evaluateMathExpressionMethod = apiClient(\"evaluateMathExpression\"/*, {timeoutMs: 600}*/);\nconst httpPingMethod = apiClient(\"httpPing\"/*, {timeoutMs: 600}*/);\n\nevaluateMathExpressionMethod(\"32 * 2\")\n    .then(console.log)\n    .catch(console.error);\n\nhttpPingMethod([{address: \"google.com\", attempts: 1}, {address: \"github.com\"}, {address: \"1.1.1.1\"}])\n    .subscribe(console.log, console.error);\n```\n\nAnd here is how API methods test structure might look (we leverage combination of `Api` model and TypeScript's `Record` type to make sure that tests for all the methods got defined, see [provider/api.spec.ts](src/example/readme/provider/api.spec.ts)):\n```typescript\nimport test, {ExecutionContext, ImplementationResult} from \"ava\";\nimport {bufferCount} from \"rxjs/operators\";\n\nimport {API_IMPLEMENTATION} from \".\";\n\nconst apiActionTests: Record\u003ckeyof typeof API_IMPLEMENTATION, (t: ExecutionContext) =\u003e ImplementationResult\u003e = {\n    evaluateMathExpression: async (t) =\u003e {\n        t.is(25, await API_IMPLEMENTATION.evaluateMathExpression(\"12 * 2 + 1\"));\n    },\n    httpPing: async (t) =\u003e {\n        const entries = [\n            {address: \"google.com\", attempts: 1},\n            {address: \"github.com\"},\n            {address: \"1.1.1.1\"},\n        ];\n\n        const results = await API_IMPLEMENTATION\n            .httpPing(...entries)\n            .pipe(bufferCount(entries.length))\n            .toPromise();\n\n        // type checking like assertions implemented below are not really needed since TypeScript handles the type checking\n\n        t.is(results.length, entries.length);\n\n        for (const result of results) {\n            if (\"time\" in result) {\n                t.true(typeof result.time === \"number\");\n                t.false(\"error\" in result);\n                continue;\n            }\n\n            t.true(\"error\" in result \u0026\u0026 typeof result.error === \"string\");\n        }\n    },\n};\n\nfor (const [apiMethodName, apiMethodTest] of Object.entries(apiActionTests)) {\n    test(`API: ${apiMethodName}`, apiMethodTest);\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvladimiry%2Fpubsub-to-rpc-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvladimiry%2Fpubsub-to-rpc-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvladimiry%2Fpubsub-to-rpc-api/lists"}