import type { IncomingMessage } from "node:http" import type { RawData, WebSocket } from "ws" import type { BuildEvents } from "#Source/event/index.ts" import { EventManager } from "#Source/event/index.ts" import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts" import { Logger } from "#Source/log/index.ts" import type { SocketUnitBaseSnapshot, SocketUnitLifecycleState, SocketUnitHeartbeatOptions, SocketUnitHeartbeatSnapshot, } from "../common/index.ts" import { SocketUnitHeartbeat } from "../common/index.ts" /** * @description 表示服务端 SocketUnit 的传输状态。 */ export type SocketUnitStatus = "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED" /** * @description 表示服务端 SocketUnit 对外派发的事件表。 */ export type SocketUnitEvents = BuildEvents<{ status: (status: SocketUnitStatus) => void message: (message: Message) => void }> /** * @description 表示服务端 SocketUnit 的诊断快照。 */ export interface SocketUnitSnapshot extends SocketUnitBaseSnapshot< string, SocketUnitStatus, SocketUnitHeartbeatSnapshot > { adapterState: { kind: "server" isStarted: boolean isClosed: boolean } } interface Action { action: () => void } /** * @description 表示服务端初始消息的构造器。 */ export type InitialMessageBuilder = (clientId: string) => Message /** * @description 表示服务端 SocketUnit 向外转发业务消息时的负载。 */ export interface SocketUnitMessage { clientId: string message: Message } /** * @description 表示服务端 SocketUnit 的构造参数。 */ export interface SocketUnitOptions extends LoggerFriendlyOptions { clientId: string webSocket: WebSocket incomingMessage: IncomingMessage /** * @description 是否启用初始消息。 * * 初始消息遵循 ready 语义: * 只有在 SocketUnit 已具备稳定发送条件后才会发送。 * * @default false */ enableInitialMessage?: boolean | undefined initialMessageBuilder?: InitialMessageBuilder | undefined heartbeat?: SocketUnitHeartbeatOptions | undefined onMessage: (message: SocketUnitMessage) => void onClose: (clientId: string) => void } /** * @description 验证服务端 SocketUnit 配置是否合法。 */ export const validateSocketUnitOptions = (options: SocketUnitOptions): void => { if (options.enableInitialMessage === true && options.initialMessageBuilder === undefined) { throw new Error("Initialize message builder is required when enable initial message.") } } /** * @description 表示补齐默认值后的服务端 SocketUnit 配置。 */ export interface ResolvedSocketUnitOptions extends LoggerFriendlyOptions { clientId: string webSocket: WebSocket incomingMessage: IncomingMessage enableInitialMessage: boolean initialMessageBuilder?: InitialMessageBuilder | undefined heartbeat: SocketUnitHeartbeatOptions onMessage: (message: SocketUnitMessage) => void onClose: (clientId: string) => void } /** * @description 为服务端 SocketUnit 配置补齐默认值。 */ export const resolveSocketUnitOptions = ( options: SocketUnitOptions, ): ResolvedSocketUnitOptions => { return { ...options, clientId: options.clientId, webSocket: options.webSocket, incomingMessage: options.incomingMessage, enableInitialMessage: options.enableInitialMessage ?? false, initialMessageBuilder: options.initialMessageBuilder, heartbeat: options.heartbeat ?? {}, onMessage: options.onMessage, onClose: options.onClose, } } export class SocketUnit implements LoggerFriendly { protected readonly options: ResolvedSocketUnitOptions readonly logger: Logger protected readonly heartbeat: SocketUnitHeartbeat protected isStarted: boolean protected isClosed: boolean protected actionQueue: Action[] eventManager: EventManager> constructor(options: SocketUnitOptions) { validateSocketUnitOptions(options) this.options = resolveSocketUnitOptions(options) this.logger = Logger.fromOptions(options).setDefaultName("SocketUnit") this.heartbeat = new SocketUnitHeartbeat( { ...this.options.heartbeat, logger: this.logger.derive().setName("SocketUnitHeartbeat"), }, { sendMessage: (message) => { return this.safeSendMessage(message) }, close: () => { this.options.webSocket.close() }, }, ) this.isStarted = false this.isClosed = false this.actionQueue = [] this.eventManager = new EventManager>() } /** * @description 启动当前服务端 SocketUnit 的运行时监听。 */ start(): void { if (this.isStarted === true) { throw new Error("SocketUnit has already started.") } this.options.webSocket.on("close", this.handleClose) this.options.webSocket.on("error", this.handleError) this.options.webSocket.on("message", this.handleMessage) this.isStarted = true this.eventManager.emit("status", this.getStatus()) this.triggerReadyToWork() } /** * @description 关闭当前服务端 SocketUnit。 */ async close(): Promise { if (this.isClosed === true) { return } const webSocket = this.options.webSocket const closedState = webSocket.CLOSED const closingState = webSocket.CLOSING if (webSocket.readyState === closedState) { this.finalizeClose(undefined, Buffer.alloc(0)) return } await new Promise((resolve) => { const handleClose = (): void => { webSocket.off("close", handleClose) resolve() } webSocket.on("close", handleClose) if (webSocket.readyState !== closingState) { webSocket.close() } }) } /** * @description 返回当前服务端 SocketUnit 的快照。 */ getSnapshot(): SocketUnitSnapshot { const heartbeatSnapshot: SocketUnitHeartbeatSnapshot = this.heartbeat.getSnapshot() return { status: this.getStatus(), clientId: this.options.clientId, lifecycleState: this.getLifecycleState(), isReadyToWork: this.isReadyToWork(), hasTransport: true, pendingActionCount: this.actionQueue.length, transportOwnership: "INJECTED", heartbeat: heartbeatSnapshot, adapterState: { kind: "server", isStarted: this.isStarted, isClosed: this.isClosed, }, } } /** * @description 发送一条业务消息;若尚未 ready,则先进入待发送队列。 */ sendMessage(message: Message): void { if (this.isClosed === true) { this.logger.warn( `Business message send skipped because SocketUnit is closed: ${this.options.clientId}`, ) return } this.pushToQueueOrExecute({ action: (): void => { this.safeSendMessage({ action: "Business message send", messageString: JSON.stringify(message), }) }, }) } protected getStatus(): SocketUnitStatus { const readyStateToStatusMap: Record = { [this.options.webSocket.CONNECTING]: "CONNECTING", [this.options.webSocket.OPEN]: "OPEN", [this.options.webSocket.CLOSING]: "CLOSING", [this.options.webSocket.CLOSED]: "CLOSED", } const status = readyStateToStatusMap[this.options.webSocket.readyState] if (status === undefined) { throw new Error(`Unexpected readyState: ${this.options.webSocket.readyState}`) } return status } protected getLifecycleState(): SocketUnitLifecycleState { if (this.isClosed === true) { return "CLOSED" } if (this.isStarted === true) { return "RUNNING" } return "IDLE" } protected isReadyToWork(): boolean { return ( this.isStarted === true && this.options.webSocket.readyState === this.options.webSocket.OPEN ) } protected pushToQueueOrExecute(action: Action): void { if (this.isReadyToWork() === true) { action.action() return } this.actionQueue.push(action) } protected executeActionQueue(): void { const pendingActions = [...this.actionQueue] this.actionQueue = [] pendingActions.forEach((action) => { action.action() }) } protected clearPendingActions(): void { this.actionQueue = [] } protected triggerReadyToWork(): void { if (this.isReadyToWork() === false) { return } this.heartbeat.start(this.options.clientId) this.sendInitialMessageIfEnabled() this.executeActionQueue() } protected sendInitialMessageIfEnabled(): void { if (this.options.enableInitialMessage === false) { return } const initialMessage = this.options.initialMessageBuilder!(this.options.clientId) const initialMessageString = JSON.stringify(initialMessage) const sendResult = this.safeSendMessage({ action: "Initial message send", messageString: initialMessageString, }) if (sendResult === true) { this.logger.log(`Initial message sent: ${this.options.clientId}, ${initialMessageString}`) } } protected parseMessage(data: RawData): Message | undefined { try { const messageString = data.toString() const parsedMessage = JSON.parse(messageString) as Message return parsedMessage } catch (exception) { this.logger.error("Error when handling message:", exception) return undefined } } protected handleParsedMessage(parsedMessage: Message): void { const heartbeatHandleResult = this.heartbeat.handleMessage(this.options.clientId, parsedMessage) if (heartbeatHandleResult === true) { return } this.handleBusinessMessage(parsedMessage) } protected handleBusinessMessage(message: Message): void { this.eventManager.emit("message", message) this.options.onMessage({ clientId: this.options.clientId, message, }) } protected readonly handleMessage = (message: RawData): void => { const parsedMessage = this.parseMessage(message) if (parsedMessage === undefined) { return } this.handleParsedMessage(parsedMessage) } protected readonly handleClose = (code: number, reason: Buffer): void => { this.finalizeClose(code, reason) } protected readonly handleError = (error: Error): void => { this.logger.error(`WebSocket error: ${this.options.clientId}`, error) const webSocket = this.options.webSocket if (webSocket.readyState === webSocket.CLOSED) { this.finalizeClose(undefined, Buffer.alloc(0)) return } if (webSocket.readyState !== webSocket.CLOSING) { webSocket.close() } } protected finalizeClose(code: number | undefined, reason: Buffer): void { if (this.isClosed === true) { return } this.isClosed = true this.logger.log(`WebSocket closed: ${this.options.clientId} ${code} ${reason.toString("utf8")}`) this.dispose() this.eventManager.emit("status", this.getStatus()) this.options.onClose(this.options.clientId) this.logger.log(`WebSocket closed, related resources collected: ${this.options.clientId}`) } protected dispose(): void { this.clearPendingActions() if (this.isStarted === false) { return } this.options.webSocket.off("close", this.handleClose) this.options.webSocket.off("error", this.handleError) this.options.webSocket.off("message", this.handleMessage) this.heartbeat.stop() this.isStarted = false } protected safeSendMessage(message: { action: string; messageString: string }): boolean { if (this.options.webSocket.readyState !== this.options.webSocket.OPEN) { this.logger.warn( `${message.action} skipped because WebSocket is not open: ${this.options.clientId}`, ) return false } try { this.options.webSocket.send(message.messageString, (error) => { if (error !== undefined) { this.logger.error(`${message.action} failed: ${this.options.clientId}`, error) this.options.webSocket.close() } }) return true } catch (exception) { this.logger.error(`${message.action} failed: ${this.options.clientId}`, exception) this.options.webSocket.close() return false } } }