import { CookieSerializeOptions, parse } from 'cookie'; import { DestroyMessage } from '../messages/DestroyMessage'; import { DestroyAnswerMessage } from '../messages/DestroyAnswerMessage'; import { AnswerError } from '../messages/AnswerError'; import { BinaryMessage } from '../messages/BinaryMessage'; import type { OutgoingMessage } from '../messages/OutgoingMessage'; import type { CallAnswerMessage } from '../messages/CallAnswerMessage'; import type { CreateAnswerMessage } from '../messages/CreateAnswerMessage'; import type { CallMessage } from '../messages/CallMessage'; import type { CreateMessage } from '../messages/CreateMessage'; import type { IncomingMessage } from '../messages/IncomingMessage'; import { ActionInstance } from './ActionInstance'; import { ActionBox } from './ActionBox'; import { CallController } from './CallController'; import { ActionProxyManager } from './ActionProxyManager'; import { getContext } from './utils/createActionProxy'; import { TransactionManager } from './TransactionManager'; import type { Connection } from './adapter/Connection'; import type { Server } from './Server'; import { fireOnAfterCreateInstance } from './utils/fireOnAfterCreateInstance'; import { FailMessage } from '../messages'; /** * 4. Хранилище с нэймспэйсами для контекста */ export class ConnectionController { public headers: Record = {}; public readonly activeActionProxies: ActionProxyManager; private instanceCounter = 0; private initPromise!: Promise; private transactions = new TransactionManager(); // new Map>(); // public readonly contextManager!: ContextManager; constructor(public readonly connection: Connection, public readonly server: Server) { this.activeActionProxies = new ActionProxyManager(this); // this.contextManager = new ContextManager(); } public init(): Promise { if (!this.initPromise) { // eslint-disable-next-line no-async-promise-executor this.initPromise = new Promise(async (resolve, reject) => { try { await this.validateHeaders(); this.connection.setConnectionController(this); await this.activeActionProxies.init(); await this.onConnect(); resolve(); } catch (e) { reject(e); } }); } return this.initPromise; } public async onMessage(message: IncomingMessage) { await this.init(); let answer: OutgoingMessage; this.transactions.beginTransaction(message); try { // eslint-disable-next-line default-case switch (message?.type) { case 'call': answer = await this.call(message); break; case 'destroy': answer = await this.destroy(message); break; case 'create': answer = await this.create(message); break; default: throw new Error('Bad incoming messages.'); } } catch (e) { answer = { transactionId: message.transactionId, type: 'fail', success: false, result: e, } as FailMessage; } this.transactions.endTransaction(message); await this.reply(answer); } public onBinaryMessage(binaryMessage: BinaryMessage) { this.transactions.addChunk(binaryMessage); //todo может нужна какая система приостановки клиента на случай если ебланит обработка... // todo в случае успеха можно отправлять информацию о прогрессе получения... } public async onClose() { await this.activeActionProxies.onClose(); await this.server.config.onClientDisconnect?.(this); this.server.getServerController().onClientDisconnected(this); } public async send(message: OutgoingMessage) { await this.connection.send(message); } public getCookies(): Record { const { cookie } = this.headers; if (!cookie) { return {}; } try { return parse(cookie); } catch (e: any) { console.error(e); return {}; } } public async sendSetCookieMessage(actionBox: ActionBox, ticket: string): Promise { await this.send({ type: 'set:cookie', actionName: actionBox.name, ticket, }); } public getActionInstance(actionName: string, instanceId = 'default'): A | undefined { return this.activeActionProxies.getActiveActionProxyInstance({actionName, instanceId}); } protected async call(message: CallMessage): Promise { const actionBox = this.server.getActionBox(message.actionName); const actionInstance = await this.activeActionProxies.getActionProxy(message); const exec = new CallController(message, actionBox); exec.setActionProxy(actionInstance); return exec.call(); } protected async create(message: CreateMessage): Promise { const actionBox = this.server.getActionBox(message.actionName); const actionName = actionBox.name; if (!actionBox.config.permanent) { throw new Error(`The action "${actionBox.name}" must be permanent.`); } let instanceId: string; let proxy: ActionInstance; if (actionBox.config?.momentOfInit === 'connect') { instanceId = 'default'; proxy = await this.activeActionProxies.getActiveActionProxyInstance({actionName, instanceId}); } else { this.instanceCounter += 1; instanceId = `srv-inst-${this.instanceCounter}`; proxy = await this.activeActionProxies.createActionProxy(actionBox, instanceId); } const context = getContext(proxy); await context.setIncomingContext(message.context, true); await fireOnAfterCreateInstance(proxy); return { transactionId: message.transactionId, type: 'create:answer', context: await context.getOutgoingContext(), meta: actionBox.config.sendMetaOnCreate ? actionBox.config as any : undefined, success: true, result: instanceId, }; } protected async destroy(message: DestroyMessage): Promise { const instance = this.activeActionProxies.getActiveActionProxyInstance(message); if (instance) { this.activeActionProxies.removeActionProxy(message); const context = getContext(instance); await context.onDestroy(); return { transactionId: message.transactionId, type: 'destroy:answer', success: true, result: true, }; } throw new Error('Instance not found.'); } protected async onConnect() { await this.server.config.onClientConnect?.(this); } protected async validateHeaders() { const { headerSchema, headerValidator } = this.server.config; if (headerSchema) { try { this.headers = await headerSchema.parseAsync(this.connection.headers); } catch (e) { throw new AnswerError('HeadersSchemaError', e, 'Headers do not match the declared scheme.'); } } else { this.headers = this.connection.headers; } try { await headerValidator?.(this); } catch (e) { throw new AnswerError('HeadersInvalid', e); } } protected async reply(answer: OutgoingMessage) { if (answer.result instanceof Error) { const err = answer.result; // eslint-disable-next-line no-param-reassign answer.result = { ...err, name: err.name, message: err.message, stack: err.stack, }; } await this.connection.reply(answer); } }