import { type Either, type ErrorResolver } from '@lokalise/node-core'; import type { MessageInvalidFormatError, MessageValidationError, ProcessedMessageMetadata, ResolvedMessage } from '@message-queue-toolkit/core'; import { type BarrierResult, type MessageSchemaContainer, type PreHandlingOutputs, type Prehandler, type QueueConsumer, type QueueConsumerDependencies, type QueueConsumerOptions } from '@message-queue-toolkit/core'; import type { PubSubMessage } from '../types/MessageTypes.ts'; import type { PubSubCreationConfig, PubSubDependencies, PubSubQueueLocatorType } from './AbstractPubSubService.ts'; import { AbstractPubSubService } from './AbstractPubSubService.ts'; /** * Configuration options for subscription-level error retry behavior. * This handles transient errors that occur at the subscription level * (e.g., NOT_FOUND, PERMISSION_DENIED after Terraform deployments). */ export type SubscriptionRetryOptions = { /** * Maximum number of retry attempts before giving up. * @default 5 */ maxRetries?: number; /** * Base delay in milliseconds for exponential backoff. * Actual delay = min(baseRetryDelayMs * 2^attempt, maxRetryDelayMs) * @default 1000 */ baseRetryDelayMs?: number; /** * Maximum delay in milliseconds between retries. * @default 30000 */ maxRetryDelayMs?: number; }; export type PubSubDeadLetterQueueOptions = { deadLetterPolicy: { maxDeliveryAttempts: number; }; creationConfig?: { topic: { name: string; }; }; locatorConfig?: { topicName: string; }; }; export type PubSubConsumerDependencies = PubSubDependencies & QueueConsumerDependencies; export type PubSubConsumerOptions = QueueConsumerOptions & { consumerOverrides?: { flowControl?: { maxMessages?: number; maxBytes?: number; }; batching?: { maxMessages?: number; maxMilliseconds?: number; }; }; /** * Configuration for subscription-level error retry behavior. * Handles transient errors like NOT_FOUND and PERMISSION_DENIED * that can occur after Terraform deployments due to GCP's eventual consistency. */ subscriptionRetryOptions?: SubscriptionRetryOptions; }; export declare abstract class AbstractPubSubConsumer = PubSubConsumerOptions> extends AbstractPubSubService implements QueueConsumer { private readonly transactionObservabilityManager?; private readonly consumerOverrides; private readonly handlerContainer; private readonly deadLetterQueueOptions?; private readonly isDeduplicationEnabled; private readonly subscriptionRetryOptions; private maxRetryDuration; private isConsuming; private isReinitializing; private _fatalError; protected readonly errorResolver: ErrorResolver; protected readonly executionContext: ExecutionContext; dlqTopicName?: string; readonly _messageSchemaContainer: MessageSchemaContainer; /** * Returns the fatal error that caused the consumer to stop, or null if healthy. * Use this in health checks to detect permanent subscription failures. * * @example * ```typescript * app.get('/health', (req, res) => { * const error = consumer.fatalError * if (error) { * return res.status(503).json({ status: 'unhealthy', error: error.message }) * } * return res.status(200).json({ status: 'healthy' }) * }) * ``` */ get fatalError(): Error | null; protected constructor(dependencies: PubSubConsumerDependencies, options: ConsumerOptionsType, executionContext: ExecutionContext); init(): Promise; start(): Promise; /** * Initializes the consumer with retry logic for transient errors. * * This handles eventual consistency issues that can occur after Terraform deployments * where topics/subscriptions may not be immediately visible across all GCP endpoints. * * @param attempt - Current retry attempt number (1-based) */ private initWithRetry; /** * Sets up event handlers for the subscription. * Extracted to allow reattachment after reinitialization. */ private setupSubscriptionEventHandlers; /** * Handles subscription-level errors. * * For retryable errors (NOT_FOUND, PERMISSION_DENIED), attempts to reinitialize * the subscription with exponential backoff. These errors commonly occur after * Terraform deployments due to GCP's eventual consistency. * * @see https://cloud.google.com/pubsub/docs/reference/error-codes */ private handleSubscriptionError; /** * Handles unexpected subscription close events. * * If the subscription closes while we're still supposed to be consuming, * attempts to reinitialize. This can happen due to: * - Network issues * - GCP service restarts * - Subscription deletion/recreation */ private handleSubscriptionClose; /** * Reinitializes the subscription with exponential backoff retry. * * This method: * 1. Closes the existing subscription (if any) * 2. Waits with exponential backoff * 3. Reinitializes the subscription * 4. Reattaches event handlers * * Uses an iterative loop to keep isReinitializing true for the entire * retry sequence, preventing concurrent callers from starting their own * reinitialization attempts. * * @param startAttempt - Starting retry attempt number (1-based) */ private reinitializeWithRetry; private waitForSubscriptionReady; close(): Promise; private handleMessage; private internalProcessMessage; protected resolveMessage(message: PubSubMessage): Either; protected resolveSchema(messagePayload: MessagePayloadType, messageAttributes?: Record): Either>; protected processMessage(message: MessagePayloadType, messageType: string, preHandlingOutputs: PreHandlingOutputs): Promise>; protected processPrehandlers(message: MessagePayloadType, messageType: string): Promise; protected preHandlerBarrier(message: MessagePayloadType, messageType: string, preHandlerOutput: PrehandlerOutput): Promise>; protected resolveNextFunction(preHandlers: Prehandler[], message: MessagePayloadType, index: number, preHandlerOutput: PrehandlerOutput, resolve: (value: PrehandlerOutput | PromiseLike) => void, reject: (err: Error) => void): (preHandlerResult: import("@message-queue-toolkit/core").PrehandlerResult) => void; protected resolveMessageLog(processedMessageMetadata: ProcessedMessageMetadata): unknown | null; protected isDeduplicationEnabledForMessage(message: MessagePayloadType): boolean; private isRetryDateExceeded; /** * Handles terminal errors by either nacking (if DLQ is configured) or acking (if no DLQ). * When no DLQ is configured, acking prevents infinite redelivery of unprocessable messages. */ private handleTerminalError; }