@hotmeshio/hotmesh 0.18.1 → 0.19.1

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.
@@ -133,6 +133,7 @@ export declare const MAX_STREAM_RETRIES: number;
133
133
  export declare const MAX_DELAY = 2147483647;
134
134
  export declare const HMSH_MAX_RETRIES: number;
135
135
  export declare const HMSH_POISON_MESSAGE_THRESHOLD: number;
136
+ export declare const HMSH_MAX_CYCLES: number;
136
137
  export declare const HMSH_MAX_TIMEOUT_MS: number;
137
138
  export declare const HMSH_GRADUATED_INTERVAL_MS: number;
138
139
  /**
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMSH_RESERVATION_TIMEOUT_MAX_S = exports.HMSH_RESERVATION_TIMEOUT_S = exports.HMSH_ENGINE_CONCURRENCY = exports.HMSH_BATCH_SIZE_MIN = exports.HMSH_BATCH_SIZE = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_INITIAL_INTERVAL = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_CONTINUE = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_PENDING_SIGNAL_EXPIRE = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
4
- exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = void 0;
3
+ exports.HMSH_RESERVATION_TIMEOUT_S = exports.HMSH_ENGINE_CONCURRENCY = exports.HMSH_BATCH_SIZE_MIN = exports.HMSH_BATCH_SIZE = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_INITIAL_INTERVAL = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_CYCLES = exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_CONTINUE = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_PENDING_SIGNAL_EXPIRE = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
4
+ exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_RESERVATION_TIMEOUT_MAX_S = void 0;
5
5
  /**
6
6
  * Determines the log level for the application. The default is 'info'.
7
7
  */
@@ -143,6 +143,7 @@ exports.MAX_STREAM_RETRIES = parseInt(process.env.MAX_STREAM_RETRIES, 10) || 2;
143
143
  exports.MAX_DELAY = 2147483647; // Maximum allowed delay in milliseconds for setTimeout
144
144
  exports.HMSH_MAX_RETRIES = parseInt(process.env.HMSH_MAX_RETRIES, 10) || 3;
145
145
  exports.HMSH_POISON_MESSAGE_THRESHOLD = parseInt(process.env.HMSH_POISON_MESSAGE_THRESHOLD, 10) || 5;
146
+ exports.HMSH_MAX_CYCLES = parseInt(process.env.HMSH_MAX_CYCLES, 10) || 10000;
146
147
  exports.HMSH_MAX_TIMEOUT_MS = parseInt(process.env.HMSH_MAX_TIMEOUT_MS, 10) || 60000;
147
148
  exports.HMSH_GRADUATED_INTERVAL_MS = parseInt(process.env.HMSH_GRADUATED_INTERVAL_MS, 10) || 5000;
148
149
  // DURABLE
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.18.1",
3
+ "version": "0.19.1",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CollatorService = void 0;
4
4
  const errors_1 = require("../../modules/errors");
5
+ const enums_1 = require("../../modules/enums");
5
6
  const collator_1 = require("../../types/collator");
