tgrid
Version:
Grid Computing Framework for TypeScript
308 lines (280 loc) • 9.66 kB
text/typescript
import type http from "http";
import { sleep_for } from "tstl";
import type WebSocket from "ws";
import { Invoke } from "../../components/Invoke";
import { AcceptorBase } from "../internal/AcceptorBase";
import { IHeaderWrapper } from "../internal/IHeaderWrapper";
import { WebSocketError } from "./WebSocketError";
import { IWebSocketCommunicator } from "./internal/IWebSocketCommunicator";
/**
* Web Socket Acceptor.
*
* - available only in the NodeJS.
*
* The `WebSocketAcceptor` is a communicator class interacting with the remote
* {@link WebSocketConnector websocket client} through RPC (Remote Procedure Call),
* created by the {@link WebSocketServer} class whenever a remote client
* connects to the websocket server.
*
* When a remote client connects to the {@link WebSocketServer websocket server},
* so that a new `WebSocketAcceptor` instance being created, you can determine
* whether to {@link accept} the client's connection or {@link reject not},
* reading the {@link header} and {@link path} properties. If you've decided to
* accept the connection, call the {@link accept} method with `Provider` instance.
* Otherwise, reject it through the {@link reject} method.
*
* After {@link accept accepting} the connection, don't forget to
* {@link close closing} the connection after your business 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 {@link join} method.
*
* Also, when declaring this {@link WebSocketAcceptor} type, you have to define three
* generic arguments; `Header`, `Provider` and `Remote`. Those generic arguments must
* be same with the ones defined in the {@link WebSocketServer} class.
*
* For reference, the first `Header` type represents 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.
*
* @template Header Type of the header containing initial data.
* @template Provider Type of features provided for the remote client.
* @template Remote Type of features provided by remote client.
* @author Jeongho Nam - https://github.com/samchon
*/
export class WebSocketAcceptor<
Header,
Provider extends object | null,
Remote extends object | null,
>
extends AcceptorBase<Header, Provider, Remote>
implements IWebSocketCommunicator
{
/**
* @hidden
*/
private request_: http.IncomingMessage;
/**
* @hidden
*/
private socket_: WebSocket;
/* ----------------------------------------------------------------
CONSTRUCTORS
---------------------------------------------------------------- */
/**
* Upgrade to WebSocket protocol.
*
* If you've not opened websocket server from {@link WebSocketServer}, you can
* still compose the `WebSocketAcceptor` instance by yourself, by upgrading
* the HTTP connection to the websocket protocol.
*
* For reference, this `upgrade()` method is useful when you're planning to
* make a server supporting both HTTP and WebSocket protocols, and
* distinguishing the protocol by the path of URL.
*
* - ex) [NestJS `@WebSocketRoute()` case](https://nestia.io/docs/core/WebSocketRoute/)
*
* @param request HTTP incoming message.
* @param socket WebSocket instance
* @param handler A callback function after the connection has been established.
*/
public static upgrade<
Header,
Provider extends object | null,
Remote extends object | null,
>(
request: http.IncomingMessage,
socket: WebSocket,
handler?: (
acceptor: WebSocketAcceptor<Header, Provider, Remote>,
) => Promise<any>,
): void {
socket.once("message", async (data: WebSocket.Data) => {
// @todo: custom code is required
if (typeof data !== "string") socket.close();
else
try {
const wrapper: IHeaderWrapper<Header> = JSON.parse(data as string);
const acceptor: WebSocketAcceptor<Header, Provider, Remote> =
new WebSocketAcceptor(request, socket, wrapper.header);
if (handler !== undefined) await handler(acceptor);
} catch (exp) {
socket.close();
}
});
}
/**
* @hidden
*/
private constructor(
request: http.IncomingMessage,
socket: WebSocket,
header: Header,
) {
super(header);
this.request_ = request;
this.socket_ = socket;
}
/**
* @inheritDoc
*/
public async close(code?: number, reason?: string): Promise<void> {
// TEST CONDITION
const error: Error | null = this.inspectReady("close");
if (error) throw error;
//----
// CLOSE WITH JOIN
//----
// PREPARE LAZY RETURN
const ret: Promise<void> = this.join();
// DO CLOSE
this.state_ = WebSocketAcceptor.State.CLOSING;
if (code === 1000) this.socket_!.close();
else this.socket_!.close(code!, reason!);
// state would be closed in destructor() via _Handle_close()
await ret;
}
/**
* @hidden
*/
protected async destructor(error?: Error): Promise<void> {
await super.destructor(error);
this.state_ = WebSocketAcceptor.State.CLOSED;
}
/* ----------------------------------------------------------------
ACCESSORS
---------------------------------------------------------------- */
/**
* IP Address of client.
*/
public get ip(): string {
return this.request_.connection.remoteAddress!;
}
/**
* Path of client has connected.
*/
public get path(): string {
return this.request_.url!;
}
/**
* Get state.
*
* Get current state of connection state with the remote client.
*
* List of values are such like below:
*
* - `REJECTING`: The {@link WebSocketAcceptor.reject} method is on running.
* - `NONE`: The {@link WebSocketAcceptor} instance is newly created, but did nothing yet.
* - `ACCEPTING`: The {@link WebSocketAcceptor.accept} method is on running.
* - `OPEN`: The connection is online.
* - `CLOSING`: The {@link WebSocketAcceptor.close} method is on running.
* - `CLOSED`: The connection is offline.
*/
public get state(): WebSocketAcceptor.State {
return this.state_;
}
/* ----------------------------------------------------------------
HANDSHAKES
---------------------------------------------------------------- */
/**
* @inheritDoc
*/
public async accept(provider: Provider): Promise<void> {
// VALIDATION
if (this.state_ !== WebSocketAcceptor.State.NONE)
throw new Error(
"Error on WebSocketAcceptor.accept(): you've already accepted (or rejected) the connection.",
);
// PREPARE ASSETS
this.state_ = WebSocketAcceptor.State.ACCEPTING;
this.provider_ = provider;
// REGISTER EVENTS
this.socket_.on("message", this._Handle_message.bind(this));
this.socket_.on("close", this._Handle_close.bind(this));
this.socket_.send(WebSocketAcceptor.State.OPEN.toString());
// FINISHED
this.state_ = WebSocketAcceptor.State.OPEN;
}
/**
* Reject connection.
*
* Reject without acceptance, any interaction. The connection would be closed immediately.
*
* @param status Status code.
* @param reason Detailed reason to reject.
*/
public async reject(status?: number, reason?: string): Promise<void> {
// VALIDATION
if (this.state_ !== WebSocketAcceptor.State.NONE)
throw new Error(
"Error on WebSocketAcceptor.reject(): you've already accepted (or rejected) the connection.",
);
// SEND CLOSING FRAME
this.state_ = WebSocketAcceptor.State.REJECTING;
this.socket_.close(status, reason);
// FINALIZATION
await this.destructor();
}
/* ----------------------------------------------------------------
COMMUNICATOR
---------------------------------------------------------------- */
/**
* Ping to the remote client.
*
* Send a ping message to the remote client repeatedly.
*
* The ping message would be sent every internal milliseconds, until the
* connection be disconnected. The remote client will reply with a pong
* message, so that the connection would be alive until be explicitly
* disconnected.
*
* @param ms Interval milliseconds
* @throws Error when the connection is not accepted.
*/
public ping(ms: number): void {
// TEST CONDITION
const error: Error | null = this.inspectReady("close");
if (error) throw error;
(async (): Promise<void> => {
while (this.state_ === WebSocketAcceptor.State.OPEN) {
await sleep_for(ms);
try {
this.socket_.ping();
} catch {}
}
})().catch(() => {});
}
/**
* @hidden
*/
protected async sendData(invoke: Invoke): Promise<void> {
this.socket_.send(JSON.stringify(invoke));
}
/**
* @hidden
*/
private _Handle_message(data: WebSocket.Data): void {
if (typeof data === "string") {
const invoke: Invoke = JSON.parse(data);
this.replyData(invoke);
}
}
/**
* @hidden
*/
private async _Handle_close(code: number, reason: string): Promise<void> {
const error: WebSocketError | undefined =
code !== 100 ? new WebSocketError(code, reason) : undefined;
await this.destructor(error);
}
}
/**
*
*/
export namespace WebSocketAcceptor {
/**
* Current state of the {@link WebSocketAcceptor}.
*/
export import State = AcceptorBase.State;
}