@znight/messaging 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/async-CFh_bvex.mjs +216 -0
- package/dist/async-CFh_bvex.mjs.map +1 -0
- package/dist/codec-Dku-NXXU.mjs +83 -0
- package/dist/codec-Dku-NXXU.mjs.map +1 -0
- package/dist/compiler.d.mts +75 -0
- package/dist/compiler.mjs +1023 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/driver-ZxLqy_Ap.d.mts +216 -0
- package/dist/index.d.mts +459 -0
- package/dist/index.mjs +870 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kafka.d.mts +51 -0
- package/dist/kafka.mjs +517 -0
- package/dist/kafka.mjs.map +1 -0
- package/dist/memory.d.mts +124 -0
- package/dist/memory.mjs +429 -0
- package/dist/memory.mjs.map +1 -0
- package/dist/rabbitmq.d.mts +65 -0
- package/dist/rabbitmq.mjs +686 -0
- package/dist/rabbitmq.mjs.map +1 -0
- package/dist/shared-BJBpLS8V.mjs +52 -0
- package/dist/shared-BJBpLS8V.mjs.map +1 -0
- package/dist/sqs.d.mts +38 -0
- package/dist/sqs.mjs +421 -0
- package/dist/sqs.mjs.map +1 -0
- package/package.json +123 -4
- package/src/broker.ts +156 -0
- package/src/codec.ts +98 -0
- package/src/compiler/analyze.ts +800 -0
- package/src/compiler/constants.ts +73 -0
- package/src/compiler/data.ts +75 -0
- package/src/compiler/generate.ts +131 -0
- package/src/compiler/index.ts +52 -0
- package/src/compiler/patterns.ts +54 -0
- package/src/compiler/retry.ts +136 -0
- package/src/compiler/validate.ts +57 -0
- package/src/compiler/values.ts +76 -0
- package/src/consumers.ts +260 -0
- package/src/decorators.ts +131 -0
- package/src/driver.ts +142 -0
- package/src/drivers/kafka.ts +765 -0
- package/src/drivers/memory.ts +609 -0
- package/src/drivers/rabbitmq.ts +944 -0
- package/src/drivers/shared.ts +81 -0
- package/src/drivers/sqs.ts +583 -0
- package/src/errors.ts +95 -0
- package/src/headers.ts +70 -0
- package/src/hooks.ts +2 -0
- package/src/index.ts +89 -0
- package/src/internal/async.ts +115 -0
- package/src/message.ts +96 -0
- package/src/metadata.ts +65 -0
- package/src/plugin.ts +236 -0
- package/src/retry.ts +129 -0
- package/src/runtime.ts +424 -0
- package/src/standard-schema.ts +68 -0
- package/src/tokens.ts +14 -0
- package/src/validation.ts +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rabbitmq.mjs","names":["amqpConnect"],"sources":["../src/drivers/rabbitmq.ts"],"sourcesContent":["/**\n * `@znight/messaging/rabbitmq`: RabbitMQ driver built on [amqplib](https://amqp-node.github.io/amqplib/)\n * 2.x (optional peer dependency).\n *\n * - Publishes through a confirm channel (`publish()` resolves on broker confirmation) with\n * `mandatory` routing: publishing to a queue that does not exist fails instead of dropping the message.\n * - One channel per consumer, `prefetch = concurrency`, manual acknowledgements.\n * - Retries are republished copies carrying `x-znight-attempt`: immediately to the queue, or through a\n * delay queue (`<queue>.delay.<ms>`, queue-level TTL dead-lettering back into the queue: no\n * head-of-line blocking between delays). The original is acknowledged once the copy is confirmed.\n * - Delayed publications go through a fixed set of 32 delay levels shared by every queue\n * (`znight.delays.<n>`: a topic exchange and a queue with a TTL of 2^n ms each). The delay is\n * written in binary in the routing key: the message waits in the level of each set bit, then the last\n * level dead-letters it to the default exchange, which routes it to its queue (`CC` header).\n * - Attempts = `x-znight-attempt` + broker redeliveries (`x-delivery-count` of quorum queues, or the\n * `redelivered` flag of classic queues).\n * - Dead letters are published (confirmed) to `<queue>.dlq`, then the original is acknowledged; without\n * dead-letter queue the message is rejected (`nack` without requeue), so a queue DLX policy applies.\n * - Declares the queues it uses (`assertTopology`, default) or verifies they exist. On RabbitMQ >= 4,\n * quorum queues are declared with `x-delivery-limit: -1`: Znight counts the attempts itself, the\n * broker's default limit (20 deliveries) would drop messages released repeatedly.\n * - Recovers from connection losses (reconnection with exponential backoff), closed consumer channels\n * and consumers cancelled by the broker: every subscription is resumed, with backoff, until it\n * consumes again.\n */\nimport type {\n Channel,\n ChannelModel,\n ConfirmChannel,\n ConsumeMessage,\n Message,\n MessageProperties,\n Options,\n SocketOptions,\n} from 'amqplib'\nimport type {\n DeadLetterRequest,\n DeliveryHandler,\n DriverCapabilities,\n DriverContext,\n DriverDelivery,\n MessagingDriver,\n RetryRequest,\n SubscribeOptions,\n Subscription,\n} from '../driver.ts'\nimport type { OutboundMessage } from '../message.ts'\n\nimport { connect as amqpConnect } from 'amqplib'\n\nimport { brokerUnavailableError, consumerError } from '../errors.ts'\nimport { attemptFromHeaders, MessagingHeaders } from '../headers.ts'\nimport { errorMessage, sleep, truncate } from '../internal/async.ts'\nimport { durationOption, MAX_ERROR_HEADER_LENGTH, readHeader, toHeaderRecord } from './shared.ts'\n\n/** Reconnection settings. */\nexport interface RabbitMQReconnectOptions {\n /** First delay before reconnecting. Default: 100 ms. */\n readonly initialDelayMs?: number\n /** Longest delay between attempts. Default: 30 000 ms. */\n readonly maxDelayMs?: number\n}\n\n/** Options of {@link rabbitmq}. */\nexport interface RabbitMQDriverOptions {\n /** `amqp://user:pass@host:5672/vhost` or amqplib connection options. */\n readonly url: string | Options.Connect\n /** Socket / TLS options passed to amqplib. */\n readonly socketOptions?: SocketOptions\n /**\n * Declare the queues used by consumers (consumed, dead-letter and delay queues) and by delayed\n * publications. When `false`, consumed and dead-letter queues must exist (verified at subscribe).\n * Default: `true`.\n */\n readonly assertTopology?: boolean\n /** Type of the declared queues. Default: `quorum` (replicated, native `x-delivery-count`). */\n readonly queueType?: 'quorum' | 'classic'\n /**\n * Extra arguments of declared consumed and dead-letter queues (`x-max-length`...). Quorum queues get\n * `x-delivery-limit: -1` on RabbitMQ >= 4 unless this sets `x-delivery-limit`.\n */\n readonly queueArguments?: Readonly<Record<string, unknown>>\n /** Longest retry delay waited in-process (holding the message). Default: 5000 ms. */\n readonly maxInProcessDelayMs?: number\n /** Name of the retry delay queue of `queue` for `delayMs`. Default: `${queue}.delay.${delayMs}`. */\n readonly delayQueueName?: (queue: string, delayMs: number) => string\n /**\n * Prefix of the delay levels of delayed publications: `<prefix>.0` to `<prefix>.31` (one topic\n * exchange and one queue each, shared by every queue of the virtual host). Default: `znight.delays`.\n */\n readonly delayLevelPrefix?: string\n /** Default dead-letter queue name. Default: `${queue}.dlq`. */\n readonly deadLetterName?: (queue: string) => string\n /** Publish with `mandatory`: unroutable messages make `publish()` fail. Default: `true`. */\n readonly mandatory?: boolean\n /** Persistent messages (delivery mode 2). Default: `true`. */\n readonly persistent?: boolean\n /** Use the routing key as message type when the message has none (topic exchanges). Default: `false`. */\n readonly typeFromRoutingKey?: boolean\n /** Reconnect after a connection loss. `false` disables. Default: enabled. */\n readonly reconnect?: RabbitMQReconnectOptions | false\n /** Connection name shown in the management UI. Default: `znight`. */\n readonly connectionName?: string\n}\n\n/** Largest TTL RabbitMQ accepts (2^32 - 1 ms). */\nconst MAX_TTL_MS = 4_294_967_295\nconst DEFAULT_IN_PROCESS_DELAY_MS = 5000\nconst DEFAULT_DELAY_LEVEL_PREFIX = 'znight.delays'\n/** Delay levels of delayed publications: level n waits 2^n ms, all of them together `MAX_TTL_MS`. */\nconst DELAY_LEVELS = 32\n/** First word of the routing key of a delayed publication, followed by one bit per level (highest first). */\nconst DELAY_ROUTING_WORD = 'znight-delay'\n/**\n * Headers never copied to republished messages (retries, dead letters): the ones managed by the broker,\n * and the sender-selected destinations (`CC`, `BCC`), which would route the copy to other queues.\n */\nconst UNCOPIED_HEADERS = new Set([\n 'x-delivery-count',\n 'x-death',\n 'x-first-death-exchange',\n 'x-first-death-queue',\n 'x-first-death-reason',\n 'x-last-death-exchange',\n 'x-last-death-queue',\n 'x-last-death-reason',\n 'CC',\n 'BCC',\n])\n\n/** Creates a {@link RabbitMQDriver}. */\nexport function rabbitmq(options: RabbitMQDriverOptions): RabbitMQDriver {\n return new RabbitMQDriver(options)\n}\n\ninterface PendingConfirm {\n returned: boolean\n}\n\ninterface ConsumerSubscription {\n readonly queue: string\n readonly handler: DeliveryHandler\n readonly options: SubscribeOptions\n channel: Channel | undefined\n consumerTag: string | undefined\n active: boolean\n /** In-progress (re)subscription. */\n consuming: Promise<void> | undefined\n /** The broker cancelled the consumer (queue deleted...): declare the queues again before consuming. */\n redeclare: boolean\n}\n\n/** RabbitMQ {@link MessagingDriver}. See the module documentation. */\nexport class RabbitMQDriver implements MessagingDriver {\n readonly name = 'rabbitmq'\n readonly capabilities: DriverCapabilities\n readonly #options: RabbitMQDriverOptions\n readonly #queueType: 'quorum' | 'classic'\n readonly #assertTopology: boolean\n readonly #subscriptions = new Set<ConsumerSubscription>()\n /** Retry delay queues already declared, by name. */\n readonly #delayQueues = new Map<string, Promise<void>>()\n /** Publications awaiting their confirmation, by destination and message id (to detect returned messages). */\n readonly #pending = new Map<string, PendingConfirm[]>()\n readonly #abort = new AbortController()\n #logger: DriverContext['logger'] = { debug() {}, info() {}, warn() {}, error() {} }\n #model: ChannelModel | undefined\n #publisher: Promise<ConfirmChannel> | undefined\n /** Declaration of the delay levels on the current connection. */\n #delayLevels: Promise<void> | undefined\n #closing = false\n /** Running recovery loop (reconnection and resubscriptions). */\n #recovering: Promise<void> | undefined\n\n constructor(options: RabbitMQDriverOptions) {\n if (options === null || typeof options !== 'object' || options.url === undefined) {\n throw new TypeError('[znight] rabbitmq(): the \"url\" option is required')\n }\n if (\n options.delayLevelPrefix !== undefined &&\n (typeof options.delayLevelPrefix !== 'string' || options.delayLevelPrefix === '')\n ) {\n throw new TypeError('[znight] rabbitmq(): \"delayLevelPrefix\" must be a non-empty string')\n }\n this.#options = options\n this.#queueType = options.queueType ?? 'quorum'\n this.#assertTopology = options.assertTopology ?? true\n this.capabilities = Object.freeze({\n nativeDelay: true,\n maxNativeDelayMs: MAX_TTL_MS,\n maxInProcessDelayMs: durationOption(\n options.maxInProcessDelayMs,\n DEFAULT_IN_PROCESS_DELAY_MS,\n 'rabbitmq maxInProcessDelayMs',\n ),\n deliveryCount: this.#queueType === 'quorum' ? 'native' : 'header',\n maxPublishDelayMs: MAX_TTL_MS,\n })\n }\n\n // -----------------------------------------------------------------------------------------------\n // Connection\n // -----------------------------------------------------------------------------------------------\n\n async connect(context: DriverContext): Promise<void> {\n this.#logger = context.logger\n if (this.#model !== undefined) return\n if (this.#closing) throw brokerUnavailableError('RabbitMQ: the driver is closed, create a new one')\n await this.#open()\n }\n\n async close(): Promise<void> {\n if (this.#closing) return\n this.#closing = true\n this.#abort.abort()\n for (const subscription of this.#subscriptions) {\n subscription.active = false\n const channel = subscription.channel\n subscription.channel = undefined\n if (channel !== undefined) await closeQuietly(channel)\n }\n this.#subscriptions.clear()\n const publisher = this.#publisher\n this.#publisher = undefined\n if (publisher !== undefined) {\n try {\n await closeQuietly(await publisher)\n } catch {\n // the channel never opened\n }\n }\n const model = this.#model\n this.#model = undefined\n if (model !== undefined) {\n try {\n await model.close()\n } catch {\n // already closed\n }\n }\n }\n\n deadLetterName(queue: string): string {\n return this.#options.deadLetterName?.(queue) ?? `${queue}.dlq`\n }\n\n async #open(): Promise<void> {\n const socketOptions: SocketOptions = {\n ...this.#options.socketOptions,\n clientProperties: {\n connection_name: this.#options.connectionName ?? 'znight',\n ...this.#options.socketOptions?.clientProperties,\n },\n }\n const model = await amqpConnect(this.#options.url, socketOptions)\n model.on('error', (error: Error) => {\n this.#logger.error('RabbitMQ connection error', { err: error })\n })\n model.on('close', (error?: Error) => this.#onConnectionClosed(model, error))\n model.on('blocked', (reason: string) => {\n this.#logger.warn(`RabbitMQ connection blocked: ${reason}`)\n })\n model.on('unblocked', () => {\n this.#logger.info('RabbitMQ connection unblocked')\n })\n this.#model = model\n this.#publisher = undefined\n this.#delayQueues.clear()\n this.#delayLevels = undefined\n }\n\n #onConnectionClosed(model: ChannelModel, error: Error | undefined): void {\n if (this.#model !== model) return\n this.#model = undefined\n this.#publisher = undefined\n for (const subscription of this.#subscriptions) {\n subscription.channel = undefined\n subscription.consumerTag = undefined\n }\n if (this.#closing) return\n const reconnect = this.#options.reconnect\n this.#logger.error(\n reconnect === false ? 'RabbitMQ connection lost' : 'RabbitMQ connection lost, reconnecting',\n error === undefined ? {} : { err: error },\n )\n if (reconnect !== false) this.#recover()\n }\n\n /**\n * Starts the recovery loop unless it is running. The loop re-reads the whole state (connection,\n * subscriptions without channel) before it ends, so a failure happening while it runs is never lost.\n */\n #recover(): void {\n if (this.#closing) return\n this.#recovering ??= this.#recoverLoop()\n }\n\n /** Reconnects if needed, then resumes every subscription, with backoff, until everything consumes. */\n async #recoverLoop(): Promise<void> {\n const reconnect = this.#options.reconnect\n const settings = reconnect === false ? {} : (reconnect ?? {})\n const initialDelay = settings.initialDelayMs ?? 100\n const maxDelay = settings.maxDelayMs ?? 30_000\n let delay = initialDelay\n let reconnected = false\n try {\n while (!this.#closing) {\n try {\n await sleep(delay, this.#abort.signal)\n } catch {\n return\n }\n if (this.#model === undefined) {\n if (reconnect === false) return\n try {\n await this.#open()\n reconnected = true\n } catch (error) {\n delay = Math.min(delay * 2, maxDelay)\n this.#logger.warn(`RabbitMQ reconnection failed, next attempt in ${delay} ms`, { err: error })\n continue\n }\n }\n const model = this.#model\n for (const subscription of this.#subscriptions) {\n if (!subscription.active || subscription.channel !== undefined) continue\n try {\n if (subscription.redeclare) await this.#redeclare(subscription)\n await this.#ensureConsuming(subscription)\n if (!reconnected) this.#logger.info(`RabbitMQ: consuming \"${subscription.queue}\" again`)\n } catch (error) {\n this.#logger.error(`RabbitMQ: cannot consume \"${subscription.queue}\" again`, { err: error })\n }\n }\n if (this.#closing) return\n if (this.#model !== model) {\n // the connection was lost again while the consumers were resuming\n delay = Math.min(delay * 2, maxDelay)\n continue\n }\n const waiting = [...this.#subscriptions].filter(\n subscription => subscription.active && subscription.channel === undefined,\n )\n if (waiting.length > 0) {\n delay = Math.min(delay * 2, maxDelay)\n this.#logger.warn(\n `RabbitMQ: ${waiting.map(subscription => `\"${subscription.queue}\"`).join(', ')} not consumed yet, next attempt in ${delay} ms`,\n )\n continue\n }\n if (reconnected) this.#logger.info('RabbitMQ connection restored')\n return\n }\n } finally {\n this.#recovering = undefined\n }\n }\n\n #connection(action: string): ChannelModel {\n const model = this.#model\n if (model === undefined) {\n throw brokerUnavailableError(\n this.#closing\n ? `RabbitMQ: cannot ${action}, the driver is closed`\n : `RabbitMQ: cannot ${action}, the connection is not available`,\n )\n }\n return model\n }\n\n /** Runs `fn` on a temporary channel (failed declarations close the channel they run on). */\n async #withChannel<T>(action: string, fn: (channel: Channel) => Promise<T>): Promise<T> {\n const channel = await this.#connection(action).createChannel()\n channel.on('error', () => {\n // reported through the rejected operation\n })\n try {\n return await fn(channel)\n } finally {\n await closeQuietly(channel)\n }\n }\n\n // -----------------------------------------------------------------------------------------------\n // Publishing\n // -----------------------------------------------------------------------------------------------\n\n async publish(queue: string, message: OutboundMessage): Promise<void> {\n const headers: Record<string, unknown> = { ...message.headers }\n const properties: Options.Publish = {\n messageId: message.id,\n contentType: message.contentType,\n timestamp: Math.floor(message.timestamp / 1000),\n headers,\n persistent: this.#options.persistent ?? true,\n ...(message.type === undefined ? {} : { type: message.type }),\n }\n const content = Buffer.from(message.body)\n if (message.delayMs > 0) {\n await this.#publishDelayed(queue, content, properties, message.delayMs)\n return\n }\n await this.#confirmPublish('', queue, content, properties, queue)\n }\n\n #publisherChannel(): Promise<ConfirmChannel> {\n const existing = this.#publisher\n if (existing !== undefined) return existing\n const model = this.#connection('publish')\n const opening = model.createConfirmChannel().then(channel => {\n channel.on('error', (error: Error) => {\n this.#logger.error('RabbitMQ publisher channel error', { err: error })\n })\n channel.on('close', () => {\n if (this.#publisher === opening) this.#publisher = undefined\n })\n // a return precedes the confirmation of its publication: mark the oldest pending publication\n // of the same exchange, routing key and message id (never one already returned)\n channel.on('return', (returned: Message) => {\n const key = pendingKey(returned.fields.exchange, returned.fields.routingKey, returned.properties.messageId)\n const pending = this.#pending.get(key)?.find(entry => !entry.returned)\n if (pending !== undefined) pending.returned = true\n })\n return channel\n })\n opening.catch(() => {\n if (this.#publisher === opening) this.#publisher = undefined\n })\n this.#publisher = opening\n return opening\n }\n\n /**\n * Publishes to `exchange` with `routingKey` and waits for the broker confirmation. `destination`\n * names the target queue in errors.\n */\n async #confirmPublish(\n exchange: string,\n routingKey: string,\n content: Buffer,\n properties: Options.Publish,\n destination: string,\n ): Promise<void> {\n const channel = await this.#publisherChannel()\n const mandatory = this.#options.mandatory ?? true\n const id = properties.messageId as string\n const key = pendingKey(exchange, routingKey, id)\n const pending: PendingConfirm = { returned: false }\n let waiting = this.#pending.get(key)\n if (waiting === undefined) {\n waiting = []\n this.#pending.set(key, waiting)\n }\n waiting.push(pending)\n try {\n await new Promise<void>((resolve, reject) => {\n try {\n channel.publish(exchange, routingKey, content, { ...properties, mandatory }, (error: unknown) => {\n if (error !== null && error !== undefined) reject(error instanceof Error ? error : new Error(String(error)))\n else resolve()\n })\n } catch (error) {\n reject(error)\n }\n })\n } catch (error) {\n throw brokerUnavailableError(\n `RabbitMQ: publishing to \"${destination}\" was not confirmed: ${errorMessage(error)}`,\n error,\n )\n } finally {\n const index = waiting.indexOf(pending)\n if (index !== -1) waiting.splice(index, 1)\n if (waiting.length === 0 && this.#pending.get(key) === waiting) this.#pending.delete(key)\n }\n if (pending.returned) {\n throw brokerUnavailableError(\n exchange === ''\n ? `RabbitMQ: message ${id} is unroutable, queue \"${destination}\" does not exist`\n : `RabbitMQ: message ${id} to \"${destination}\" is unroutable through the exchange \"${exchange}\"`,\n )\n }\n }\n\n /**\n * Publishes through the delay levels: the delay, rounded up to an odd number of milliseconds (so\n * that every route ends with level 0, whose queue dead-letters to the default exchange), is written\n * in binary in the routing key; the `CC` header routes the message to `queue` at the end.\n */\n async #publishDelayed(queue: string, content: Buffer, properties: Options.Publish, delayMs: number): Promise<void> {\n const rounded = Math.min(MAX_TTL_MS, Math.ceil(delayMs))\n const delay = rounded % 2 === 0 ? rounded + 1 : rounded\n await this.#declareDelayLevels()\n let top = 0\n while (top < DELAY_LEVELS - 1 && 2 ** (top + 1) <= delay) top++\n try {\n await this.#confirmPublish(\n this.#delayLevelName(top),\n delayRoutingKey(delay),\n content,\n { ...properties, headers: { ...properties.headers, CC: [queue] } },\n queue,\n )\n } catch (error) {\n // the levels may have been deleted: declare them again on the next delayed publication\n this.#delayLevels = undefined\n throw error\n }\n }\n\n #delayLevelName(level: number): string {\n return `${this.#options.delayLevelPrefix ?? DEFAULT_DELAY_LEVEL_PREFIX}.${level}`\n }\n\n /**\n * Declares (once per connection) the delay levels: for each level n, a topic exchange routing the\n * messages whose bit n is set to the queue of the level (TTL 2^n ms, dead-lettering to the level\n * below), and the others straight to the level below.\n */\n async #declareDelayLevels(): Promise<void> {\n let declared = this.#delayLevels\n if (declared === undefined) {\n const levels: Promise<void> = this.#withChannel('declare the delay levels', async channel => {\n for (let level = 0; level < DELAY_LEVELS; level++) {\n const name = this.#delayLevelName(level)\n const below = level === 0 ? '' : this.#delayLevelName(level - 1)\n await channel.assertExchange(name, 'topic', { durable: true })\n await channel.assertQueue(name, { durable: true, arguments: this.#delayQueueArguments(2 ** level, below) })\n await channel.bindQueue(name, name, delayBindingKey(level, '1'))\n if (level > 0) await channel.bindExchange(below, name, delayBindingKey(level, '0'))\n }\n })\n declared = levels\n this.#delayLevels = levels\n levels.catch(() => {\n if (this.#delayLevels === levels) this.#delayLevels = undefined\n })\n }\n try {\n await declared\n } catch (error) {\n const message = errorMessage(error)\n throw consumerError(\n `RabbitMQ: cannot declare the delay levels \"${this.#delayLevelName(0)}\" to \"${this.#delayLevelName(DELAY_LEVELS - 1)}\": ${message}`,\n message.includes('PRECONDITION_FAILED')\n ? [\n 'They exist with other arguments (another queueType?): set rabbitmq({ delayLevelPrefix }) to use other names',\n ]\n : [],\n )\n }\n }\n\n /** Arguments of a delay queue: TTL, then dead-lettering (at-least-once on quorum queues). */\n #delayQueueArguments(ttl: number, exchange: string, routingKey?: string): Record<string, unknown> {\n const args: Record<string, unknown> = {\n 'x-queue-type': this.#queueType,\n 'x-message-ttl': ttl,\n 'x-dead-letter-exchange': exchange,\n }\n if (routingKey !== undefined) args['x-dead-letter-routing-key'] = routingKey\n if (this.#queueType === 'quorum') {\n // never lose a message while moving it on\n args['x-dead-letter-strategy'] = 'at-least-once'\n args['x-overflow'] = 'reject-publish'\n }\n return args\n }\n\n /** Declares (once per connection) the retry delay queue of `queue` for `delayMs` and returns its name. */\n async #delayQueue(queue: string, delayMs: number): Promise<string> {\n const name = this.#options.delayQueueName?.(queue, delayMs) ?? `${queue}.delay.${delayMs}`\n let declared = this.#delayQueues.get(name)\n if (declared === undefined) {\n const args = this.#delayQueueArguments(delayMs, '', queue)\n declared = this.#withChannel(`declare the delay queue \"${name}\"`, async channel => {\n await channel.assertQueue(name, { durable: true, arguments: args })\n })\n this.#delayQueues.set(name, declared)\n declared.catch(() => this.#delayQueues.delete(name))\n }\n try {\n await declared\n } catch (error) {\n throw topologyError(name, error)\n }\n return name\n }\n\n // -----------------------------------------------------------------------------------------------\n // Consuming\n // -----------------------------------------------------------------------------------------------\n\n async subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription> {\n await this.#prepareTopology(queue, options)\n const subscription: ConsumerSubscription = {\n queue,\n handler,\n options,\n channel: undefined,\n consumerTag: undefined,\n active: true,\n consuming: undefined,\n redeclare: false,\n }\n // registered first: a channel closing right after the subscription is resumed by the recovery\n this.#subscriptions.add(subscription)\n try {\n await this.#ensureConsuming(subscription)\n } catch (error) {\n subscription.active = false\n this.#subscriptions.delete(subscription)\n throw error\n }\n return {\n cancel: async (): Promise<void> => {\n subscription.active = false\n const { channel, consumerTag } = subscription\n if (channel !== undefined && consumerTag !== undefined) {\n subscription.consumerTag = undefined\n try {\n await channel.cancel(consumerTag)\n } catch (error) {\n this.#logger.warn(`RabbitMQ: failed to cancel the consumer of \"${queue}\"`, { err: error })\n }\n }\n },\n close: async (): Promise<void> => {\n subscription.active = false\n this.#subscriptions.delete(subscription)\n const channel = subscription.channel\n subscription.channel = undefined\n if (channel !== undefined) await closeQuietly(channel)\n },\n }\n }\n\n async #prepareTopology(queue: string, options: SubscribeOptions): Promise<void> {\n const dlq = options.deadLetterQueue\n const names = dlq === undefined ? [queue] : [queue, dlq]\n if (this.#assertTopology) {\n for (const name of names) {\n try {\n await this.#declareQueue(name)\n } catch (error) {\n throw topologyError(name, error)\n }\n }\n } else {\n for (const name of names) {\n try {\n await this.#withChannel(`check the queue \"${name}\"`, channel => channel.checkQueue(name))\n } catch (error) {\n throw consumerError(\n `RabbitMQ: queue \"${name}\" of consumer ${options.consumer} does not exist (${errorMessage(error)})`,\n ['Declare it, or set assertTopology: true to let Znight declare it'],\n )\n }\n }\n }\n for (const delay of options.retryDelays) await this.#delayQueue(queue, Math.round(delay))\n }\n\n /**\n * Declares a consumed or dead-letter queue. Quorum queues get `x-delivery-limit: -1` on RabbitMQ >= 4\n * (whose default limit of 20 deliveries counts the releases); an existing queue declared without it\n * cannot be changed by a declaration: it is declared as it is, with a warning.\n */\n async #declareQueue(name: string): Promise<void> {\n const args: Record<string, unknown> = { 'x-queue-type': this.#queueType, ...this.#options.queueArguments }\n const declare = (queueArgs: Record<string, unknown>): Promise<void> =>\n this.#withChannel(`declare the queue \"${name}\"`, async channel => {\n await channel.assertQueue(name, { durable: true, arguments: queueArgs })\n })\n if (args['x-queue-type'] === 'quorum' && !Object.hasOwn(args, 'x-delivery-limit') && this.#serverMajor() >= 4) {\n try {\n await declare({ ...args, 'x-delivery-limit': -1 })\n return\n } catch (error) {\n if (!errorMessage(error).includes(\"'x-delivery-limit'\")) throw error\n this.#logger.warn(\n `RabbitMQ: queue \"${name}\" exists without \"x-delivery-limit\": unless a policy sets \"delivery-limit\" to -1, RabbitMQ drops (or dead-letters through a queue DLX) a message delivered 20 times, releases included (shutdowns, failed dead-lettering). Apply such a policy, or re-create the queue`,\n )\n }\n }\n await declare(args)\n }\n\n /** Major version of the connected RabbitMQ (0 when unknown). */\n #serverMajor(): number {\n const version = this.#model?.connection?.serverProperties?.version\n const major = typeof version === 'string' ? Number.parseInt(version, 10) : Number.NaN\n return Number.isNaN(major) ? 0 : major\n }\n\n /** After a consumer cancellation, declares its queues again (`assertTopology`) before consuming. */\n async #redeclare(subscription: ConsumerSubscription): Promise<void> {\n if (this.#assertTopology) {\n const dlq = subscription.options.deadLetterQueue\n for (const name of dlq === undefined ? [subscription.queue] : [subscription.queue, dlq]) {\n try {\n await this.#declareQueue(name)\n } catch (error) {\n throw topologyError(name, error)\n }\n }\n }\n subscription.redeclare = false\n }\n\n /** Opens the consumer channel of `subscription` unless it is open or opening. */\n #ensureConsuming(subscription: ConsumerSubscription): Promise<void> {\n if (!subscription.active || subscription.channel !== undefined) return Promise.resolve()\n subscription.consuming ??= this.#consume(subscription).finally(() => {\n subscription.consuming = undefined\n })\n return subscription.consuming\n }\n\n async #consume(subscription: ConsumerSubscription): Promise<void> {\n const channel = await this.#connection(`consume \"${subscription.queue}\"`).createChannel()\n channel.on('error', (error: Error) => {\n this.#logger.error(`RabbitMQ consumer channel of \"${subscription.queue}\" failed`, { err: error })\n })\n channel.on('close', () => {\n if (subscription.channel !== channel) return\n subscription.channel = undefined\n subscription.consumerTag = undefined\n // a channel error (or the loss of the connection, handled by the same loop): consume again\n if (subscription.active) this.#recover()\n })\n try {\n await channel.prefetch(subscription.options.concurrency)\n subscription.channel = channel\n const { consumerTag } = await channel.consume(\n subscription.queue,\n message => {\n if (message === null) {\n this.#onConsumerCancelled(subscription, channel)\n return\n }\n this.#deliver(subscription, channel, message)\n },\n { noAck: false },\n )\n subscription.consumerTag = consumerTag\n } catch (error) {\n subscription.channel = undefined\n await closeQuietly(channel)\n throw brokerUnavailableError(`RabbitMQ: cannot consume \"${subscription.queue}\": ${errorMessage(error)}`, error)\n }\n }\n\n /**\n * The broker cancelled the consumer (`basic.cancel`: queue deleted, node down...). The channel stays\n * open, so nothing else would notice: close it (its unsettled messages go back to the queue, when it\n * still exists) and resume the subscription.\n */\n #onConsumerCancelled(subscription: ConsumerSubscription, channel: Channel): void {\n if (subscription.channel !== channel || !subscription.active) return\n this.#logger.error(`RabbitMQ cancelled the consumer of \"${subscription.queue}\" (queue deleted?), consuming again`)\n subscription.channel = undefined\n subscription.consumerTag = undefined\n subscription.redeclare = true\n closeQuietly(channel).then(\n () => undefined,\n () => undefined,\n )\n this.#recover()\n }\n\n #deliver(subscription: ConsumerSubscription, channel: Channel, message: ConsumeMessage): void {\n const { queue } = subscription\n const properties = message.properties\n const rawHeaders: Record<string, unknown> = { ...(properties.headers ?? {}) }\n const headers = toHeaderRecord(Object.entries(rawHeaders))\n // quorum queues count redeliveries; classic queues only flag them\n const deliveryCount = Number(rawHeaders['x-delivery-count'])\n let redeliveries = message.fields.redelivered ? 1 : 0\n if (Number.isSafeInteger(deliveryCount) && deliveryCount > 0) redeliveries = deliveryCount\n const type =\n stringProperty(properties.type) ??\n readHeader(headers, MessagingHeaders.Type) ??\n (this.#options.typeFromRoutingKey === true && message.fields.exchange !== ''\n ? message.fields.routingKey\n : undefined)\n const id = stringProperty(properties.messageId) ?? `${queue}:${message.fields.deliveryTag}`\n const timestamp = typeof properties.timestamp === 'number' ? properties.timestamp * 1000 : undefined\n let settled = false\n const settle = (action: string): void => {\n if (settled) throw new Error(`[rabbitmq] message ${id} was already settled (${action})`)\n if (subscription.channel !== channel) {\n throw brokerUnavailableError(\n `RabbitMQ: cannot ${action} message ${id}, its channel is closed (it will be redelivered)`,\n )\n }\n }\n /** Properties of a republished copy: the original ones, with the type that selected the handler. */\n const copyProperties = (extraHeaders: Record<string, unknown>, removeAttempt: boolean): Options.Publish => {\n const copied: Record<string, unknown> = {}\n for (const [name, value] of Object.entries(rawHeaders)) {\n if (UNCOPIED_HEADERS.has(name) || (removeAttempt && name === MessagingHeaders.Attempt)) continue\n copied[name] = value\n }\n return {\n ...definedProperties(properties),\n // a type read from the routing key would be lost: copies go through the default exchange\n ...(type === undefined ? {} : { type }),\n messageId: id,\n headers: { ...copied, ...extraHeaders },\n persistent: this.#options.persistent ?? true,\n }\n }\n const delivery: DriverDelivery = {\n id,\n body: message.content,\n contentType: stringProperty(properties.contentType),\n headers,\n type,\n key: undefined,\n attempt: attemptFromHeaders(headers) + redeliveries,\n redelivered: message.fields.redelivered,\n timestamp,\n raw: message,\n ack: async (): Promise<void> => {\n await Promise.resolve()\n settle('ack')\n channel.ack(message)\n settled = true\n },\n release: async (): Promise<void> => {\n await Promise.resolve()\n settle('release')\n channel.nack(message, false, true)\n settled = true\n },\n retry: async (request: RetryRequest): Promise<void> => {\n settle('retry')\n const target = request.delayMs > 0 ? await this.#delayQueue(queue, Math.round(request.delayMs)) : queue\n try {\n await this.#confirmPublish(\n '',\n target,\n message.content,\n copyProperties({ [MessagingHeaders.Attempt]: String(request.attempt) }, false),\n target,\n )\n } catch (error) {\n // the delay queue may have been deleted: declare it again on the next retry\n if (target !== queue) this.#delayQueues.delete(target)\n throw error\n }\n settle('retry')\n channel.ack(message)\n settled = true\n },\n deadLetter: async (request: DeadLetterRequest): Promise<void> => {\n settle('dead-letter')\n if (request.queue === undefined) {\n channel.nack(message, false, false)\n settled = true\n return\n }\n await this.#confirmPublish(\n '',\n request.queue,\n message.content,\n copyProperties(\n {\n [MessagingHeaders.DeadLetterReason]: request.reason,\n [MessagingHeaders.Error]: truncate(errorMessage(request.error), MAX_ERROR_HEADER_LENGTH),\n [MessagingHeaders.OriginalQueue]: queue,\n [MessagingHeaders.Attempts]: String(request.attempt),\n },\n true,\n ),\n request.queue,\n )\n settle('dead-letter')\n channel.ack(message)\n settled = true\n },\n }\n subscription.handler(delivery).catch((error: unknown) => {\n this.#logger.error(`RabbitMQ: unexpected failure handling message ${id}`, { err: error })\n })\n }\n}\n\nfunction stringProperty(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined\n}\n\n/** Key of the publications awaiting their confirmation (a returned message carries the same fields). */\nfunction pendingKey(exchange: string, routingKey: string, messageId: unknown): string {\n return `${exchange}\\u0000${routingKey}\\u0000${String(messageId)}`\n}\n\n/** Routing key of a delayed publication: `znight-delay.<bit 31>.<bit 30>...<bit 0>`. */\nfunction delayRoutingKey(delayMs: number): string {\n const words = [DELAY_ROUTING_WORD]\n for (let level = DELAY_LEVELS - 1; level >= 0; level--) {\n words.push(Math.floor(delayMs / 2 ** level) % 2 === 1 ? '1' : '0')\n }\n return words.join('.')\n}\n\n/** Binding key matching the delayed publications whose bit `level` is `bit` (exact word count). */\nfunction delayBindingKey(level: number, bit: '0' | '1'): string {\n const words = [DELAY_ROUTING_WORD]\n for (let index = DELAY_LEVELS - 1; index >= 0; index--) words.push(index === level ? bit : '*')\n return words.join('.')\n}\n\n/** AMQP properties of a received message, reusable for a republication. */\nfunction definedProperties(properties: MessageProperties): Options.Publish {\n const result: Options.Publish = {}\n if (properties.contentType !== undefined) result.contentType = properties.contentType\n if (properties.contentEncoding !== undefined) result.contentEncoding = properties.contentEncoding\n if (properties.correlationId !== undefined) result.correlationId = properties.correlationId\n if (properties.replyTo !== undefined) result.replyTo = properties.replyTo\n if (properties.timestamp !== undefined) result.timestamp = properties.timestamp\n if (properties.type !== undefined) result.type = properties.type\n if (properties.appId !== undefined) result.appId = properties.appId\n if (properties.priority !== undefined) result.priority = properties.priority\n if (properties.userId !== undefined) result.userId = properties.userId\n return result\n}\n\nfunction topologyError(queue: string, error: unknown): Error {\n const message = errorMessage(error)\n const hints = message.includes('PRECONDITION_FAILED')\n ? ['The queue exists with different arguments: align queueType/queueArguments, or set assertTopology: false']\n : []\n return consumerError(`RabbitMQ: cannot declare the queue \"${queue}\": ${message}`, hints)\n}\n\nasync function closeQuietly(channel: Channel): Promise<void> {\n try {\n await channel.close()\n } catch {\n // already closed\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { l as headerRecord, o as truncate, r as errorMessage, s as MessagingHeaders } from "./async-CFh_bvex.mjs";
|
|
2
|
+
// src/drivers/shared.ts
|
|
3
|
+
/** Longest error message copied into the `x-znight-error` header. */
|
|
4
|
+
const MAX_ERROR_HEADER_LENGTH = 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Headers of a dead-lettered copy: the original headers without the attempt counter (so that a
|
|
7
|
+
* redrive starts over) plus the reason, the error, the original queue and the attempts made.
|
|
8
|
+
*/
|
|
9
|
+
function deadLetterHeaders(headers, request, originalQueue) {
|
|
10
|
+
const result = headerRecord();
|
|
11
|
+
for (const [name, value] of Object.entries(headers)) if (name !== MessagingHeaders.Attempt) result[name] = value;
|
|
12
|
+
result[MessagingHeaders.DeadLetterReason] = request.reason;
|
|
13
|
+
result[MessagingHeaders.Error] = truncate(errorMessage(request.error), MAX_ERROR_HEADER_LENGTH);
|
|
14
|
+
result[MessagingHeaders.OriginalQueue] = originalQueue;
|
|
15
|
+
result[MessagingHeaders.Attempts] = String(request.attempt);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
/** Converts a header value of any type to a string (`undefined` for null/undefined). */
|
|
19
|
+
function headerValueToString(value) {
|
|
20
|
+
if (value === void 0 || value === null) return void 0;
|
|
21
|
+
if (typeof value === "string") return value;
|
|
22
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
23
|
+
if (value instanceof Uint8Array) return new TextDecoder().decode(value);
|
|
24
|
+
if (value instanceof Date) return value.toISOString();
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner);
|
|
27
|
+
} catch {
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Builds a string header record from arbitrary entries (null-prototype, `undefined` values skipped). */
|
|
32
|
+
function toHeaderRecord(entries) {
|
|
33
|
+
const result = headerRecord();
|
|
34
|
+
for (const [name, value] of entries) {
|
|
35
|
+
const text = headerValueToString(value);
|
|
36
|
+
if (text !== void 0) result[name] = text;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/** Reads a header (exact name). */
|
|
41
|
+
function readHeader(headers, name) {
|
|
42
|
+
return Object.hasOwn(headers, name) ? headers[name] : void 0;
|
|
43
|
+
}
|
|
44
|
+
/** Positive finite number option with a default. */
|
|
45
|
+
function durationOption(value, fallback, name) {
|
|
46
|
+
if (value === void 0) return fallback;
|
|
47
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) throw new TypeError(`[znight] ${name} must be a number of milliseconds >= 0, got ${String(value)}`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export { toHeaderRecord as a, readHeader as i, deadLetterHeaders as n, durationOption as r, MAX_ERROR_HEADER_LENGTH as t };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=shared-BJBpLS8V.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-BJBpLS8V.mjs","names":[],"sources":["../src/drivers/shared.ts"],"sourcesContent":["/** Helpers shared by the drivers (internal). */\nimport type { DeadLetterRequest } from '../driver.ts'\n\nimport { headerRecord, MessagingHeaders } from '../headers.ts'\nimport { errorMessage, truncate } from '../internal/async.ts'\n\n/** Longest error message copied into the `x-znight-error` header. */\nexport const MAX_ERROR_HEADER_LENGTH = 1024\n\n/**\n * Headers of a dead-lettered copy: the original headers without the attempt counter (so that a\n * redrive starts over) plus the reason, the error, the original queue and the attempts made.\n */\nexport function deadLetterHeaders(\n headers: Readonly<Record<string, string>>,\n request: DeadLetterRequest,\n originalQueue: string,\n): Record<string, string> {\n const result = headerRecord()\n for (const [name, value] of Object.entries(headers)) if (name !== MessagingHeaders.Attempt) result[name] = value\n result[MessagingHeaders.DeadLetterReason] = request.reason\n result[MessagingHeaders.Error] = truncate(errorMessage(request.error), MAX_ERROR_HEADER_LENGTH)\n result[MessagingHeaders.OriginalQueue] = originalQueue\n result[MessagingHeaders.Attempts] = String(request.attempt)\n return result\n}\n\n/** Converts a header value of any type to a string (`undefined` for null/undefined). */\nexport function headerValueToString(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value)\n if (value instanceof Uint8Array) return new TextDecoder().decode(value)\n if (value instanceof Date) return value.toISOString()\n try {\n return JSON.stringify(value, (_key, inner: unknown) => (typeof inner === 'bigint' ? inner.toString() : inner))\n } catch {\n return String(value)\n }\n}\n\n/** Builds a string header record from arbitrary entries (null-prototype, `undefined` values skipped). */\nexport function toHeaderRecord(entries: Iterable<readonly [string, unknown]>): Record<string, string> {\n const result = headerRecord()\n for (const [name, value] of entries) {\n const text = headerValueToString(value)\n if (text !== undefined) result[name] = text\n }\n return result\n}\n\n/** Reads a header (exact name). */\nexport function readHeader(headers: Readonly<Record<string, string>>, name: string): string | undefined {\n return Object.hasOwn(headers, name) ? headers[name] : undefined\n}\n\n/** Positive finite number option with a default. */\nexport function durationOption(value: unknown, fallback: number, name: string): number {\n if (value === undefined) return fallback\n if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {\n throw new TypeError(`[znight] ${name} must be a number of milliseconds >= 0, got ${String(value)}`)\n }\n return value\n}\n\n/** A promise with its resolvers. */\nexport interface Deferred<T> {\n readonly promise: Promise<T>\n resolve(value: T): void\n reject(error: unknown): void\n}\n\nexport function deferred<T>(): Deferred<T> {\n let resolve!: (value: T) => void\n let reject!: (error: unknown) => void\n const promise = new Promise<T>((res, rej) => {\n resolve = res\n reject = rej\n })\n return { promise, resolve, reject }\n}\n"],"mappings":";;;AAOA,MAAa,0BAA0B;;;;;AAMvC,SAAgB,kBACd,SACA,SACA,eACwB;CACxB,MAAM,SAAS,aAAa;CAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG,IAAI,SAAS,iBAAiB,SAAS,OAAO,QAAQ;CAC3G,OAAO,iBAAiB,oBAAoB,QAAQ;CACpD,OAAO,iBAAiB,SAAS,SAAS,aAAa,QAAQ,KAAK,GAAG,uBAAuB;CAC9F,OAAO,iBAAiB,iBAAiB;CACzC,OAAO,iBAAiB,YAAY,OAAO,QAAQ,OAAO;CAC1D,OAAO;AACT;;AAGA,SAAgB,oBAAoB,OAAoC;CACtE,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO,KAAA;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,aAAa,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;CAC7G,IAAI,iBAAiB,YAAY,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK;CACtE,IAAI,iBAAiB,MAAM,OAAO,MAAM,YAAY;CACpD,IAAI;EACF,OAAO,KAAK,UAAU,QAAQ,MAAM,UAAoB,OAAO,UAAU,WAAW,MAAM,SAAS,IAAI,KAAM;CAC/G,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;AAGA,SAAgB,eAAe,SAAuE;CACpG,MAAM,SAAS,aAAa;CAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS;EACnC,MAAM,OAAO,oBAAoB,KAAK;EACtC,IAAI,SAAS,KAAA,GAAW,OAAO,QAAQ;CACzC;CACA,OAAO;AACT;;AAGA,SAAgB,WAAW,SAA2C,MAAkC;CACtG,OAAO,OAAO,OAAO,SAAS,IAAI,IAAI,QAAQ,QAAQ,KAAA;AACxD;;AAGA,SAAgB,eAAe,OAAgB,UAAkB,MAAsB;CACrF,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,IAAI,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,KAAK,QAAQ,GAC9D,MAAM,IAAI,UAAU,YAAY,KAAK,8CAA8C,OAAO,KAAK,GAAG;CAEpG,OAAO;AACT"}
|
package/dist/sqs.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { c as SubscribeOptions, f as OutboundMessage, i as DriverContext, l as Subscription, n as DeliveryHandler, o as MessagingDriver, r as DriverCapabilities } from "./driver-ZxLqy_Ap.mjs";
|
|
2
|
+
import { SQSClient, SQSClientConfig } from "@aws-sdk/client-sqs";
|
|
3
|
+
/** Options of {@link sqs}. */
|
|
4
|
+
export interface SqsDriverOptions {
|
|
5
|
+
/** An existing client (you keep ownership: it is not destroyed on close). */
|
|
6
|
+
readonly client?: SQSClient;
|
|
7
|
+
/** Configuration of the client created by the driver (region, endpoint, credentials...). */
|
|
8
|
+
readonly clientConfig?: SQSClientConfig;
|
|
9
|
+
/** Queue URLs by queue name, to skip `GetQueueUrl`. Queue arguments may also be full URLs. */
|
|
10
|
+
readonly queueUrls?: Readonly<Record<string, string>>;
|
|
11
|
+
/** Long polling duration, in seconds (0–20). Default: 20. */
|
|
12
|
+
readonly waitTimeSeconds?: number;
|
|
13
|
+
/** Visibility timeout of received messages, in seconds. Default: the queue attribute. */
|
|
14
|
+
readonly visibilityTimeout?: number;
|
|
15
|
+
/** Extend the visibility of messages while they are handled. Default: `true`. */
|
|
16
|
+
readonly extendVisibility?: boolean;
|
|
17
|
+
/** Longest retry delay waited in-process. Default: 1000 ms (SQS delays have a 1 s granularity). */
|
|
18
|
+
readonly maxInProcessDelayMs?: number;
|
|
19
|
+
/** Default dead-letter queue name. Default: `<queue>-dlq` (`<name>-dlq.fifo` for FIFO queues). */
|
|
20
|
+
readonly deadLetterName?: (queue: string) => string;
|
|
21
|
+
}
|
|
22
|
+
/** Creates a {@link SqsDriver}. */
|
|
23
|
+
export declare function sqs(options?: SqsDriverOptions): SqsDriver;
|
|
24
|
+
/** Amazon SQS {@link MessagingDriver}. See the module documentation. */
|
|
25
|
+
export declare class SqsDriver implements MessagingDriver {
|
|
26
|
+
#private;
|
|
27
|
+
readonly name = "sqs";
|
|
28
|
+
readonly capabilities: DriverCapabilities;
|
|
29
|
+
constructor(options?: SqsDriverOptions);
|
|
30
|
+
/** The SQS client (available once connected). */
|
|
31
|
+
get client(): SQSClient | undefined;
|
|
32
|
+
connect(context: DriverContext): Promise<void>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
deadLetterName(queue: string): string;
|
|
35
|
+
publish(queue: string, message: OutboundMessage): Promise<void>;
|
|
36
|
+
subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=sqs.d.mts.map
|
package/dist/sqs.mjs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { _ as consumerError, a as sleep, g as brokerUnavailableError, l as headerRecord, o as truncate, r as errorMessage, s as MessagingHeaders } from "./async-CFh_bvex.mjs";
|
|
2
|
+
import { i as bodyToBytes } from "./codec-Dku-NXXU.mjs";
|
|
3
|
+
import { r as durationOption } from "./shared-BJBpLS8V.mjs";
|
|
4
|
+
import { ChangeMessageVisibilityCommand, DeleteMessageCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ReceiveMessageCommand, SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
5
|
+
// src/drivers/sqs.ts
|
|
6
|
+
/** Longest visibility timeout (12 hours). */
|
|
7
|
+
const MAX_VISIBILITY_SECONDS = 43200;
|
|
8
|
+
/** Longest `DelaySeconds` (15 minutes). */
|
|
9
|
+
const MAX_DELAY_SECONDS = 900;
|
|
10
|
+
const MAX_ATTRIBUTES = 10;
|
|
11
|
+
const ATTRIBUTE_NAME = /^(?!aws\.|amazon\.)(?!\.)(?!.*\.\.)[\w.-]{1,256}(?<!\.)$/i;
|
|
12
|
+
/** Message attributes carrying transport fields (not exposed as headers). */
|
|
13
|
+
const TRANSPORT_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
14
|
+
MessagingHeaders.Id,
|
|
15
|
+
MessagingHeaders.Type,
|
|
16
|
+
MessagingHeaders.ContentType,
|
|
17
|
+
MessagingHeaders.Encoding
|
|
18
|
+
]);
|
|
19
|
+
/** Creates a {@link SqsDriver}. */
|
|
20
|
+
function sqs(options = {}) {
|
|
21
|
+
return new SqsDriver(options);
|
|
22
|
+
}
|
|
23
|
+
/** Amazon SQS {@link MessagingDriver}. See the module documentation. */
|
|
24
|
+
var SqsDriver = class {
|
|
25
|
+
name = "sqs";
|
|
26
|
+
capabilities;
|
|
27
|
+
#options;
|
|
28
|
+
#subscriptions = /* @__PURE__ */ new Set();
|
|
29
|
+
#queues = /* @__PURE__ */ new Map();
|
|
30
|
+
#client;
|
|
31
|
+
#ownsClient = false;
|
|
32
|
+
#logger = {
|
|
33
|
+
debug() {},
|
|
34
|
+
info() {},
|
|
35
|
+
warn() {},
|
|
36
|
+
error() {}
|
|
37
|
+
};
|
|
38
|
+
#closed = false;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
const wait = options.waitTimeSeconds;
|
|
41
|
+
if (wait !== void 0 && !(Number.isInteger(wait) && wait >= 0 && wait <= 20)) throw new TypeError(`[znight] sqs(): waitTimeSeconds must be an integer between 0 and 20, got ${String(wait)}`);
|
|
42
|
+
const visibility = options.visibilityTimeout;
|
|
43
|
+
if (visibility !== void 0 && !(Number.isInteger(visibility) && visibility >= 0 && visibility <= MAX_VISIBILITY_SECONDS)) throw new TypeError("[znight] sqs(): visibilityTimeout must be an integer number of seconds (0–43200)");
|
|
44
|
+
this.#options = options;
|
|
45
|
+
this.capabilities = Object.freeze({
|
|
46
|
+
nativeDelay: true,
|
|
47
|
+
maxNativeDelayMs: MAX_VISIBILITY_SECONDS * 1e3,
|
|
48
|
+
maxInProcessDelayMs: durationOption(options.maxInProcessDelayMs, 1e3, "sqs maxInProcessDelayMs"),
|
|
49
|
+
deliveryCount: "native",
|
|
50
|
+
maxPublishDelayMs: MAX_DELAY_SECONDS * 1e3
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** The SQS client (available once connected). */
|
|
54
|
+
get client() {
|
|
55
|
+
return this.#client;
|
|
56
|
+
}
|
|
57
|
+
async connect(context) {
|
|
58
|
+
await Promise.resolve();
|
|
59
|
+
this.#logger = context.logger;
|
|
60
|
+
if (this.#closed) throw brokerUnavailableError("SQS: the driver is closed, create a new one");
|
|
61
|
+
if (this.#client !== void 0) return;
|
|
62
|
+
if (this.#options.client === void 0) {
|
|
63
|
+
this.#client = new SQSClient(this.#options.clientConfig ?? {});
|
|
64
|
+
this.#ownsClient = true;
|
|
65
|
+
} else this.#client = this.#options.client;
|
|
66
|
+
}
|
|
67
|
+
async close() {
|
|
68
|
+
if (this.#closed) return;
|
|
69
|
+
this.#closed = true;
|
|
70
|
+
const subscriptions = [...this.#subscriptions];
|
|
71
|
+
this.#subscriptions.clear();
|
|
72
|
+
for (const subscription of subscriptions) this.#stopPolling(subscription);
|
|
73
|
+
await Promise.allSettled(subscriptions.map((subscription) => subscription.loop));
|
|
74
|
+
if (this.#ownsClient) this.#client?.destroy();
|
|
75
|
+
this.#client = void 0;
|
|
76
|
+
}
|
|
77
|
+
deadLetterName(queue) {
|
|
78
|
+
if (this.#options.deadLetterName !== void 0) return this.#options.deadLetterName(queue);
|
|
79
|
+
const name = queueName(queue);
|
|
80
|
+
return name.endsWith(".fifo") ? `${name.slice(0, -5)}-dlq.fifo` : `${name}-dlq`;
|
|
81
|
+
}
|
|
82
|
+
#sqs(action) {
|
|
83
|
+
const client = this.#client;
|
|
84
|
+
if (client === void 0) throw brokerUnavailableError(this.#closed ? `SQS: cannot ${action}, the driver is closed` : `SQS: cannot ${action} before connect()`);
|
|
85
|
+
return client;
|
|
86
|
+
}
|
|
87
|
+
/** Resolves (and caches) the URL of a queue name or URL. */
|
|
88
|
+
#queue(queue) {
|
|
89
|
+
let info = this.#queues.get(queue);
|
|
90
|
+
if (info === void 0) {
|
|
91
|
+
info = this.#resolveQueue(queue);
|
|
92
|
+
this.#queues.set(queue, info);
|
|
93
|
+
info.catch(() => this.#queues.delete(queue));
|
|
94
|
+
}
|
|
95
|
+
return info;
|
|
96
|
+
}
|
|
97
|
+
async #resolveQueue(queue) {
|
|
98
|
+
const name = queueName(queue);
|
|
99
|
+
const configured = this.#options.queueUrls?.[queue];
|
|
100
|
+
if (configured !== void 0) return {
|
|
101
|
+
name,
|
|
102
|
+
url: configured,
|
|
103
|
+
fifo: name.endsWith(".fifo")
|
|
104
|
+
};
|
|
105
|
+
if (/^https?:\/\//.test(queue)) return {
|
|
106
|
+
name,
|
|
107
|
+
url: queue,
|
|
108
|
+
fifo: name.endsWith(".fifo")
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.#sqs(`resolve the queue "${queue}"`).send(new GetQueueUrlCommand({ QueueName: queue }));
|
|
112
|
+
if (result.QueueUrl === void 0) throw new Error("no QueueUrl in the response");
|
|
113
|
+
return {
|
|
114
|
+
name,
|
|
115
|
+
url: result.QueueUrl,
|
|
116
|
+
fifo: name.endsWith(".fifo")
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw consumerError(`SQS: queue "${queue}" not found (${errorMessage(error)})`, ["Create the queue, or map its URL with sqs({ queueUrls: { name: url } })"]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async publish(queue, message) {
|
|
123
|
+
const info = await this.#queue(queue);
|
|
124
|
+
const binary = typeof message.body !== "string";
|
|
125
|
+
const attributes = headerRecord();
|
|
126
|
+
attributes[MessagingHeaders.Id] = message.id;
|
|
127
|
+
if (message.type !== void 0) attributes[MessagingHeaders.Type] = message.type;
|
|
128
|
+
attributes[MessagingHeaders.ContentType] = message.contentType;
|
|
129
|
+
if (binary) attributes[MessagingHeaders.Encoding] = "base64";
|
|
130
|
+
for (const [name, value] of Object.entries(message.headers)) {
|
|
131
|
+
if (TRANSPORT_ATTRIBUTES.has(name)) continue;
|
|
132
|
+
attributes[name] = value;
|
|
133
|
+
}
|
|
134
|
+
const input = {
|
|
135
|
+
QueueUrl: info.url,
|
|
136
|
+
MessageBody: encodeBody(message.body),
|
|
137
|
+
MessageAttributes: toAttributes(attributes, queue)
|
|
138
|
+
};
|
|
139
|
+
if (message.delayMs > 0) {
|
|
140
|
+
if (info.fifo) throw new RangeError(`[znight] SQS FIFO queue "${queue}" does not support per-message delays`);
|
|
141
|
+
input.DelaySeconds = Math.min(MAX_DELAY_SECONDS, Math.ceil(message.delayMs / 1e3));
|
|
142
|
+
}
|
|
143
|
+
if (info.fifo) {
|
|
144
|
+
input.MessageGroupId = message.key ?? "default";
|
|
145
|
+
input.MessageDeduplicationId = message.id;
|
|
146
|
+
}
|
|
147
|
+
await this.#sqs(`publish to "${queue}"`).send(new SendMessageCommand(input));
|
|
148
|
+
}
|
|
149
|
+
async subscribe(queue, handler, options) {
|
|
150
|
+
const info = await this.#queue(queue);
|
|
151
|
+
const { Attributes: attributes = {} } = await this.#sqs(`subscribe to "${queue}"`).send(new GetQueueAttributesCommand({
|
|
152
|
+
QueueUrl: info.url,
|
|
153
|
+
AttributeNames: ["VisibilityTimeout", "RedrivePolicy"]
|
|
154
|
+
}));
|
|
155
|
+
const visibilityTimeout = this.#options.visibilityTimeout ?? (Number(attributes.VisibilityTimeout) || 30);
|
|
156
|
+
const redrive = parseRedrivePolicy(attributes.RedrivePolicy);
|
|
157
|
+
if (redrive !== void 0 && redrive.maxReceiveCount < options.maxAttempts) throw consumerError(`SQS: queue "${queue}" moves messages to its dead-letter queue after ${redrive.maxReceiveCount} receive(s), but ${options.consumer} allows ${options.maxAttempts} attempt(s)`, [`Raise the RedrivePolicy maxReceiveCount of "${queue}" to at least ${options.maxAttempts}`, "Or lower the @Retry attempts"]);
|
|
158
|
+
let deadLetter;
|
|
159
|
+
let dlqName = options.deadLetterQueue;
|
|
160
|
+
if (dlqName !== void 0 && redrive !== void 0) {
|
|
161
|
+
const target = queueName(redrive.deadLetterTargetArn);
|
|
162
|
+
if (dlqName === this.deadLetterName(queue)) dlqName = target;
|
|
163
|
+
else if (dlqName !== target) this.#logger.warn(`SQS: ${options.consumer} dead-letters to "${dlqName}" but the RedrivePolicy of "${queue}" targets "${target}"`);
|
|
164
|
+
}
|
|
165
|
+
if (dlqName !== void 0) deadLetter = await this.#queue(dlqName);
|
|
166
|
+
const subscription = {
|
|
167
|
+
queue: info,
|
|
168
|
+
handler,
|
|
169
|
+
options,
|
|
170
|
+
deadLetter,
|
|
171
|
+
visibilityTimeout,
|
|
172
|
+
abort: new AbortController(),
|
|
173
|
+
groups: /* @__PURE__ */ new Map(),
|
|
174
|
+
inflight: 0,
|
|
175
|
+
active: true,
|
|
176
|
+
wake: void 0,
|
|
177
|
+
loop: void 0
|
|
178
|
+
};
|
|
179
|
+
this.#subscriptions.add(subscription);
|
|
180
|
+
subscription.loop = this.#poll(subscription);
|
|
181
|
+
return {
|
|
182
|
+
cancel: async () => {
|
|
183
|
+
this.#stopPolling(subscription);
|
|
184
|
+
await subscription.loop;
|
|
185
|
+
},
|
|
186
|
+
close: async () => {
|
|
187
|
+
this.#stopPolling(subscription);
|
|
188
|
+
this.#subscriptions.delete(subscription);
|
|
189
|
+
await subscription.loop;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
#stopPolling(subscription) {
|
|
194
|
+
subscription.active = false;
|
|
195
|
+
subscription.abort.abort();
|
|
196
|
+
subscription.wake?.();
|
|
197
|
+
}
|
|
198
|
+
async #poll(subscription) {
|
|
199
|
+
const { queue, options } = subscription;
|
|
200
|
+
const signal = subscription.abort.signal;
|
|
201
|
+
let errorDelay = 500;
|
|
202
|
+
while (subscription.active) {
|
|
203
|
+
const free = options.concurrency - subscription.inflight;
|
|
204
|
+
if (free <= 0) {
|
|
205
|
+
await new Promise((resolve) => {
|
|
206
|
+
subscription.wake = resolve;
|
|
207
|
+
});
|
|
208
|
+
subscription.wake = void 0;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const result = await this.#sqs(`receive from "${queue.name}"`).send(new ReceiveMessageCommand({
|
|
213
|
+
QueueUrl: queue.url,
|
|
214
|
+
MaxNumberOfMessages: Math.min(10, free),
|
|
215
|
+
WaitTimeSeconds: this.#options.waitTimeSeconds ?? 20,
|
|
216
|
+
VisibilityTimeout: subscription.visibilityTimeout,
|
|
217
|
+
MessageSystemAttributeNames: [
|
|
218
|
+
"ApproximateReceiveCount",
|
|
219
|
+
"SentTimestamp",
|
|
220
|
+
"MessageGroupId"
|
|
221
|
+
],
|
|
222
|
+
MessageAttributeNames: ["All"]
|
|
223
|
+
}), { abortSignal: signal });
|
|
224
|
+
errorDelay = 500;
|
|
225
|
+
for (const message of result.Messages ?? []) if (subscription.active) this.#deliver(subscription, message);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (!subscription.active) break;
|
|
228
|
+
this.#logger.warn(`SQS: receiving from "${queue.name}" failed, retrying in ${errorDelay} ms`, { err: error });
|
|
229
|
+
try {
|
|
230
|
+
await sleep(errorDelay, signal);
|
|
231
|
+
} catch {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
errorDelay = Math.min(errorDelay * 2, 3e4);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
#deliver(subscription, message) {
|
|
239
|
+
const { queue } = subscription;
|
|
240
|
+
const client = this.#sqs("handle a message");
|
|
241
|
+
const receiptHandle = message.ReceiptHandle;
|
|
242
|
+
const attributes = message.MessageAttributes ?? {};
|
|
243
|
+
const transport = headerRecord();
|
|
244
|
+
const headers = headerRecord();
|
|
245
|
+
for (const [name, attribute] of Object.entries(attributes)) {
|
|
246
|
+
const value = attributeToString(attribute);
|
|
247
|
+
if (value === void 0) continue;
|
|
248
|
+
if (TRANSPORT_ATTRIBUTES.has(name)) transport[name] = value;
|
|
249
|
+
else headers[name] = value;
|
|
250
|
+
}
|
|
251
|
+
const encoded = message.Body ?? "";
|
|
252
|
+
const body = transport[MessagingHeaders.Encoding] === "base64" ? Buffer.from(encoded, "base64") : encoded;
|
|
253
|
+
const system = message.Attributes ?? {};
|
|
254
|
+
const receiveCount = Number(system.ApproximateReceiveCount);
|
|
255
|
+
const sentAt = Number(system.SentTimestamp);
|
|
256
|
+
const id = transport[MessagingHeaders.Id] ?? message.MessageId ?? receiptHandle;
|
|
257
|
+
const groupId = system.MessageGroupId;
|
|
258
|
+
subscription.inflight++;
|
|
259
|
+
let lease;
|
|
260
|
+
/** Lease extension in flight (one at a time). */
|
|
261
|
+
let extending;
|
|
262
|
+
const extendEvery = Math.floor(subscription.visibilityTimeout * 1e3 / 2);
|
|
263
|
+
if ((this.#options.extendVisibility ?? true) && extendEvery >= 1e3) lease = setInterval(() => {
|
|
264
|
+
if (extending !== void 0) return;
|
|
265
|
+
extending = client.send(new ChangeMessageVisibilityCommand({
|
|
266
|
+
QueueUrl: queue.url,
|
|
267
|
+
ReceiptHandle: receiptHandle,
|
|
268
|
+
VisibilityTimeout: subscription.visibilityTimeout
|
|
269
|
+
})).then(() => void 0, (error) => {
|
|
270
|
+
this.#logger.warn(`SQS: failed to extend the visibility of message ${id}`, { err: error });
|
|
271
|
+
}).finally(() => {
|
|
272
|
+
extending = void 0;
|
|
273
|
+
});
|
|
274
|
+
}, extendEvery);
|
|
275
|
+
/**
|
|
276
|
+
* Stops extending the lease before a settlement, once the extension in flight landed: otherwise it
|
|
277
|
+
* could reach SQS after the settlement and override the visibility it set (retry delay, release).
|
|
278
|
+
* When the settlement fails, the runtime releases the message (or its visibility timeout expires).
|
|
279
|
+
*/
|
|
280
|
+
const stopLease = async () => {
|
|
281
|
+
if (lease !== void 0) clearInterval(lease);
|
|
282
|
+
lease = void 0;
|
|
283
|
+
await extending;
|
|
284
|
+
};
|
|
285
|
+
let settled = false;
|
|
286
|
+
const check = (action) => {
|
|
287
|
+
if (settled) throw new Error(`[sqs] message ${id} was already settled (${action})`);
|
|
288
|
+
};
|
|
289
|
+
const done = () => {
|
|
290
|
+
settled = true;
|
|
291
|
+
};
|
|
292
|
+
const changeVisibility = async (seconds) => {
|
|
293
|
+
await client.send(new ChangeMessageVisibilityCommand({
|
|
294
|
+
QueueUrl: queue.url,
|
|
295
|
+
ReceiptHandle: receiptHandle,
|
|
296
|
+
VisibilityTimeout: seconds
|
|
297
|
+
}));
|
|
298
|
+
};
|
|
299
|
+
const remove = async () => {
|
|
300
|
+
await client.send(new DeleteMessageCommand({
|
|
301
|
+
QueueUrl: queue.url,
|
|
302
|
+
ReceiptHandle: receiptHandle
|
|
303
|
+
}));
|
|
304
|
+
};
|
|
305
|
+
const delivery = {
|
|
306
|
+
id,
|
|
307
|
+
body,
|
|
308
|
+
contentType: transport[MessagingHeaders.ContentType],
|
|
309
|
+
headers,
|
|
310
|
+
type: transport[MessagingHeaders.Type],
|
|
311
|
+
key: groupId,
|
|
312
|
+
attempt: Number.isSafeInteger(receiveCount) && receiveCount > 0 ? receiveCount : 1,
|
|
313
|
+
redelivered: receiveCount > 1,
|
|
314
|
+
timestamp: Number.isFinite(sentAt) ? sentAt : void 0,
|
|
315
|
+
raw: message,
|
|
316
|
+
ack: async () => {
|
|
317
|
+
check("ack");
|
|
318
|
+
await stopLease();
|
|
319
|
+
await remove();
|
|
320
|
+
done();
|
|
321
|
+
},
|
|
322
|
+
release: async () => {
|
|
323
|
+
check("release");
|
|
324
|
+
await stopLease();
|
|
325
|
+
await changeVisibility(0);
|
|
326
|
+
done();
|
|
327
|
+
},
|
|
328
|
+
retry: async (request) => {
|
|
329
|
+
check("retry");
|
|
330
|
+
await stopLease();
|
|
331
|
+
await changeVisibility(Math.min(MAX_VISIBILITY_SECONDS, Math.ceil(request.delayMs / 1e3)));
|
|
332
|
+
done();
|
|
333
|
+
},
|
|
334
|
+
deadLetter: async (request) => {
|
|
335
|
+
check("dead-letter");
|
|
336
|
+
await stopLease();
|
|
337
|
+
const target = request.queue === void 0 ? void 0 : subscription.deadLetter ?? await this.#queue(request.queue);
|
|
338
|
+
if (target !== void 0) await client.send(new SendMessageCommand(this.#deadLetterInput(target, queue, encoded, transport, headers, request, groupId, id)));
|
|
339
|
+
await remove();
|
|
340
|
+
done();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const serialKey = queue.fifo ? groupId : void 0;
|
|
344
|
+
const previous = serialKey === void 0 ? void 0 : subscription.groups.get(serialKey);
|
|
345
|
+
const run = () => subscription.handler(delivery);
|
|
346
|
+
const handled = (previous === void 0 ? run() : previous.then(run, run)).catch((error) => {
|
|
347
|
+
this.#logger.error(`SQS: unexpected failure handling message ${id}`, { err: error });
|
|
348
|
+
}).finally(() => {
|
|
349
|
+
if (lease !== void 0) clearInterval(lease);
|
|
350
|
+
subscription.inflight--;
|
|
351
|
+
if (serialKey !== void 0 && subscription.groups.get(serialKey) === handled) subscription.groups.delete(serialKey);
|
|
352
|
+
subscription.wake?.();
|
|
353
|
+
});
|
|
354
|
+
if (serialKey !== void 0) subscription.groups.set(serialKey, handled);
|
|
355
|
+
}
|
|
356
|
+
#deadLetterInput(target, source, body, transport, headers, request, groupId, id) {
|
|
357
|
+
const attributes = headerRecord();
|
|
358
|
+
Object.assign(attributes, transport);
|
|
359
|
+
attributes[MessagingHeaders.DeadLetterReason] = request.reason;
|
|
360
|
+
attributes[MessagingHeaders.Error] = truncate(errorMessage(request.error), 1024) || "unknown error";
|
|
361
|
+
attributes[MessagingHeaders.OriginalQueue] = source.name;
|
|
362
|
+
attributes[MessagingHeaders.Attempts] = String(request.attempt);
|
|
363
|
+
const dropped = [];
|
|
364
|
+
for (const [name, value] of Object.entries(headers)) if (Object.keys(attributes).length >= MAX_ATTRIBUTES) dropped.push(name);
|
|
365
|
+
else attributes[name] = value;
|
|
366
|
+
if (dropped.length > 0) this.#logger.warn(`SQS: dead-letter copy of message ${id} exceeds ${MAX_ATTRIBUTES} attributes, dropped: ${dropped.join(", ")}`);
|
|
367
|
+
const input = {
|
|
368
|
+
QueueUrl: target.url,
|
|
369
|
+
MessageBody: body,
|
|
370
|
+
MessageAttributes: toAttributes(attributes, target.name)
|
|
371
|
+
};
|
|
372
|
+
if (target.fifo) {
|
|
373
|
+
input.MessageGroupId = groupId ?? "default";
|
|
374
|
+
input.MessageDeduplicationId = `${id}-dead-letter`.slice(0, 128);
|
|
375
|
+
}
|
|
376
|
+
return input;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
/** Queue name of a name, URL or ARN (last segment). */
|
|
380
|
+
function queueName(queue) {
|
|
381
|
+
const segments = queue.split(/[/:]/);
|
|
382
|
+
return segments[segments.length - 1] ?? queue;
|
|
383
|
+
}
|
|
384
|
+
function encodeBody(body) {
|
|
385
|
+
return typeof body === "string" ? body : Buffer.from(bodyToBytes(body)).toString("base64");
|
|
386
|
+
}
|
|
387
|
+
function toAttributes(attributes, queue) {
|
|
388
|
+
const entries = Object.entries(attributes);
|
|
389
|
+
if (entries.length > MAX_ATTRIBUTES) throw new RangeError(`[znight] SQS messages carry at most ${MAX_ATTRIBUTES} attributes (Znight uses up to 4): too many headers for "${queue}"`);
|
|
390
|
+
const result = {};
|
|
391
|
+
for (const [name, value] of entries) {
|
|
392
|
+
if (!ATTRIBUTE_NAME.test(name)) throw new TypeError(`[znight] "${name}" is not a valid SQS message attribute name`);
|
|
393
|
+
if (value === "") throw new TypeError(`[znight] SQS message attributes cannot be empty (header "${name}")`);
|
|
394
|
+
result[name] = {
|
|
395
|
+
DataType: "String",
|
|
396
|
+
StringValue: value
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
function attributeToString(attribute) {
|
|
402
|
+
if (attribute.StringValue !== void 0) return attribute.StringValue;
|
|
403
|
+
if (attribute.BinaryValue !== void 0) return Buffer.from(attribute.BinaryValue).toString("base64");
|
|
404
|
+
}
|
|
405
|
+
function parseRedrivePolicy(value) {
|
|
406
|
+
if (value === void 0 || value === "") return void 0;
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(value);
|
|
409
|
+
const maxReceiveCount = Number(parsed.maxReceiveCount);
|
|
410
|
+
if (typeof parsed.deadLetterTargetArn !== "string" || !Number.isFinite(maxReceiveCount)) return void 0;
|
|
411
|
+
return {
|
|
412
|
+
deadLetterTargetArn: parsed.deadLetterTargetArn,
|
|
413
|
+
maxReceiveCount
|
|
414
|
+
};
|
|
415
|
+
} catch {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
export { SqsDriver, sqs };
|
|
420
|
+
|
|
421
|
+
//# sourceMappingURL=sqs.mjs.map
|