@@ -70,6 +70,19 @@ export class FrameCryptor extends BaseFrameCryptor {
7070
7171 private isTransformActive : boolean = false ;
7272
73+ /**
74+ * Throttling mechanism for decryption errors to prevent memory leaks
75+ */
76+ private lastErrorTimestamp : Map < string , number > = new Map ( ) ;
77+
78+ private errorCounts : Map < string , number > = new Map ( ) ;
79+
80+ private readonly ERROR_THROTTLE_MS = 1000 ; // Emit error at most once per second
81+
82+ private readonly MAX_ERRORS_PER_MINUTE = 5 ; // Maximum errors to emit per minute per key
83+
84+ private readonly ERROR_WINDOW_MS = 60000 ; // 1 minute window
85+
7386 constructor ( opts : {
7487 keys : ParticipantKeyHandler ;
7588 participantIdentity : string ;
@@ -119,6 +132,8 @@ export class FrameCryptor extends BaseFrameCryptor {
119132 unsetParticipant ( ) {
120133 workerLogger . debug ( 'unsetting participant' , this . logContext ) ;
121134 this . participantIdentity = undefined ;
135+ this . lastErrorTimestamp = new Map ( ) ;
136+ this . errorCounts = new Map ( ) ;
122137 }
123138
124139 isEnabled ( ) {
@@ -210,6 +225,66 @@ export class FrameCryptor extends BaseFrameCryptor {
210225 this . sifTrailer = trailer ;
211226 }
212227
228+ /**
229+ * Checks if we should emit an error based on throttling rules to prevent memory leaks
230+ * @param errorKey - unique key identifying the error context
231+ * @returns true if the error should be emitted, false otherwise
232+ */
233+ private shouldEmitError ( errorKey : string ) : boolean {
234+ const now = Date . now ( ) ;
235+ const lastErrorTime = this . lastErrorTimestamp . get ( errorKey ) ?? 0 ;
236+ const errorCount = this . errorCounts . get ( errorKey ) ?? 0 ;
237+
238+ // Reset count if we're in a new time window
239+ if ( now - lastErrorTime > this . ERROR_WINDOW_MS ) {
240+ this . errorCounts . set ( errorKey , 0 ) ;
241+ this . lastErrorTimestamp . set ( errorKey , now ) ;
242+ return true ;
243+ }
244+
245+ // Check if we've exceeded the throttle time
246+ if ( now - lastErrorTime < this . ERROR_THROTTLE_MS ) {
247+ return false ;
248+ }
249+
250+ // Check if we've exceeded the max errors per window
251+ if ( errorCount >= this . MAX_ERRORS_PER_MINUTE ) {
252+ // Only log a warning once when hitting the limit
253+ if ( errorCount === this . MAX_ERRORS_PER_MINUTE ) {
254+ workerLogger . warn ( `Suppressing further decryption errors for ${ this . participantIdentity } ` , {
255+ ...this . logContext ,
256+ errorKey,
257+ } ) ;
258+ this . errorCounts . set ( errorKey , errorCount + 1 ) ;
259+ }
260+ return false ;
261+ }
262+
263+ // Update tracking
264+ this . lastErrorTimestamp . set ( errorKey , now ) ;
265+ this . errorCounts . set ( errorKey , errorCount + 1 ) ;
266+ return true ;
267+ }
268+
269+ /**
270+ * Emits a throttled error to prevent memory leaks from repeated decryption failures
271+ * @param error - the CryptorError to emit
272+ */
273+ private emitThrottledError ( error : CryptorError ) {
274+ const errorKey = `${ this . participantIdentity } -${ error . reason } -decrypt` ;
275+
276+ if ( this . shouldEmitError ( errorKey ) ) {
277+ const errorCount = this . errorCounts . get ( errorKey ) ?? 0 ;
278+ if ( errorCount > 1 ) {
279+ workerLogger . debug ( `Decryption error (${ errorCount } occurrences in window)` , {
280+ ...this . logContext ,
281+ reason : CryptorErrorReason [ error . reason ] ,
282+ } ) ;
283+ }
284+ this . emit ( CryptorEvent . Error , error ) ;
285+ }
286+ }
287+
213288 /**
214289 * Function that will be injected in a stream and will encrypt the given encoded frames.
215290 *
@@ -245,8 +320,7 @@ export class FrameCryptor extends BaseFrameCryptor {
245320 }
246321 const keySet = this . keys . getKeySet ( ) ;
247322 if ( ! keySet ) {
248- this . emit (
249- CryptorEvent . Error ,
323+ this . emitThrottledError (
250324 new CryptorError (
251325 `key set not found for ${
252326 this . participantIdentity
@@ -318,8 +392,7 @@ export class FrameCryptor extends BaseFrameCryptor {
318392 }
319393 } else {
320394 workerLogger . debug ( 'failed to encrypt, emitting error' , this . logContext ) ;
321- this . emit (
322- CryptorEvent . Error ,
395+ this . emitThrottledError (
323396 new CryptorError (
324397 `encryption key missing for encoding` ,
325398 CryptorErrorReason . MissingKey ,
@@ -379,7 +452,7 @@ export class FrameCryptor extends BaseFrameCryptor {
379452 if ( error instanceof CryptorError && error . reason === CryptorErrorReason . InvalidKey ) {
380453 // emit an error if the key handler thinks we have a valid key
381454 if ( this . keys . hasValidKey ) {
382- this . emit ( CryptorEvent . Error , error ) ;
455+ this . emitThrottledError ( error ) ;
383456 this . keys . decryptionFailure ( keyIndex ) ;
384457 }
385458 } else {
@@ -389,8 +462,7 @@ export class FrameCryptor extends BaseFrameCryptor {
389462 } else {
390463 // emit an error if the key index is out of bounds but the key handler thinks we still have a valid key
391464 workerLogger . warn ( `skipping decryption due to missing key at index ${ keyIndex } ` ) ;
392- this . emit (
393- CryptorEvent . Error ,
465+ this . emitThrottledError (
394466 new CryptorError (
395467 `missing key at index ${ keyIndex } for participant ${ this . participantIdentity } ` ,
396468 CryptorErrorReason . MissingKey ,
0 commit comments