/* eslint-disable no-prototype-builtins */ import type { ActionBox } from './ActionBox'; import type { Connection } from './adapter/Connection'; import type { ConnectionController } from './ConnectionController'; import type { IncomingContext } from '../messages/IncomingContext'; import type { Server } from './Server'; import { PropertyConfig } from './PropertyConfig'; import { OutgoingContext } from '../messages/OutgoingContext'; import { SpecialProperty } from './SpecialProperty'; import { ActionInstance } from './ActionInstance'; import { AnswerError } from '../messages/AnswerError'; import { ActionConfig } from './ActionConfig'; import { PropertyConfigMap } from './PropertyConfigMap'; type ContextEachCb = (prop: string, cfg: PropertyConfig) => Promise; export class Context { public readonly connection: Connection; public readonly server: Server; public contextData: Record = {}; public operationalData: Record = {}; public specialProperties: Record = {}; public readonly staticMode: boolean; private instanceId?: string; private readonly config: ActionConfig; private readonly properties: ActionConfig['properties']; constructor( public readonly controller: ConnectionController, public readonly actionBox: ActionBox, protected readonly staticContext?: Context, ) { this.connection = controller.connection; this.server = controller.server; this.staticMode = !staticContext; this.config = actionBox.config; this.properties = actionBox.config.properties; } public has(propertyName: string): boolean { return this.actionBox.hasPropertyConfig(propertyName) || !!this.specialProperties[propertyName]; } public get(propertyName: string): any { const propertyConfig = this.actionBox.getPropertyConfig(propertyName, this.staticMode); if (propertyConfig) { if (propertyConfig.get && !this.contextData[propertyName]) { return this.getSystemItem(propertyConfig.get); } return this.contextData[propertyName]; } if (this.specialProperties[propertyName]) { return this.specialProperties[propertyName]; } return this.staticContext?.get(propertyName) || undefined; } public set(propertyName: string, value: any): boolean { const propertyConfig = this.actionBox.getPropertyConfig(propertyName, this.staticMode); if (propertyConfig) { this.contextData[propertyName] = value; return true; } return this.staticContext?.set(propertyName, value) || false; } public hasOperationalValue(propertyName: string) { return this.operationalData.hasOwnProperty(propertyName); } public setOperationalValue(propertyName: string, value: any) { const { permanent, iKnow } = this.config; if (!permanent && !iKnow) { throw new Error(`Attempting to change the ${propertyName} field in a ${this.actionBox.name} action. Action marked as non-permanent; On each call, fields not marked with the "property" decorator will contain their initial value. Add the iKnow flag to suppress this exception. @action({iKnow: true})`); } this.operationalData[propertyName] = value; return true; } public getOperationalValue(propertyName: string) { return this.operationalData[propertyName]; } public async init(): Promise { await this.staticContext?.init(); this.properties.forEach((cfg, prop) => { if (cfg.static === this.staticMode) { this.setDefaultPropValue(prop); } // if (cfg.config) { // this.cfgProperties[prop] = cfg; // } else if (cfg.in) { // this.incomingProperties[prop] = cfg; // } // if (cfg.out) { // this.outgoingProperties[prop] = cfg; // } }); } public async initSpecial(instance: ActionInstance): Promise { if (!this.config.permanent) { return; } const all: any[] = []; this.actionBox.getSpecialPropertyNames().forEach((name) => { const sp = this.createSpecial(name, instance); sp.action = instance; sp.propertyName = name; all.push(sp.init?.()); this.specialProperties[name] = sp; }); await Promise.all(all); } public createSpecial(prop: string, instance: ActionInstance): SpecialProperty { const { originalInstance } = this.actionBox; const { specialFactories } = this.server; const osp = originalInstance[prop]; const factory = specialFactories.get(osp.constructor)!; return factory(prop, instance); } public async onClose() { await this.staticContext?.onClose(); await this.onDestroy(); } public async setIncomingContext(data: IncomingContext, withCfg = false) { this.checkIncomingContext(data); await this.staticContext?.setIncomingContext(data, withCfg); await this.each(this.properties, async (prop, cfg) => { const canIncome = cfg.in || (cfg.config && withCfg); if (cfg.static === this.staticMode) { if (cfg.hooks.onInitContextValue) { this.contextData[prop] = await cfg.hooks.onInitContextValue(prop, this, data); } } if (!canIncome) { return; } let value = await this.parseProperty(false, prop, cfg, data[prop]); const set = (...v: any[]) => { if (v.length > 0) { [value] = v; } }; await cfg.hooks.onBeforeSet?.(value, prop, set, this, data); const canIgnore = (cfg.static && this.contextData.hasOwnProperty(prop)) || !cfg.required; if (!canIgnore && !data.hasOwnProperty(prop) && value === undefined) { throw new Error(`Context value "${prop}" for the action "${this.actionBox.name}" didn't come.`); } if (value !== undefined) { this.contextData[prop] = value; } }); if (!this.staticMode) { await this.onChanged(); } } public async getOutgoingContext(): Promise { const ctx: Record = {}; await this.each(this.properties, async (prop, cfg) => { if (cfg.static !== this.staticMode || !cfg.out) { return; } let value = await this.parseProperty(true, prop, cfg, this.contextData[prop]); const set = (...v: any[]) => { if (v.length > 0) { [value] = v; } }; await cfg.hooks.onBeforeSend?.(value, prop, set, this); ctx[prop] = value; }); if (this.staticContext) { const sctx = await this.staticContext.getStaticContext(); Object.assign(ctx, sctx); } return ctx; } public async getStaticContext(): Promise> { const outContext = await this.getOutgoingContext(); return outContext || {}; } public setInstanceId(instanceId: string) { if (!this.staticMode) { this.instanceId = instanceId; } } public getInstanceId(): string | undefined { return this.instanceId; } public async onDestroy() { const all: any[] = []; this.properties.forEach((cfg, prop) => { if (cfg.static === this.staticMode) { all.push(cfg.hooks.onBeforeOblivion?.(this.get(prop), prop, this)); } }); if (!this.staticMode) { Object.values(this.specialProperties) .forEach((sp) => { if (sp.onDestroy) { all.push(sp.onDestroy()); } }); } await Promise.all(all); } // todo нужно распараллеливать, но при этом как-то учесть поля которые друг от друга зависят....... protected async each(propsPack: PropertyConfigMap, handler: ContextEachCb): Promise { // eslint-disable-next-line no-restricted-syntax for (const [prop, cfg] of propsPack) { await handler(prop, cfg); } } protected checkIncomingContext(data: IncomingContext) { if (!data || typeof data !== 'object') { throw new Error('Invalid incoming context.'); } } protected setDefaultPropValue(prop: string) { const { originalInstance } = this.actionBox; const propertyConfig = this.actionBox.getPropertyConfig(prop, this.staticMode); const staticProperty = !!propertyConfig?.static; const srcDefaultValues = staticProperty ? originalInstance.constructor : originalInstance; if (!this.contextData.hasOwnProperty(prop) && srcDefaultValues.hasOwnProperty(prop)) { this.contextData[prop] = srcDefaultValues[prop]; } } protected getSystemItem(nameItem: PropertyConfig['get']): any { switch (nameItem) { case 'server': return this.server; case 'connection': return this.connection; case 'connection-controller': return this.controller; case 'adapter': return this.server.getAdapter(); case 'box': return this.actionBox; case 'socket': return this.connection.getSocket(); case 'store': return this.connection.getStore(); case 'headers': return this.controller.headers; case 'cookies': return this.controller.getCookies(); case 'set-cookies': return (ticket: string) => this.controller.sendSetCookieMessage(this.actionBox, ticket); default: return undefined; } } protected async parseProperty(out: boolean, prop: string, cfg: PropertyConfig, value: any): Promise { const schema = out ? cfg.outputSchema : cfg.schema; if (schema) { try { return await schema.parseAsync(value); } catch (e) { const type = out ? 'OutgoingPropertySchemaError' : 'IncomingPropertySchemaError'; throw new AnswerError(type, e, `${out ? 'Outgoing' : 'Incoming'} property "${prop}" do not match the declared scheme.`); } } return value; } protected async onChanged(): Promise { const all: Array | void> = []; Object.values(this.specialProperties).forEach((sp) => { if (sp.onContextChanged) { all.push(sp.onContextChanged()); } }); await Promise.all(all); } }