@hotmeshio/hotmesh 0.14.6 → 0.14.8

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,4 @@
1
- import { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_RESERVATION_TIMEOUT_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD } from '../../../modules/enums';
1
+ import { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_BATCH_SIZE_MIN, HMSH_RESERVATION_TIMEOUT_S, HMSH_RESERVATION_TIMEOUT_MAX_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD } from '../../../modules/enums';
2
2
  import { RouterConfig } from '../../../types/stream';
3
3
  export declare class RouterConfigManager {
4
4
  static validateThrottle(delayInMillis: number): void;
@@ -8,4 +8,4 @@ export declare class RouterConfigManager {
8
8
  readonly: boolean;
9
9
  };
10
10
  }
11
- export { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_RESERVATION_TIMEOUT_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD, };
11
+ export { HMSH_BLOCK_TIME_MS, HMSH_MAX_RETRIES, HMSH_MAX_TIMEOUT_MS, HMSH_GRADUATED_INTERVAL_MS, HMSH_CODE_UNACKED, HMSH_CODE_UNKNOWN, HMSH_STATUS_UNKNOWN, HMSH_XCLAIM_COUNT, HMSH_XCLAIM_DELAY_MS, HMSH_XPENDING_COUNT, HMSH_BATCH_SIZE, HMSH_BATCH_SIZE_MIN, HMSH_RESERVATION_TIMEOUT_S, HMSH_RESERVATION_TIMEOUT_MAX_S, MAX_DELAY, MAX_STREAM_BACKOFF, INITIAL_STREAM_BACKOFF, MAX_STREAM_RETRIES, HMSH_POISON_MESSAGE_THRESHOLD, };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.MAX_DELAY = exports.HMSH_RESERVATION_TIMEOUT_S = exports.HMSH_BATCH_SIZE = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_XCLAIM_COUNT = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_UNACKED = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_BLOCK_TIME_MS = exports.RouterConfigManager = void 0;
3
+ exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.MAX_DELAY = exports.HMSH_RESERVATION_TIMEOUT_MAX_S = exports.HMSH_RESERVATION_TIMEOUT_S = exports.HMSH_BATCH_SIZE_MIN = exports.HMSH_BATCH_SIZE = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_XCLAIM_COUNT = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_UNACKED = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_BLOCK_TIME_MS = exports.RouterConfigManager = void 0;
4
4
  const enums_1 = require("../../../modules/enums");
5
5
  Object.defineProperty(exports, "HMSH_BLOCK_TIME_MS", { enumerable: true, get: function () { return enums_1.HMSH_BLOCK_TIME_MS; } });
6
6
  Object.defineProperty(exports, "HMSH_MAX_RETRIES", { enumerable: true, get: function () { return enums_1.HMSH_MAX_RETRIES; } });
