11import { JetStreamClient , JetStreamError } from '@nats-io/jetstream' ;
2- import { errors } from '@nats-io/nats-core' ;
2+ import { errors , NatsConnection } from '@nats-io/nats-core' ;
33
44import { formatNatsError } from '../utils' ;
55import { store } from '../../store' ;
@@ -9,10 +9,13 @@ import i18n from '../i18n';
99interface Message {
1010 subject : string ;
1111 payload : any ;
12+ useJetStream ?: boolean ;
13+ retries ?: number ;
1214}
1315
1416// A Map where the key is the full, unique NATS subject.
1517type SubjectQueues = Map < string , Array < Message > > ;
18+ const MAX_RETRIES = 2 ; // Max number of retries for transient errors
1619
1720/**
1821 * A message queue that processes messages in independent, parallel queues
@@ -23,10 +26,10 @@ type SubjectQueues = Map<string, Array<Message>>;
2326export default class MessageQueue {
2427 private _isConnected : boolean = false ;
2528 private _js : JetStreamClient | undefined ;
29+ private _nc : NatsConnection | undefined ;
2630
2731 private readonly queues : SubjectQueues = new Map ( ) ;
2832 private readonly processingSubjects : Set < string > = new Set ( ) ;
29-
3033 private _isHoldingNotificationShown = false ;
3134
3235 public setIsConnected = ( value : boolean ) => {
@@ -42,6 +45,10 @@ export default class MessageQueue {
4245 this . _js = value ;
4346 } ;
4447
48+ public setNc = ( value : NatsConnection ) => {
49+ this . _nc = value ;
50+ } ;
51+
4552 public addToQueue = ( message : Message ) => {
4653 const { subject } = message ;
4754
@@ -65,62 +72,99 @@ export default class MessageQueue {
6572 }
6673
6774 this . processingSubjects . add ( subject ) ;
68-
6975 const request = subjectQueue [ 0 ] ;
7076
7177 try {
72- if ( ! this . _js ) {
73- return ;
78+ if ( request . useJetStream ) {
79+ // Assuming this._js is always available if _isConnected is true
80+ await this . _js ! . publish ( request . subject , request . payload ) ;
81+ } else {
82+ // Assuming this._nc is always available if _isConnected is true
83+ this . _nc ! . publish ( request . subject , request . payload ) ;
7484 }
7585
76- await this . _js . publish ( request . subject , request . payload ) ;
86+ // Message sent successfully.
7787 subjectQueue . shift ( ) ;
88+ this . processingSubjects . delete ( subject ) ;
7889 this . _isHoldingNotificationShown = false ;
90+ if ( subjectQueue . length > 0 ) {
91+ this . processSubjectQueue ( subject ) . then ( ) ;
92+ }
7993 } catch ( e : any ) {
94+ const formattedError = formatNatsError ( e ) ; // Format the error here
95+
96+ // Check for transient errors that are eligible for retry.
8097 if (
81- e instanceof errors . TimeoutError ||
82- e instanceof errors . NoRespondersError ||
83- e instanceof JetStreamError
98+ request . useJetStream && // Only retry JetStream messages
99+ ( e instanceof errors . TimeoutError ||
100+ e instanceof errors . NoRespondersError ||
101+ e instanceof JetStreamError )
84102 ) {
85- console . error (
86- `NATS transient error for subject ' ${ subject } ': ${ e . message } . Holding queue.` ,
87- ) ;
88- if ( e . message . includes ( 'connection draining' ) ) {
89- return ;
90- }
91- if ( ! this . _isHoldingNotificationShown ) {
92- const msg = formatNatsError ( e ) ;
103+ request . retries = ( request . retries || 0 ) + 1 ;
104+
105+ if ( request . retries > MAX_RETRIES ) {
106+ // Exceeded max retries, discard the message.
107+ console . error (
108+ `NATS message for subject ' ${ subject } ' failed after ${ MAX_RETRIES } retries. Discarding.` ,
109+ { error : formattedError , message : request } ,
110+ ) ;
93111 store . dispatch (
94112 addUserNotification ( {
95- message : i18n . t ( 'notifications.queue-holding-messages ' , {
96- error : msg ,
113+ message : i18n . t ( 'notifications.queue-discarded-message ' , {
114+ error : formattedError ,
97115 } ) ,
98- typeOption : 'warning ' ,
116+ typeOption : 'error ' ,
99117 } ) ,
100118 ) ;
101- this . _isHoldingNotificationShown = true ;
119+
120+ subjectQueue . shift ( ) ; // Discard
121+ this . processingSubjects . delete ( subject ) ;
122+ if ( subjectQueue . length > 0 ) {
123+ this . processSubjectQueue ( subject ) . then ( ) ;
124+ }
125+ } else {
126+ // Retry with a delay.
127+ console . warn (
128+ `NATS transient error for subject '${ subject } '. Retrying (${ request . retries } /${ MAX_RETRIES } )...` ,
129+ { error : formattedError } ,
130+ ) ;
131+ if ( ! this . _isHoldingNotificationShown ) {
132+ store . dispatch (
133+ addUserNotification ( {
134+ message : i18n . t ( 'notifications.queue-holding-messages' , {
135+ error : formattedError ,
136+ } ) ,
137+ typeOption : 'warning' ,
138+ } ) ,
139+ ) ;
140+ this . _isHoldingNotificationShown = true ;
141+ }
142+
143+ this . processingSubjects . delete ( subject ) ;
144+ setTimeout ( ( ) => {
145+ this . processSubjectQueue ( subject ) . then ( ) ;
146+ } , 500 * request . retries ) ; // Simple exponential backoff
102147 }
103- return ;
104148 } else {
149+ // This is a non-recoverable or non-JetStream error. Discard immediately.
105150 console . error (
106- `Found poison message for subject '${ subject } '. Discarding.` ,
107- { error : e . message , message : request } ,
151+ `Found poison message or non-retryable error for subject '${ subject } '. Discarding.` ,
152+ { error : formattedError , message : request } , // Use formatted error
108153 ) ;
109154 store . dispatch (
110155 addUserNotification ( {
111156 message : i18n . t ( 'notifications.queue-discarded-message' , {
112- error : e . message ,
157+ error : formattedError , // Use formatted error
113158 } ) ,
114159 typeOption : 'error' ,
115160 } ) ,
116161 ) ;
117- subjectQueue . shift ( ) ;
118- }
119- } finally {
120- this . processingSubjects . delete ( subject ) ;
121162
122- if ( subjectQueue . length > 0 ) {
123- this . processSubjectQueue ( subject ) . then ( ) ;
163+ subjectQueue . shift ( ) ; // Discard
164+ this . processingSubjects . delete ( subject ) ;
165+ if ( subjectQueue . length > 0 ) {
166+ this . processSubjectQueue ( subject ) . then ( ) ;
167+ }
124168 }
125169 }
126170 }
0 commit comments