UNPKG

@energyweb/node-red-contrib-green-proof-worker

Version:
188 lines (187 loc) 7.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SourceHttpApi = void 0; const zod_1 = require("zod"); const errors_1 = require("../errors"); const node_1 = require("../node"); const nodes_config_env_1 = require("../nodes-config-env"); const host_api_1 = require("../host-api"); const promises_1 = require("node:timers/promises"); const InputMessage = zod_1.z.union([ zod_1.z.object({ topic: zod_1.z.literal('finished-processing'), payload: zod_1.z.object({ httpApiMessageId: zod_1.z.string(), }) }), zod_1.z.object({ topic: zod_1.z.literal('force-reset') }) ]); const Config = zod_1.z.object({ host: zod_1.z.string(), sqliteConfig: zod_1.z.string().optional(), appId: zod_1.z.string(), }); const HttpMessage = zod_1.z.object({ id: zod_1.z.string(), content: zod_1.z.any(), }); const SourceHttpApi = (api) => class SourceHttpApi extends node_1.Node { constructor(config) { super(api, config, InputMessage); /** Used to orchestrate consumer retry */ this.isDestroying = false; this.pendingProcessing = null; const envConfig = this.getNodeEnvConfig(); this.nextMessageWaitTime = nodes_config_env_1.nodesGlobalEnvConfig.SOURCE_HTTP_API_RETRY_TIME ? Number(nodes_config_env_1.nodesGlobalEnvConfig.SOURCE_HTTP_API_RETRY_TIME) : 10000; this.config = (async () => { return Config.parse({ host: config.host || nodes_config_env_1.nodesGlobalEnvConfig.SOURCE_HTTP_API_HOST || await this.getBaseUrls().then(c => c.kafka_proxy_url), appId: config.appId || envConfig.EWX_SOLUTION_ID, }); })(); this.parsedConfig = Config.parse(config); const configNode = this.api.getNode(this.parsedConfig.sqliteConfig || ''); if (!configNode) { throw new errors_1.GGPError(errors_1.ErrorCode.SqliteConfigNotFound, {}); } this.database = configNode.database; void this.getMessageLoop(); } async getMessageLoop() { this.api.log('HTTP API Source started'); while (!this.isDestroying) { try { const result = await this.getMessage(); if (result) { const message = HttpMessage.parse(result); this.api.log(`Processing message id: ${message.id}`); this.setStatus('pending'); this .sendBuilder({}) .addPayload({ ...message.content, httpApiMessageId: message.id, }) .sendToOutput(0); await new Promise((resolve, reject) => { this.pendingProcessing = { messageId: message.id, resolve, reject, }; }); await this.ackMessage(message.id); this.api.log(`Message processed: ${message.id}`); } else { this.api.log('No message available. Waiting before next retry'); this.setStatus('waiting'); await (0, promises_1.setTimeout)(this.nextMessageWaitTime); } } catch (e) { this.api.error(e.message); this.setStatus('error'); await (0, promises_1.setTimeout)(this.nextMessageWaitTime); } } } setStatus(status) { switch (status) { case 'waiting': const waitingTime = (this.nextMessageWaitTime / 1000).toFixed(2); this.api.status({ fill: 'green', shape: 'ring', text: `Waiting ${waitingTime} seconds` }); break; case 'error': const waitingTimeError = (this.nextMessageWaitTime / 1000).toFixed(2); this.api.status({ fill: 'red', shape: 'ring', text: `Encountered error. Waiting ${waitingTimeError} seconds` }); break; case 'pending': this.api.status({ fill: 'yellow', shape: 'dot', text: `Waiting for message (offset ${this.pendingProcessing?.messageId}) to be processed` }); break; } } /** * Input is responsible for resolving promise that http api is waiting for */ onInput(message) { if (!this.pendingProcessing) { return; } if (message.topic === 'force-reset') { this.pendingProcessing.reject(new Error('HTTP API: Force resetted')); this.pendingProcessing = null; return; } if (this.pendingProcessing.messageId !== message.payload.httpApiMessageId) { throw new errors_1.GGPError(errors_1.ErrorCode.SourceKafkaUnexpectedMessage, { expected: this.pendingProcessing.messageId, received: message.payload.httpApiMessageId }); } this.pendingProcessing.resolve(); this.pendingProcessing = null; } async getMessage() { const { url, headers } = await this.buildGetMessageUrl(); const response = await fetch(url, { method: 'GET', headers }); const body = await response.text(); try { return JSON.parse(body); } catch (error) { this.api.error(`Failed parsing message with error: ${error instanceof Error ? error.message : 'unknown error'}. Message to parse: ${body}`); return null; } } async buildGetMessageUrl() { const config = await this.config; const url = new URL('/api/v2/message', config.host); url.searchParams.set('appId', this.parsedConfig.appId); const lastAckedMessageId = await this.getLastAckedMessageId(); if (lastAckedMessageId) { url.searchParams.set('lastMessageId', lastAckedMessageId); } const authToken = await host_api_1.hostApi.getAuthToken().catch((_err) => { // We are not logging the error because currently no WNS or Marketplace support auth token // and we would spam errors // this.api.error(`Failed getting auth token: ${err instanceof Error ? err.message : `unknown error: ${JSON.stringify(err)}`}`); return null; }); return { url, headers: authToken !== null ? { Authorization: `Bearer ${authToken}` } : {} }; } async getLastAckedMessageId() { const database = await this.database; const lastAcked = await database .selectFrom('acked_message') .select('messageId') .orderBy('created_at', 'desc') .limit(1) .executeTakeFirst(); return lastAcked ? lastAcked.messageId : null; } async ackMessage(messageId) { const database = await this.database; await database .insertInto('acked_message') .values({ messageId, created_at: Date.now(), }) .execute(); } onDestroy() { this.isDestroying = true; if (this.pendingProcessing) { this.pendingProcessing.reject(new Error('HTTP API: Destroying node')); } } }; exports.SourceHttpApi = SourceHttpApi;