@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
250 lines (249 loc) • 9.86 kB
JavaScript
"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;