@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
294 lines (293 loc) • 13.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollatorService = void 0;
const errors_1 = require("../../modules/errors");
const collator_1 = require("../../types/collator");
class CollatorService {
/**
* Upon re/entry, verify that the job status is active
*/
static assertJobActive(status, jobId, activityId, threshold = 0) {
if (status <= threshold) {
throw new errors_1.InactiveJobError(jobId, status, activityId);
}
}
/**
* returns the dimensional address (dad) for the target; due
* to the nature of the notary system, the dad for leg 2 entry
* must target the `0` index while leg 2 exit must target the
* current index (0)
*/
static getDimensionalAddress(activity, isEntry = false) {
let dad = activity.context.metadata.dad || activity.metadata.dad;
if (isEntry && dad && activity.leg === 2) {
dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
}
return CollatorService.getDimensionsById([...activity.config.ancestors, activity.metadata.aid], dad);
}
/**
* resolves the dimensional address for the
* ancestor in the graph to go back to. this address
* is determined by trimming the last digits from
* the `dad` (including the target).
* the target activity index is then set to `0`, so that
* the origin node can be queried for approval/entry.
*/
static resolveReentryDimension(activity) {
const targetActivityId = activity.config.ancestor;
const ancestors = activity.config.ancestors;
const ancestorIndex = ancestors.indexOf(targetActivityId);
const dimensions = activity.metadata.dad.split(','); //e.g., `,0,0,1,0`
dimensions.length = ancestorIndex + 1;
dimensions.push('0');
return dimensions.join(',');
}
static async notarizeEntry(activity, transaction) {
//decrement by -100_000_000_000_000
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100000000000000, this.getDimensionalAddress(activity), transaction);
this.verifyInteger(amount, 1, 'enter');
return amount;
}
static async authorizeReentry(activity, transaction) {
//set second digit to 8, allowing for re-entry
//decrement by -10_000_000_000_000
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10000000000000, this.getDimensionalAddress(activity), transaction);
return amount;
}
static async notarizeEarlyExit(activity, transaction) {
//decrement the 2nd and 3rd digits to fully deactivate (`cycle` activities use this command to fully exit after leg 1) (should result in `888000000000000`)
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -11000000000000, this.getDimensionalAddress(activity), transaction);
}
static async notarizeEarlyCompletion(activity, transaction) {
//initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd
//3rd digit is optionally kept open if the activity might be used in a cycle
const decrement = activity.config.cycle
? 10000000000000
: 11000000000000;
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - decrement, this.getDimensionalAddress(activity), transaction);
}
/**
* sets the synthetic inception key (in case of an overage occurs).
*/
static async notarizeInception(activity, guid, transaction) {
if (guid) {
await activity.store.collateSynthetic(activity.context.metadata.jid, guid, 1000000, transaction);
}
}
/**
* ignore those ID collisions that are due to re-entry overages
*/
static async isInceptionOverage(activity, guid) {
if (guid) {
const amount = await activity.store.collateSynthetic(activity.context.metadata.jid, guid, 1000000);
return amount > 1000000;
}
return false;
}
/**
* verifies both the concrete and synthetic keys for the activity; concrete keys
* exist in the original model and are effectively the 'real' keys. In reality,
* hook activities are atomized during compilation to create a synthetic DAG that
* is used to track the status of the graph in a distributed environment. The
* synthetic key represents different dimensional realities and is used to
* track re-entry overages (it distinguishes between the original and re-entry).
* The essential challenge is: is this a re-entry that is purposeful in
* order to induce cycles, or is the re-entry due to a failure in the system?
*/
static async notarizeReentry(activity, guid, transaction) {
const jid = activity.context.metadata.jid;
const localMulti = transaction || activity.store.transact();
//increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
await activity.store.collate(jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity, true), localMulti);
await activity.store.collateSynthetic(jid, guid, 1000000, localMulti);
const [_amountConcrete, _amountSynthetic] = await localMulti.exec();
const amountConcrete = Array.isArray(_amountConcrete)
? _amountConcrete[1]
: _amountConcrete;
const amountSynthetic = Array.isArray(_amountSynthetic)
? _amountSynthetic[1]
: _amountSynthetic;
this.verifyInteger(amountConcrete, 2, 'enter');
this.verifySyntheticInteger(amountSynthetic);
return amountConcrete;
}
static async notarizeContinuation(activity, transaction) {
//keep open; actualize the leg2 dimension (+1)
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1, this.getDimensionalAddress(activity), transaction);
}
static async notarizeCompletion(activity, transaction) {
//1) ALWAYS actualize leg2 dimension (+1)
//2) IF the activity is used in a cycle, don't close leg 2!
const decrement = activity.config.cycle ? 0 : 1000000000000;
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), transaction);
}
static getDigitAtIndex(num, targetDigitIndex) {
const numStr = num.toString();
if (targetDigitIndex < 0 || targetDigitIndex >= numStr.length) {
return null;
}
const digit = parseInt(numStr[targetDigitIndex], 10);
return digit;
}
static getDimensionalIndex(num) {
const numStr = num.toString();
if (numStr.length < 9) {
return null;
}
const extractedStr = numStr.substring(3, 9);
const extractedInt = parseInt(extractedStr, 10);
return extractedInt - 1;
}
static isDuplicate(num, targetDigitIndex) {
return this.getDigitAtIndex(num, targetDigitIndex) < 8;
}
static isInactive(num) {
return this.getDigitAtIndex(num, 2) < 9;
}
static isPrimed(amount, leg) {
//activity entry is not allowed if paths not properly pre-set
if (leg == 1) {
return amount != -100000000000000;
}
else {
return (this.getDigitAtIndex(amount, 0) < 9 &&
this.getDigitAtIndex(amount, 1) < 9);
}
}
/**
* During compilation, the graphs are compiled into structures necessary
* for distributed processing; these are referred to as 'synthetic DAGs',
* because they are not part of the original graph, but are used to track
* the status of the graph in a distributed environment. This check ensures
* that the 'synthetic key' is not a duplicate. (which is different than
* saying the 'key' is not a duplicate)
*/
static verifySyntheticInteger(amount) {
const samount = amount.toString();
const isCompletedValue = parseInt(samount[samount.length - 1], 10);
if (isCompletedValue > 0) {
//already done error (ack/delete clearly failed; this is a duplicate)
throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.INACTIVE);
}
else if (amount >= 2000000) {
//duplicate synthetic key (this is a duplicate job ID)
throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.DUPLICATE);
}
}
static verifyInteger(amount, leg, stage) {
let faultType;
if (leg === 1 && stage === 'enter') {
if (!this.isPrimed(amount, 1)) {
faultType = collator_1.CollationFaultType.MISSING;
}
else if (this.isDuplicate(amount, 0)) {
faultType = collator_1.CollationFaultType.DUPLICATE;
}
else if (amount != 899000000000000) {
faultType = collator_1.CollationFaultType.INVALID;
}
}
else if (leg === 1 && stage === 'exit') {
if (amount === -10000000000000) {
faultType = collator_1.CollationFaultType.MISSING;
}
else if (this.isDuplicate(amount, 1)) {
faultType = collator_1.CollationFaultType.DUPLICATE;
}
}
else if (leg === 2 && stage === 'enter') {
if (!this.isPrimed(amount, 2)) {
faultType = collator_1.CollationFaultType.FORBIDDEN;
}
else if (this.isInactive(amount)) {
faultType = collator_1.CollationFaultType.INACTIVE;
}
}
if (faultType) {
throw new errors_1.CollationError(amount, leg, stage, faultType);
}
}
static getDimensionsById(ancestors, dad) {
//ancestors is an ordered list of all ancestors, starting with the trigger (['t1', 'a1', 'a2'])
//dad is the dimensional address of the ancestors list (',0,5,3')
//loop through the ancestors list and create a map of the ancestor to the dimensional address.
//return { 't1': ',0', 'a1': ',0,5', 'a1': ',0,5,3', $ADJACENT: ',0,5,3,0' };
// `adjacent` is a special key that is used to track the dimensional address of adjacent activities
const map = { $ADJACENT: `${dad},0` };
let dadStr = dad;
ancestors.reverse().forEach((ancestor) => {
map[ancestor] = dadStr;
dadStr = dadStr.substring(0, dadStr.lastIndexOf(','));
});
return map;
}
/**
* All non-trigger activities are assigned a status seed by their parent
*/
static getSeed() {
return '999000000000000';
}
/**
* All trigger activities are assigned a status seed in a completed state
*/
static getTriggerSeed() {
return '888000001000001';
}
/**
* entry point for compiler-type activities. This is called by the compiler
* to bind the sorted activity IDs to the trigger activity. These are then used
* at runtime by the activities to track job/activity status.
* @param graphs
*/
static compile(graphs) {
CollatorService.bindAncestorArray(graphs);
}
/**
* binds the ancestor array to each activity.
* Used in conjunction with the dimensional
* address (dad). If dad is `,0,1,0,0` and the
* ancestor array is `['t1', 'a1', 'a2']` for
* activity 'a3', then the SAVED DAD
* will always have the trailing
* 0's removed. This ensures that the addressing
* remains consistent even if the graph changes.
* id DAD SAVED DAD
* * t1 => ,0 => [empty]
* * a1 => ,0,1 => ,0,1
* * a2 => ,0,1,0 => ,0,1
* * a3 => ,0,1,0,0 => ,0,1
*
*/
static bindAncestorArray(graphs) {
graphs.forEach((graph) => {
const ancestors = {};
const startingNode = Object.keys(graph.activities).find((activity) => graph.activities[activity].type === 'trigger');
if (!startingNode) {
throw new Error('collator-trigger-activity-not-found');
}
const dfs = (node, parentList) => {
ancestors[node] = parentList;
graph.activities[node]['ancestors'] = parentList;
const transitions = graph.transitions?.[node] || [];
transitions.forEach((transition) => {
dfs(transition.to, [...parentList, node]);
});
};
// Start the DFS traversal
dfs(startingNode, []);
});
}
/**
* All activities exist on a dimensional plane. Zero
* is the default. A value of
* `AxY,0,0,0,0,1,0,0` would reflect that
* an ancestor activity was dimensionalized beyond
* the default.
*/
static getDimensionalSeed(index = 0) {
return `,${index}`;
}
}
exports.CollatorService = CollatorService;
//max int digit count that supports `hincrby`
CollatorService.targetLength = 15;