Outline
If you develop websocket application, I recommend integrate TGrid
with NestJS
/ Nestia
.
It's because you can manage WebSocket API endpoints much effectively and easily by NestJS
controller patterns. Also, you can make your server to support both HTTP and WebSocket protocols at the same time. NestJS controllers are compatible with both HTTP and WebSocket operations.
Furthermore, you can generate SDK (Software Development Kit) library for your client application through Nestia
. With the automatically generated SDK, client developers no more need to write the WebSocket connection and RPC (Remote Procedure Call) codes manually, so that the client development becomes much easier and safer.
- References
Demonstration
You can run the example program on Playground Website (opens in a new tab), or local machine.
git clone https://github.com/samchon/tgrid.example.nestjs
npm install
npm start
Server Program
Bootstrap
import { WebSocketAdaptor } from "@nestia/core";
import { INestApplication } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { CalculateModule } from "./calculate.module";
export const bootstrap = async (): Promise<INestApplication> => {
const app: INestApplication = await NestFactory.create(CalculateModule);
await WebSocketAdaptor.upgrade(app);
await app.listen(37_000, "0.0.0.0");
return app;
};
To integrate TGrid
with NestJS
, you have to upgrade the NestJS application like above.
Just call the WebSocketAdaptor.upgrade()
(opens in a new tab), then you can utilize TGrid
in the NestJS
server.
Controller
import { TypedRoute, WebSocketRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { Driver, WebSocketAcceptor } from "tgrid";
import { ICalcConfig } from "./api/interfaces/ICalcConfig";
import { ICalcEventListener } from "./api/interfaces/ICalcEventListener";
import { ICompositeCalculator } from "./api/interfaces/ICompositeCalculator";
import { IScientificCalculator } from "./api/interfaces/IScientificCalculator";
import { ISimpleCalculator } from "./api/interfaces/ISimpleCalculator";
import { IStatisticsCalculator } from "./api/interfaces/IStatisticsCalculator";
import { CompositeCalculator } from "./providers/CompositeCalculator";
import { ScientificCalculator } from "./providers/ScientificCalculator";
import { SimpleCalculator } from "./providers/SimpleCalculator";
import { StatisticsCalculator } from "./providers/StatisticsCalculator";
@Controller("calculate")
export class CalculateController {
/**
* Health check API (HTTP GET).
*/
@TypedRoute.Get("health")
public health(): string {
return "Health check OK";
}
/**
* Prepare a composite calculator.
*/
@WebSocketRoute("composite")
public async composite(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig,
ICompositeCalculator,
ICalcEventListener
>,
@WebSocketRoute.Header() header: ICalcConfig,
@WebSocketRoute.Driver() listener: Driver<ICalcEventListener>
): Promise<void> {
const provider: CompositeCalculator = new CompositeCalculator(
header,
listener
);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
/**
* Prepare a simple calculator.
*/
@WebSocketRoute("simple")
public async simple(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig, // header
ISimpleCalculator, // provider for remote client
ICalcEventListener // provider from remote client
>
): Promise<void> {
const header: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
const provider: SimpleCalculator = new SimpleCalculator(header, listener);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
/**
* Prepare a scientific calculator.
*/
@WebSocketRoute("scientific")
public async scientific(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig,
IScientificCalculator,
ICalcEventListener
>
): Promise<void> {
const header: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
const provider: ScientificCalculator = new ScientificCalculator(
header,
listener
);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
/**
* Prepare a statistics calculator.
*/
@WebSocketRoute("statistics")
public async statistics(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig,
IStatisticsCalculator,
ICalcEventListener
>
): Promise<void> {
const header: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
const provider: IStatisticsCalculator = new StatisticsCalculator(
header,
listener
);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
}
As you can see from the above code, CalculateController
has many API operations, including both HTTP and WebSocket protocols. The CalculatorController.health()
is an HTTP Get method operation, and the others are all WebSocket operations.
When defining WebSocket operation, attach @WebSocketRoute()
decorator to the target controller method with path
specification. Also, the controller method must have the @WebSocketRoute.Acceptor()
decorated parameter with WebSocketAcceptor
type, because you have to determine whether to WebSocketAcceptor.accept()
the client's connection or WebSocketAcceptor.reject()
it.
With such controller patterned WebSocket operation, you can manage WebSocket API endpoints much effectively and easily. Also, you can generate SDK (Software Development Kit) library for your client application through Nestia
. Let's see how to generate SDK library, and how it would be looked like in the next section.
Ping
If client comes from web browser, the connection would be closed automatically after a certain period of time if there's no signal. In the Google Chrome case, it automatically closes the connection after 60 seconds.
To make the connection alive forcibly, you can ping a signal to the remote client repeatedly in the specified interval by calling the WebSocketAcceptor.ping()
method. Therefore, when developing WebSocket server application, consider to calling the WebSocketAcceptor.ping()
method after accepting the connection.
Software Development Kit
/**
* @packageDocumentation
* @module api.functional.calculate
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
//================================================================
import type { IConnection, Primitive } from "@nestia/fetcher";
import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher";
import { WebSocketConnector } from "tgrid";
import type { Driver } from "tgrid";
import type { ICalcConfig } from "../../interfaces/ICalcConfig";
import type { ICalcEventListener } from "../../interfaces/ICalcEventListener";
import type { ICompositeCalculator } from "../../interfaces/ICompositeCalculator";
import type { IScientificCalculator } from "../../interfaces/IScientificCalculator";
import type { ISimpleCalculator } from "../../interfaces/ISimpleCalculator";
import type { IStatisticsCalculator } from "../../interfaces/IStatisticsCalculator";
/**
* Health check API (HTTP GET).
*
* @controller CalculateController.health
* @path GET /calculate/health
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function health(connection: IConnection): Promise<health.Output> {
return PlainFetcher.fetch(connection, {
...health.METADATA,
path: health.path(),
});
}
export namespace health {
export type Output = Primitive<string>;
export const METADATA = {
method: "GET",
path: "/calculate/health",
request: null,
response: {
type: "application/json",
encrypted: false,
},
status: null,
} as const;
export const path = () => "/calculate/health";
}
/**
* Prepare a composite calculator.
*
* @controller CalculateController.composite
* @path /calculate/composite
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function composite(
connection: IConnection<composite.Header>,
provider: composite.Provider,
): Promise<composite.Output> {
const connector: WebSocketConnector<
composite.Header,
composite.Provider,
composite.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${composite.path()}`,
);
const driver: Driver<composite.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace composite {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = ICompositeCalculator;
export const path = () => "/calculate/composite";
}
/**
* Prepare a simple calculator.
*
* @controller CalculateController.simple
* @path /calculate/simple
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function simple(
connection: IConnection<simple.Header>,
provider: simple.Provider,
): Promise<simple.Output> {
const connector: WebSocketConnector<
simple.Header,
simple.Provider,
simple.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${simple.path()}`,
);
const driver: Driver<simple.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace simple {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = ISimpleCalculator;
export const path = () => "/calculate/simple";
}
/**
* Prepare a scientific calculator.
*
* @controller CalculateController.scientific
* @path /calculate/scientific
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function scientific(
connection: IConnection<scientific.Header>,
provider: scientific.Provider,
): Promise<scientific.Output> {
const connector: WebSocketConnector<
scientific.Header,
scientific.Provider,
scientific.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${scientific.path()}`,
);
const driver: Driver<scientific.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace scientific {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = IScientificCalculator;
export const path = () => "/calculate/scientific";
}
/**
* Prepare a statistics calculator.
*
* @controller CalculateController.statistics
* @path /calculate/statistics
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function statistics(
connection: IConnection<statistics.Header>,
provider: statistics.Provider,
): Promise<statistics.Output> {
const connector: WebSocketConnector<
statistics.Header,
statistics.Provider,
statistics.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${statistics.path()}`,
);
const driver: Driver<statistics.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace statistics {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = IStatisticsCalculator;
export const path = () => "/calculate/statistics";
}
npx nestia sdk
When you run npx nestia sdk
command, SDK (Software Development Kit) library be generated.
Above file is one of the SDK library corresponding to the CalculateController
class we've seen in the previous NestJS Controller section. Client developers can utilize the automatically generated SDK functions to connect to the WebSocket server, and interact it type safely. Also, HTTP operation is compatible with the WebSocket operation.
Let's see how client developer utilizes the SDK library in the next section.
Client Program
import api from "./api";
import { ICalcEvent } from "./api/interfaces/ICalcEvent";
import { ICalcEventListener } from "./api/interfaces/ICalcEventListener";
export const testCalculateSdk = async () => {
//----
// HTTP PROTOCOL
//---
// CALL HEALTH CHECK API
console.log(
await api.functional.calculate.health({
host: "http://127.0.0.1:37000",
})
);
//----
// WEBSOCKET PROTOCOL
//---
// PROVIDER FOR WEBSOCKET SERVER
const stack: ICalcEvent[] = [];
const listener: ICalcEventListener = {
on: (evt: ICalcEvent) => stack.push(evt),
};
// DO CONNECT
const { connector, driver } = await api.functional.calculate.composite(
{
host: "ws://127.0.0.1:37000",
headers: {
precision: 2,
},
},
listener
);
// CALL FUNCTIONS OF REMOTE SERVER
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
);
// TERMINATE
await connector.close();
console.log(stack);
};
Terminal$ npm start [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [NestFactory] Starting Nest application... [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [InstanceLoader] CalculateModule dependencies initialized +5ms [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [RoutesResolver] CalculateController {/calculate}: +5ms [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [NestApplication] Nest application successfully started +2ms Health check OK 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 } ]
Do import the SDK, and enjoy the type-safe and easy-to-use RPC (Remote Procedure Call).
Looking at the above code, the client application is calling a function of the automatically generated SDK (Software Development Kit) library, so that connecting to the websocket server, and starting interaction through RPC (Remote Procedure Call) concept with Driver
<ICompositeCalculator
> instance.
Doesn't the "SDK based development" seems much easier and safer than native websocket classes case? This is the reason why I've recommended to combine with the NestJS
when using websocket protocol based network system.
This is the integration of TGrid
with NestJS
.
Next Chapter
We've learned how to utilize TGrid
with many examples.
By the way, don't you want to know how to utilize TGrid
in the real project?
In the next chapter, we'll see how TGrid
be utilized in the real world.
- Learn from Projects