@hotmeshio/hotmesh 0.14.7 → 0.14.9

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.
@@ -377,15 +377,13 @@ class WorkerService {
377
377
  //use code 599 as a proxy for all retryable errors
378
378
  // (basically anything not 596, 597, 598)
379
379
  return {
380
- status: stream_1.StreamStatus.SUCCESS,
380
+ status: stream_1.StreamStatus.ERROR,
381
381
  code: 599,
382
382
  metadata: { ...data.metadata },
383
383
  data: {
384
- $error: {
385
- message: err.message,
386
- stack: err.stack,
387
- code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
388
- },
384
+ message: err.message,
385
+ stack: err.stack,
386
+ code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
389
387
  },
390
388
  };
391
389
  }
@@ -607,15 +605,13 @@ class WorkerService {
607
605
  //use code 599 as a proxy for all retryable errors
608
606
  // (basically anything not 596, 597, 598)
609
607
  return {
610
- status: stream_1.StreamStatus.SUCCESS,
608
+ status: stream_1.StreamStatus.ERROR,
611
609
  code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
612
610
  metadata: { ...data.metadata },
613
611
  data: {
614
- $error: {
615
- message: err.message,
616
- stack: err.stack,
617
- timestamp: (0, utils_1.formatISODate)(new Date()),
618
- },
612
+ message: err.message,
613
+ stack: err.stack,
614
+ code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
619
615
  },
620
616
  };
621
617
  }
@@ -963,9 +959,24 @@ class WorkerService {
963
959
  }, telemetry_3.SpanStatusCode.ERROR, err.message);
964
960
  }
965
961
  isProcessing = true;