@@ -13,7 +13,9 @@ Object.defineProperty(exports, "HMSH_XCLAIM_COUNT", { enumerable: true, get: fun
13
13
  Object.defineProperty(exports, "HMSH_XCLAIM_DELAY_MS", { enumerable: true, get: function () { return enums_1.HMSH_XCLAIM_DELAY_MS; } });
14
14
  Object.defineProperty(exports, "HMSH_XPENDING_COUNT", { enumerable: true, get: function () { return enums_1.HMSH_XPENDING_COUNT; } });
15
15
  Object.defineProperty(exports, "HMSH_BATCH_SIZE", { enumerable: true, get: function () { return enums_1.HMSH_BATCH_SIZE; } });
16
+ Object.defineProperty(exports, "HMSH_BATCH_SIZE_MIN", { enumerable: true, get: function () { return enums_1.HMSH_BATCH_SIZE_MIN; } });
16
17
  Object.defineProperty(exports, "HMSH_RESERVATION_TIMEOUT_S", { enumerable: true, get: function () { return enums_1.HMSH_RESERVATION_TIMEOUT_S; } });
18
+ Object.defineProperty(exports, "HMSH_RESERVATION_TIMEOUT_MAX_S", { enumerable: true, get: function () { return enums_1.HMSH_RESERVATION_TIMEOUT_MAX_S; } });
17
19
  Object.defineProperty(exports, "MAX_DELAY", { enumerable: true, get: function () { return enums_1.MAX_DELAY; } });
18
20
  Object.defineProperty(exports, "MAX_STREAM_BACKOFF", { enumerable: true, get: function () { return enums_1.MAX_STREAM_BACKOFF; } });
19
21
  Object.defineProperty(exports, "INITIAL_STREAM_BACKOFF", { enumerable: true, get: function () { return enums_1.INITIAL_STREAM_BACKOFF; } });
@@ -27,19 +27,20 @@ export declare class ConsumptionManager<S extends StreamService<ProviderClient,
27
27
  private router;
28
28
  private retry;
29
29
  private adaptiveReservationTimeout;
30
+ private adaptiveBatchSize;
30
31
  private lastDepthCheckAt;
31
32
  private static readonly DEPTH_CHECK_INTERVAL_MS;
32
33
  private static readonly DEPTH_SCALE_UP_THRESHOLD;
33
34
  private static readonly DEPTH_SCALE_DOWN_THRESHOLD;
34
- private static readonly RESERVATION_TIMEOUT_MAX_S;
35
35
  constructor(stream: S, logger: ILogger, throttleManager: ThrottleManager, errorHandler: ErrorHandler, lifecycleManager: LifecycleManager<S>, reclaimDelay: number, reclaimCount: number, appId: string, role: any, router: any, retry?: import('../../../types/stream').RetryPolicy);
36
36
  /**
37
37
  * Adjusts reservation timeout based on stream depth. Called periodically
38
- * from the consume loop. When depth is high, messages take longer to
39
- * process, so the reservation window must grow to prevent re-delivery.
40
- * When depth drops, the timeout shrinks back toward the configured default.
38
+ * from the consume loop. When depth is high:
39
+ * - reservation timeout grows (prevents duplicate re-reservation)
40
+ * - batch size shrinks (reduces in-memory blocking, shares the stream)
41
+ * When depth drops, both restore toward configured defaults.
41
42
  */
42
- private adjustReservationTimeout;
43
+ private adjustConsumptionPressure;
43
44
  createGroup(stream: string, group: string): Promise<void>;
44
45
  publishMessage(topic: string, streamData: StreamData | StreamDataResponse, transaction?: ProviderTransaction): Promise<string | ProviderTransaction>;
45
46
  consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
@@ -17,10 +17,13 @@ class ConsumptionManager {
17
17
  get hasReachedMaxBackoff() { return this.router.hasReachedMaxBackoff; }
18
18
  set hasReachedMaxBackoff(v) { this.router.hasReachedMaxBackoff = v; }
19
19
  constructor(stream, logger, throttleManager, errorHandler, lifecycleManager, reclaimDelay, reclaimCount, appId, role, router, retry) {
20
- // Adaptive reservation timeout — scales with stream depth to prevent
21
- // duplicate message delivery under load. When the stream backs up,
22
- // processing takes longer, so the reservation window must grow.
20
+ // Adaptive consumption pressure — scales reservation timeout AND batch
21
+ // size based on stream depth. Under load: timeout grows (prevents
22
+ // duplicate re-reservation) and batch size shrinks (reduces in-memory
23
+ // blocking, lets other consumers share the stream). When idle, both
24
+ // restore toward configured defaults.
23
25
  this.adaptiveReservationTimeout = config_1.HMSH_RESERVATION_TIMEOUT_S;
26
+ this.adaptiveBatchSize = config_1.HMSH_BATCH_SIZE;
24
27
  this.lastDepthCheckAt = 0;
25
28
  this.stream = stream;
26
29
  this.logger = logger;
@@ -36,11 +39,12 @@ class ConsumptionManager {
36
39
  }
37
40
  /**
38
41
  * Adjusts reservation timeout based on stream depth. Called periodically
39
- * from the consume loop. When depth is high, messages take longer to
40
- * process, so the reservation window must grow to prevent re-delivery.
41
- * When depth drops, the timeout shrinks back toward the configured default.
42
+ * from the consume loop. When depth is high:
43
+ * - reservation timeout grows (prevents duplicate re-reservation)
44
+ * - batch size shrinks (reduces in-memory blocking, shares the stream)
45
+ * When depth drops, both restore toward configured defaults.
42
46
  */
43
- async adjustReservationTimeout(stream) {
47
+ async adjustConsumptionPressure(stream) {
44
48
  const now = Date.now();
45
49
  if (now - this.lastDepthCheckAt < ConsumptionManager.DEPTH_CHECK_INTERVAL_MS) {
46
50
  return;
@@ -48,27 +52,37 @@ class ConsumptionManager {
48
52
  this.lastDepthCheckAt = now;
49
53
  try {
50
54
  const depth = await this.stream.getStreamDepth(stream);
51
- const prev = this.adaptiveReservationTimeout;
55
+ const prevTimeout = this.adaptiveReservationTimeout;
56
+ const prevBatch = this.adaptiveBatchSize;
52
57
  if (depth > ConsumptionManager.DEPTH_SCALE_UP_THRESHOLD) {
53
- // Scale up: double the timeout, capped at max
54
- this.adaptiveReservationTimeout = Math.min(this.adaptiveReservationTimeout * 2, ConsumptionManager.RESERVATION_TIMEOUT_MAX_S);
58
+ // Scale up timeout, scale down batch size
59
+ this.adaptiveReservationTimeout = Math.min(this.adaptiveReservationTimeout * 2, config_1.HMSH_RESERVATION_TIMEOUT_MAX_S);
60
+ this.adaptiveBatchSize = Math.max(Math.floor(this.adaptiveBatchSize / 2), config_1.HMSH_BATCH_SIZE_MIN);
55
61
  }
56
62
  else if (depth < ConsumptionManager.DEPTH_SCALE_DOWN_THRESHOLD) {
57
- // Scale down: halve toward the configured default
63
+ // Scale down timeout, scale up batch size
58
64
  this.adaptiveReservationTimeout = Math.max(Math.floor(this.adaptiveReservationTimeout / 2), config_1.HMSH_RESERVATION_TIMEOUT_S);
65
+ this.adaptiveBatchSize = Math.min(this.adaptiveBatchSize * 2, config_1.HMSH_BATCH_SIZE);
59
66
  }
60
- if (this.adaptiveReservationTimeout !== prev) {
61
- // Update the stream provider so notification-path fetches
62
- // also use the adaptive timeout
67
+ if (this.adaptiveReservationTimeout !== prevTimeout) {
63
68
  this.stream.reservationTimeout = this.adaptiveReservationTimeout;
64
69
  this.logger.info('stream-reservation-timeout-adjusted', {
65
70
  stream,
66
71
  depth,
67
- previousTimeoutS: prev,
72
+ previousTimeoutS: prevTimeout,
68
73
  newTimeoutS: this.adaptiveReservationTimeout,
69
74
  configuredDefaultS: config_1.HMSH_RESERVATION_TIMEOUT_S,
70
75
  });
71
76
  }
77
+ if (this.adaptiveBatchSize !== prevBatch) {
78
+ this.logger.info('stream-batch-size-adjusted', {
79
+ stream,
80
+ depth,
81
+ previousBatchSize: prevBatch,
82
+ newBatchSize: this.adaptiveBatchSize,
83
+ configuredDefaultBatchSize: config_1.HMSH_BATCH_SIZE,
84
+ });
85
+ }
72
86
  }
73
87
  catch {
74
88
  // Stream depth check is best-effort; don't fail the consume loop
@@ -153,7 +167,7 @@ class ConsumptionManager {
153
167
  return;
154
168
  }
155
169
  // Adapt reservation timeout based on stream depth
156
- await this.adjustReservationTimeout(stream);
170
+ await this.adjustConsumptionPressure(stream);
157
171
  await this.throttleManager.customSleep(); // respect throttle
158
172
  if (this.lifecycleManager.isStopped(group, consumer, stream) ||
159
173
  this.throttleManager.isPaused()) {
@@ -274,12 +288,12 @@ class ConsumptionManager {
274
288
  try {
275
289
  let messages = [];
276
290
  // Adapt reservation timeout based on stream depth
277
- await this.adjustReservationTimeout(stream);
291
+ await this.adjustConsumptionPressure(stream);
278
292
  if (!this.hasReachedMaxBackoff) {
279
293
  // Normal mode: try with backoff and finite retries
280
294
  const features = this.stream.getProviderSpecificFeatures();
281
295
  const isPostgres = features.supportsParallelProcessing;
282
- const batchSize = isPostgres ? config_1.HMSH_BATCH_SIZE : 1;
296
+ const batchSize = isPostgres ? this.adaptiveBatchSize : 1;
283
297
  messages = await this.stream.consumeMessages(stream, group, consumer, {
284
298
  blockTimeout: streamDuration,
285
299
  batchSize,
@@ -294,7 +308,7 @@ class ConsumptionManager {
294
308
  // Fallback mode: just try once, no backoff
295
309
  const features = this.stream.getProviderSpecificFeatures();
296
310
  const isPostgres = features.supportsParallelProcessing;
297
- const batchSize = isPostgres ? config_1.HMSH_BATCH_SIZE : 1;
311
+ const batchSize = isPostgres ? this.adaptiveBatchSize : 1;
298
312
  messages = await this.stream.consumeMessages(stream, group, consumer, {
299
313
  blockTimeout: streamDuration,
300
314
  batchSize,
@@ -571,14 +585,23 @@ class ConsumptionManager {
571
585
  // Extract retry policy with priority:
572
586
  // 1. Use message-level _streamRetryConfig (from database columns or previous retry)
573
587
  // 2. Fall back to router-level retry (from worker config)
574
- const retry = input._streamRetryConfig
588
+ const streamRetryConfig = input._streamRetryConfig;
589
+ const retry = streamRetryConfig
575
590
  ? {
576
- maximumAttempts: input._streamRetryConfig.max_retry_attempts,
577
- backoffCoefficient: input._streamRetryConfig.backoff_coefficient,
578
- 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,
579
595
  }
580
596
  : this.retry;
581
- 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
+ });
582
605
  }
583
606
  else if (typeof output.metadata !== 'object') {
584
607
  output.metadata = { ...input.metadata, guid: (0, utils_1.guid)() };
@@ -597,5 +620,4 @@ class ConsumptionManager {
597
620
  ConsumptionManager.DEPTH_CHECK_INTERVAL_MS = 10000;
598
621
  ConsumptionManager.DEPTH_SCALE_UP_THRESHOLD = 100;
599
622
  ConsumptionManager.DEPTH_SCALE_DOWN_THRESHOLD = 10;
600
- ConsumptionManager.RESERVATION_TIMEOUT_MAX_S = 600;
601
623
  exports.ConsumptionManager = ConsumptionManager;
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.14.6",
3
+ "version": "0.14.8",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",