From f2f78a60c0880b3c19afc9682a6bc247fb12bbcc Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Thu, 16 May 2024 00:57:48 +0900 Subject: [PATCH] Writing #68: remote-procedure-call --- website/pages/docs/features/components.mdx | 36 +- website/pages/docs/remote-procedure-call.mdx | 545 ++++++++++++++++++ website/src/movies/home/HomeStrengthMovie.tsx | 2 +- 3 files changed, 559 insertions(+), 24 deletions(-) diff --git a/website/pages/docs/features/components.mdx b/website/pages/docs/features/components.mdx index a338e46..ffaf2b0 100644 --- a/website/pages/docs/features/components.mdx +++ b/website/pages/docs/features/components.mdx @@ -11,7 +11,6 @@ Otherwise, let's study about the key components of the `TGrid`. - [`Communicator`](#communicator): network communication with remote system - [`Provider`](#provider): object provided for remote system - - [`Listener`](#listener): interface of the remote system's `Provider` - [`Driver`](#driver): proxy instance for calling functions of the remote system's `Provider` @@ -22,7 +21,7 @@ Otherwise, let's study about the key components of the `TGrid`. Communicates with a remote system. -`Communicator` is a class taking full responsibility to network communication with remote system. You can register a Provider, an object would be provided to the remote system, to the `Communicator`. Also, [Driver](#driver)\<[Listener](#listener)\>, which can access to the remote system's [Provider](#provider), is created by this `Communicator`. +`Communicator` is a class taking full responsibility to network communication with remote system. You can register a Provider, an object would be provided to the remote system, to the `Communicator`. Also, [`Driver`](#driver), which can access to the remote system's [Provider](#provider), is created by this `Communicator`. For reference, actual `Communicator` is the top-level abstract class, and all the classes responsible for network communication in `TGrid` are inheriting from this `Communicator` class. Here is the list of every communicator classes in `TGrid`. @@ -40,9 +39,9 @@ Object provided for remote system. `Provider` is an object provided for the remote system. -The remote system can call the `Provider`'s functions through [Driver](#driver)\<[Listener](#listener)\>. +The remote system can call the `Provider`'s functions through [`Driver`](#driver). -```typescript filename="examples/websocket/server.ts" showLineNumbers +```typescript filename="examples/websocket/server.ts" showLineNumbers {22-23, 26-33} import { Driver, WebSocketServer } from "tgrid"; import { ICalcConfig } from "../interfaces/ICalcConfig"; @@ -64,18 +63,18 @@ export const webSocketServerMain = async () => { > = new WebSocketServer(); await server.open(37_000, async (acceptor) => { // LIST UP PROPERTIES - const config: ICalcConfig = acceptor.header; + const header: ICalcConfig = acceptor.header; const listener: Driver = acceptor.getDriver(); // ACCEPT OR REJECT if (acceptor.path === "/composite") - await acceptor.accept(new CompositeCalculator(config, listener)); + await acceptor.accept(new CompositeCalculator(header, listener)); else if (acceptor.path === "/simple") - await acceptor.accept(new SimpleCalculator(config, listener)); + await acceptor.accept(new SimpleCalculator(header, listener)); else if (acceptor.path === "/statistics") - await acceptor.accept(new StatisticsCalculator(config, listener)); + await acceptor.accept(new StatisticsCalculator(header, listener)); else if (acceptor.path === "/scientific") - await acceptor.accept(new ScientificCalculator(config, listener)); + await acceptor.accept(new ScientificCalculator(header, listener)); else await acceptor.reject(1002, `WebSocket API endpoint not found.`); }); return server; @@ -84,18 +83,9 @@ export const webSocketServerMain = async () => { -## Listener -Interface of the remote system's [Provider](#provider). - -`Listener` is a type definition (interface is recommended) of the remote system's [Provider](#provider). If you wrap the `Listener` type into the [Driver](#driver)\<`Listener`\> through `Communicator.getDriver()` method, you can call the functions of the remote system's [Provider](#provider) asynchronously. - -For reference, it is possible to using the original [Provider](#provider) type of the remote system, so that composing [Driver](#driver)\<[Provider](#provider)\> type. However, such way is not recommended due to the dependency problem. Instead, consider to defining the [Provider](#provider) class to implement the `Listener` interface type. - - - ## Driver -```typescript filename="examples/websocket/client.ts" showLineNumbers +```typescript filename="examples/websocket/client.ts" showLineNumbers {23-30} import { Driver, WebSocketConnector } from "tgrid"; import { ICalcConfig } from "../interfaces/ICalcConfig"; @@ -144,10 +134,10 @@ export const webSocketClientMain = async () => { > ] > ``` -Driver of Listener for RPC (Remote Procedure Call). +Driver of RPC (Remote Procedure Call). -`Driver` is a proxy instance designed to call functions of the remote system. It has a generic argument [Listener](#listener) which means the type of remote system's [Provider](#provider), and you can remotely call the functions of the [Listener](#listener) asynchronously through the `Drive` instance. +`Driver` is a proxy instance designed to call functions of the remote system. It has a generic argument `Remote` which means the type of remote system's [Provider](#provider), and you can remotely call the functions of the [Provider](#provider) asynchronously through the `Drive` instance. -When you call some function of [Listener](#listener) by the `Driver` instance, it hooks the function call expression, and delivers the function name and arguments to the remote system through the [Communicator](#communicator). If the remote system suceeded to reply the result of the function call, [Communicator](#communicator) resolves the promise of the function call expression with the result, so that makes `Driver` working. +When you call some function of remote [Provider](#provider) by the `Driver` instance, it hooks the function call expression, and delivers the function name and arguments (parameter values) to the remote system through the [Communicator](#communicator). If the remote system suceeded to reply the result of the function call, [Communicator](#communicator) resolves the promise of the function call expression with the result, so that makes `Driver` working. -Otherwise exception is thrown in the [Provider](#provider) function, [Communicator](#communicator) deliveries the exception instance instead to the remote system, so that actual exception being thrown from the `Driver` instance. \ No newline at end of file +Otherwise exception is thrown in the remote [Provider](#provider) function, [Communicator](#communicator) deliveries the exception instance instead to the remote system, so that actual exception being thrown from the `Driver` instance. \ No newline at end of file diff --git a/website/pages/docs/remote-procedure-call.mdx b/website/pages/docs/remote-procedure-call.mdx index e69de29..9d0ae36 100644 --- a/website/pages/docs/remote-procedure-call.mdx +++ b/website/pages/docs/remote-procedure-call.mdx @@ -0,0 +1,545 @@ +import { Callout, Tabs, Tab } from 'nextra-theme-docs' + +## Outline +With `TGrid`, you can call remote procedures of provided by remote system. + +If remote system provides a function, `TGrid` lets you call it as if it were your own. If remote system provides some functions that are capsuled in hierarchical structured objects, you still can call them as if they were your own. This is the concept of RPC (Remote Procedure Call) what `TGrid` is saying. + +By the way, there are many other RPC (Remote Procedure Call) frameworks or libraries in the world. However, `TGrid` is different from them. RPC of `TGrid` does not mean only calling and getting returned value from the remote system's procedure, but also ensuring type safety. With the type safety, you can actually feel like that the remote procedure is your own. + + + + +## Demonstration + + +```typescript filename="examples/src/websocket/client.ts" showLineNumbers {23-30} +import { Driver, WebSocketConnector } from "tgrid"; + +import { ICalcConfig } from "../interfaces/ICalcConfig"; +import { ICalcEvent } from "../interfaces/ICalcEvent"; +import { ICalcEventListener } from "../interfaces/ICalcEventListener"; +import { ICompositeCalculator } from "../interfaces/ICompositeCalculator"; + +export const webSocketClientMain = async () => { + const stack: ICalcEvent[] = []; + const listener: ICalcEventListener = { + on: (evt: ICalcEvent) => stack.push(evt), + }; + const connector: WebSocketConnector< + ICalcConfig, + ICalcEventListener, + ICompositeCalculator + > = new WebSocketConnector( + { precision: 2 }, // header + listener, // provider for remote server + ); + await connector.connect("ws://127.0.0.1:37000/composite"); + + const remote: Driver = connector.getDriver(); + console.log( + await driver.plus(10, 20), // returns 30 + await driver.multiplies(3, 4), // returns 12 + await driver.divides(5, 3), // returns 1.67 + await driver.scientific.sqrt(2), // returns 1.41 + await driver.statistics.mean(1, 3, 9), // returns 4.33 + ); + + await connector.close(); + console.log(stack); +}; +``` + + +```typescript filename="examples/websocket/server.ts" showLineNumbers {25-32} +import { Driver, WebSocketServer } from "tgrid"; + +import { ICalcConfig } from "../interfaces/ICalcConfig"; +import { ICalcEventListener } from "../interfaces/ICalcEventListener"; +import { CompositeCalculator } from "../providers/CompositeCalculator"; +import { ScientificCalculator } from "../providers/ScientificCalculator"; +import { SimpleCalculator } from "../providers/SimpleCalculator"; +import { StatisticsCalculator } from "../providers/StatisticsCalculator"; + +export const webSocketServerMain = async () => { + const server: WebSocketServer< + ICalcConfig, + | CompositeCalculator + | SimpleCalculator + | StatisticsCalculator + | ScientificCalculator, + ICalcEventListener + > = new WebSocketServer(); + await server.open(37_000, async (acceptor) => { + // LIST UP PROPERTIES + const config: ICalcConfig = acceptor.header; + const listener: Driver = acceptor.getDriver(); + + // ACCEPT OR REJECT + if (acceptor.path === "/composite") + await acceptor.accept(new CompositeCalculator(config, listener)); + else if (acceptor.path === "/simple") + await acceptor.accept(new SimpleCalculator(config, listener)); + else if (acceptor.path === "/statistics") + await acceptor.accept(new StatisticsCalculator(config, listener)); + else if (acceptor.path === "/scientific") + await acceptor.accept(new ScientificCalculator(config, listener)); + else await acceptor.reject(1002, `WebSocket API endpoint not found.`); + }); + return server; +}; +``` + + +```typescript filename="examples/src/interfaces/*.ts" showLineNumbers +export interface ICalcConfig { + precision: number; +} +export interface ICalcEvent { + type: string; + input: number[]; + output: number; +} +export interface ICalcEventListener { + on(event: ICalcEvent): void; +} + +export interface ICompositeCalculator extends ISimpleCalculator { + scientific: IScientificCalculator; + statistics: IStatisticsCalculator; +} +export interface ISimpleCalculator { + plus(x: number, y: number): number; + minus(x: number, y: number): number; + multiplies(x: number, y: number): number; + divides(x: number, y: number): number; +} +export interface IScientificCalculator { + pow(x: number, y: number): number; + sqrt(x: number): number; + log(x: number, base: number): number; +} +export interface IStatisticsCalculator { + mean(...values: number[]): number; + stdev(...values: number[]): number; +} +``` + + +```typescript filename="examples/src/providers/*.ts" showLineNumbers +import { Driver } from "tgrid"; + +import { ICalcConfig } from "../interfaces/ICalcConfig"; +import { ICalcEventListener } from "../interfaces/ICalcEventListener"; +import { ICompositeCalculator } from "../interfaces/ICompositeCalculator"; +import { IScientificCalculator } from "../interfaces/IScientificCalculator"; +import { ISimpleCalculator } from "../interfaces/ISimpleCalculator"; +import { IStatisticsCalculator } from "../interfaces/IStatisticsCalculator"; + +export abstract class CalculatorBase { + public constructor( + private readonly config: ICalcConfig, + private readonly listener: Driver, + ) {} + + protected compute(type: string, input: number[], output: number): number { + const pow: number = Math.pow(10, this.config.precision); + output = Math.round(output * pow) / pow; + this.listener.on({ type, input, output }).catch(() => {}); + return output; + } +} + +export class SimpleCalculator + extends CalculatorBase + implements ISimpleCalculator +{ + public plus(x: number, y: number): number { + return this.compute("plus", [x, y], x + y); + } + public minus(x: number, y: number): number { + return this.compute("minus", [x, y], x - y); + } + public multiplies(x: number, y: number): number { + return this.compute("multiplies", [x, y], x * y); + } + public divides(x: number, y: number): number { + return this.compute("divides", [x, y], x / y); + } +} + +export class ScientificCalculator + extends CalculatorBase + implements IScientificCalculator +{ + public pow(x: number, y: number): number { + return this.compute("pow", [x, y], Math.pow(x, y)); + } + public sqrt(x: number): number { + return this.compute("sqrt", [x], Math.sqrt(x)); + } + public log(x: number, base: number): number { + return this.compute("log", [x, base], Math.log(x) / Math.log(base)); + } +} + +export class StatisticsCalculator + extends CalculatorBase + implements IStatisticsCalculator +{ + public mean(...values: number[]): number { + const sum: number = values.reduce((x, y) => x + y); + return this.compute("mean", values, sum / values.length); + } + public stdev(...values: number[]): number { + const mean: number = values.reduce((x, y) => x + y) / values.length; + const sum: number = values.reduce((x, y) => x + Math.pow(y - mean, 2)); + return this.compute("stdev", values, Math.sqrt(sum / values.length)); + } +} + +export class CompositeCalculator + extends SimpleCalculator + implements ICompositeCalculator +{ + public readonly scientific: ScientificCalculator; + public readonly statistics: StatisticsCalculator; + + public constructor( + config: ICalcConfig, + listener: Driver, + ) { + super(config, listener); + this.scientific = new ScientificCalculator(config, listener); + this.statistics = new StatisticsCalculator(config, listener); + } +} +``` + + +```typescript filename="examples/src/interfaces/*.ts" showLineNumbers +export interface ICalcConfig { + precision: number; +} +export interface ICalcEvent { + type: string; + input: number[]; + output: number; +} +export interface ICalcEventListener { + on(event: ICalcEvent): void; +} + +export interface ICompositeCalculator extends ISimpleCalculator { + scientific: IScientificCalculator; + statistics: IStatisticsCalculator; +} +export interface ISimpleCalculator { + plus(x: number, y: number): number; + minus(x: number, y: number): number; + multiplies(x: number, y: number): number; + divides(x: number, y: number): number; +} +export interface IScientificCalculator { + pow(x: number, y: number): number; + sqrt(x: number): number; + log(x: number, base: number): number; +} +export interface IStatisticsCalculator { + mean(...values: number[]): number; + stdev(...values: number[]): number; +} +``` + + + +> ```bash filename="Terminal" +> $ npx ts-node examples/src/websocket +> 30 12 1.67 1.41 4.33 +> [ +> { type: 'plus', input: [ 10, 20 ], output: 30 }, +> { type: 'multiplies', input: [ 3, 4 ], output: 12 }, +> { type: 'divides', input: [ 5, 3 ], output: 1.67 }, +> { type: 'sqrt', input: [ 2 ], output: 1.41 }, +> { type: 'mean', input: [ 1, 3, 9 ], output: 4.33 } +> ] +> ``` + +Here is the one of example programs that demonstrating the RPC (Remote Procedure Call) of `TGrid`. + +At first, looking at the "Client Program" tab, you can find out that the "Client Program" is calling the "Server Program"'s functions as if they were its own, through the `Driver` typed instance with `await` symbols. + +At next, change the tab to "Server Program", then you can find out that the "Server Program" is serving `CompositeCalculator` class to the "Client Program". Calling the functions of `CompositeCalculator` in the "Server Program" from the "Client Program" through the `Driver` typed instance, this is the RPC (Remote Procedure Call) of `TGrid`. + + + + +## RPC Driver + + +```typescript filename="examples/src/websocket/client.ts" showLineNumbers +import { Driver, WebSocketConnector } from "tgrid"; + +import { ICalcConfig } from "../interfaces/ICalcConfig"; +import { ICalcEvent } from "../interfaces/ICalcEvent"; +import { ICalcEventListener } from "../interfaces/ICalcEventListener"; +import { ICompositeCalculator } from "../interfaces/ICompositeCalculator"; + +export const webSocketClientMain = async () => { + const stack: ICalcEvent[] = []; + const listener: ICalcEventListener = { + on: (evt: ICalcEvent) => stack.push(evt), + }; + const connector: WebSocketConnector< + ICalcConfig, + ICalcEventListener, + ICompositeCalculator + > = new WebSocketConnector( + { precision: 2 }, // header + listener, // provider for remote server + ); + await connector.connect("ws://127.0.0.1:37000/composite"); + + const remote: Driver = connector.getDriver(); + console.log( + await driver.plus(10, 20), // returns 30 + await driver.multiplies(3, 4), // returns 12 + await driver.divides(5, 3), // returns 1.67 + await driver.scientific.sqrt(2), // returns 1.41 + await driver.statistics.mean(1, 3, 9), // returns 4.33 + ); + + await connector.close(); + console.log(stack); +}; +``` + + +```typescript filename="examples/websocket/server.ts" showLineNumbers +import { Driver, WebSocketServer } from "tgrid"; + +import { ICalcConfig } from "../interfaces/ICalcConfig"; +import { ICalcEventListener } from "../interfaces/ICalcEventListener"; +import { CompositeCalculator } from "../providers/CompositeCalculator"; +import { ScientificCalculator } from "../providers/ScientificCalculator"; +import { SimpleCalculator } from "../providers/SimpleCalculator"; +import { StatisticsCalculator } from "../providers/StatisticsCalculator"; + +export const webSocketServerMain = async () => { + const server: WebSocketServer< + ICalcConfig, + | CompositeCalculator + | SimpleCalculator + | StatisticsCalculator + | ScientificCalculator, + ICalcEventListener + > = new WebSocketServer(); + await server.open(37_000, async (acceptor) => { + // LIST UP PROPERTIES + const config: ICalcConfig = acceptor.header; + const listener: Driver = acceptor.getDriver(); + + // ACCEPT OR REJECT + if (acceptor.path === "/composite") + await acceptor.accept(new CompositeCalculator(config, listener)); + else if (acceptor.path === "/simple") + await acceptor.accept(new SimpleCalculator(config, listener)); + else if (acceptor.path === "/statistics") + await acceptor.accept(new StatisticsCalculator(config, listener)); + else if (acceptor.path === "/scientific") + await acceptor.accept(new ScientificCalculator(config, listener)); + else await acceptor.reject(1002, `WebSocket API endpoint not found.`); + }); + return server; +}; +``` + + +```typescript filename="examples/src/interfaces/*.ts" showLineNumbers +export interface ICalcConfig { + precision: number; +} +export interface ICalcEvent { + type: string; + input: number[]; + output: number; +} +export interface ICalcEventListener { + on(event: ICalcEvent): void; +} + +export interface ICompositeCalculator extends ISimpleCalculator { + scientific: IScientificCalculator; + statistics: IStatisticsCalculator; +} +export interface ISimpleCalculator { + plus(x: number, y: number): number; + minus(x: number, y: number): number; + multiplies(x: number, y: number): number; + divides(x: number, y: number): number; +} +export interface IScientificCalculator { + pow(x: number, y: number): number; + sqrt(x: number): number; + log(x: number, base: number): number; +} +export interface IStatisticsCalculator { + mean(...values: number[]): number; + stdev(...values: number[]): number; +} +``` + + +```typescript filename="examples/src/providers/*.ts" showLineNumbers +import { Driver } from "tgrid"; + +import { ICalcConfig } from "../interfaces/ICalcConfig"; +import { ICalcEventListener } from "../interfaces/ICalcEventListener"; +import { ICompositeCalculator } from "../interfaces/ICompositeCalculator"; +import { IScientificCalculator } from "../interfaces/IScientificCalculator"; +import { ISimpleCalculator } from "../interfaces/ISimpleCalculator"; +import { IStatisticsCalculator } from "../interfaces/IStatisticsCalculator"; + +export abstract class CalculatorBase { + public constructor( + private readonly config: ICalcConfig, + private readonly listener: Driver, + ) {} + + protected compute(type: string, input: number[], output: number): number { + const pow: number = Math.pow(10, this.config.precision); + output = Math.round(output * pow) / pow; + this.listener.on({ type, input, output }).catch(() => {}); + return output; + } +} + +export class SimpleCalculator + extends CalculatorBase + implements ISimpleCalculator +{ + public plus(x: number, y: number): number { + return this.compute("plus", [x, y], x + y); + } + public minus(x: number, y: number): number { + return this.compute("minus", [x, y], x - y); + } + public multiplies(x: number, y: number): number { + return this.compute("multiplies", [x, y], x * y); + } + public divides(x: number, y: number): number { + return this.compute("divides", [x, y], x / y); + } +} + +export class ScientificCalculator + extends CalculatorBase + implements IScientificCalculator +{ + public pow(x: number, y: number): number { + return this.compute("pow", [x, y], Math.pow(x, y)); + } + public sqrt(x: number): number { + return this.compute("sqrt", [x], Math.sqrt(x)); + } + public log(x: number, base: number): number { + return this.compute("log", [x, base], Math.log(x) / Math.log(base)); + } +} + +export class StatisticsCalculator + extends CalculatorBase + implements IStatisticsCalculator +{ + public mean(...values: number[]): number { + const sum: number = values.reduce((x, y) => x + y); + return this.compute("mean", values, sum / values.length); + } + public stdev(...values: number[]): number { + const mean: number = values.reduce((x, y) => x + y) / values.length; + const sum: number = values.reduce((x, y) => x + Math.pow(y - mean, 2)); + return this.compute("stdev", values, Math.sqrt(sum / values.length)); + } +} + +export class CompositeCalculator + extends SimpleCalculator + implements ICompositeCalculator +{ + public readonly scientific: ScientificCalculator; + public readonly statistics: StatisticsCalculator; + + public constructor( + config: ICalcConfig, + listener: Driver, + ) { + super(config, listener); + this.scientific = new ScientificCalculator(config, listener); + this.statistics = new StatisticsCalculator(config, listener); + } +} +``` + + +```typescript filename="examples/src/interfaces/*.ts" showLineNumbers +export interface ICalcConfig { + precision: number; +} +export interface ICalcEvent { + type: string; + input: number[]; + output: number; +} +export interface ICalcEventListener { + on(event: ICalcEvent): void; +} + +export interface ICompositeCalculator extends ISimpleCalculator { + scientific: IScientificCalculator; + statistics: IStatisticsCalculator; +} +export interface ISimpleCalculator { + plus(x: number, y: number): number; + minus(x: number, y: number): number; + multiplies(x: number, y: number): number; + divides(x: number, y: number): number; +} +export interface IScientificCalculator { + pow(x: number, y: number): number; + sqrt(x: number): number; + log(x: number, base: number): number; +} +export interface IStatisticsCalculator { + mean(...values: number[]): number; + stdev(...values: number[]): number; +} +``` + + + +Looking at the above `ICompositeCalculator` type from the "Interfaces" tab, none of the functions are actually asynchrounous. However, the "Client Program" is attaching `await` symbols. It's because every return types of `ICompositeCalculator` have changed to `Promise` types through the `Driver` type. + +As the `Driver` typed instance is not a "Client Program"'s own, but the "Server Program"'s own (`CompositeCalculator`), the function call must be passed through the asynchronous network communication. In such reason, the remote function calling cannot be synchronous, but asynchronous, and `Driver` type is casting them. + + + +Description of `Driver` type in the [Features > Components](./features/components) chatper. + +`Driver` is a proxy instance designed to call functions of the remote system. It has a generic argument `Remote` which means the type of remote system's [Provider](./features/components/#provider), and you can remotely call the functions of the [Provider](#provider) asynchronously through the `Drive` instance. + +When you call some function of remote [Provider](./features/components/#provider) by the `Driver` instance, it hooks the function call expression, and delivers the function name and arguments (parameter values) to the remote system through the [Communicator](./features/components/#communicator). If the remote system suceeded to reply the result of the function call, [Communicator](./features/components/#communicator) resolves the promise of the function call expression with the result, so that makes `Driver` working. + +Otherwise exception is thrown in the remote [Provider](./features/components/#provider) function, [Communicator](./features/components/#communicator) deliveries the exception instance instead to the remote system, so that actual exception being thrown from the `Driver` instance. + + + + + +## Restrictions +`TGrid` has implemented the RPC (Remote Procedure Call) by communicating JSON message. Therefore, if parameters or return values of the remote provided functions are not compatible JSON, you can't use them. + +For example, JSON does not support `bigint` type of JavaScript. Therefore, if some of the remote provided functions are using `bigint` type on their parameters or return value, it would throw an exception. + +Also, as JSON does not contain class transformation spec, every parameters and return values must be primitive types. If you try to deliver the class instance as a parameter or return value, it would be downgraded to primitive instance in the remote system. \ No newline at end of file diff --git a/website/src/movies/home/HomeStrengthMovie.tsx b/website/src/movies/home/HomeStrengthMovie.tsx index 1c9c57f..b87252c 100644 --- a/website/src/movies/home/HomeStrengthMovie.tsx +++ b/website/src/movies/home/HomeStrengthMovie.tsx @@ -123,7 +123,7 @@ const sections: HomeStrengthSectionMovie.Props[] = [

WebSocket RPC in NestJS


-

Compatible with both HTTP/WebSocket protocols

+

Compatible with both HTTP and WebSocket protocols


Supports SDK (Software Development Kit) generation