962
+ const errorCode = err.code || new errors_1.DurableRetryError(err.message).code;
963
+ if (errorCode === enums_1.HMSH_CODE_DURABLE_RETRYABLE) {
964
+ // Retryable errors use status: ERROR so the engine-level
965
+ // retry mechanism (handleRetry + _streamRetryConfig) kicks in
966
+ return withPatchMarkers({
967
+ status: stream_1.StreamStatus.ERROR,
968
+ code: errorCode,
969
+ metadata: { ...data.metadata },
970
+ data: {
971
+ message: err.message,
972
+ stack: err.stack,
973
+ code: errorCode,
974
+ },
975
+ });
976
+ }
966
977
  return withPatchMarkers({
967
978
  status: stream_1.StreamStatus.SUCCESS,
968
- code: err.code || new errors_1.DurableRetryError(err.message).code,
979
+ code: errorCode,
969
980
  metadata: { ...data.metadata },
970
981
  data: {
971
982
  $error: {
@@ -973,7 +984,7 @@ class WorkerService {
973
984
  type: err.name,
974
985
  name: err.name,
975
986
  stack: err.stack,
976
- code: err.code || new errors_1.DurableRetryError(err.message).code,
987
+ code: errorCode,
977
988
  },
978
989
  },
979
990
  });
@@ -1,4 +1,3 @@
1
- import { ILogger } from '../../../types/logger';
2
1
  /**
3
2
  * Provides cron-related utility functions based on the
4
3
  * [cron](https://en.wikipedia.org/wiki/Cron) format for use
@@ -8,16 +7,14 @@ import { ILogger } from '../../../types/logger';
8
7
  * Invoked in mapping rules using `{@cron.<method>}` syntax.
9
8
  */
10
9
  declare class CronHandler {
11
- static logger: ILogger;
12
10
  /**
13
- * Safely calculates the delay in seconds until the next execution
14
- * of a cron job. Calculates the next date and time (per the system
15
- * clock) that satisfies the cron expression, then returns the value
16
- * in seconds from now. Fails silently and returns `-1` if the cron
17
- * expression is invalid or the next execution time is in the past.
11
+ * Calculates the delay in seconds until the next execution
12
+ * of a cron job. Throws on invalid expressions rather than
13
+ * degrading silently.
18
14
  *
19
15
  * @param {string} cronExpression - The cron expression to parse (e.g. `'0 0 * * *'`)
20
- * @returns {number} The delay in seconds until the next cron job execution (minimum `HMSH_FIDELITY_SECONDS`), or `-1` on error
16
+ * @returns {number} The delay in seconds until the next cron job execution (minimum `HMSH_FIDELITY_SECONDS`)
17
+ * @throws {Error} If the cron expression is invalid
21
18
  * @example
22
19
  * ```yaml
23
20
  * cron_next_result:
@@ -4,7 +4,6 @@ exports.CronHandler = void 0;
4
4
  const cron_parser_1 = require("cron-parser");
5
5
  const enums_1 = require("../../../modules/enums");
6
6
  const utils_1 = require("../../../modules/utils");
7
- const logger_1 = require("../../logger");
8
7
  /**
9
8
  * Provides cron-related utility functions based on the
10
9
  * [cron](https://en.wikipedia.org/wiki/Cron) format for use
@@ -15,14 +14,13 @@ const logger_1 = require("../../logger");
15
14
  */
16
15
  class CronHandler {
17
16
  /**
18
- * Safely calculates the delay in seconds until the next execution
19
- * of a cron job. Calculates the next date and time (per the system
20
- * clock) that satisfies the cron expression, then returns the value
21
- * in seconds from now. Fails silently and returns `-1` if the cron
22
- * expression is invalid or the next execution time is in the past.
17
+ * Calculates the delay in seconds until the next execution
18
+ * of a cron job. Throws on invalid expressions rather than
19
+ * degrading silently.
23
20
  *
24
21
  * @param {string} cronExpression - The cron expression to parse (e.g. `'0 0 * * *'`)
25
- * @returns {number} The delay in seconds until the next cron job execution (minimum `HMSH_FIDELITY_SECONDS`), or `-1` on error
22
+ * @returns {number} The delay in seconds until the next cron job execution (minimum `HMSH_FIDELITY_SECONDS`)
23
+ * @throws {Error} If the cron expression is invalid
26
24
  * @example
27
25
  * ```yaml
28
26
  * cron_next_result:
@@ -32,28 +30,17 @@ class CronHandler {
32
30
  * ```
33
31
  */
34
32
  nextDelay(cronExpression) {
35
- try {
36
- if (!(0, utils_1.isValidCron)(cronExpression)) {
37
- return -1;
38
- }
39
- const interval = (0, cron_parser_1.parseExpression)(cronExpression, { utc: true });
40
- const nextDate = interval.next().toDate();
41
- const now = new Date();
42
- const delay = (nextDate.getTime() - now.getTime()) / 1000;
43
- if (delay <= 0) {
44
- return -1;
45
- }
46
- if (delay < enums_1.HMSH_FIDELITY_SECONDS) {
47
- return enums_1.HMSH_FIDELITY_SECONDS;
48
- }
49
- const iDelay = Math.round(delay);
50
- return iDelay;
33
+ if (!(0, utils_1.isValidCron)(cronExpression)) {
34
+ throw new Error(`Invalid cron expression: ${cronExpression}`);
51
35
  }
52
- catch (error) {
53
- CronHandler.logger.error('Error calculating next cron job execution delay:', { error });
54
- return -1;
36
+ const interval = (0, cron_parser_1.parseExpression)(cronExpression, { utc: true });
37
+ const nextDate = interval.next().toDate();
38
+ const now = new Date();
39
+ const delay = (nextDate.getTime() - now.getTime()) / 1000;
40
+ if (delay <= 0) {
41
+ return enums_1.HMSH_FIDELITY_SECONDS;
55
42
  }
43
+ return Math.max(Math.round(delay), enums_1.HMSH_FIDELITY_SECONDS);
56
44
  }
57
45
  }
58
46
  exports.CronHandler = CronHandler;
59
- CronHandler.logger = new logger_1.LoggerService('hotmesh', 'cron');
@@ -585,14 +585,23 @@ class ConsumptionManager {
585
585
  // Extract retry policy with priority:
586
586
  // 1. Use message-level _streamRetryConfig (from database columns or previous retry)
587
587
  // 2. Fall back to router-level retry (from worker config)
588
- const retry = input._streamRetryConfig
588
+ const streamRetryConfig = input._streamRetryConfig;
589
+ const retry = streamRetryConfig
589
590
  ? {
590
- maximumAttempts: input._streamRetryConfig.max_retry_attempts,
591
- backoffCoefficient: input._streamRetryConfig.backoff_coefficient,
592
- maximumInterval: input._streamRetryConfig.maximum_interval_seconds,
591
+ maximumAttempts: streamRetryConfig.max_retry_attempts,
592
+ backoffCoefficient: streamRetryConfig.backoff_coefficient,
593
+ maximumInterval: streamRetryConfig.maximum_interval_seconds,
594
+ initialInterval: streamRetryConfig.initialInterval ?? input.data?.initialInterval ?? 1,
593
595
  }
594
596
  : this.retry;
595
- return await this.errorHandler.handleRetry(input, output, this.publishMessage.bind(this), retry);
597
+ return await this.errorHandler.handleRetry(input, output, this.publishMessage.bind(this), retry, (topic, delayMs) => {
598
+ // Schedule a targeted NOTIFY so the consumer wakes up
599
+ // when the visibility-delayed retry message becomes visible
600
+ if (typeof this.stream.scheduleStreamNotify === 'function') {
601
+ const streamKey = this.stream.mintKey(key_1.KeyType.STREAMS, { topic });
602
+ this.stream.scheduleStreamNotify(streamKey, delayMs);
603
+ }
604
+ });
596
605
  }
597
606
  else if (typeof output.metadata !== 'object') {
598
607
  output.metadata = { ...input.metadata, guid: (0, utils_1.guid)() };
@@ -4,5 +4,5 @@ export declare class ErrorHandler {
4
4
  structureUnhandledError(input: StreamData, err: Error): StreamDataResponse;
5
5
  structureUnacknowledgedError(input: StreamData): StreamDataResponse;
6
6
  structureError(input: StreamData, output: StreamDataResponse): StreamDataResponse;
7
- handleRetry(input: StreamData, output: StreamDataResponse, publishMessage: (topic: string, streamData: StreamData | StreamDataResponse) => Promise<string>, retry?: RetryPolicy): Promise<string>;
7
+ handleRetry(input: StreamData, output: StreamDataResponse, publishMessage: (topic: string, streamData: StreamData | StreamDataResponse) => Promise<string>, retry?: RetryPolicy, onRetryScheduled?: (topic: string, delayMs: number) => void): Promise<string>;
8
8
  }
@@ -14,16 +14,11 @@ class ErrorHandler {
14
14
  const maxInterval = typeof retry.maximumInterval === 'string'
15
15
  ? parseInt(retry.maximumInterval)
16
16
  : (retry.maximumInterval || 120);
17
- // Check if we can retry (next attempt would be attempt #tryCount+2, must be <= maxAttempts)
18
- // tryCount=0 is 1st attempt, tryCount=1 is 2nd attempt, etc.
19
- // So after tryCount, we've made (tryCount + 1) attempts
20
- // We can retry if (tryCount + 1) < maxAttempts
17
+ const initialIntervalS = retry.initialInterval || 1;
21
18
  if ((tryCount + 1) < maxAttempts) {
22
- // Exponential backoff: min(coefficient^(try+1), maxInterval)
23
- // First retry (after try=0): coefficient^1
24
- // Second retry (after try=1): coefficient^2, etc.
25
- const backoffSeconds = Math.min(Math.pow(backoffCoeff, tryCount + 1), maxInterval);
26
- return [true, backoffSeconds * 1000]; // Convert to milliseconds
19
+ // Exponential backoff: min(initialInterval * coefficient^(try+1), maxInterval)
20
+ const backoffSeconds = Math.min(initialIntervalS * Math.pow(backoffCoeff, tryCount + 1), maxInterval);
21
+ return [true, backoffSeconds * 1000];
27
22
  }
28
23
  return [false, 0];
29
24
  }
@@ -101,7 +96,7 @@ class ErrorHandler {
101
96
  data,
102
97
  };
103
98
  }
104
- async handleRetry(input, output, publishMessage, retry) {
99
+ async handleRetry(input, output, publishMessage, retry, onRetryScheduled) {
105
100
  const [shouldRetry, timeout] = this.shouldRetry(input, output, retry);
106
101
  if (shouldRetry) {
107
102
  // Only sleep if no retry (legacy behavior for backward compatibility)
@@ -126,7 +121,13 @@ class ErrorHandler {
126
121
  // Track retry attempt count in database
127
122
  const currentAttempt = input._retryAttempt || 0;
128
123
  newMessage._retryAttempt = currentAttempt + 1;
129
- return (await publishMessage(input.metadata.topic, newMessage));
124
+ const messageId = (await publishMessage(input.metadata.topic, newMessage));
125
+ // Schedule a targeted NOTIFY so the consumer wakes up when
126
+ // the visibility-delayed message becomes visible
127
+ if (retry && timeout > 0 && onRetryScheduled) {
128
+ onRetryScheduled(input.metadata.topic, timeout);
129
+ }
130
+ return messageId;
130
131
  }
131
132
  else {
132
133
  const structuredError = this.structureError(input, output);
@@ -15,6 +15,10 @@ export declare const GET_JOB_ATTRIBUTES = "\n SELECT symbol || dimension AS fie
15
15
  * Matches all activity jobs for the given workflow and extracts their input arguments.
16
16
  */
17
17
  export declare const GET_ACTIVITY_INPUTS = "\n SELECT j.key, ja.value\n FROM {schema}.jobs j\n JOIN {schema}.jobs_attributes ja ON ja.job_id = j.id\n WHERE j.key LIKE $1\n AND ja.symbol = $2 AND ja.dimension = $3\n";
18
+ /**
19
+ * Fetch activity inputs from worker_streams for direct worker proxyer activities.
20
+ */
21
+ export declare const GET_PROXYER_STREAM_INPUTS = "\n SELECT message\n FROM {schema}.worker_streams\n WHERE jid = $1\n AND aid IN ('proxyer', 'collator_proxyer', 'signaler_proxyer')\n ORDER BY created_at, id\n";
18
22
  /**
19
23
  * Fetch all worker stream messages for a job AND its child activities.
20
24
  * Child activity jobs use the pattern: -{parentJobId}-$activityName-N
@@ -4,7 +4,7 @@
4
4
  * These queries support the exporter's input enrichment and direct query features.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.buildChildWorkflowInputsQuery = exports.GET_STREAM_HISTORY_BY_JID_AND_AID = exports.GET_STREAM_HISTORY_BY_JID_AND_TYPE = exports.GET_STREAM_HISTORY_BY_JID = exports.GET_ACTIVITY_INPUTS = exports.GET_JOB_ATTRIBUTES = exports.GET_JOB_BY_KEY = void 0;
7
+ exports.buildChildWorkflowInputsQuery = exports.GET_STREAM_HISTORY_BY_JID_AND_AID = exports.GET_STREAM_HISTORY_BY_JID_AND_TYPE = exports.GET_STREAM_HISTORY_BY_JID = exports.GET_PROXYER_STREAM_INPUTS = exports.GET_ACTIVITY_INPUTS = exports.GET_JOB_ATTRIBUTES = exports.GET_JOB_BY_KEY = void 0;
8
8
  /**
9
9
  * Fetch job record by key.
10
10
  */
@@ -34,6 +34,16 @@ exports.GET_ACTIVITY_INPUTS = `
34
34
  WHERE j.key LIKE $1
35
35
  AND ja.symbol = $2 AND ja.dimension = $3
36
36
  `;
37
+ /**
38
+ * Fetch activity inputs from worker_streams for direct worker proxyer activities.
39
+ */
40
+ exports.GET_PROXYER_STREAM_INPUTS = `
41
+ SELECT message
42
+ FROM {schema}.worker_streams
43
+ WHERE jid = $1
44
+ AND aid IN ('proxyer', 'collator_proxyer', 'signaler_proxyer')
45
+ ORDER BY created_at, id
46
+ `;
37
47
  /**
38
48
  * Fetch all worker stream messages for a job AND its child activities.
39
49
  * Child activity jobs use the pattern: -{parentJobId}-$activityName-N
@@ -1442,6 +1442,30 @@ class PostgresStoreService extends __1.StoreService {
1442
1442
  // Skip unparseable values
1443
1443
  }
1444
1444
  }
1445
+ // If no results from legacy approach, try direct worker approach:
1446
+ // extract arguments from proxyer messages in worker_streams
1447
+ if (byNameIndex.size === 0) {
1448
+ const { GET_PROXYER_STREAM_INPUTS } = await Promise.resolve().then(() => __importStar(require('./exporter-sql')));
1449
+ const streamSql = GET_PROXYER_STREAM_INPUTS.replace(/{schema}/g, schemaName);
1450
+ const streamResult = await this.pgClient.query(streamSql, [workflowId]);
1451
+ for (const row of streamResult.rows) {
1452
+ try {
1453
+ const msg = typeof row.message === 'string' ? JSON.parse(row.message) : row.message;
1454
+ const data = msg?.data;
1455
+ if (data?.activityName && data?.arguments) {
1456
+ const activityName = data.activityName;
1457
+ const wfId = data.workflowId || '';
1458
+ const idxMatch = wfId.match(/-(\d+)$/);
1459
+ const execIndex = idxMatch ? idxMatch[1] : '0';
1460
+ byNameIndex.set(`${activityName}:${execIndex}`, data.arguments);
1461
+ byJobId.set(wfId, data.arguments);
1462
+ }
1463
+ }
1464
+ catch {
1465
+ // Skip unparseable messages
1466
+ }
1467
+ }
1468
+ }
1445
1469
  return { byJobId, byNameIndex };
1446
1470
  }
1447
1471
  /**
@@ -58,6 +58,12 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
58
58
  * added to the transaction for atomic execution.
59
59
  */
60
60
  publishMessages(streamName: string, messages: string[], options?: PublishMessageConfig): Promise<string[] | ProviderTransaction>;
61
+ /**
62
+ * Schedule a NOTIFY for a worker stream after a delay. Used to wake up
63
+ * consumers when a visibility-delayed retry message becomes visible,
64
+ * avoiding the need to wait for the scout's fallback poll.
65
+ */
66
+ scheduleStreamNotify(streamName: string, delayMs: number): void;
61
67
  _publishMessages(streamName: string, messages: string[], options?: PublishMessageConfig): {
62
68
  sql: string;
63
69
  params: any[];
@@ -174,6 +174,31 @@ class PostgresStreamService extends index_1.StreamService {
174
174
  const target = this.resolveStreamTarget(streamName);
175
175
  return Messages.publishMessages(this.streamClient, target.tableName, target.streamName, target.isEngine, messages, options, this.logger);
176
176
  }
177
+ /**
178
+ * Schedule a NOTIFY for a worker stream after a delay. Used to wake up
179
+ * consumers when a visibility-delayed retry message becomes visible,
180
+ * avoiding the need to wait for the scout's fallback poll.
181
+ */
182
+ scheduleStreamNotify(streamName, delayMs) {
183
+ const target = this.resolveStreamTarget(streamName);
184
+ const prefix = target.isEngine ? 'eng_' : 'wrk_';
185
+ let channelName = `${prefix}${target.streamName}`;
186
+ if (channelName.length > 63) {
187
+ channelName = channelName.substring(0, 63);
188
+ }
189
+ const payload = JSON.stringify({
190
+ stream_name: target.streamName,
191
+ table_type: target.isEngine ? 'engine' : 'worker',
192
+ });
193
+ setTimeout(async () => {
194
+ try {
195
+ await this.streamClient.query(`SELECT pg_notify($1, $2)`, [channelName, payload]);
196
+ }
197
+ catch {
198
+ // Best-effort; the scout fallback will pick it up
199
+ }
200
+ }, delayMs);
201
+ }
177
202
  _publishMessages(streamName, messages, options) {
178
203
  const target = this.resolveStreamTarget(streamName);
179
204
  return Messages.buildPublishSQL(target.tableName, target.streamName, target.isEngine, messages, options);
@@ -320,11 +320,13 @@ class Virtual {
320
320
  if ((0, utils_1.isValidCron)(params.options.interval)) {
321
321
  //cron syntax
322
322
  cron = params.options.interval;
323
- const nextDelay = new cron_1.CronHandler().nextDelay(cron);
324
- delay = nextDelay > 0 ? nextDelay : undefined;
323
+ delay = new cron_1.CronHandler().nextDelay(cron);
325
324
  }
326
325
  else {
327
326
  const seconds = (0, utils_1.s)(params.options.interval);
327
+ if (isNaN(seconds)) {
328
+ throw new Error(`Invalid cron/interval expression: ${params.options.interval}`);
329
+ }
328
330
  interval = Math.max(seconds, enums_1.HMSH_FIDELITY_SECONDS);
329
331
  delay = params.options.delay ? (0, utils_1.s)(params.options.delay) : undefined;
330
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.14.7",
3
+ "version": "0.14.9",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",