UNPKG

@hotmeshio/hotmesh

Version:

Permanent-Memory Workflows & AI Agents

250 lines (249 loc) 9.86 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Trigger = void 0; const errors_1 = require("../../modules/errors"); const utils_1 = require("../../modules/utils"); const collator_1 = require("../collator"); const pipe_1 = require("../pipe"); const reporter_1 = require("../reporter"); const serializer_1 = require("../serializer"); const telemetry_1 = require("../telemetry"); const mapper_1 = require("../mapper"); const activity_1 = require("./activity"); class Trigger extends activity_1.Activity { constructor(config, data, metadata, hook, engine, context) { super(config, data, metadata, hook, engine, context); } async process(options) { this.logger.debug('trigger-process', { subscribes: this.config.subscribes, }); let telemetry; try { this.setLeg(2); await this.getState(); telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context); telemetry.startJobSpan(); telemetry.startActivitySpan(this.leg); this.mapJobData(); this.adjacencyList = await this.filterAdjacent(); const initialStatus = this.initStatus(options, this.adjacencyList.length); //config.entity is a pipe expression; if 'entity' exists, it will resolve const resolvedEntity = new mapper_1.MapperService({ entity: this.config.entity }, this.context).mapRules()?.entity; await this.setStateNX(initialStatus, options?.entity || resolvedEntity); await this.setStatus(initialStatus); this.bindSearchData(options); this.bindMarkerData(options); const transaction = this.store.transact(); await this.setState(transaction); await this.setStats(transaction); if (options?.pending) { await this.setExpired(options?.pending, transaction); } await collator_1.CollatorService.notarizeInception(this, this.context.metadata.guid, transaction); await transaction.exec(); this.execAdjacentParent(); telemetry.mapActivityAttributes(); const jobStatus = Number(this.context.metadata.js); telemetry.setJobAttributes({ 'app.job.jss': jobStatus }); const attrs = { 'app.job.jss': jobStatus }; await this.transitionAndLogAdjacent(options, jobStatus, attrs); telemetry.setActivityAttributes(attrs); return this.context.metadata.jid; } catch (error) { telemetry?.setActivityError(error.message); if (error instanceof errors_1.DuplicateJobError) { //todo: verify baseline in x-AZ rollover await (0, utils_1.sleepFor)(1000); const isOverage = await collator_1.CollatorService.isInceptionOverage(this, this.context.metadata.guid); if (isOverage) { this.logger.info('trigger-collation-overage', { job_id: error.jobId, guid: this.context.metadata.guid, }); return; } this.logger.error('duplicate-job-error', { job_id: error.jobId, guid: this.context.metadata.guid, }); } else { this.logger.error('trigger-process-error', { error }); } throw error; } finally { telemetry?.endJobSpan(); telemetry?.endActivitySpan(); this.logger.debug('trigger-process-end', { subscribes: this.config.subscribes, jid: this.context.metadata.jid, gid: this.context.metadata.gid, }); } } async transitionAndLogAdjacent(options = {}, jobStatus, attrs) { //todo: enable resume from pending state if (isNaN(options.pending)) { const messageIds = await this.transition(this.adjacencyList, jobStatus); if (messageIds.length) { attrs['app.activity.mids'] = messageIds.join(','); } } } /** * `pending` flows will not transition from the trigger to adjacent children until resumed */ initStatus(options = {}, count) { if (options.pending) { return -1; } return count; } async setExpired(seconds, transaction) { await this.store.expireJob(this.context.metadata.jid, seconds, transaction); } safeKey(key) { return `_${key}`; } bindSearchData(options) { if (options?.search) { Object.keys(options.search).forEach((key) => { this.context.data[this.safeKey(key)] = options.search[key].toString(); }); } } bindMarkerData(options) { if (options?.marker) { Object.keys(options.marker).forEach((key) => { if (key.startsWith('-')) { this.context.data[key] = options.marker[key].toString(); } }); } } async setStatus(amount) { this.context.metadata.js = amount; } /** * if the parent (spawner) chose not to await, emit the job_id * as the data payload { job_id } */ async execAdjacentParent() { if (this.context.metadata.px) { const timestamp = (0, utils_1.formatISODate)(new Date()); const jobStartedConfirmationMessage = { metadata: this.context.metadata, data: { job_id: this.context.metadata.jid, jc: timestamp, ju: timestamp, }, }; await this.engine.execAdjacentParent(this.context, jobStartedConfirmationMessage); } } createInputContext() { const input = { [this.metadata.aid]: { input: { data: this.data }, }, $self: { input: { data: this.data }, output: { data: this.data }, }, }; return input; } async getState() { const inputContext = this.createInputContext(); const jobId = this.resolveJobId(inputContext); const jobKey = this.resolveJobKey(inputContext); const utc = (0, utils_1.formatISODate)(new Date()); const { id, version } = await this.engine.getVID(); this.initDimensionalAddress(collator_1.CollatorService.getDimensionalSeed()); const activityMetadata = { ...this.metadata, jid: jobId, key: jobKey, as: collator_1.CollatorService.getTriggerSeed(), }; this.context = { metadata: { ...this.metadata, gid: (0, utils_1.guid)(), ngn: this.context.metadata.ngn, pj: this.context.metadata.pj, pg: this.context.metadata.pg, pd: this.context.metadata.pd, pa: this.context.metadata.pa, px: this.context.metadata.px, app: id, vrs: version, tpc: this.config.subscribes, trc: this.context.metadata.trc, spn: this.context.metadata.spn, guid: this.context.metadata.guid, jid: jobId, dad: collator_1.CollatorService.getDimensionalSeed(), key: jobKey, jc: utc, ju: utc, ts: (0, utils_1.getTimeSeries)(this.resolveGranularity()), js: 0, }, data: {}, [this.metadata.aid]: { input: { data: this.data, metadata: activityMetadata, }, output: { data: this.data, metadata: activityMetadata, }, settings: { data: {} }, errors: { data: {} }, }, }; this.context['$self'] = this.context[this.metadata.aid]; this.context['$job'] = this.context; //NEVER call STRINGIFY! (circular) } bindJobMetadataPaths() { return serializer_1.MDATA_SYMBOLS.JOB.KEYS.map((key) => `metadata/${key}`); } bindActivityMetadataPaths() { return serializer_1.MDATA_SYMBOLS.ACTIVITY.KEYS.map((key) => `output/metadata/${key}`); } resolveGranularity() { return (this.config.stats?.granularity || reporter_1.ReporterService.DEFAULT_GRANULARITY); } getJobStatus() { return this.context.metadata.js; } resolveJobId(context) { const jobId = this.config.stats?.id; return jobId ? pipe_1.Pipe.resolve(jobId, context) : (0, utils_1.guid)(); } resolveJobKey(context) { const jobKey = this.config.stats?.key; return jobKey ? pipe_1.Pipe.resolve(jobKey, context) : ''; } async setStateNX(status, entity) { const jobId = this.context.metadata.jid; if (!await this.store.setStateNX(jobId, this.engine.appId, status, entity)) { throw new errors_1.DuplicateJobError(jobId); } } async setStats(transaction) { const md = this.context.metadata; if (md.key && this.config.stats?.measures) { const config = await this.engine.getVID(); const reporter = new reporter_1.ReporterService(config, this.store, this.logger); await this.store.setStats(md.key, md.jid, md.ts, reporter.resolveTriggerStatistics(this.config, this.context), config, transaction); } } } exports.Trigger = Trigger;