diff --git a/packages/core/package.json b/packages/core/package.json
index c57d33260..cd9c60bd5 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -43,10 +43,13 @@
"detect-ts-node": "^1.0.5",
"glob": "^7.2.0",
"multer": "1.4.5-lts.1",
+ "path-parser": "^6.1.0",
"raw-body": "^2.0.0",
"reflect-metadata": ">=0.1.12",
"rxjs": ">=6.0.3",
- "typia": "^6.0.3"
+ "tgrid": "^0.10.3",
+ "typia": "^6.0.3",
+ "ws": "^7.5.3"
},
"peerDependencies": {
"@nestia/fetcher": ">=3.0.5",
@@ -65,6 +68,7 @@
"@types/inquirer": "^9.0.3",
"@types/multer": "^1.4.11",
"@types/ts-expose-internals": "npm:ts-expose-internals@5.2.2",
+ "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"commander": "^10.0.0",
diff --git a/packages/core/src/adaptors/WebSocketAdaptor.ts b/packages/core/src/adaptors/WebSocketAdaptor.ts
new file mode 100644
index 000000000..66b82eb4c
--- /dev/null
+++ b/packages/core/src/adaptors/WebSocketAdaptor.ts
@@ -0,0 +1,419 @@
+///
+import { INestApplication, VersioningType } from "@nestjs/common";
+import {
+ HOST_METADATA,
+ MODULE_PATH,
+ PATH_METADATA,
+ SCOPE_OPTIONS_METADATA,
+ VERSION_METADATA,
+} from "@nestjs/common/constants";
+import { VERSION_NEUTRAL, VersionValue } from "@nestjs/common/interfaces";
+import { NestContainer } from "@nestjs/core";
+import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
+import { Module } from "@nestjs/core/injector/module";
+import getFunctionLocation from "get-function-location";
+import { IncomingMessage, Server } from "http";
+import { Path } from "path-parser";
+import { Duplex } from "stream";
+import { WebAcceptor } from "tgrid";
+import typia from "typia";
+import WebSocket from "ws";
+
+import { IWebSocketRouteReflect } from "../decorators/internal/IWebSocketRouteReflect";
+import { ArrayUtil } from "../utils/ArrayUtil";
+
+export class WebSocketAdaptor {
+ public static async upgrade(
+ app: INestApplication,
+ ): Promise {
+ return new this(app, await visitApplication(app));
+ }
+
+ public readonly close = async (): Promise =>
+ new Promise((resolve) => {
+ this.http.off("close", this.close);
+ this.http.off("upgrade", this.handleUpgrade);
+ this.ws.close(() => resolve());
+ });
+
+ protected constructor(app: INestApplication, operations: IOperator[]) {
+ this.operators = operations;
+ this.ws = new WebSocket.Server({ noServer: true });
+ this.http = app.getHttpServer();
+ this.http.on("close", this.close);
+ this.http.on("upgrade", this.handleUpgrade);
+ }
+
+ private readonly handleUpgrade = (
+ request: IncomingMessage,
+ duplex: Duplex,
+ head: Buffer,
+ ) => {
+ this.ws.handleUpgrade(request, duplex, head, (client, request) =>
+ WebAcceptor.upgrade(
+ request,
+ client as any,
+ async (acceptor): Promise => {
+ for (const op of this.operators) {
+ const params: Record | null = op.parser.test(
+ acceptor.path,
+ );
+ if (params !== null)
+ try {
+ await op.handler({ params, acceptor });
+ } catch (error) {
+ if (
+ acceptor.state === WebAcceptor.State.OPEN ||
+ acceptor.state === WebAcceptor.State.ACCEPTING
+ )
+ await acceptor.reject(
+ 1008,
+ error instanceof Error
+ ? JSON.stringify({ ...error })
+ : "unknown error",
+ );
+ } finally {
+ return;
+ }
+ }
+ await acceptor.reject(1002, `Cannot GET ${acceptor.path}`);
+ },
+ ),
+ );
+ };
+
+ private readonly http: Server;
+ private readonly operators: IOperator[];
+ private readonly ws: WebSocket.Server;
+}
+
+const visitApplication = async (
+ app: INestApplication,
+): Promise => {
+ const operators: IOperator[] = [];
+ const errors: IControllerError[] = [];
+
+ const config: IConfig = {
+ globalPrefix:
+ typeof (app as any).config?.globalPrefix === "string"
+ ? (app as any).config.globalPrefix
+ : undefined,
+ versioning: (() => {
+ const versioning = (app as any).config?.versioningOptions;
+ return versioning === undefined || versioning.type !== VersioningType.URI
+ ? undefined
+ : {
+ prefix:
+ versioning.prefix === undefined || versioning.prefix === false
+ ? "v"
+ : versioning.prefix,
+ defaultVersion: versioning.defaultVersion,
+ };
+ })(),
+ };
+ const container: NestContainer = (app as any).container as NestContainer;
+ const modules: Module[] = [...container.getModules().values()].filter(
+ (m) => !!m.controllers?.size,
+ );
+ for (const m of modules) {
+ const modulePrefix: string =
+ Reflect.getMetadata(
+ MODULE_PATH + container.getModules().applicationId,
+ m.metatype,
+ ) ??
+ Reflect.getMetadata(MODULE_PATH, m.metatype) ??
+ "";
+ for (const controller of m.controllers.values())
+ visitController({
+ config,
+ errors,
+ operators,
+ controller,
+ modulePrefix,
+ });
+ }
+ if (errors.length) {
+ throw new Error(
+ [
+ `WebSocketAdaptor: ${errors.length} error(s) found:`,
+ ``,
+ ...errors.map((e) =>
+ [
+ ` - controller: ${e.name}`,
+ ` - methods:`,
+ ...e.methods.map((m) =>
+ [
+ ` - name: ${m.name}`,
+ ` - file: ${m.source}:${m.line}:${m.column}`,
+ ` - reasons:`,
+ ...m.messages.map(
+ (msg) =>
+ ` - ${msg
+ .split("\n")
+ .map((str) => ` ${str}`)
+ .join("\n")}`,
+ ),
+ ]
+ .map((str) => ` ${str}`)
+ .join("\n"),
+ ),
+ ]
+ .map((str) => ` ${str}`)
+ .join("\n"),
+ ),
+ ].join("\n"),
+ );
+ }
+ return operators;
+};
+
+const visitController = async (props: {
+ config: IConfig;
+ errors: IControllerError[];
+ operators: IOperator[];
+ controller: InstanceWrapper