Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/vladimiry/pubsub-to-rpc-api
Converting IPC-like / publish-subscribe interaction model to the reactive RPC-like / request-response model
https://github.com/vladimiry/pubsub-to-rpc-api
api pubsub reactive rpc rxjs
Last synced: about 1 month ago
JSON representation
Converting IPC-like / publish-subscribe interaction model to the reactive RPC-like / request-response model
- Host: GitHub
- URL: https://github.com/vladimiry/pubsub-to-rpc-api
- Owner: vladimiry
- License: mit
- Created: 2018-06-21T21:15:53.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2024-01-10T08:25:51.000Z (12 months ago)
- Last Synced: 2024-10-22T21:29:22.335Z (2 months ago)
- Topics: api, pubsub, reactive, rpc, rxjs
- Language: TypeScript
- Homepage:
- Size: 1.21 MB
- Stars: 3
- Watchers: 2
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pubsub-to-rpc-api
Is 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.
[![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)
## Getting started
Your project needs `rxjs@6` to be installed, which is a peer dependency of this module.
Example-related source code is located [here](src/example/readme), can be executed by running `yarn example` console command.
Let's first describe API methods and create service instance ([shared/index.ts](src/example/readme/shared/index.ts)):
```typescript
// no need to put implementation logic here
// but only API definition and service instance creating
// as this file is supposed to be shared between provider and client implementationsimport {ActionType, ScanService, createService} from "lib";
const apiDefinition = {
evaluateMathExpression: ActionType.Promise(),
httpPing: ActionType.Observable, { domain: string } & ({ time: number } | { error: string })>(),
};export const API_SERVICE = createService({
channel: "some-event-name", // event name used to communicate between the event emitters
apiDefinition,
});// optionally exposing inferred API structure
export type ScannedApiService = ScanService;
````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.
API implementation, ie provider side ([provider/index.ts](src/example/readme/provider/index.ts)):
```typescript
import tcpPing from "tcp-ping";
import {evaluate} from "maths.ts";
import {from, merge} from "rxjs";
import {promisify} from "util";import {API_SERVICE, ScannedApiService} from "../shared";
import {EM_CLIENT, EM_PROVIDER} from "../shared/event-emitters-mock";export const API_IMPLEMENTATION: ScannedApiService["ApiImpl"] = {
evaluateMathExpression: async (input) => Number(String(evaluate(input))),
httpPing(entries) {
const promises = entries.map(async (entry) => {
const ping = await promisify(tcpPing.ping)(entry);
const baseResponse = {domain: ping.address};
const failed = typeof ping.avg === "undefined" || isNaN(ping.avg);return failed
? {...baseResponse, error: JSON.stringify(ping)}
: {...baseResponse, time: ping.avg};
});return merge(
...promises.map((promise) => from(promise)),
);
},
};API_SERVICE.register(
API_IMPLEMENTATION,
EM_PROVIDER,
// 3-rd parameter is optional
// if not defined, then "EM_PROVIDER" would be used for listening and emitting
// but normally listening and emitting happens on different instances, so specifying separate emitting instance as 3rd parameter
{
onEventResolver: (payload) => ({payload, emitter: EM_CLIENT}),
// in a more real world scenario you would extract emitter from the payload, see Electron.js example:
// onEventResolver: ({sender}, payload) => ({payload, emitter: {emit: sender.send.bind(sender)}}),
},
);
```Now we can call the defined and implemented methods in a type-safe way ([client/index.ts](src/example/readme/client/index.ts)):
```typescript
// tslint:disable:no-consoleimport {API_SERVICE} from "../shared";
import {EM_CLIENT, EM_PROVIDER} from "../shared/event-emitters-mock";const apiClient = API_SERVICE.caller({emitter: EM_PROVIDER, listener: EM_CLIENT});
const evaluateMathExpressionMethod = apiClient("evaluateMathExpression"/*, {timeoutMs: 600}*/);
const httpPingMethod = apiClient("httpPing"/*, {timeoutMs: 600}*/);evaluateMathExpressionMethod("32 * 2")
.then(console.log)
.catch(console.error);httpPingMethod([{address: "google.com", attempts: 1}, {address: "github.com"}, {address: "1.1.1.1"}])
.subscribe(console.log, console.error);
```And 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)):
```typescript
import test, {ExecutionContext, ImplementationResult} from "ava";
import {bufferCount} from "rxjs/operators";import {API_IMPLEMENTATION} from ".";
const apiActionTests: Record ImplementationResult> = {
evaluateMathExpression: async (t) => {
t.is(25, await API_IMPLEMENTATION.evaluateMathExpression("12 * 2 + 1"));
},
httpPing: async (t) => {
const entries = [
{address: "google.com", attempts: 1},
{address: "github.com"},
{address: "1.1.1.1"},
];const results = await API_IMPLEMENTATION
.httpPing(...entries)
.pipe(bufferCount(entries.length))
.toPromise();// type checking like assertions implemented below are not really needed since TypeScript handles the type checking
t.is(results.length, entries.length);
for (const result of results) {
if ("time" in result) {
t.true(typeof result.time === "number");
t.false("error" in result);
continue;
}t.true("error" in result && typeof result.error === "string");
}
},
};for (const [apiMethodName, apiMethodTest] of Object.entries(apiActionTests)) {
test(`API: ${apiMethodName}`, apiMethodTest);
}
```