@energyweb/node-red-contrib-green-proof-worker
Version:
188 lines (187 loc) • 7.52 kB
JavaScript
"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;