@hotmeshio/hotmesh 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { RedisClient, RedisMulti } from '../types/redis';
5
5
  import { StringAnyType } from '../types/serializer';
6
6
  import { StreamCode, StreamStatus } from '../types/stream';
7
7
  import { SystemHealth } from '../types/quorum';
8
+ export declare const hashOptions: (options: any) => string;
8
9
  export declare function getSystemHealth(): Promise<SystemHealth>;
9
10
  export declare function sleepFor(ms: number): Promise<unknown>;
10
11
  export declare function sleepImmediate(): Promise<void>;
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.guid = exports.deepCopy = exports.sleepImmediate = exports.sleepFor = exports.getSystemHealth = void 0;
6
+ exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.guid = exports.deepCopy = exports.sleepImmediate = exports.sleepFor = exports.getSystemHealth = exports.hashOptions = void 0;
7
7
  const os_1 = __importDefault(require("os"));
8
+ const crypto_1 = require("crypto");
8
9
  const nanoid_1 = require("nanoid");
9
10
  const enums_1 = require("./enums");
10
11
  async function safeExecute(operation, defaultValue) {
@@ -16,10 +17,17 @@ async function safeExecute(operation, defaultValue) {
16
17
  return defaultValue;
17
18
  }
18
19
  }
20
+ const hashOptions = (options) => {
21
+ const str = JSON.stringify(options);
22
+ return (0, crypto_1.createHash)('sha256').update(str).digest('hex');
23
+ };
24
+ exports.hashOptions = hashOptions;
19
25
  async function getSystemHealth() {
20
26
  const totalMemory = os_1.default.totalmem();
21
27
  const freeMemory = os_1.default.freemem();
22
28
  const usedMemory = totalMemory - freeMemory;
29
+ //NOTE: enable the following if desired; for now, only
30
+ // `memory` is emitted when system health is requested
23
31
  //const cpus = os.cpus();
24
32
  // CPU load calculation remains unchanged
25
33
  // const cpuLoad = cpus.map((cpu, i) => {
@@ -28,9 +36,8 @@ async function getSystemHealth() {
28
36
  // const usage = ((total - idle) / total) * 100;
29
37
  // return { [`CPU ${i} Usage`]: `${usage.toFixed(2)}%` };
30
38
  // });
31
- // Wrap each systeminformation call with safeExecute
39
+ // Wrap each systeminformation call with safeExecute (systeminformation npm package)
32
40
  //const networkStats = await safeExecute(si.networkStats(), []);
33
- // Construct the system health object with error handling in mind
34
41
  const systemHealth = {
35
42
  TotalMemoryGB: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
36
43
  FreeMemoryGB: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -84,7 +84,6 @@
84
84
  "js-yaml": "^4.1.0",
85
85
  "ms": "^2.1.3",
86
86
  "nanoid": "^3.3.6",
87
- "systeminformation": "^5.22.2",
88
87
  "winston": "^3.8.2"
89
88
  },
90
89
  "devDependencies": {
@@ -27,19 +27,23 @@ declare class Activity {
27
27
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
28
28
  setLeg(leg: ActivityLeg): void;
29
29
  /**
30
- * Upon entering leg 1 of a duplexed activty, verify
31
- * all aspects of the entry including job and activty state
30
+ * A job is assumed to be complete when its status (a semaphore)
31
+ * reaches `0`. A different threshold can be set in the
32
+ * activity YAML, in support of Dynamic Activation Control.
33
+ */
34
+ mapStatusThreshold(): number;
35
+ /**
36
+ * Upon entering leg 1 of a duplexed activity
32
37
  */
33
38
  verifyEntry(): Promise<void>;
34
39
  /**
35
- * Upon entering leg 2 of a duplexed activty, verify
36
- * all aspects of the re-entry including job and activty state
40
+ * Upon entering leg 2 of a duplexed activity
37
41
  */
38
42
  verifyReentry(): Promise<number>;
39
43
  processEvent(status?: StreamStatus, code?: StreamCode, type?: 'hook' | 'output'): Promise<void>;
40
- processPending(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags>;
41
- processSuccess(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags>;
42
- processError(telemetry: TelemetryService, type: string): Promise<MultiResponseFlags>;
44
+ processPending(type: 'hook' | 'output'): Promise<MultiResponseFlags>;
45
+ processSuccess(type: 'hook' | 'output'): Promise<MultiResponseFlags>;
46
+ processError(): Promise<MultiResponseFlags>;
43
47
  transitionAdjacent(multiResponse: MultiResponseFlags, telemetry: TelemetryService): Promise<void>;
44
48
  resolveStatus(multiResponse: MultiResponseFlags): number;
45
49
  mapJobData(): void;
@@ -57,7 +61,7 @@ declare class Activity {
57
61
  bindJobError(data: Record<string, unknown>): void;
58
62
  getTriggerConfig(): Promise<ActivityType>;
59
63
  getJobStatus(): null | number;
60
- setStatus(amount: number, multi?: RedisMulti): Promise<void>;
64
+ setStatus(amount: number, multi?: RedisMulti): Promise<void | any>;
61
65
  authorizeEntry(state: StringAnyType): string[];
62
66
  bindDimensionalAddress(state: StringAnyType): void;
63
67
  setState(multi?: RedisMulti): Promise<string>;
@@ -31,18 +31,47 @@ class Activity {
31
31
  this.leg = leg;
32
32
  }
33
33
  /**
34
- * Upon entering leg 1 of a duplexed activty, verify
35
- * all aspects of the entry including job and activty state
34
+ * A job is assumed to be complete when its status (a semaphore)
35
+ * reaches `0`. A different threshold can be set in the
36
+ * activity YAML, in support of Dynamic Activation Control.
37
+ */
38
+ mapStatusThreshold() {
39
+ if (this.config.statusThreshold !== undefined) {
40
+ const threshold = pipe_1.Pipe.resolve(this.config.statusThreshold, this.context);
41
+ if (threshold !== undefined && !isNaN(Number(threshold))) {
42
+ return threshold;
43
+ }
44
+ }
45
+ return 0;
46
+ }
47
+ /**
48
+ * Upon entering leg 1 of a duplexed activity
36
49
  */
37
50
  async verifyEntry() {
38
51
  this.setLeg(1);
39
52
  await this.getState();
40
- collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
53
+ const threshold = this.mapStatusThreshold();
54
+ try {
55
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid, threshold);
56
+ }
57
+ catch (error) {
58
+ if (threshold > 0) {
59
+ if (this.context.metadata.js === threshold) {
60
+ //conclude job EXACTLY ONCE
61
+ const status = await this.setStatus(-threshold);
62
+ if (Number(status) === 0) {
63
+ await this.engine.runJobCompletionTasks(this.context);
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ throw error;
69
+ }
70
+ }
41
71
  await collator_1.CollatorService.notarizeEntry(this);
42
72
  }
43
73
  /**
44
- * Upon entering leg 2 of a duplexed activty, verify
45
- * all aspects of the re-entry including job and activty state
74
+ * Upon entering leg 2 of a duplexed activity
46
75
  */
47
76
  async verifyReentry() {
48
77
  const guid = this.context.metadata.guid;
@@ -79,13 +108,13 @@ class Activity {
79
108
  telemetry.startActivitySpan(this.leg);
80
109
  let multiResponse;
81
110
  if (status === stream_1.StreamStatus.PENDING) {
82
- multiResponse = await this.processPending(telemetry, type);
111
+ multiResponse = await this.processPending(type);
83
112
  }
84
113
  else if (status === stream_1.StreamStatus.SUCCESS) {
85
- multiResponse = await this.processSuccess(telemetry, type);
114
+ multiResponse = await this.processSuccess(type);
86
115
  }
87
116
  else {
88
- multiResponse = await this.processError(telemetry, type);
117
+ multiResponse = await this.processError();
89
118
  }
90
119
  this.transitionAdjacent(multiResponse, telemetry);
91
120
  }
@@ -120,7 +149,7 @@ class Activity {
120
149
  this.logger.debug('activity-process-event-end', { jid, aid });
121
150
  }
122
151
  }
123
- async processPending(telemetry, type) {
152
+ async processPending(type) {
124
153
  this.bindActivityData(type);
125
154
  this.adjacencyList = await this.filterAdjacent();
126
155
  this.mapJobData();
@@ -130,7 +159,7 @@ class Activity {
130
159
  await this.setStatus(this.adjacencyList.length, multi);
131
160
  return (await multi.exec());
132
161
  }
133
- async processSuccess(telemetry, type) {
162
+ async processSuccess(type) {
134
163
  this.bindActivityData(type);
135
164
  this.adjacencyList = await this.filterAdjacent();
136
165
  this.mapJobData();
@@ -140,7 +169,7 @@ class Activity {
140
169
  await this.setStatus(this.adjacencyList.length - 1, multi);
141
170
  return (await multi.exec());
142
171
  }
143
- async processError(telemetry, type) {
172
+ async processError() {
144
173
  this.bindActivityError(this.data);
145
174
  this.adjacencyList = await this.filterAdjacent();
146
175
  if (!this.adjacencyList.length) {
@@ -249,7 +278,7 @@ class Activity {
249
278
  }
250
279
  async setStatus(amount, multi) {
251
280
  const { id: appId } = await this.engine.getVID();
252
- await this.store.setStatus(amount, this.context.metadata.jid, appId, multi);
281
+ return await this.store.setStatus(amount, this.context.metadata.jid, appId, multi);
253
282
  }
254
283
  authorizeEntry(state) {
255
284
  //pre-authorize activity state to allow entry for adjacent activities
@@ -9,7 +9,7 @@ declare class CollatorService {
9
9
  /**
10
10
  * Upon re/entry, verify that the job status is active
11
11
  */
12
- static assertJobActive(status: number, jobId: string, activityId: string): void;
12
+ static assertJobActive(status: number, jobId: string, activityId: string, threshold?: number): void;
13
13
  /**
14
14
  * returns the dimensional address (dad) for the target; due
15
15
  * to the nature of the notary system, the dad for leg 2 entry
@@ -7,8 +7,8 @@ class CollatorService {
7
7
  /**
8
8
  * Upon re/entry, verify that the job status is active
9
9
  */
10
- static assertJobActive(status, jobId, activityId) {
11
- if (status <= 0) {
10
+ static assertJobActive(status, jobId, activityId, threshold = 0) {
11
+ if (status <= threshold) {
12
12
  throw new errors_1.InactiveJobError(jobId, status, activityId);
13
13
  }
14
14
  }
@@ -20,7 +20,7 @@ export declare class ClientService {
20
20
  * creating the stream. This method will verify that the stream
21
21
  * exists and if not, create it.
22
22
  */
23
- static verifyStream: (workflowTopic: string, namespace?: string) => Promise<HotMesh>;
23
+ verifyStream: (hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => Promise<void>;
24
24
  search: (hotMeshClient: HotMesh, index: string, query: string[]) => Promise<string[]>;
25
25
  workflow: {
26
26
  start: (options: WorkflowOptions) => Promise<WorkflowHandleService>;
@@ -17,15 +17,14 @@ const factory_1 = require("./schemas/factory");
17
17
  class ClientService {
18
18
  constructor(config) {
19
19
  this.getHotMeshClient = async (workflowTopic, namespace) => {
20
+ //namespace isolation requires the connection options to be hashed
21
+ //as multiple intersecting databases can be used by the same service
22
+ const optionsHash = (0, utils_1.hashOptions)(this.connection.options);
20
23
  const targetNS = namespace ?? factory_1.APP_ID;
21
- if (ClientService.instances.has(targetNS)) {
22
- const targetTopic = `${namespace ?? factory_1.APP_ID}.${workflowTopic}`;
23
- const hotMeshClient = await ClientService.instances.get(targetNS);
24
+ const connectionNS = `${optionsHash}.${targetNS}`;
25
+ if (ClientService.instances.has(connectionNS)) {
26
+ const hotMeshClient = await ClientService.instances.get(connectionNS);
24
27
  await this.verifyWorkflowActive(hotMeshClient, targetNS);
25
- if (!ClientService.topics.includes(targetTopic)) {
26
- ClientService.topics.push(targetTopic);
27
- await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
28
- }
29
28
  return hotMeshClient;
30
29
  }
31
30
  //create and cache an instance
@@ -39,11 +38,24 @@ class ClientService {
39
38
  },
40
39
  },
41
40
  });
42
- ClientService.instances.set(targetNS, hotMeshClient);
43
- await ClientService.createStream(await hotMeshClient, workflowTopic, namespace);
41
+ ClientService.instances.set(connectionNS, hotMeshClient);
44
42
  await this.activateWorkflow(await hotMeshClient, targetNS);
45
43
  return hotMeshClient;
46
44
  };
45
+ /**
46
+ * It is possible for a client to invoke a workflow without first
47
+ * creating the stream. This method will verify that the stream
48
+ * exists and if not, create it.
49
+ */
50
+ this.verifyStream = async (hotMeshClient, workflowTopic, namespace) => {
51
+ const optionsHash = (0, utils_1.hashOptions)(this.connection.options);
52
+ const targetNS = namespace ?? factory_1.APP_ID;
53
+ const targetTopic = `${optionsHash}.${targetNS}.${workflowTopic}`;
54
+ if (!ClientService.topics.includes(targetTopic)) {
55
+ ClientService.topics.push(targetTopic);
56
+ await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
57
+ }
58
+ };
47
59
  this.search = async (hotMeshClient, index, query) => {
48
60
  const store = hotMeshClient.engine.store;
49
61
  if (query[0]?.startsWith('FT.')) {
@@ -57,14 +69,16 @@ class ClientService {
57
69
  const workflowName = options.entity ?? options.workflowName;
58
70
  const trc = options.workflowTrace;
59
71
  const spn = options.workflowSpan;
60
- //NOTE: HotMesh 'workflowTopic' is a created by concatenating
61
- // the taskQueue and workflowName used by the Durable module
72
+ //hotmesh topic is a combination of the durable queue+workflowname
62
73
  const workflowTopic = `${taskQueueName}-${workflowName}`;
63
74
  const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
75
+ //verify that the stream channel exists before enqueueing
76
+ await this.verifyStream(hotMeshClient, workflowTopic, options.namespace);
64
77
  const payload = {
65
78
  arguments: [...options.args],
66
79
  originJobId: options.originJobId,
67
80
  expire: options.expire ?? enums_1.HMSH_EXPIRE_JOB_SECONDS,
81
+ signalIn: options.signalIn,
68
82
  parentWorkflowId: options.parentWorkflowId,
69
83
  workflowId: options.workflowId || hotmesh_1.HotMeshService.guid(),
70
84
  workflowTopic: workflowTopic,
@@ -200,21 +214,4 @@ ClientService.createStream = async (hotMeshClient, workflowTopic, namespace) =>
200
214
  //ignore if already exists
201
215
  }
202
216
  };
203
- /**
204
- * It is possible for a client to invoke a workflow without first
205
- * creating the stream. This method will verify that the stream
206
- * exists and if not, create it.
207
- */
208
- ClientService.verifyStream = async (workflowTopic, namespace) => {
209
- const targetTopic = `${namespace ?? factory_1.APP_ID}.${workflowTopic}`;
210
- const targetNS = namespace ?? factory_1.APP_ID;
211
- if (ClientService.instances.has(targetNS)) {
212
- const hotMeshClient = await ClientService.instances.get(targetNS);
213
- if (!ClientService.topics.includes(targetTopic)) {
214
- ClientService.topics.push(targetTopic);
215
- await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
216
- }
217
- return hotMeshClient;
218
- }
219
- };
220
217
  exports.ClientService = ClientService;
@@ -21,7 +21,7 @@
21
21
  * * Service Meshes
22
22
  * * Master Data Management systems
23
23
  */
24
- declare const APP_VERSION = "1";
24
+ declare const APP_VERSION = "2";
25
25
  declare const APP_ID = "durable";
26
26
  /**
27
27
  * returns a new durable workflow schema
@@ -24,7 +24,7 @@
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.APP_ID = exports.APP_VERSION = exports.getWorkflowYAML = void 0;
27
- const APP_VERSION = '1';
27
+ const APP_VERSION = '2';
28
28
  exports.APP_VERSION = APP_VERSION;
29
29
  const APP_ID = 'durable';
30
30
  exports.APP_ID = APP_ID;
@@ -81,6 +81,9 @@ const getWorkflowYAML = (app, version) => {
81
81
  expire:
82
82
  description: the time in seconds to expire the workflow in Redis once it completes
83
83
  type: number
84
+ signalIn:
85
+ description: if false, the job will not support subordinated hooks
86
+ type: boolean
84
87
 
85
88
  output:
86
89
  schema:
@@ -806,6 +809,7 @@ const getWorkflowYAML = (app, version) => {
806
809
  signaler:
807
810
  title: Signal-In Reentry point for subordinated hook flows
808
811
  type: hook
812
+ statusThreshold: 1
809
813
  hook:
810
814
  type: object
811
815
  properties:
@@ -1462,6 +1466,13 @@ const getWorkflowYAML = (app, version) => {
1462
1466
  trigger:
1463
1467
  - to: cycle_hook
1464
1468
  - to: signaler
1469
+ conditions:
1470
+ match:
1471
+ - expected: true
1472
+ actual:
1473
+ '@pipe':
1474
+ - ['{$self.output.data.signalIn}', true]
1475
+ - ['{@conditional.nullish}']
1465
1476
  ## MAIN PROCESS TRANSITIONS ##
1466
1477
  cycle_hook:
1467
1478
  - to: throttler
@@ -1,13 +1,11 @@
1
1
  import { HotMeshService as HotMesh } from '../hotmesh';
2
- import { Connection, Registry, WorkerConfig, WorkerOptions } from '../../types/durable';
2
+ import { Registry, WorkerConfig, WorkerOptions } from '../../types/durable';
3
3
  export declare class WorkerService {
4
4
  static activityRegistry: Registry;
5
- static connections: Map<string, import("../../types/durable").ConnectionConfig>;
6
5
  static instances: Map<string, HotMesh | Promise<HotMesh>>;
7
6
  workflowRunner: HotMesh;
8
7
  activityRunner: HotMesh;
9
8
  static getHotMesh: (workflowTopic: string, config?: Partial<WorkerConfig>, options?: WorkerOptions) => Promise<HotMesh>;
10
- static findConnectionByNamespace: (namespace: string) => Connection;
11
9
  static activateWorkflow(hotMesh: HotMesh): Promise<void>;
12
10
  static registerActivities<ACT>(activities: ACT): Registry;
13
11
  static create(config: WorkerConfig): Promise<WorkerService>;
@@ -58,8 +58,6 @@ class WorkerService {
58
58
  return WorkerService.activityRegistry;
59
59
  }
60
60
  static async create(config) {
61
- const targetNamespace = config.namespace ?? factory_1.APP_ID;
62
- WorkerService.connections.set(targetNamespace, config.connection);
63
61
  const workflow = config.workflow;
64
62
  const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
65
63
  const baseTopic = `${config.taskQueue}-${workflowFunctionName}`;
@@ -94,9 +92,12 @@ class WorkerService {
94
92
  class: config.connection.class,
95
93
  options: config.connection.options,
96
94
  };
95
+ const targetNamespace = config?.namespace ?? factory_1.APP_ID;
96
+ const optionsHash = (0, utils_1.hashOptions)(config?.connection?.options);
97
+ const targetTopic = `${optionsHash}.${targetNamespace}.${activityTopic}`;
97
98
  const hotMeshWorker = await hotmesh_1.HotMeshService.init({
98
99
  logLevel: config.options?.logLevel ?? enums_1.HMSH_LOGLEVEL,
99
- appId: config.namespace ?? factory_1.APP_ID,
100
+ appId: targetNamespace,
100
101
  engine: { redis: redisConfig },
101
102
  workers: [
102
103
  {
@@ -106,10 +107,9 @@ class WorkerService {
106
107
  },
107
108
  ],
108
109
  });
109
- WorkerService.instances.set(activityTopic, hotMeshWorker);
110
+ WorkerService.instances.set(targetTopic, hotMeshWorker);
110
111
  return hotMeshWorker;
111
112
  }
112
- //this is the linked worker function in the reentrant workflow test
113
113
  wrapActivityFunctions() {
114
114
  return async (data) => {
115
115
  try {
@@ -172,6 +172,9 @@ class WorkerService {
172
172
  class: config.connection.class,
173
173
  options: config.connection.options,
174
174
  };
175
+ const targetNamespace = config?.namespace ?? factory_1.APP_ID;
176
+ const optionsHash = (0, utils_1.hashOptions)(config?.connection?.options);
177
+ const targetTopic = `${optionsHash}.${targetNamespace}.${workflowTopic}`;
175
178
  const hotMeshWorker = await hotmesh_1.HotMeshService.init({
176
179
  logLevel: config.options?.logLevel ?? enums_1.HMSH_LOGLEVEL,
177
180
  appId: config.namespace ?? factory_1.APP_ID,
@@ -184,7 +187,7 @@ class WorkerService {
184
187
  },
185
188
  ],
186
189
  });
187
- WorkerService.instances.set(workflowTopic, hotMeshWorker);
190
+ WorkerService.instances.set(targetTopic, hotMeshWorker);
188
191
  return hotMeshWorker;
189
192
  }
190
193
  wrapWorkflowFunction(workflowFunction, workflowTopic, config) {
@@ -199,6 +202,7 @@ class WorkerService {
199
202
  context.set('canRetry', workflowInput.canRetry);
200
203
  context.set('counter', counter);
201
204
  context.set('interruptionRegistry', interruptionRegistry);
205
+ context.set('connection', config.connection);
202
206
  context.set('namespace', config.namespace ?? factory_1.APP_ID);
203
207
  context.set('raw', data);
204
208
  context.set('workflowId', workflowInput.workflowId);
@@ -372,11 +376,11 @@ class WorkerService {
372
376
  }
373
377
  _a = WorkerService;
374
378
  WorkerService.activityRegistry = {}; //user's activities
375
- WorkerService.connections = new Map();
376
379
  WorkerService.instances = new Map();
377
380
  WorkerService.getHotMesh = async (workflowTopic, config, options) => {
378
381
  const targetNamespace = config?.namespace ?? factory_1.APP_ID;
379
- const targetTopic = `${targetNamespace}.${workflowTopic}`;
382
+ const optionsHash = (0, utils_1.hashOptions)(config?.connection?.options);
383
+ const targetTopic = `${optionsHash}.${targetNamespace}.${workflowTopic}`;
380
384
  if (WorkerService.instances.has(targetTopic)) {
381
385
  return await WorkerService.instances.get(targetTopic);
382
386
  }
@@ -384,25 +388,13 @@ WorkerService.getHotMesh = async (workflowTopic, config, options) => {
384
388
  logLevel: options?.logLevel ?? enums_1.HMSH_LOGLEVEL,
385
389
  appId: targetNamespace,
386
390
  engine: {
387
- redis: { ...WorkerService.findConnectionByNamespace(targetNamespace) },
391
+ redis: { ...config?.connection },
388
392
  },
389
393
  });
390
394
  WorkerService.instances.set(targetTopic, hotMeshClient);
391
395
  await WorkerService.activateWorkflow(await hotMeshClient);
392
396
  return hotMeshClient;
393
397
  };
394
- WorkerService.findConnectionByNamespace = (namespace) => {
395
- let defaultConnection;
396
- for (const [ns, value] of WorkerService.connections) {
397
- if (ns === namespace) {
398
- return value;
399
- }
400
- else if (!defaultConnection) {
401
- defaultConnection = value;
402
- }
403
- }
404
- return defaultConnection;
405
- };
406
398
  WorkerService.Context = {
407
399
  info: () => {
408
400
  return {
@@ -69,6 +69,7 @@ class WorkflowService {
69
69
  const interruptionRegistry = store.get('interruptionRegistry');
70
70
  const workflowDimension = store.get('workflowDimension') ?? '';
71
71
  const workflowTopic = store.get('workflowTopic');
72
+ const connection = store.get('connection');
72
73
  const namespace = store.get('namespace');
73
74
  const originJobId = store.get('originJobId');
74
75
  const workflowTrace = store.get('workflowTrace');
@@ -82,6 +83,7 @@ class WorkflowService {
82
83
  counter: COUNTER.counter,
83
84
  cursor,
84
85
  interruptionRegistry,
86
+ connection,
85
87
  namespace,
86
88
  originJobId,
87
89
  raw,
@@ -100,8 +102,9 @@ class WorkflowService {
100
102
  static async getHotMesh() {
101
103
  const store = storage_1.asyncLocalStorage.getStore();
102
104
  const workflowTopic = store.get('workflowTopic');
105
+ const connection = store.get('connection');
103
106
  const namespace = store.get('namespace');
104
- return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
107
+ return await worker_1.WorkerService.getHotMesh(workflowTopic, { connection, namespace });
105
108
  }
106
109
  /**
107
110
  * Spawns a child workflow and awaits the return.
@@ -315,10 +318,12 @@ class WorkflowService {
315
318
  const workflowId = store.get('workflowId');
316
319
  const workflowDimension = store.get('workflowDimension') ?? '';
317
320
  const workflowTopic = store.get('workflowTopic');
321
+ const connection = store.get('connection');
318
322
  const namespace = store.get('namespace');
319
323
  const COUNTER = store.get('counter');
320
324
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
321
325
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, {
326
+ connection,
322
327
  namespace,
323
328
  });
324
329
  //this ID is used as a item key with a hash (dash prefix ensures no collision)
@@ -347,8 +352,10 @@ class WorkflowService {
347
352
  static async signal(signalId, data) {
348
353
  const store = storage_1.asyncLocalStorage.getStore();
349
354
  const workflowTopic = store.get('workflowTopic');
355
+ const connection = store.get('connection');
350
356
  const namespace = store.get('namespace');
351
357
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, {
358
+ connection,
352
359
  namespace,
353
360
  });
354
361
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
@@ -365,8 +372,9 @@ class WorkflowService {
365
372
  * @param {HookOptions} options - the hook options
366
373
  */
367
374
  static async hook(options) {
368
- const { workflowId, namespace, workflowTopic } = WorkflowService.getContext();
375
+ const { workflowId, connection, namespace, workflowTopic } = WorkflowService.getContext();
369
376
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, {
377
+ connection,
370
378
  namespace,
371
379
  });
372
380
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
@@ -395,13 +403,14 @@ class WorkflowService {
395
403
  * @template T - the result type
396
404
  */
397
405
  static async once(fn, ...args) {
398
- const { COUNTER, namespace, workflowId, workflowTopic, workflowDimension, replay, } = WorkflowService.getContext();
406
+ const { COUNTER, connection, namespace, workflowId, workflowTopic, workflowDimension, replay, } = WorkflowService.getContext();
399
407
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
400
408
  const sessionId = `-once${workflowDimension}-${execIndex}-`;
401
409
  if (sessionId in replay) {
402
410
  return serializer_1.SerializerService.fromString(replay[sessionId]).data;
403
411
  }
404
412
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, {
413
+ connection,
405
414
  namespace,
406
415
  });
407
416
  const keyParams = {
@@ -424,8 +433,9 @@ class WorkflowService {
424
433
  * Interrupts a running job
425
434
  */
426
435
  static async interrupt(jobId, options = {}) {
427
- const { workflowTopic, namespace } = WorkflowService.getContext();
436
+ const { workflowTopic, connection, namespace, } = WorkflowService.getContext();
428
437
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, {
438
+ connection,
429
439
  namespace,
430
440
  });
431
441
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
@@ -2,6 +2,8 @@ declare class ConditionalHandler {
2
2
  ternary(condition: boolean, valueIfTrue: any, valueIfFalse: any): any;
3
3
  equality(value1: any, value2: any): boolean;
4
4
  strict_equality(value1: any, value2: any): boolean;
5
+ inequality(value1: any, value2: any): boolean;
6
+ strict_inequality(value1: any, value2: any): boolean;
5
7
  greater_than(value1: number, value2: number): boolean;
6
8
  less_than(value1: number, value2: number): boolean;
7
9
  greater_than_or_equal(value1: number, value2: number): boolean;
@@ -11,6 +11,12 @@ class ConditionalHandler {
11
11
  strict_equality(value1, value2) {
12
12
  return value1 === value2;
13
13
  }
14
+ inequality(value1, value2) {
15
+ return value1 != value2;
16
+ }
17
+ strict_inequality(value1, value2) {
18
+ return value1 !== value2;
19
+ }
14
20
  greater_than(value1, value2) {
15
21
  return value1 > value2;
16
22
  }
@@ -6,6 +6,8 @@ interface BaseActivity {
6
6
  title?: string;
7
7
  type?: ActivityExecutionType;
8
8
  subtype?: string;
9
+ statusThreshold?: number;
10
+ statusThresholdType?: 'stop' | 'throw' | 'stall';
9
11
  input?: Record<string, any>;
10
12
  output?: Record<string, any>;
11
13
  settings?: Record<string, any>;
@@ -88,6 +88,10 @@ type WorkflowContext = {
88
88
  * the native HotMesh message that encapsulates the arguments, metadata, and raw data for the workflow
89
89
  */
90
90
  raw: StreamData;
91
+ /**
92
+ * the HotMesh connection configuration (io/redis NPM package reference and login credentials)
93
+ */
94
+ connection: Connection;
91
95
  };
92
96
  /**
93
97
  * The schema for the full-text-search (RediSearch) index.
@@ -256,6 +260,10 @@ type WorkflowOptions = {
256
260
  * sets the number of seconds a workflow may exist after completion. As the process engine is an in-memory cache, the default policy is to expire and scrub the job hash as soon as it completes.
257
261
  */
258
262
  expire?: number;
263
+ /**
264
+ * default is true; set to false to optimize workflows that do not require a `signal in`
265
+ */
266
+ signalIn?: boolean;
259
267
  /**
260
268
  * default is true; if false, will not await the execution
261
269
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -84,7 +84,6 @@
84
84
  "js-yaml": "^4.1.0",
85
85
  "ms": "^2.1.3",
86
86
  "nanoid": "^3.3.6",
87
- "systeminformation": "^5.22.2",
88
87
  "winston": "^3.8.2"
89
88
  },
90
89
  "devDependencies": {
package/types/activity.ts CHANGED
@@ -18,6 +18,8 @@ interface BaseActivity {
18
18
  title?: string;
19
19
  type?: ActivityExecutionType;
20
20
  subtype?: string;
21
+ statusThreshold?: number; //default is 0; set to 1 to ensure not last standing; message will be ignored as if too late to process
22
+ statusThresholdType?: 'stop' | 'throw' | 'stall'; //default is stop; must explicitly set to throw to throw an error or stall to stall
21
23
  input?: Record<string, any>;
22
24
  output?: Record<string, any>;
23
25
  settings?: Record<string, any>;
package/types/durable.ts CHANGED
@@ -106,6 +106,11 @@ type WorkflowContext = {
106
106
  * the native HotMesh message that encapsulates the arguments, metadata, and raw data for the workflow
107
107
  */
108
108
  raw: StreamData;
109
+
110
+ /**
111
+ * the HotMesh connection configuration (io/redis NPM package reference and login credentials)
112
+ */
113
+ connection: Connection;
109
114
  };
110
115
 
111
116
  /**
@@ -309,6 +314,11 @@ type WorkflowOptions = {
309
314
  */
310
315
  expire?: number;
311
316
 
317
+ /**
318
+ * default is true; set to false to optimize workflows that do not require a `signal in`
319
+ */
320
+ signalIn?: boolean;
321
+
312
322
  /**
313
323
  * default is true; if false, will not await the execution
314
324
  */