6
7
  class CollatorService {
7
8
  /**
@@ -38,6 +39,13 @@ class CollatorService {
38
39
  const ancestors = activity.config.ancestors;
39
40
  const ancestorIndex = ancestors.indexOf(targetActivityId);
40
41
  const dimensions = activity.metadata.dad.split(','); //e.g., `,0,0,1,0`
42
+ // Safety cap: prevent infinite cycle loops
43
+ const currentCycle = parseInt(dimensions[ancestorIndex] || '0', 10);
44
+ if (currentCycle >= enums_1.HMSH_MAX_CYCLES) {
45
+ throw new Error(`Cycle limit exceeded for job ${activity.context.metadata.jid} ` +
46
+ `(${currentCycle} >= HMSH_MAX_CYCLES=${enums_1.HMSH_MAX_CYCLES}) at DAD ${activity.metadata.dad}. ` +
47
+ `Set HMSH_MAX_CYCLES env var to increase the limit.`);
48
+ }
41
49
  dimensions.length = ancestorIndex + 1;
42
50
  dimensions.push('0');
43
51
  return dimensions.join(',');
@@ -16,6 +16,7 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.processStreamMessage = void 0;
19
+ const errors_1 = require("../../modules/errors");
19
20
  const stream_1 = require("../../types/stream");
20
21
  async function processStreamMessage(instance, streamData) {
21
22
  instance.logger.debug('engine-process', {
@@ -86,7 +87,24 @@ async function dispatchAwait(instance, streamData, context) {
86
87
  spn: streamData.metadata.spn,
87
88
  };
88
89
  const handler = (await instance.initActivity(streamData.metadata.topic, streamData.data, context));
89
- await handler.process();
90
+ try {
91
+ await handler.process();
92
+ }
93
+ catch (error) {
94
+ if (error instanceof errors_1.DuplicateJobError) {
95
+ // The child workflow already exists from a prior spawn attempt
96
+ // (crash recovery). This AWAIT message is a replay — the child
97
+ // will deliver its RESULT back to the parent via the normal path.
98
+ // Acknowledge the message so it doesn't loop.
99
+ instance.logger.info('dispatch-await-child-exists', {
100
+ childJobId: error.jobId,
101
+ parentJobId: streamData.metadata.jid,
102
+ parentDad: streamData.metadata.dad,
103
+ });
104
+ return;
105
+ }
106
+ throw error;
107
+ }
90
108
  }
91
109
  async function dispatchResult(instance, streamData, context) {
92
110
  const handler = (await instance.initActivity(`.${context.metadata.aid}`, streamData.data, context));
@@ -25,7 +25,19 @@ async function throttle(instance, options) {
25
25
  if (options.guid) {
26
26
  throttleMessage.guid = options.guid;
27
27
  }
28
- if (options.topic !== undefined) {
28
+ // Scope determines channel targeting:
29
+ // 'engines' → set topic to null (workers suppress null-topic throttles)
30
+ // 'workers' → requires a topic to route to worker channels only
31
+ // 'all' / undefined → default behavior (engines + workers)
32
+ if (options.scope === 'engines') {
33
+ throttleMessage.topic = null;
34
+ }
35
+ else if (options.scope === 'workers' && !options.topic) {
36
+ // Workers-only without a specific topic: broadcast to all worker channels
37
+ // by omitting topic (workers receive on global channel) but NOT setting null
38
+ // (which would suppress workers). The existing behavior is correct here.
39
+ }
40
+ else if (options.topic !== undefined) {
29
41
  throttleMessage.topic = options.topic;
30
42
  }
31
43
  await instance.engine.store.setThrottleRate(throttleMessage);
@@ -142,6 +142,7 @@ class QuorumService {
142
142
  timestamp: (0, utils_1.formatISODate)(new Date()),
143
143
  inited: this.engine.inited,
144
144
  throttle: this.engine.router.throttle,
145
+ is_scout: this.engine.stream.isScout(),
145
146
  reclaimDelay: this.engine.router.reclaimDelay,
146
147
  reclaimCount: this.engine.router.reclaimCount,
147
148
  system: await (0, utils_1.getSystemHealth)(),
@@ -44,6 +44,8 @@ export declare abstract class StreamService<ClientProvider extends ProviderClien
44
44
  }): Promise<StreamMessage[]>;
45
45
  reservationTimeout: number;
46
46
  abstract getStreamStats(streamName: string): Promise<StreamStats>;
47
+ /** Whether this instance currently holds the scout role. */
48
+ abstract isScout(): boolean;
47
49
  abstract getStreamDepth(streamName: string): Promise<number>;
48
50
  abstract getStreamDepths(streamName: {
49
51
  stream: string;
@@ -33,6 +33,7 @@ declare class NatsStreamService extends StreamService<NatsClientType, NatsPubAck
33
33
  maxRetries?: number;
34
34
  limit?: number;
35
35
  }): Promise<StreamMessage[]>;
36
+ isScout(): boolean;
36
37
  getStreamStats(streamName: string): Promise<StreamStats>;
37
38
  getStreamDepth(streamName: string): Promise<number>;
38
39
  getStreamDepths(streamNames: {
@@ -153,6 +153,9 @@ class NatsStreamService extends index_1.StreamService {
153
153
  async retryMessages(streamName, groupName, options) {
154
154
  return [];
155
155
  }
156
+ isScout() {
157
+ return false; // NATS doesn't use the scout pattern
158
+ }
156
159
  async getStreamStats(streamName) {
157
160
  try {
158
161
  const info = await this.jsm.streams.info(streamName);
@@ -96,6 +96,7 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
96
96
  maxRetries?: number;
97
97
  limit?: number;
98
98
  }): Promise<StreamMessage[]>;
99
+ isScout(): boolean;
99
100
  getStreamStats(streamName: string): Promise<StreamStats>;
100
101
  getStreamDepth(streamName: string): Promise<number>;
101
102
  getStreamDepths(streamNames: {
@@ -290,6 +290,9 @@ class PostgresStreamService extends index_1.StreamService {
290
290
  async retryMessages(streamName, groupName, options) {
291
291
  return Messages.retryMessages(streamName, groupName, options);
292
292
  }
293
+ isScout() {
294
+ return this.scoutManager?.isCurrentlyScout() ?? false;
295
+ }
293
296
  async getStreamStats(streamName) {
294
297
  const target = this.resolveStreamTarget(streamName);
295
298
  return Stats.getStreamStats(this.streamClient, target.tableName, target.streamName, this.logger);
@@ -29,6 +29,8 @@ export type ThrottleOptions = {
29
29
  guid?: string;
30
30
  /** target a worker quorum */
31
31
  topic?: string;
32
+ /** target engines only, workers only, or all (default: 'all') */
33
+ scope?: 'engines' | 'workers' | 'all';
32
34
  /** delay in milliseconds: 0 = resume, -1 = pause, >0 = delay per message */
33
35
  throttle: number;
34
36
  };
@@ -78,6 +80,8 @@ export interface QuorumProfile {
78
80
  reclaimDelay?: number;
79
81
  /** Max messages to reclaim per cycle. */
80
82
  reclaimCount?: number;
83
+ /** Whether this engine currently holds the scout role (polls for delayed messages). */
84
+ is_scout?: boolean;
81
85
  /** Host-level memory, CPU, and network stats. */
82
86
  system?: SystemHealth;
83
87
  /** Stringified worker callback function (only if `signature: true` in rollcall). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.18.1",
3
+ "version": "0.19.1",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",