Outline
export function WebSocketRoute(path?: string): MethodDecorator;
export namespace WebSocketRoute {
export function Acceptor(): ParameterDecorator;
export function Driver(): ParameterDecorator;
export function Header(): ParameterDecorator;
export function Param(field: string): ParameterDecorator;
export function Query(): ParameterDecorator;
}
WebSocket route decorators.
@WebSocketRoute()
is a collection of decorators for WebSocket routes.
Also, supports SDK (Software Development Kit), so that you can easily develop the WebSocket client.
- References
How to use
Application Setup
import { WebSocketAdaptor } from "@nestia/core";
import { INestApplication } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { CalculateModule } from "./CalculateModule";
export namespace CalculateBackend {
export const start = async (): Promise<INestApplication> => {
const app: INestApplication = await NestFactory.create(CalculateModule);
await WebSocketAdaptor.upgrade(app);
await app.listen(3_000, "0.0.0.0");
return app;
};
}
At first, you need to upgrade your NestJS application to support WebSocket protocol.
Import WebSocketAdaptor
class from @nestia/core
, and call WebSocketAdaptor.upgrade()
function with the NestJS application instance like above.
If you don’t upgrade it, @WebSocketRoute()
decorated methods never work.
@WebSocketRoute()
import { WebSocketRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { Driver, WebSocketAcceptor } from "tgrid";
import { ICalculator } from "./api/structures/ICalculator";
import { IListener } from "./api/structures/IListener";
import { Calculator } from "./providers/Calculator";
@Controller("calculate")
export class CalculateController {
/**
* Start simple calculator.
*
* Start simple calculator through WebSocket.
*/
@WebSocketRoute("start")
public async start(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<any, ICalculator, IListener>,
@WebSocketRoute.Driver() driver: Driver<IListener>,
): Promise<void> {
await acceptor.accept(new Calculator(driver));
}
}
After that, attach @WebSocketRoute()
decorator function onto target method like above.
Note that, never forget to defining the @WebSocketRoute.Acceptor()
decorated parameter. It is essential for both WebSocket route method and SDK library generation. Each generic arguments of WebSocketAcceptor<Header, Provider, Listener>
means like below:
Header
: Header information received by clientProvider
: Service provider for clientListener
: Remote service provider from client
Also, the Driver<IListener>
is a type of the remote provider by client. If you call any function of the remote provider, your function call request will be sent to the remote client, and returned value would be recived from the client asynchronouly.
Therefore, the Driver<T>
type converts every functions’ return type to be Promise<R>
. In the client side, your Provider
would be also wrapped into the Driver<Provider>
, so that client can call your functions asynchronously, too.
Nested Decorators
import { WebSocketRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { Driver, WebSocketAcceptor } from "tgrid";
import { tags } from "typia";
import { IAdvancedCalculator } from "./api/structures/IAdvancedCalculator";
import { IHeader } from "./api/structures/IHeader";
import { IListener } from "./api/structures/IListener";
import { IMemo } from "./api/structures/IMemo";
import { AdvancedCalculator } from "./providers/AdvancedCalculator";
@Controller("calculate")
export class CalculateController {
/**
* Start advanced calculator.
*
* Start advanced calculator through WebSocket with additional informations.
*
* @param id ID to assign
* @param header Header information
* @param memo Memo to archive
*/
@WebSocketRoute(":id/advance")
public async advance(
@WebSocketRoute.Param("id") id: string & tags.Format<"uuid">,
@WebSocketRoute.Header() header: undefined | Partial<IHeader>,
@WebSocketRoute.Query() memo: IMemo,
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<undefined, IAdvancedCalculator, IListener>,
): Promise<void> {
if (header?.precision !== undefined && header.precision < 0)
await acceptor.reject(1008, "Invalid precision value");
else
await acceptor.accept(
new AdvancedCalculator(
id,
{ precision: header?.precision ?? 2 },
memo,
acceptor.getDriver(),
),
);
}
}
If you need additional parameters, you can use nested decorators.
@WebSocketRoute.Acceptor()
: Acceptor for the client connection@WebSocketRoute.Driver()
: Driver for the remote provider by client@WebSocketRoute.Header()
: Header information from the client@WebSocketRoute.Param()
: URL path parameter@WebSocketRoute.Query()
: URL query parameter
For reference, those decorators are almost same with @TypedHeaders()
, @TypedParam()
and @TypedQuery()
. However, they can’t be used in @WebSocketRoute()
decorated method. Only nested decorator functions under the WebSocketRoute
module are allowed.
Also, if you don’t want to accept the client connection, reject it through WebSocketAcceptor.close()
function.
Software Development Kit
Related Document: Software Development Kit
When you configure a nestia.config.ts
file and run npx nestia sdk
command, @nestia/sdk
will generate a SDK (Software Development Kit) library for the WebSocket route. With the SDK library, you can easily develop the WebSocket client application with TypeScript types.
Also, as I’ve mentioned above, remote provider by WebSocket server is wrapped into the Driver<T>
type, so that the client application can call the remote provider’s function asynchronously. For example, ICalculator.plus()
function returned number
value in the server side, but Driver<T>
returns Promise<number>
type.
In the same reason, the IListener
type would be wrapped into the Driver<IListener>
in the server side, and the listener
provider would be called asynchronously in the server side through the WebSocket network communication.
import { TestValidator } from "@nestia/e2e";
import api from "@samchon/calculator-api/lib/index";
import { IListener } from "@samchon/calculator-api/lib/structures/IListener";
export const test_api_calculate_start = async (
connection: api.IConnection,
): Promise<void> => {
const stack: IListener.IEvent[] = [];
const listener: IListener = {
on: (event) => stack.push(event),
};
const { connector, driver } = await api.functional.calculate.start(
connection,
listener,
);
try {
TestValidator.equals("plus")(await driver.plus(4, 2))(6);
TestValidator.equals("minus")(await driver.minus(4, 2))(2);
TestValidator.equals("multiply")(await driver.multiply(4, 2))(8);
TestValidator.equals("divide")(await driver.divide(4, 2))(2);
TestValidator.equals("events")(stack)([
{ type: "plus", x: 4, y: 2, z: 6 },
{ type: "minus", x: 4, y: 2, z: 2 },
{ type: "multiply", x: 4, y: 2, z: 8 },
{ type: "divide", x: 4, y: 2, z: 2 },
]);
} catch (exp) {
throw exp;
} finally {
await connector.close();
}
};
Restrictions
@WebSocketAcceptor()
When defining @WebSocketRoute()
decorated method, you must define the @WebSocketRoute.Acceptor()
decorated parameter. It is essential for both WebSocket route method and SDK library generation, because its target type WebSocketAcceptor<Header, Provider, Listener>
has significant type definitions for WebSocket communication.
Header
: Header information received by clientProvider
: Service provider for clientListener
: Remote service provider from client
import { WebSocketRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { Driver, WebSocketAcceptor } from "tgrid";
import { ICalculator } from "./api/structures/ICalculator";
import { IListener } from "./api/structures/IListener";
import { Calculator } from "./providers/Calculator";
@Controller("calculate")
export class CalculateController {
/**
* Start simple calculator.
*
* Start simple calculator through WebSocket.
*/
@WebSocketRoute("start")
public async start(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<any, ICalculator, IListener>,
@WebSocketRoute.Driver() driver: Driver<IListener>,
): Promise<void> {
await acceptor.accept(new Calculator(driver));
}
}
@WebSocketRoute.Param()
@WebSocketRoute.Param()
allows only atomic type.
boolean
number
string
Also, @WebSocketRoute.Param()
allows nullable like number | null
, but undefindable type is not.
number | null
is allowedstring | undefined
is prohibited
If you violate above condition, and try to declare object or union type, compilation error would be occured:
Error on nestia.core.WebSocketRoute.Param(): only atomic type is allowed
@WebSocketRoute.Query()
When using @WebSocketRoute.Query()
, you’ve to follow such restrction.
At first, type of @WebSocketRoute.Query()
must be a pure object type. It does not allow union type. Also, nullable and undefindable types are not allowed, either. Note that, query parameter type must be a sole object type without any extra definition.
At next, type of properties must be atomic, or array of atomic type. In the atomic type case, the atomic type allows both nullable and undefindable types. However, mixed union atomic type like string | number
or "1" | "2" | 3
are not allowed. Also, the array type does not allow both nullable and undefindable types, either.
boolean
number
bigint
string
export interface SomeQueryDto {
//----
// ATOMIC TYPES
//----
// ALLOWED
boolean: boolean;
number: number;
string: string;
bigint: bigint;
optional_number?: number;
nullable_string: string | null;
literal_union: "A" | "B" | "C" | "D";
// NOT ALLOWED
mixed_union: string | number | boolean;
mixed_literal: "A" | "B" | 3;
//----
// ARRAY TYPES
//----
// ALLOWED
nullable_element_array: (string | null)[];
string_array: string[];
number_array: number[];
literal_union_array: ("A" | "B" | "C")[];
literal_tuple: ["A", "B", "C"];
// NOT ALLOWED
optional_element_array: (string | undefined)[];
optional_array: string[] | undefined;
nullable_array: string[] | null;
union_atomic_array: (string | number)[];
mixed_literal_array: ("A", "B", 3)[];
mixed_tuple: ["A", "B", 3];
}