define(["require", "exports", "@essential-projects/errors_ts", "debug"], function (require, exports, ProcessEngineErrors, debug) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    const debugInfo = debug('processengine:info');
    const debugErr = debug('processengine:error');
    class ProcessEngineService {
        constructor(messageBusService, eventAggregator, processDefEntityTypeService, executeProcessService, featureService, iamService, processRepository, datastoreService, nodeInstanceEntityTypeServiceFactory, applicationService, invoker) {
            this._messageBusService = undefined;
            this._eventAggregator = undefined;
            this._processDefEntityTypeService = undefined;
            this._executeProcessService = undefined;
            this._featureService = undefined;
            this._iamService = undefined;
            this._processRepository = undefined;
            this._datastoreService = undefined;
            this._nodeInstanceEntityTypeServiceFactory = undefined;
            this._nodeInstanceEntityTypeService = undefined;
            this._applicationService = undefined;
            this._invoker = undefined;
            this._errorDeserializer = undefined;
            this.config = undefined;
            this._messageBusService = messageBusService;
            this._eventAggregator = eventAggregator;
            this._processDefEntityTypeService = processDefEntityTypeService;
            this._executeProcessService = executeProcessService;
            this._featureService = featureService;
            this._iamService = iamService;
            this._processRepository = processRepository;
            this._datastoreService = datastoreService;
            this._nodeInstanceEntityTypeServiceFactory = nodeInstanceEntityTypeServiceFactory;
            this._applicationService = applicationService;
            this._invoker = invoker;
        }
        get messageBusService() {
            return this._messageBusService;
        }
        get eventAggregator() {
            return this._eventAggregator;
        }
        get processDefEntityTypeService() {
            return this._processDefEntityTypeService;
        }
        get featureService() {
            return this._featureService;
        }
        get iamService() {
            return this._iamService;
        }
        get processRepository() {
            return this._processRepository;
        }
        get datastoreService() {
            return this._datastoreService;
        }
        async _getNodeInstanceEntityTypeService() {
            if (!this._nodeInstanceEntityTypeService) {
                this._nodeInstanceEntityTypeService = await this._nodeInstanceEntityTypeServiceFactory();
            }
            return this._nodeInstanceEntityTypeService;
        }
        get applicationService() {
            return this._applicationService;
        }
        get invoker() {
            return this._invoker;
        }
        get errorDeserializer() {
            return this._errorDeserializer;
        }
        async _getInternalContext() {
            if (!this._internalContext) {
                this._internalContext = await this.iamService.createInternalContext('processengine_system');
            }
            return this._internalContext;
        }
        _initializeDefaultErrorDeserializer() {
            const defaultDeserializer = (serializedError) => {
                if (typeof serializedError !== 'string') {
                    return serializedError;
                }
                try {
                    return ProcessEngineErrors.BaseError.deserialize(serializedError);
                }
                catch (error) {
                    debugErr('an error occured deserializing this error: ', serializedError);
                    throw new Error('an error occured during error deserialization');
                }
            };
            this.setErrorDeserializer(defaultDeserializer);
        }
        async initialize() {
            this._initializeDefaultErrorDeserializer();
            await this._initializeMessageBus();
            await this._initializeProcesses();
            await this._startTimers();
            this._continueOwnProcesses();
        }
        async start(context, params, options) {
            const processEntity = await this.processDefEntityTypeService.start(context, params, options);
            return processEntity.id;
        }
        async getUserTaskData(context, userTaskId) {
            const nodeInstanceEntityTypeService = await this._getNodeInstanceEntityTypeService();
            const userTaskEntityQueryOptions = {
                expandEntity: [{
                        attribute: 'nodeDef',
                        childAttributes: [{
                                attribute: 'lane',
                            }, {
                                attribute: 'extensions',
                            }],
                    }, {
                        attribute: 'processToken',
                    }],
            };
            const userTaskEntityType = await this.datastoreService.getEntityType('UserTask');
            const userTask = await userTaskEntityType.getById(userTaskId, context, userTaskEntityQueryOptions);
            return userTask.getUserTaskData(context);
        }
        async createBpmnFromXml(context, xml) {
            const bpmnDiagram = await this.processDefEntityTypeService.parseBpmnXml(xml);
            const processDef = await this.datastoreService.getEntityType('ProcessDef');
            const processes = bpmnDiagram.getProcesses();
            if (processes.length === 0) {
                throw new Error('Model must contain a process');
            }
            await this.processDefEntityTypeService.importBpmnFromXml(context, { xml: xml });
            const queryObject = {
                query: {
                    attribute: 'key',
                    operator: '=',
                    value: processes[0].id,
                },
            };
            const processDefEntity = await processDef.findOne(context, queryObject);
            return processDefEntity.toPojo(context);
        }
        async _messageHandler(msg) {
            debugInfo('we got a message: ', msg);
            await this.messageBusService.verifyMessage(msg);
            const action = (msg && msg.data && msg.data.action) ? msg.data.action : null;
            const key = (msg && msg.data && msg.data.key) ? msg.data.key : null;
            const initialToken = (msg && msg.data && msg.data.token) ? msg.data.token : null;
            let source = (msg && msg.metadata && msg.metadata.applicationId) ? msg.metadata.applicationId : null;
            const participant = (msg && msg.metadata && msg.metadata.options && msg.metadata.options.participantId) ? msg.metadata.options.participantId : null;
            if (!source) {
                source = (msg && msg.origin && msg.origin.id) ? msg.origin.id : null;
            }
            const isSubProcess = (msg && msg.data && msg.data.isSubProcess) ? msg.data.isSubProcess : false;
            const context = (msg && msg.metadata && msg.metadata.context) ? msg.metadata.context : {};
            switch (action) {
                case 'start':
                    const params = {
                        key: key,
                        initialToken: initialToken,
                        source: source,
                        isSubProcess: isSubProcess,
                        participant: participant,
                    };
                    await this.processDefEntityTypeService.start(context, params);
                    break;
                default:
                    debugInfo('unhandled action: ', msg);
                    break;
            }
        }
        async handleProcessEngineMessage(message) {
            if (message.data.event === 'executeProcess') {
                const responseChannel = message.metadata.response;
                await this.messageBusService.verifyMessage(message);
                const responseData = await this.executeProcess(message.metadata.context, message.data.id, message.data.key, message.data.initialToken, message.data.version);
                const responseMessage = this.messageBusService.createDataMessage(responseData, message.metadata.context);
                this.messageBusService.publish(responseChannel, responseMessage);
            }
            if (message.data.event === 'createProcessInstance') {
                const responseChannel = message.metadata.response;
                await this.messageBusService.verifyMessage(message);
                const responseData = await this.createProcessInstance(message.metadata.context, message.data.id, message.data.key, message.data.version);
                const responseMessage = this.messageBusService.createDataMessage(responseData, message.metadata.context);
                this.messageBusService.publish(responseChannel, responseMessage);
            }
            if (message.data.event === 'getInstanceId') {
                const responseChannel = message.metadata.response;
                const responseMessage = this.messageBusService.createDataMessage({
                    instanceId: this.applicationService.instanceId,
                }, null);
                this.messageBusService.publish(responseChannel, responseMessage);
            }
        }
        async _initializeMessageBus() {
            try {
                this.messageBusService.subscribe(`/processengine/${this.applicationService.id}`, (message) => {
                    this.handleProcessEngineMessage(message);
                });
                this.messageBusService.subscribe(`/processengine/${this.applicationService.instanceId}`, (message) => {
                    this.handleProcessEngineMessage(message);
                });
                if (this.messageBusService.isMaster) {
                    this.messageBusService.subscribe(`/processengine`, this._messageHandler.bind(this));
                    debugInfo(`subscribed on Messagebus Master`);
                }
            }
            catch (err) {
                debugErr('subscription failed on Messagebus', err.message);
                throw new Error(err.message);
            }
        }
        async _initializeProcesses() {
            const internalContext = await this.iamService.createInternalContext('processengine_system');
            const options = {
                overwriteExisting: true,
            };
            const processes = this.processRepository.getProcessesByCategory('internal');
            for (let i = 0; i < processes.length; i++) {
                const process = processes[i];
                const params = {
                    xml: process.bpmnXml,
                    internalName: process.name,
                    category: process.category,
                    module: process.module,
                    path: process.path,
                    readonly: process.readonly,
                };
                await this.processDefEntityTypeService.importBpmnFromXml(internalContext, params, options);
            }
        }
        async _startTimers() {
            const internalContext = await this.iamService.createInternalContext('processengine_system');
            const nodeDefEntityType = await this.datastoreService.getEntityType('NodeDef');
            const queryObject = {
                operator: 'and',
                queries: [
                    { attribute: 'type', operator: '=', value: 'bpmn:StartEvent' },
                    { attribute: 'eventType', operator: '=', value: 'bpmn:TimerEventDefinition' },
                ],
            };
            const startEventColl = await nodeDefEntityType.query(internalContext, { query: queryObject });
            startEventColl.each(internalContext, async (nodeDef) => {
                const processDef = await nodeDef.getProcessDef(internalContext);
                await processDef.startTimer(internalContext);
            });
        }
        async _continueOwnProcesses() {
            const [allWaitingNodes, internalContext,] = await Promise.all([
                this._getAllWaitingNodes(),
                this._getInternalContext(),
            ]);
            if (allWaitingNodes.length === 0) {
                return;
            }
            await this._waitForMessagebus();
            return Promise.all(allWaitingNodes.map((runningNode) => {
                return this._continueOwnProcess(internalContext, runningNode);
            }));
        }
        async _continueOwnProcess(context, waitingNode) {
            debugInfo(`Checking, if node ${waitingNode.id} is abandoned`);
            if (await this._nodeAlreadyBelongsToOtherProcessEngine(context, waitingNode)) {
                debugInfo(`node ${waitingNode.id} is not abandoned`);
                return;
            }
            debugInfo(`node ${waitingNode.id} is indeed abandoned. Taking over responsibility`);
            const specificEntity = await this._getSpecificEntityByNodeInstance(context, waitingNode);
            const processToContinue = await specificEntity.getProcess(context);
            await processToContinue.initializeProcess();
            const nodeInstanceEntityTypeService = await this._getNodeInstanceEntityTypeService();
            nodeInstanceEntityTypeService.subscibeToNodeChannels(specificEntity);
        }
        async _getAllWaitingNodes() {
            const [internalContext, nodeInstanceEntityType,] = await Promise.all([
                this._getInternalContext(),
                this.datastoreService.getEntityType('NodeInstance'),
            ]);
            const waitingNodesQuery = {
                query: {
                    attribute: 'state',
                    operator: '=',
                    value: 'wait',
                },
            };
            const allWaitingNodesCollection = await nodeInstanceEntityType.query(internalContext, waitingNodesQuery);
            debugInfo(`There are ${allWaitingNodesCollection.length} potentially abandoned nodeInstances`);
            const allWaitingNodes = [];
            await allWaitingNodesCollection.each(internalContext, (nodeInstance) => {
                allWaitingNodes.push(nodeInstance);
            });
            return allWaitingNodes;
        }
        async _nodeAlreadyBelongsToOtherProcessEngine(context, node) {
            const checkMessageData = {
                action: 'checkResponsibleInstance',
            };
            const checkMessage = this.messageBusService.createDataMessage(checkMessageData, context);
            try {
                await this.messageBusService.request(`/processengine/node/${node.id}`, checkMessage);
                return true;
            }
            catch (error) {
                if (error.message !== 'request timed out') {
                    throw error;
                }
            }
            return false;
        }
        async _getSpecificEntityByNodeInstance(context, nodeInstance) {
            const nodeInstanceEntityTypeService = await this._getNodeInstanceEntityTypeService();
            const specificEntityQueryOptions = {
                expandEntity: [{
                        attribute: 'nodeDef',
                        childAttributes: [{
                                attribute: 'lane',
                            }],
                    }, {
                        attribute: 'processToken',
                    }],
            };
            const specificEntityType = await nodeInstanceEntityTypeService.getEntityTypeFromBpmnType(nodeInstance.type);
            return specificEntityType.getById(nodeInstance.id, context, specificEntityQueryOptions);
        }
        _timeoutPromise(milliseconds) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve();
                }, milliseconds);
            });
        }
        async _waitForMessagebus() {
            const initSubscription = await this.messageBusService.subscribe(`/processengine/bootup`, null);
            initSubscription.dispose();
            if (this.messageBusService.isMaster) {
                debugInfo(`This instance is messagebus-master. Giving clients 15 seconds time to connect now.`);
                const defaultclientConnectTime = 15000;
                const clientConnectTime = this.config.messagebusClientConnectTime || defaultclientConnectTime;
                await this._timeoutPromise(clientConnectTime);
            }
        }
        async executeProcess(context, id, key, initialToken, version) {
            if (id === undefined && key === undefined) {
                throw new Error(`Couldn't execute process: neither id nor key of processDefinition is provided`);
            }
            let processDefinition;
            if (id !== undefined) {
                processDefinition = await this.processDefEntityTypeService.getProcessDefinitionById(context, id, version);
            }
            else {
                processDefinition = await this.processDefEntityTypeService.getProcessDefinitionByKey(context, key, version);
            }
            if (!processDefinition) {
                throw new Error(`couldn't execute process: no processDefinition with key ${key} or id ${id} was found`);
            }
            const requiredFeatures = processDefinition.features;
            const canStartProcessLocally = requiredFeatures === undefined
                || requiredFeatures.length === 0
                || this.featureService.hasFeatures(requiredFeatures);
            if (canStartProcessLocally) {
                return this._executeProcessLocally(context, processDefinition, initialToken);
            }
            return this._executeProcessRemotely(context, requiredFeatures, id, key, initialToken, version);
        }
        async executeProcessInstance(context, processInstanceId, participantId, initialToken) {
            const processEntityType = await this.datastoreService.getEntityType('Process');
            const processInstance = await processEntityType.getById(processInstanceId, context);
            const processDefinition = await processInstance.getProcessDef(context);
            const requiredFeatures = processDefinition.features;
            const canStartProcessLocally = requiredFeatures === undefined
                || requiredFeatures.length === 0
                || this.featureService.hasFeatures(requiredFeatures);
            if (canStartProcessLocally) {
                return this._executeProcessInstanceLocally(context, processInstance, participantId, initialToken);
            }
            return this._executeProcessInstanceRemotely(context, requiredFeatures, processInstanceId, participantId, initialToken);
        }
        async createProcessInstance(context, processDefId, key, version) {
            if (processDefId === undefined && key === undefined) {
                throw new Error(`Couldn't execute process: neither id nor key of processDefinition is provided`);
            }
            let processDefinition;
            if (processDefId !== undefined) {
                processDefinition = await this.processDefEntityTypeService.getProcessDefinitionById(context, processDefId, version);
            }
            else {
                processDefinition = await this.processDefEntityTypeService.getProcessDefinitionByKey(context, key, version);
            }
            if (!processDefinition) {
                throw new Error(`couldn't execute process: no processDefinition with key ${key} or id ${processDefId} was found`);
            }
            const requiredFeatures = processDefinition.features;
            const canStartProcessLocally = requiredFeatures === undefined
                || requiredFeatures.length === 0
                || this.featureService.hasFeatures(requiredFeatures);
            if (canStartProcessLocally) {
                const processInstance = await processDefinition.createProcessInstance(context);
                await processInstance.save(context);
                return processInstance.id;
            }
            return this._createProcessInstanceRemotely(context, requiredFeatures, processDefId, key, version);
        }
        setErrorDeserializer(deserializer) {
            this._errorDeserializer = deserializer;
        }
        _executeProcessLocally(context, processDefinition, initialToken) {
            return new Promise(async (resolve, reject) => {
                const processInstance = await processDefinition.createProcessInstance(context);
                const processInstanceChannel = `/processengine/process/${processInstance.id}`;
                const processEndSubscription = await this.messageBusService.subscribe(processInstanceChannel, (message) => {
                    if (message.data.event === 'error') {
                        if (!this.errorDeserializer) {
                            throw new Error('error deserializer not found.');
                        }
                        let finalError;
                        if (message.data.data.serializedError) {
                            finalError = this.errorDeserializer(message.data.data.serializedError);
                        }
                        else {
                            finalError = message.data.data.error;
                        }
                        reject(finalError);
                        processEndSubscription.cancel();
                        return;
                    }
                    if (message.data.event !== 'end') {
                        return;
                    }
                    resolve(message.data.token);
                    processEndSubscription.cancel();
                });
                await this._executeProcessService.start(context, processDefinition, processInstance);
            });
        }
        _executeProcessInstanceLocally(context, processInstance, participantId, initialToken) {
            return new Promise(async (resolve, reject) => {
                const processInstanceChannel = `/processengine/process/${processInstance.id}`;
                const processEndSubscription = await this.messageBusService.subscribe(processInstanceChannel, (message) => {
                    if (message.data.event === 'error') {
                        if (!this.errorDeserializer) {
                            throw new Error('error deserializer not found.');
                        }
                        const deserializedError = this.errorDeserializer(message.data.data);
                        reject(deserializedError);
                        processEndSubscription.cancel();
                        return;
                    }
                    if (message.data.event !== 'end') {
                        return;
                    }
                    resolve(message.data.token);
                    processEndSubscription.cancel();
                });
                await this.invoker.invoke(processInstance, 'start', undefined, context, context, {
                    initialToken: initialToken,
                    participant: participantId,
                });
            });
        }
        async _executeProcessRemotely(context, requiredFeatures, id, key, initialToken, version) {
            const possibleRemoteTargets = this.featureService.getApplicationIdsByFeatures(requiredFeatures);
            if (possibleRemoteTargets.length === 0) {
                throw new Error(`couldn't execute process: the process-engine instance doesn't have the required features to execute the process, and does not know of any other process-engine that does`);
            }
            const getInstanceIdMessage = this.messageBusService.createDataMessage({
                event: 'getInstanceId',
            }, null);
            const executeProcessMessage = this.messageBusService.createDataMessage({
                event: 'executeProcess',
                id: id,
                key: key,
                initialToken: initialToken,
                version: version,
            }, context);
            const targetApplicationChannel = `/processengine/${possibleRemoteTargets[0]}`;
            const target = await this.messageBusService.request(targetApplicationChannel, getInstanceIdMessage);
            const targetInstanceChannel = `/processengine/${target.data.instanceId}`;
            const executeProcessResponse = await this.messageBusService.request(targetInstanceChannel, executeProcessMessage);
            return executeProcessResponse.data;
        }
        async _executeProcessInstanceRemotely(context, requiredFeatures, processInstanceId, participantId, initialToken) {
            const possibleRemoteTargets = this.featureService.getApplicationIdsByFeatures(requiredFeatures);
            if (possibleRemoteTargets.length === 0) {
                throw new Error(`couldn't execute process: the process-engine instance doesn't have the required features to execute the process, and does not know of any other process-engine that does`);
            }
            const getInstanceIdMessage = this.messageBusService.createDataMessage({
                event: 'getInstanceId',
            }, null);
            const executeProcessInstanceMessage = this.messageBusService.createDataMessage({
                event: 'executeProcessInstance',
                processInstanceId: processInstanceId,
                initialToken: initialToken,
                participantId: participantId,
            }, context);
            const targetApplicationChannel = `/processengine/${possibleRemoteTargets[0]}`;
            const target = await this.messageBusService.request(targetApplicationChannel, getInstanceIdMessage);
            const targetInstanceChannel = `/processengine/${target.data.instanceId}`;
            const executeProcessInstanceResponse = await this.messageBusService.request(targetInstanceChannel, executeProcessInstanceMessage);
            return executeProcessInstanceResponse.data;
        }
        async _createProcessInstanceRemotely(context, requiredFeatures, id, key, version) {
            const possibleRemoteTargets = this.featureService.getApplicationIdsByFeatures(requiredFeatures);
            if (possibleRemoteTargets.length === 0) {
                throw new Error(`couldn't create process: the process-engine instance doesn't have the required features to execute the process, and does not know of any other process-engine that does`);
            }
            const getInstanceIdMessage = this.messageBusService.createDataMessage({
                event: 'getInstanceId',
            }, null);
            const executeProcessMessage = this.messageBusService.createDataMessage({
                event: 'createProcess',
                id: id,
                key: key,
                version: version,
            }, context);
            const targetApplicationChannel = `/processengine/${possibleRemoteTargets[0]}`;
            const target = await this.messageBusService.request(targetApplicationChannel, getInstanceIdMessage);
            const targetInstanceChannel = `/processengine/${target.data.instanceId}`;
            const createProcessResponse = await this.messageBusService.request(targetInstanceChannel, executeProcessMessage);
            return createProcessResponse.data;
        }
    }
    exports.ProcessEngineService = ProcessEngineService;
});

//# sourceMappingURL=process_engine_service.js.map
