đź“– Guide Documents
Features
WebSocket Protocol

Outline

TGrid supports WebSocket protocol.

With TGrid, you can easily develop WebSocket system under the RPC (Remote Procedure Call) concept.

By the way, when you're developing WebSocket server natively only with TGrid, you have to construct and open the WebSocket server through WebSocketServer class. Also, you have to access to the WebSocket server with manual WebSocketConnector composition.

Instead, if you develop the WebSocket server with NestJS, client can easily interact with the WebSocket server by SDK (Software Development Kit) library generated by Nestia. Also, you can make both http and websocket operations to the NestJS controllers, so that makes the server compatible on both protocols.

Therefore, when you develop WebSocket server, I recommend to use NestJS with TGrid for the best development experience.

Native Classes

WebSocketServer

examples/swebsocket/src/server.ts
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<ICalcEventListener> = 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;
};
⚠️

Available only in the NodeJS.

WebSocketServer is a class which can open an websocket server. Clients connecting to the WebSocketServer would communicate with this websocket server through WebSocketAcceptor objects with RPC (Remote Procedure Call) concept.

To open the websocket server, call the WebSocketServer.open() method with target port number, and your custom callback function which would be called whenever a WebSocketAcceptor has been newly created by a new client's connection.

Also, when declaring WebSocketServer type, you have to specify three generic arguments; Header, Provider and Remote. Those generic arguments would be propagated to the WebSocketAcceptor, so that WebSocketAcceptor would have the same generic arguments, too.

For reference, the first Header type repersents an initial data from the remote client after the connection. I recommend utilize it as an activation tool for security enhancement. The second generic argument Provider represents a provider from server to client, and the other Remote means a provider from the remote client to server.

  • Above example case:
    • Header: ICalcConfig type
    • Provider: Server is providing one of below to the client
      • SimpleCalculator
      • StatisticsCalculator
      • ScientificCalculator
    • Remote: Client is providing ICalcEventListener to the server

WebSocketAcceptor

examples/websocket/src/server.ts
import { Driver, WebSocketAcceptor, 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: WebSocketAcceptor<
        ICalcConfig,
        | CompositeCalculator
        | SimpleCalculator
        | StatisticsCalculator
        | ScientificCalculator,
        ICalcEventListener
      >,
    ) => {
      // LIST UP PROPERTIES
      const config: ICalcConfig = acceptor.header;
      const listener: Driver<ICalcEventListener> = 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;
};
⚠️

Available only in the NodeJS.

The WebSocketAcceptor is a communicator class interacting with the remote websocket client through (RPC)(Remote Procedure Call) concept, created by the WebSocketServer class whenever a remote client connects to the websocket server.

When the closure function being called by the connection of a remote client, you can determine whether to accept the client's connection or not, reading the WebSocketAcceptor.header or WebSocketAcceptor.path properties. If you've decided to accept the connection, call the WebSocketAcceptor.accept() method with Provider instance. Otherwise, reject it through the WebSocketAcceptor.reject() method.

After accepting the connection, don't forget to closing the connection after your business logic has been completed to clean up the resources. Otherwise the closing must be performed by the remote client, you can wait the remote client's closing signal by the WebSocketAcceptor.join() method.

WebSocketConnector

examples/websocket/src/client.ts
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<ICompositeCalculator> = 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);
};
Terminal
$ npm start
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 }
]
đź’ˇ

Available in both Browser/NodeJS.

Web Socket Connector.

The WebSocketConnector is a communicator class which connects to a websocket server and interacts with it through RPC (Remote Procedure Call) concept.

You can connect to the websocket server using WebSocketConnector.connect() method. The interaction would be started if the server accepts your connection by calling the WebSocketAcceptor.accept() method. If the remote server rejects your connection through WebSocketAcceptor.reject() method, the exception would be thrown.

After the connection, don't forget to closing the connection, if your business logics have been completed, to clean up the resources. Otherwise, the closing must be performed by the remote websocket server, you can wait the remote server's closing signal through the WebSocketConnector.join() method.

Also, when declaring this WebSocketConnector type, you've to define three generic arguments; Header, Provider and Remote. Those generic arguments must be same with the ones defined in the target WebSocketServer and WebSocketAcceptor classes (Provider and Remote must be reversed).

For reference, the first Header type repersents an initial data from the remote client after the connection. I recommend utilize it as an activation tool for security enhancement. The second generic argument Provider represents a provider from client to server, and the other Remote means a provider from the remote server to client.

  • Above example case:
    • Header: ICalcConfig type
    • Provider: Client is providing ICalcEventListener to the server
    • Remote: Server is providing ISimpleCalculator to the client
đź’»

Demonstration

You can run it on Playground Website (opens in a new tab), or local machine.

Terminal
git clone https://github.com/samchon/tgrid.example.websocket
npm install
npm start

NestJS Integration

If you develop websocket application, I recommend integrate TGrid with NestJS.

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.

In the client side, you also can take advantages of automatically generated SDK (Software Development Kit) library for the client developers. With the 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.

Bootstrap

examples/nestjs/src/bootstrap.ts
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 utilize TGrid in the NestJS, upgrade the NestJS application like above.

Just call the WebSocketAdaptor.upgrade() (opens in a new tab) method, then you can utilize TGrid in the NestJS server.

About detailed setup or more detailed informations, please refer below docs:

NestJS Controller

examples/nestjs/src/calculate.controller.ts
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);
  }
 
  /**
   * 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);
  }
 
  /**
   * 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);
  }
 
  /**
   * 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);
  }
}

From now on, you can define WebSocket API operations like above.

Just import and attach the @WebSocketRoute() decorator function to the target controller methods.

Note that, don't forget to define @WebSocketRoute.Acceptor() decorated parameter with WebSocketAcceptor type. It's because the websocket server must determine whether to WebSocketAcceptor.accept() the client's connection or WebSocketAcceptor.reject() it.

Also, when declaring the WebSocketAcceptor type, ou have to specify three generic arguments; Header, Provider and Remote. Those generic arguments would be propagated to the automatically generated Software Development Kit for the client, so that the client developers will utilize the same generic types what you've defined (Provider and Remote must be reversed).

For reference, the first Header type repersents an initial data from the remote client after the connection. I recommend utilize it as an activation tool for security enhancement. The second generic argument Provider represents a provider from server to client, and the other Remote means a provider from the remote client to server.

You can find more detailed informations about @WebSocketRoute():

Software Development Kit

examples/nestjs/src/api/functional/calculate.ts
/**
 * @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";
}
Terminal
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.

You can find more detailed informations about SDK generator:

Client Application

examples/nestjs/src/calculate.test.ts
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.

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 the previous Natives Classes > WebSocketConnector case? This is the reason why I've recommended to combine with the NestJS when using websocket protocol based network system.

For reference, return type of SDK function is a pair WebSocketConnector and Driver<ICompositeCalculator> instances, but it would be actually returned only when the websocket server accepts your connection. Otherwise, the websocket server rejects your connection, an exception would be thrown.

Also, don't forget to closing the connection, if your business logics have been completed, to clean up the resources. Otherwise, the closing must be performed by the remote websocket server, you can wait the remote server's closing signal through the WebSocketConnector.join() method.

đź’»

Demonstration

You can run it on Playground Website (opens in a new tab), or local machine.

Terminal
git clone https://github.com/samchon/tgrid.example.nestjs
npm install
npm start