đź“– Guide Documents
Core Library
WebSocketRoute

Outline

@nestia/core
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.

How to use

Application Setup

src/CalculateModule.ts
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()

src/CalculateController.ts
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 client
  • Provider: Service provider for client
  • Listener: 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

src/CalculateController.ts
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.

test/features/test_api_calculate_start.ts
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 client
  • Provider: Service provider for client
  • Listener: Remote service provider from client
src/CalculateController.ts
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 allowed
  • string | 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
SomeQueryDto.ts
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];
}