From 3d2070b30c9fec484c8be820132db8df2fee3c9f Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Tue, 16 Jun 2026 12:59:54 +0300 Subject: [PATCH 1/4] feat(sqs): dead-letter queue support + full consumer-option config for invalidation triggers Invalidation triggers previously forwarded only a hand-picked subset of message-queue-toolkit consumer options (creationConfig/locatorConfig/ subscriptionConfig), so there was no way to configure a dead-letter queue when creating a trigger, and any other consumer option (concurrentConsumersAmount, consumerOverrides, ...) was silently dropped. Changes (all four triggers: SnsTopic / SqsQueue and their Group variants): - buildConsumer now forwards the entire source (minus the trigger-only bindings/ messageTypeField) to the underlying consumer, with trigger-built handlers always overriding. A DLQ supplied with a creationConfig is auto-created, its redrive policy attached, and (for SNS) its subscription wired by the toolkit. - Each source config type is now the consumer options type minus `handlers`, so both styles are fully type-checked: * explicit - spell out every option with autocomplete + typo detection * spread - drop in a pre-resolved options object, e.g. @lokalise/aws-config's resolveConsumerOptions(...) - Exposed per-trigger DLQ helper types (Sns/Sqs[Group]InvalidationDeadLetterQueueConfig). Tests: - Runtime: SNS trigger auto-creates a DLQ and wires the redrive policy. - Types: test/triggers.test-d.ts asserts spread + explicit configuration, typo/wrong-type rejection, and DLQ helper shapes (vitest typecheck). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/sqs/README.md | 66 +++++++- packages/sqs/index.ts | 4 + .../SnsTopicGroupInvalidationTrigger.ts | 55 +++++-- .../triggers/SnsTopicInvalidationTrigger.ts | 55 +++++-- .../SqsQueueGroupInvalidationTrigger.ts | 51 ++++-- .../triggers/SqsQueueInvalidationTrigger.ts | 51 ++++-- .../sqs/test/SqsInvalidationTrigger.spec.ts | 85 +++++++++- packages/sqs/test/triggers.test-d.ts | 145 ++++++++++++++++++ packages/sqs/tsconfig.test-d.json | 5 + packages/sqs/vitest.config.mts | 5 + 10 files changed, 471 insertions(+), 51 deletions(-) create mode 100644 packages/sqs/test/triggers.test-d.ts create mode 100644 packages/sqs/tsconfig.test-d.json diff --git a/packages/sqs/README.md b/packages/sqs/README.md index 31ee959..0c8cc25 100644 --- a/packages/sqs/README.md +++ b/packages/sqs/README.md @@ -29,6 +29,8 @@ The implementation is built on top of [`@message-queue-toolkit/sns`](https://git - [Mixing source kinds with `composeTriggers`](#mixing-source-kinds-with-composetriggers) - [Resolver semantics](#resolver-semantics) - [Error handling and retries](#error-handling-and-retries) + - [Dead-letter queues](#dead-letter-queues) + - [Explicit vs spread configuration](#explicit-vs-spread-configuration) - [Testing with fauxqs](#testing-with-fauxqs) - [API reference](#api-reference) @@ -560,7 +562,66 @@ If the resolver or apply step throws, the trigger: 1. Invokes the optional `errorHandler(err, channel)` for observability. 2. Re-throws so `message-queue-toolkit` can apply its standard SQS retry / dead-letter behaviour. -For schema-violation errors, the message is failed by `message-queue-toolkit` before the resolver runs and goes back to the queue (and ultimately to a DLQ if you configured one). Configure DLQ on the trigger's queue creation config to bound retries. +For schema-violation errors, the message is failed by `message-queue-toolkit` before the resolver runs and goes back to the queue (and ultimately to a DLQ if you configured one). + +### Dead-letter queues + +To bound retries, add a `deadLetterQueue` to any trigger source. With a `creationConfig` the trigger **auto-creates the DLQ, attaches the redrive policy to its own queue, and (for SNS sources) wires the subscription** — no manual AWS setup required: + +```ts +const trigger = new SnsTopicInvalidationTrigger({ + target: userLoader, + dependencies: consumerDeps, + sources: [ + { + creationConfig: { + topic: { Name: 'domain-events.users' }, + queue: { QueueName: `cache-trigger-${process.env.HOSTNAME}` }, + }, + deadLetterQueue: { + // Move a message to the DLQ after this many failed receives. + redrivePolicy: { maxReceiveCount: 3 }, + // Auto-create the DLQ. Use `locatorConfig` instead to point at an existing one. + creationConfig: { queue: { QueueName: `cache-trigger-${process.env.HOSTNAME}-dlq` } }, + }, + bindings: [/* ... */], + }, + ], +}) +``` + +The `deadLetterQueue` field is the same shape `message-queue-toolkit` exposes on its consumers, surfaced per trigger as `SnsTopicInvalidationDeadLetterQueueConfig` (and the `SqsQueue*` / `*Group*` counterparts). It is available on all four trigger source types. + +### Explicit vs spread configuration + +A trigger source **is** the underlying `message-queue-toolkit` consumer options (minus the `handlers` list, which the trigger builds from `bindings`). That means both styles are fully type-checked — autocomplete and typo detection on every option: + +- **Explicit** — spell options out inline (`creationConfig`/`locatorConfig`, `deadLetterQueue`, `subscriptionConfig`, `concurrentConsumersAmount`, `consumerOverrides`, ...). An unknown key or a wrong-typed value is a compile error. +- **Spread** — resolve options elsewhere — e.g. with `@lokalise/aws-config`'s `getSnsMqtOptionsResolver()` — and spread the result straight in. + +The trigger always overrides `handlers` with the ones it builds from `bindings`; everything else flows through untouched. + +```ts + +```ts +const resolver = getSnsMqtOptionsResolver({ appEnv: 'production' }) +const options = resolver.resolveConsumerOptions(topicName, queueName, { + /* awsConfig, logger, deadLetterQueue, ... */ +}) + +const trigger = new SnsTopicInvalidationTrigger({ + target, + dependencies, + sources: [ + { + ...options, + bindings: [ + { messageSchema: PROJECT_LANGUAGE_EVENT_SCHEMA, resolver: async (message) => { /* ... */ } }, + ], + }, + ], +}) +``` ### Lifecycle @@ -624,7 +685,8 @@ Then point your AWS SDK clients at `http://localhost:4566`. | `SqsQueueInvalidationTrigger` | Flat-cache trigger consuming directly from upstream SQS queues. | | `SnsTopicGroupInvalidationTrigger` / `SqsQueueGroupInvalidationTrigger` | `GroupLoader` counterparts. | | `composeTriggers(...triggers)` | Wraps multiple `InvalidationTrigger`s into one combined `start()` / `stop()`. | -| `SnsTopicInvalidationSource` / `SqsQueueInvalidationSource` | A single upstream source plus its bindings and optional `messageTypeField`. Group counterparts have parallel names. | +| `SnsTopicInvalidationSource` / `SqsQueueInvalidationSource` | A single upstream source: every `message-queue-toolkit` consumer option (`creationConfig`/`locatorConfig`, `deadLetterQueue`, `subscriptionConfig`, ...) minus `handlers`, plus `bindings` and optional `messageTypeField`. Fully typed for both explicit and spread configuration. Group counterparts have parallel names. | +| `SnsTopicInvalidationDeadLetterQueueConfig` / `SqsQueueInvalidationDeadLetterQueueConfig` | Type for the `deadLetterQueue` field (`redrivePolicy` + `creationConfig`/`locatorConfig`). Group counterparts have parallel names. | | `FlatBinding` / `GroupBinding` | One `(messageSchema, resolver, messageType?)` triple. | | `InvalidationTarget` / `GroupInvalidationTarget` | Structural interfaces that `Loader` / `GroupLoader` satisfy. | | `InvalidationAction` / `GroupInvalidationAction` | Action ADTs returned by resolvers. | diff --git a/packages/sqs/index.ts b/packages/sqs/index.ts index f86cfad..bdcc656 100644 --- a/packages/sqs/index.ts +++ b/packages/sqs/index.ts @@ -81,6 +81,7 @@ export { export { deriveSqsQueueChannelName, SqsQueueInvalidationTrigger, + type SqsQueueInvalidationDeadLetterQueueConfig, type SqsQueueInvalidationSource, type SqsQueueInvalidationTriggerParams, type SqsQueueSourceConfig, @@ -88,6 +89,7 @@ export { export { deriveSnsTopicChannelName, SnsTopicInvalidationTrigger, + type SnsTopicInvalidationDeadLetterQueueConfig, type SnsTopicInvalidationSource, type SnsTopicInvalidationTriggerParams, type SnsTopicSourceConfig, @@ -95,6 +97,7 @@ export { export { deriveSqsQueueGroupChannelName, SqsQueueGroupInvalidationTrigger, + type SqsQueueGroupInvalidationDeadLetterQueueConfig, type SqsQueueGroupInvalidationSource, type SqsQueueGroupInvalidationTriggerParams, type SqsQueueGroupSourceConfig, @@ -102,6 +105,7 @@ export { export { deriveSnsTopicGroupChannelName, SnsTopicGroupInvalidationTrigger, + type SnsTopicGroupInvalidationDeadLetterQueueConfig, type SnsTopicGroupInvalidationSource, type SnsTopicGroupInvalidationTriggerParams, type SnsTopicGroupSourceConfig, diff --git a/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts b/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts index ad94b4a..31086e9 100644 --- a/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts @@ -1,10 +1,7 @@ import type { SNSSQSConsumerDependencies, SNSSQSConsumerOptions, - SNSSQSCreationConfig, - SNSSQSQueueLocatorType, } from '@message-queue-toolkit/sns' -import type { SqsSubscriptionOptions } from '../SqsNotificationConsumer.js' import { AbstractSqsTrigger, type InternalConsumerHandle, @@ -17,12 +14,36 @@ import { } from './bindingHelpers.js' import type { GroupInvalidationTarget, TriggerErrorHandler } from './types.js' -export type SnsTopicGroupSourceConfig = - | { creationConfig: SNSSQSCreationConfig; locatorConfig?: never } - | { creationConfig?: never; locatorConfig: SNSSQSQueueLocatorType } +/** + * Every option the underlying `message-queue-toolkit` SNS→SQS consumer accepts, + * minus the `handlers` list (which the trigger builds from `bindings`). + * + * This is the per-source config surface, so callers get the same flexibility in + * two interchangeable styles, both fully type-checked: + * + * - **Explicit** — spell out `creationConfig`/`locatorConfig`, `subscriptionConfig`, + * `deadLetterQueue`, `concurrentConsumersAmount`, etc. with autocomplete and + * typo detection. + * - **Spread** — drop in a pre-resolved options object (e.g. + * `@lokalise/aws-config`'s `resolveConsumerOptions(...)`) with `...options`. + */ +export type SnsTopicGroupSourceConfig = Omit< + SNSSQSConsumerOptions, undefined>, + 'handlers' +> + +/** + * Dead-letter-queue config for an SNS-topic group source, surfaced verbatim + * from the underlying `message-queue-toolkit` consumer. Supplying it with a + * `creationConfig` makes the toolkit auto-create the DLQ, attach the redrive + * policy to the trigger's own queue, and wire the topic subscription; a + * `locatorConfig` points the redrive policy at a pre-existing DLQ instead. + */ +export type SnsTopicGroupInvalidationDeadLetterQueueConfig = NonNullable< + SnsTopicGroupSourceConfig['deadLetterQueue'] +> export type SnsTopicGroupInvalidationSource = SnsTopicGroupSourceConfig & { - subscriptionConfig?: SqsSubscriptionOptions messageTypeField?: string // biome-ignore lint/suspicious/noExplicitAny: bindings heterogeneous over TMessage bindings: readonly GroupBinding[] @@ -61,22 +82,30 @@ export class SnsTopicGroupInvalidationTrigger extends AbstractSqsTrigger { private buildConsumer(source: SnsTopicGroupInvalidationSource): InternalConsumerHandle { const channel = this.channelOverride ?? deriveSnsTopicGroupChannelName(source) + const { bindings, messageTypeField, ...consumerOptions } = source const { handlers, context, messageTypeResolver } = buildGroupBindings( - source.bindings, - source.messageTypeField, + bindings, + messageTypeField, this.target, channel, this.errorHandler, ) - const base = { + // Forward every consumer option the caller supplied. This deliberately + // spreads the whole source (minus the trigger-only `bindings`/ + // `messageTypeField`) so a pre-resolved options object — e.g. the output of + // `@lokalise/aws-config`'s `resolveConsumerOptions(...)` — can be spread + // straight into the source and have all of its fields (`creationConfig`/ + // `locatorConfig`, `deadLetterQueue`, `concurrentConsumersAmount`, + // `consumerOverrides`, ...) flow through untouched. The trigger owns + // `handlers`, so the bindings-derived handlers always override any in the + // spread. + const options = { + ...consumerOptions, handlers, messageTypeResolver, subscriptionConfig: source.subscriptionConfig ?? { updateAttributesIfExists: false }, } - const options = source.creationConfig - ? { ...base, creationConfig: source.creationConfig } - : { ...base, locatorConfig: source.locatorConfig } return new SnsTopicTriggerConsumer( this.dependencies, diff --git a/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts b/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts index 2d1bd65..5ba0760 100644 --- a/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts @@ -1,10 +1,7 @@ import type { SNSSQSConsumerDependencies, SNSSQSConsumerOptions, - SNSSQSCreationConfig, - SNSSQSQueueLocatorType, } from '@message-queue-toolkit/sns' -import type { SqsSubscriptionOptions } from '../SqsNotificationConsumer.js' import { AbstractSqsTrigger, type InternalConsumerHandle, @@ -17,12 +14,36 @@ import { } from './bindingHelpers.js' import type { InvalidationTarget, TriggerErrorHandler } from './types.js' -export type SnsTopicSourceConfig = - | { creationConfig: SNSSQSCreationConfig; locatorConfig?: never } - | { creationConfig?: never; locatorConfig: SNSSQSQueueLocatorType } +/** + * Every option the underlying `message-queue-toolkit` SNS→SQS consumer accepts, + * minus the `handlers` list (which the trigger builds from `bindings`). + * + * This is the per-source config surface, so callers get the same flexibility in + * two interchangeable styles, both fully type-checked: + * + * - **Explicit** — spell out `creationConfig`/`locatorConfig`, `subscriptionConfig`, + * `deadLetterQueue`, `concurrentConsumersAmount`, etc. with autocomplete and + * typo detection. + * - **Spread** — drop in a pre-resolved options object (e.g. + * `@lokalise/aws-config`'s `resolveConsumerOptions(...)`) with `...options`. + */ +export type SnsTopicSourceConfig = Omit< + SNSSQSConsumerOptions, undefined>, + 'handlers' +> + +/** + * Dead-letter-queue config for an SNS-topic source, surfaced verbatim from the + * underlying `message-queue-toolkit` consumer. Supplying it with a + * `creationConfig` makes the toolkit auto-create the DLQ, attach the redrive + * policy to the trigger's own queue, and wire the topic subscription; a + * `locatorConfig` points the redrive policy at a pre-existing DLQ instead. + */ +export type SnsTopicInvalidationDeadLetterQueueConfig = NonNullable< + SnsTopicSourceConfig['deadLetterQueue'] +> export type SnsTopicInvalidationSource = SnsTopicSourceConfig & { - subscriptionConfig?: SqsSubscriptionOptions messageTypeField?: string // biome-ignore lint/suspicious/noExplicitAny: bindings heterogeneous over TMessage bindings: readonly FlatBinding[] @@ -70,22 +91,30 @@ export class SnsTopicInvalidationTrigger extends AbstractSqsTrigger { private buildConsumer(source: SnsTopicInvalidationSource): InternalConsumerHandle { const channel = this.channelOverride ?? deriveSnsTopicChannelName(source) + const { bindings, messageTypeField, ...consumerOptions } = source const { handlers, context, messageTypeResolver } = buildFlatBindings( - source.bindings, - source.messageTypeField, + bindings, + messageTypeField, this.target, channel, this.errorHandler, ) - const base = { + // Forward every consumer option the caller supplied. This deliberately + // spreads the whole source (minus the trigger-only `bindings`/ + // `messageTypeField`) so a pre-resolved options object — e.g. the output of + // `@lokalise/aws-config`'s `resolveConsumerOptions(...)` — can be spread + // straight into the source and have all of its fields (`creationConfig`/ + // `locatorConfig`, `deadLetterQueue`, `concurrentConsumersAmount`, + // `consumerOverrides`, ...) flow through untouched. The trigger owns + // `handlers`, so the bindings-derived handlers always override any in the + // spread. + const options = { + ...consumerOptions, handlers, messageTypeResolver, subscriptionConfig: source.subscriptionConfig ?? { updateAttributesIfExists: false }, } - const options = source.creationConfig - ? { ...base, creationConfig: source.creationConfig } - : { ...base, locatorConfig: source.locatorConfig } return new SnsTopicTriggerConsumer( this.dependencies, diff --git a/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts b/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts index cc365f4..d1461ec 100644 --- a/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts @@ -1,8 +1,6 @@ import type { SQSConsumerDependencies, SQSConsumerOptions, - SQSCreationConfig, - SQSQueueLocatorType, } from '@message-queue-toolkit/sqs' import { AbstractSqsTrigger, @@ -16,9 +14,33 @@ import { } from './bindingHelpers.js' import type { GroupInvalidationTarget, TriggerErrorHandler } from './types.js' -export type SqsQueueGroupSourceConfig = - | { creationConfig: SQSCreationConfig; locatorConfig?: never } - | { creationConfig?: never; locatorConfig: SQSQueueLocatorType } +/** + * Every option the underlying `message-queue-toolkit` SQS consumer accepts, + * minus the `handlers` list (which the trigger builds from `bindings`). + * + * This is the per-source config surface, so callers get the same flexibility in + * two interchangeable styles, both fully type-checked: + * + * - **Explicit** — spell out `creationConfig`/`locatorConfig`, `deadLetterQueue`, + * `concurrentConsumersAmount`, etc. with autocomplete and typo detection. + * - **Spread** — drop in a pre-resolved options object (e.g. + * `@lokalise/aws-config`'s `resolveConsumerOptions(...)`) with `...options`. + */ +export type SqsQueueGroupSourceConfig = Omit< + SQSConsumerOptions, undefined>, + 'handlers' +> + +/** + * Dead-letter-queue config for an SQS-queue group source, surfaced verbatim + * from the underlying `message-queue-toolkit` consumer. Supplying it with a + * `creationConfig` makes the toolkit auto-create the DLQ and attach the redrive + * policy to the trigger's queue; a `locatorConfig` points the redrive policy at + * a pre-existing DLQ instead. + */ +export type SqsQueueGroupInvalidationDeadLetterQueueConfig = NonNullable< + SqsQueueGroupSourceConfig['deadLetterQueue'] +> export type SqsQueueGroupInvalidationSource = SqsQueueGroupSourceConfig & { messageTypeField?: string @@ -59,18 +81,25 @@ export class SqsQueueGroupInvalidationTrigger extends AbstractSqsTrigger { private buildConsumer(source: SqsQueueGroupInvalidationSource): InternalConsumerHandle { const channel = this.channelOverride ?? deriveSqsQueueGroupChannelName(source) + const { bindings, messageTypeField, ...consumerOptions } = source const { handlers, context, messageTypeResolver } = buildGroupBindings( - source.bindings, - source.messageTypeField, + bindings, + messageTypeField, this.target, channel, this.errorHandler, ) - const base = { handlers, messageTypeResolver } - const options = source.creationConfig - ? { ...base, creationConfig: source.creationConfig } - : { ...base, locatorConfig: source.locatorConfig } + // Forward every consumer option the caller supplied. This deliberately + // spreads the whole source (minus the trigger-only `bindings`/ + // `messageTypeField`) so a pre-resolved options object — e.g. the output of + // `@lokalise/aws-config`'s `resolveConsumerOptions(...)` — can be spread + // straight into the source and have all of its fields (`creationConfig`/ + // `locatorConfig`, `deadLetterQueue`, `concurrentConsumersAmount`, + // `consumerOverrides`, ...) flow through untouched. The trigger owns + // `handlers`, so the bindings-derived handlers always override any in the + // spread. + const options = { ...consumerOptions, handlers, messageTypeResolver } return new SqsQueueTriggerConsumer( this.dependencies, diff --git a/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts b/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts index 81e7b62..45394ed 100644 --- a/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts @@ -1,8 +1,6 @@ import type { SQSConsumerDependencies, SQSConsumerOptions, - SQSCreationConfig, - SQSQueueLocatorType, } from '@message-queue-toolkit/sqs' import { AbstractSqsTrigger, @@ -16,9 +14,33 @@ import { } from './bindingHelpers.js' import type { InvalidationTarget, TriggerErrorHandler } from './types.js' -export type SqsQueueSourceConfig = - | { creationConfig: SQSCreationConfig; locatorConfig?: never } - | { creationConfig?: never; locatorConfig: SQSQueueLocatorType } +/** + * Every option the underlying `message-queue-toolkit` SQS consumer accepts, + * minus the `handlers` list (which the trigger builds from `bindings`). + * + * This is the per-source config surface, so callers get the same flexibility in + * two interchangeable styles, both fully type-checked: + * + * - **Explicit** — spell out `creationConfig`/`locatorConfig`, `deadLetterQueue`, + * `concurrentConsumersAmount`, etc. with autocomplete and typo detection. + * - **Spread** — drop in a pre-resolved options object (e.g. + * `@lokalise/aws-config`'s `resolveConsumerOptions(...)`) with `...options`. + */ +export type SqsQueueSourceConfig = Omit< + SQSConsumerOptions, undefined>, + 'handlers' +> + +/** + * Dead-letter-queue config for an SQS-queue source, surfaced verbatim from the + * underlying `message-queue-toolkit` consumer. Supplying it with a + * `creationConfig` makes the toolkit auto-create the DLQ and attach the redrive + * policy to the trigger's queue; a `locatorConfig` points the redrive policy at + * a pre-existing DLQ instead. + */ +export type SqsQueueInvalidationDeadLetterQueueConfig = NonNullable< + SqsQueueSourceConfig['deadLetterQueue'] +> export type SqsQueueInvalidationSource = SqsQueueSourceConfig & { /** @@ -83,18 +105,25 @@ export class SqsQueueInvalidationTrigger extends AbstractSqsTrigger { private buildConsumer(source: SqsQueueInvalidationSource): InternalConsumerHandle { const channel = this.channelOverride ?? deriveSqsQueueChannelName(source) + const { bindings, messageTypeField, ...consumerOptions } = source const { handlers, context, messageTypeResolver } = buildFlatBindings( - source.bindings, - source.messageTypeField, + bindings, + messageTypeField, this.target, channel, this.errorHandler, ) - const base = { handlers, messageTypeResolver } - const options = source.creationConfig - ? { ...base, creationConfig: source.creationConfig } - : { ...base, locatorConfig: source.locatorConfig } + // Forward every consumer option the caller supplied. This deliberately + // spreads the whole source (minus the trigger-only `bindings`/ + // `messageTypeField`) so a pre-resolved options object — e.g. the output of + // `@lokalise/aws-config`'s `resolveConsumerOptions(...)` — can be spread + // straight into the source and have all of its fields (`creationConfig`/ + // `locatorConfig`, `deadLetterQueue`, `concurrentConsumersAmount`, + // `consumerOverrides`, ...) flow through untouched. The trigger owns + // `handlers`, so the bindings-derived handlers always override any in the + // spread. + const options = { ...consumerOptions, handlers, messageTypeResolver } return new SqsQueueTriggerConsumer( this.dependencies, diff --git a/packages/sqs/test/SqsInvalidationTrigger.spec.ts b/packages/sqs/test/SqsInvalidationTrigger.spec.ts index 19be3fe..6453e23 100644 --- a/packages/sqs/test/SqsInvalidationTrigger.spec.ts +++ b/packages/sqs/test/SqsInvalidationTrigger.spec.ts @@ -1,4 +1,9 @@ -import { CreateQueueCommand, GetQueueUrlCommand, SendMessageCommand } from '@aws-sdk/client-sqs' +import { + CreateQueueCommand, + GetQueueAttributesCommand, + GetQueueUrlCommand, + SendMessageCommand, +} from '@aws-sdk/client-sqs' import { CreateTopicCommand, PublishCommand } from '@aws-sdk/client-sns' import { Loader, type InMemoryCacheConfiguration } from 'layered-loader' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -638,6 +643,84 @@ describe('SnsTopicInvalidationTrigger', () => { await trigger.stop() } }) + + it('auto-creates a dead-letter queue and wires its redrive policy', async () => { + const suffix = Math.random().toString(36).slice(2, 8) + + const loader = new Loader({ + inMemoryCache: IN_MEMORY_CACHE_CONFIG, + asyncCache: new StubAsyncCache('value'), + }) + + const upstreamTopicName = `dlq-domain-${suffix}` + const upstreamTopicArn = ( + await clients.snsClient.send(new CreateTopicCommand({ Name: upstreamTopicName })) + ).TopicArn! + + const queueName = `dlq-trigger-q-${suffix}` + const dlqName = `dlq-trigger-q-${suffix}-dlq` + + const trigger = new SnsTopicInvalidationTrigger({ + target: loader, + dependencies: buildConsumerDeps(clients), + sources: [ + { + creationConfig: { + topic: { Name: upstreamTopicName }, + queue: { QueueName: queueName }, + }, + deadLetterQueue: { + redrivePolicy: { maxReceiveCount: 3 }, + creationConfig: { queue: { QueueName: dlqName } }, + }, + bindings: [ + { + messageSchema: UPSTREAM_EVENT_SCHEMA, + resolver: (m: UpstreamEvent) => + m.userId ? { kind: 'delete', key: m.userId } : null, + }, + ], + }, + ], + }) + + try { + await loader.init() + await trigger.start() + + // The DLQ was created by the trigger's consumer, not by the test. + const dlqUrl = ( + await clients.sqsClient.send(new GetQueueUrlCommand({ QueueName: dlqName })) + ).QueueUrl! + expect(dlqUrl).toContain(dlqName) + + // ...and the source queue's redrive policy points at it. + const sourceQueueUrl = ( + await clients.sqsClient.send(new GetQueueUrlCommand({ QueueName: queueName })) + ).QueueUrl! + const attrs = await clients.sqsClient.send( + new GetQueueAttributesCommand({ + QueueUrl: sourceQueueUrl, + AttributeNames: ['RedrivePolicy'], + }), + ) + const redrivePolicy = JSON.parse(attrs.Attributes!.RedrivePolicy!) + expect(redrivePolicy.maxReceiveCount).toBe(3) + expect(redrivePolicy.deadLetterTargetArn).toContain(dlqName) + + // The trigger still routes invalidations normally alongside the DLQ. + await loader.getAsyncOnly('user-1') + await clients.snsClient.send( + new PublishCommand({ + TopicArn: upstreamTopicArn, + Message: JSON.stringify({ eventType: 'user.updated', userId: 'user-1' }), + }), + ) + await waitFor(() => loader.getInMemoryOnly('user-1') === undefined) + } finally { + await trigger.stop() + } + }) }) describe('SqsQueueInvalidationTrigger', () => { diff --git a/packages/sqs/test/triggers.test-d.ts b/packages/sqs/test/triggers.test-d.ts new file mode 100644 index 0000000..1024e85 --- /dev/null +++ b/packages/sqs/test/triggers.test-d.ts @@ -0,0 +1,145 @@ +import type { SNSSQSConsumerOptions } from '@message-queue-toolkit/sns' +import type { SQSConsumerOptions } from '@message-queue-toolkit/sqs' +import { assertType, describe, expectTypeOf, test } from 'vitest' +import { z } from 'zod' +import type { + SnsTopicGroupInvalidationDeadLetterQueueConfig, + SnsTopicGroupInvalidationSource, + SnsTopicInvalidationDeadLetterQueueConfig, + SnsTopicInvalidationSource, + SqsQueueGroupInvalidationDeadLetterQueueConfig, + SqsQueueGroupInvalidationSource, + SqsQueueInvalidationDeadLetterQueueConfig, + SqsQueueInvalidationSource, +} from '../index.js' + +// Pre-resolved consumer options, as produced by e.g. +// `@lokalise/aws-config`'s `resolveConsumerOptions(...)`. +declare const snsOptions: SNSSQSConsumerOptions +declare const sqsOptions: SQSConsumerOptions + +const SCHEMA = z.object({ type: z.string() }) +const flatBinding = { messageSchema: SCHEMA, resolver: () => null } as const +const groupBinding = { messageSchema: SCHEMA, resolver: () => null } as const + +describe('trigger source types: spread + explicit configuration', () => { + test('SnsTopicInvalidationSource', () => { + // Spread a pre-resolved options object. + assertType({ ...snsOptions, bindings: [flatBinding] }) + + // Explicit, minimal. + assertType({ + creationConfig: { topic: { Name: 't' }, queue: { QueueName: 'q' } }, + bindings: [flatBinding], + }) + + // Explicit, with extra consumer options + DLQ + subscriptionConfig. + assertType({ + creationConfig: { topic: { Name: 't' }, queue: { QueueName: 'q' } }, + concurrentConsumersAmount: 4, + subscriptionConfig: { updateAttributesIfExists: true }, + deadLetterQueue: { + redrivePolicy: { maxReceiveCount: 3 }, + creationConfig: { queue: { QueueName: 'q-dlq' } }, + }, + bindings: [flatBinding], + }) + + // The trigger owns `handlers`, so callers must not be forced to provide it. + expectTypeOf().not.toHaveProperty('handlers') + + assertType({ + // @ts-expect-error unknown property must be rejected (typo detection) + totallyBogusField: true, + creationConfig: { topic: { Name: 't' }, queue: { QueueName: 'q' } }, + bindings: [flatBinding], + }) + + assertType({ + // @ts-expect-error wrong creationConfig shape must be rejected + creationConfig: { nonsense: 1 }, + bindings: [flatBinding], + }) + }) + + test('SnsTopicGroupInvalidationSource', () => { + assertType({ ...snsOptions, bindings: [groupBinding] }) + + assertType({ + locatorConfig: { topicArn: 'arn:aws:sns:...', queueUrl: 'https://...' }, + deadLetterQueue: { redrivePolicy: { maxReceiveCount: 5 } }, + bindings: [groupBinding], + }) + + expectTypeOf().not.toHaveProperty('handlers') + + assertType({ + // @ts-expect-error unknown property must be rejected + nope: true, + creationConfig: { topic: { Name: 't' }, queue: { QueueName: 'q' } }, + bindings: [groupBinding], + }) + }) + + test('SqsQueueInvalidationSource', () => { + assertType({ ...sqsOptions, bindings: [flatBinding] }) + + assertType({ + locatorConfig: { queueUrl: 'https://...' }, + concurrentConsumersAmount: 2, + deadLetterQueue: { + redrivePolicy: { maxReceiveCount: 3 }, + creationConfig: { queue: { QueueName: 'q-dlq' } }, + }, + bindings: [flatBinding], + }) + + expectTypeOf().not.toHaveProperty('handlers') + + assertType({ + // @ts-expect-error unknown property must be rejected + totallyBogusField: true, + locatorConfig: { queueUrl: 'https://...' }, + bindings: [flatBinding], + }) + }) + + test('SqsQueueGroupInvalidationSource', () => { + assertType({ ...sqsOptions, bindings: [groupBinding] }) + + assertType({ + creationConfig: { queue: { QueueName: 'q' } }, + bindings: [groupBinding], + }) + + expectTypeOf().not.toHaveProperty('handlers') + + assertType({ + // @ts-expect-error unknown property must be rejected + nope: true, + creationConfig: { queue: { QueueName: 'q' } }, + bindings: [groupBinding], + }) + }) +}) + +describe('dead-letter-queue helper types', () => { + test('each exposes redrivePolicy + create/locate', () => { + const dlqConfigs = [ + {} as SnsTopicInvalidationDeadLetterQueueConfig, + {} as SnsTopicGroupInvalidationDeadLetterQueueConfig, + {} as SqsQueueInvalidationDeadLetterQueueConfig, + {} as SqsQueueGroupInvalidationDeadLetterQueueConfig, + ] + + expectTypeOf(dlqConfigs[0]!.redrivePolicy).toEqualTypeOf<{ maxReceiveCount: number }>() + expectTypeOf(dlqConfigs[0]!).toHaveProperty('creationConfig') + expectTypeOf(dlqConfigs[0]!).toHaveProperty('locatorConfig') + + // A standalone DLQ object can be built against the exported helper type. + assertType({ + redrivePolicy: { maxReceiveCount: 5 }, + creationConfig: { queue: { QueueName: 'q-dlq' } }, + }) + }) +}) diff --git a/packages/sqs/tsconfig.test-d.json b/packages/sqs/tsconfig.test-d.json new file mode 100644 index 0000000..283f4bf --- /dev/null +++ b/packages/sqs/tsconfig.test-d.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["lib/**/*.ts", "index.ts", "test/**/*.test-d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/sqs/vitest.config.mts b/packages/sqs/vitest.config.mts index c155872..b916c28 100644 --- a/packages/sqs/vitest.config.mts +++ b/packages/sqs/vitest.config.mts @@ -13,6 +13,11 @@ export default defineConfig({ hookTimeout: 30000, globalSetup: ['./test/globalSetup.ts'], exclude: ['**/node_modules/**', '**/dist/**'], + typecheck: { + enabled: true, + include: ['test/**/*.test-d.ts'], + tsconfig: './tsconfig.test-d.json', + }, coverage: { include: ['lib/**/*.ts', 'index.ts'], reporter: ['lcov', 'text'], From 799c1ff9a0e9d3d8fb1be45021e30ce5f343ea19 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Tue, 16 Jun 2026 13:41:57 +0300 Subject: [PATCH 2/4] Adjust docs --- packages/sqs/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sqs/README.md b/packages/sqs/README.md index 0c8cc25..25ac0e3 100644 --- a/packages/sqs/README.md +++ b/packages/sqs/README.md @@ -601,8 +601,6 @@ A trigger source **is** the underlying `message-queue-toolkit` consumer options The trigger always overrides `handlers` with the ones it builds from `bindings`; everything else flows through untouched. -```ts - ```ts const resolver = getSnsMqtOptionsResolver({ appEnv: 'production' }) const options = resolver.resolveConsumerOptions(topicName, queueName, { From 486f06594635666ccd851a19578be20464df18ae Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Tue, 16 Jun 2026 13:59:57 +0300 Subject: [PATCH 3/4] refactor(sqs): drop redundant *InvalidationDeadLetterQueueConfig types The deadLetterQueue field is already exposed via each *SourceConfig (Omit<*ConsumerOptions, 'handlers'>), so the standalone DLQ helper type aliases added no value and only expanded the public API surface. Remove the four types, their exports, type-test block, and README references. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/sqs/README.md | 3 +-- packages/sqs/index.ts | 4 --- .../SnsTopicGroupInvalidationTrigger.ts | 11 -------- .../triggers/SnsTopicInvalidationTrigger.ts | 11 -------- .../SqsQueueGroupInvalidationTrigger.ts | 11 -------- .../triggers/SqsQueueInvalidationTrigger.ts | 11 -------- packages/sqs/test/triggers.test-d.ts | 25 ------------------- 7 files changed, 1 insertion(+), 75 deletions(-) diff --git a/packages/sqs/README.md b/packages/sqs/README.md index 25ac0e3..f8223bf 100644 --- a/packages/sqs/README.md +++ b/packages/sqs/README.md @@ -590,7 +590,7 @@ const trigger = new SnsTopicInvalidationTrigger({ }) ``` -The `deadLetterQueue` field is the same shape `message-queue-toolkit` exposes on its consumers, surfaced per trigger as `SnsTopicInvalidationDeadLetterQueueConfig` (and the `SqsQueue*` / `*Group*` counterparts). It is available on all four trigger source types. +The `deadLetterQueue` field is the same shape `message-queue-toolkit` exposes on its consumers. It is available on all four trigger source types. ### Explicit vs spread configuration @@ -684,7 +684,6 @@ Then point your AWS SDK clients at `http://localhost:4566`. | `SnsTopicGroupInvalidationTrigger` / `SqsQueueGroupInvalidationTrigger` | `GroupLoader` counterparts. | | `composeTriggers(...triggers)` | Wraps multiple `InvalidationTrigger`s into one combined `start()` / `stop()`. | | `SnsTopicInvalidationSource` / `SqsQueueInvalidationSource` | A single upstream source: every `message-queue-toolkit` consumer option (`creationConfig`/`locatorConfig`, `deadLetterQueue`, `subscriptionConfig`, ...) minus `handlers`, plus `bindings` and optional `messageTypeField`. Fully typed for both explicit and spread configuration. Group counterparts have parallel names. | -| `SnsTopicInvalidationDeadLetterQueueConfig` / `SqsQueueInvalidationDeadLetterQueueConfig` | Type for the `deadLetterQueue` field (`redrivePolicy` + `creationConfig`/`locatorConfig`). Group counterparts have parallel names. | | `FlatBinding` / `GroupBinding` | One `(messageSchema, resolver, messageType?)` triple. | | `InvalidationTarget` / `GroupInvalidationTarget` | Structural interfaces that `Loader` / `GroupLoader` satisfy. | | `InvalidationAction` / `GroupInvalidationAction` | Action ADTs returned by resolvers. | diff --git a/packages/sqs/index.ts b/packages/sqs/index.ts index bdcc656..f86cfad 100644 --- a/packages/sqs/index.ts +++ b/packages/sqs/index.ts @@ -81,7 +81,6 @@ export { export { deriveSqsQueueChannelName, SqsQueueInvalidationTrigger, - type SqsQueueInvalidationDeadLetterQueueConfig, type SqsQueueInvalidationSource, type SqsQueueInvalidationTriggerParams, type SqsQueueSourceConfig, @@ -89,7 +88,6 @@ export { export { deriveSnsTopicChannelName, SnsTopicInvalidationTrigger, - type SnsTopicInvalidationDeadLetterQueueConfig, type SnsTopicInvalidationSource, type SnsTopicInvalidationTriggerParams, type SnsTopicSourceConfig, @@ -97,7 +95,6 @@ export { export { deriveSqsQueueGroupChannelName, SqsQueueGroupInvalidationTrigger, - type SqsQueueGroupInvalidationDeadLetterQueueConfig, type SqsQueueGroupInvalidationSource, type SqsQueueGroupInvalidationTriggerParams, type SqsQueueGroupSourceConfig, @@ -105,7 +102,6 @@ export { export { deriveSnsTopicGroupChannelName, SnsTopicGroupInvalidationTrigger, - type SnsTopicGroupInvalidationDeadLetterQueueConfig, type SnsTopicGroupInvalidationSource, type SnsTopicGroupInvalidationTriggerParams, type SnsTopicGroupSourceConfig, diff --git a/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts b/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts index 31086e9..e708405 100644 --- a/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts @@ -32,17 +32,6 @@ export type SnsTopicGroupSourceConfig = Omit< 'handlers' > -/** - * Dead-letter-queue config for an SNS-topic group source, surfaced verbatim - * from the underlying `message-queue-toolkit` consumer. Supplying it with a - * `creationConfig` makes the toolkit auto-create the DLQ, attach the redrive - * policy to the trigger's own queue, and wire the topic subscription; a - * `locatorConfig` points the redrive policy at a pre-existing DLQ instead. - */ -export type SnsTopicGroupInvalidationDeadLetterQueueConfig = NonNullable< - SnsTopicGroupSourceConfig['deadLetterQueue'] -> - export type SnsTopicGroupInvalidationSource = SnsTopicGroupSourceConfig & { messageTypeField?: string // biome-ignore lint/suspicious/noExplicitAny: bindings heterogeneous over TMessage diff --git a/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts b/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts index 5ba0760..6c4eb8b 100644 --- a/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts @@ -32,17 +32,6 @@ export type SnsTopicSourceConfig = Omit< 'handlers' > -/** - * Dead-letter-queue config for an SNS-topic source, surfaced verbatim from the - * underlying `message-queue-toolkit` consumer. Supplying it with a - * `creationConfig` makes the toolkit auto-create the DLQ, attach the redrive - * policy to the trigger's own queue, and wire the topic subscription; a - * `locatorConfig` points the redrive policy at a pre-existing DLQ instead. - */ -export type SnsTopicInvalidationDeadLetterQueueConfig = NonNullable< - SnsTopicSourceConfig['deadLetterQueue'] -> - export type SnsTopicInvalidationSource = SnsTopicSourceConfig & { messageTypeField?: string // biome-ignore lint/suspicious/noExplicitAny: bindings heterogeneous over TMessage diff --git a/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts b/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts index d1461ec..ef09ba8 100644 --- a/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts @@ -31,17 +31,6 @@ export type SqsQueueGroupSourceConfig = Omit< 'handlers' > -/** - * Dead-letter-queue config for an SQS-queue group source, surfaced verbatim - * from the underlying `message-queue-toolkit` consumer. Supplying it with a - * `creationConfig` makes the toolkit auto-create the DLQ and attach the redrive - * policy to the trigger's queue; a `locatorConfig` points the redrive policy at - * a pre-existing DLQ instead. - */ -export type SqsQueueGroupInvalidationDeadLetterQueueConfig = NonNullable< - SqsQueueGroupSourceConfig['deadLetterQueue'] -> - export type SqsQueueGroupInvalidationSource = SqsQueueGroupSourceConfig & { messageTypeField?: string // biome-ignore lint/suspicious/noExplicitAny: bindings heterogeneous over TMessage diff --git a/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts b/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts index 45394ed..e30b615 100644 --- a/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SqsQueueInvalidationTrigger.ts @@ -31,17 +31,6 @@ export type SqsQueueSourceConfig = Omit< 'handlers' > -/** - * Dead-letter-queue config for an SQS-queue source, surfaced verbatim from the - * underlying `message-queue-toolkit` consumer. Supplying it with a - * `creationConfig` makes the toolkit auto-create the DLQ and attach the redrive - * policy to the trigger's queue; a `locatorConfig` points the redrive policy at - * a pre-existing DLQ instead. - */ -export type SqsQueueInvalidationDeadLetterQueueConfig = NonNullable< - SqsQueueSourceConfig['deadLetterQueue'] -> - export type SqsQueueInvalidationSource = SqsQueueSourceConfig & { /** * Path on the message body whose value selects which binding handles each diff --git a/packages/sqs/test/triggers.test-d.ts b/packages/sqs/test/triggers.test-d.ts index 1024e85..a82018a 100644 --- a/packages/sqs/test/triggers.test-d.ts +++ b/packages/sqs/test/triggers.test-d.ts @@ -3,13 +3,9 @@ import type { SQSConsumerOptions } from '@message-queue-toolkit/sqs' import { assertType, describe, expectTypeOf, test } from 'vitest' import { z } from 'zod' import type { - SnsTopicGroupInvalidationDeadLetterQueueConfig, SnsTopicGroupInvalidationSource, - SnsTopicInvalidationDeadLetterQueueConfig, SnsTopicInvalidationSource, - SqsQueueGroupInvalidationDeadLetterQueueConfig, SqsQueueGroupInvalidationSource, - SqsQueueInvalidationDeadLetterQueueConfig, SqsQueueInvalidationSource, } from '../index.js' @@ -122,24 +118,3 @@ describe('trigger source types: spread + explicit configuration', () => { }) }) }) - -describe('dead-letter-queue helper types', () => { - test('each exposes redrivePolicy + create/locate', () => { - const dlqConfigs = [ - {} as SnsTopicInvalidationDeadLetterQueueConfig, - {} as SnsTopicGroupInvalidationDeadLetterQueueConfig, - {} as SqsQueueInvalidationDeadLetterQueueConfig, - {} as SqsQueueGroupInvalidationDeadLetterQueueConfig, - ] - - expectTypeOf(dlqConfigs[0]!.redrivePolicy).toEqualTypeOf<{ maxReceiveCount: number }>() - expectTypeOf(dlqConfigs[0]!).toHaveProperty('creationConfig') - expectTypeOf(dlqConfigs[0]!).toHaveProperty('locatorConfig') - - // A standalone DLQ object can be built against the exported helper type. - assertType({ - redrivePolicy: { maxReceiveCount: 5 }, - creationConfig: { queue: { QueueName: 'q-dlq' } }, - }) - }) -}) From bb397af87281ad4e1a815f189927f2e4ed48372e Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Tue, 16 Jun 2026 14:03:53 +0300 Subject: [PATCH 4/4] refactor(sqs): alias group source configs to their flat counterparts SqsQueueGroupSourceConfig / SnsTopicGroupSourceConfig were structurally identical to the flat SqsQueueSourceConfig / SnsTopicSourceConfig: the handler execution context is the only differing type argument and it lives entirely on the omitted `handlers` field. Alias them instead of re-declaring the same Omit<...> twice. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../SnsTopicGroupInvalidationTrigger.ts | 22 ++++++------------- .../SqsQueueGroupInvalidationTrigger.ts | 21 ++++++------------ 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts b/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts index e708405..5edf751 100644 --- a/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts @@ -12,25 +12,17 @@ import { buildGroupBindings, type GroupBinding, } from './bindingHelpers.js' +import type { SnsTopicSourceConfig } from './SnsTopicInvalidationTrigger.js' import type { GroupInvalidationTarget, TriggerErrorHandler } from './types.js' /** - * Every option the underlying `message-queue-toolkit` SNS→SQS consumer accepts, - * minus the `handlers` list (which the trigger builds from `bindings`). - * - * This is the per-source config surface, so callers get the same flexibility in - * two interchangeable styles, both fully type-checked: - * - * - **Explicit** — spell out `creationConfig`/`locatorConfig`, `subscriptionConfig`, - * `deadLetterQueue`, `concurrentConsumersAmount`, etc. with autocomplete and - * typo detection. - * - **Spread** — drop in a pre-resolved options object (e.g. - * `@lokalise/aws-config`'s `resolveConsumerOptions(...)`) with `...options`. + * The per-source config surface is identical to the flat-cache one — every + * `message-queue-toolkit` SNS→SQS consumer option minus `handlers` — so it is + * aliased to {@link SnsTopicSourceConfig} rather than re-declared. The handler + * execution context (the only place the two would differ) lives on the omitted + * `handlers` field, so the resulting types are structurally the same. */ -export type SnsTopicGroupSourceConfig = Omit< - SNSSQSConsumerOptions, undefined>, - 'handlers' -> +export type SnsTopicGroupSourceConfig = SnsTopicSourceConfig export type SnsTopicGroupInvalidationSource = SnsTopicGroupSourceConfig & { messageTypeField?: string diff --git a/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts b/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts index ef09ba8..f626e29 100644 --- a/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts +++ b/packages/sqs/lib/triggers/SqsQueueGroupInvalidationTrigger.ts @@ -12,24 +12,17 @@ import { buildGroupBindings, type GroupBinding, } from './bindingHelpers.js' +import type { SqsQueueSourceConfig } from './SqsQueueInvalidationTrigger.js' import type { GroupInvalidationTarget, TriggerErrorHandler } from './types.js' /** - * Every option the underlying `message-queue-toolkit` SQS consumer accepts, - * minus the `handlers` list (which the trigger builds from `bindings`). - * - * This is the per-source config surface, so callers get the same flexibility in - * two interchangeable styles, both fully type-checked: - * - * - **Explicit** — spell out `creationConfig`/`locatorConfig`, `deadLetterQueue`, - * `concurrentConsumersAmount`, etc. with autocomplete and typo detection. - * - **Spread** — drop in a pre-resolved options object (e.g. - * `@lokalise/aws-config`'s `resolveConsumerOptions(...)`) with `...options`. + * The per-source config surface is identical to the flat-cache one — every + * `message-queue-toolkit` SQS consumer option minus `handlers` — so it is + * aliased to {@link SqsQueueSourceConfig} rather than re-declared. The handler + * execution context (the only place the two would differ) lives on the omitted + * `handlers` field, so the resulting types are structurally the same. */ -export type SqsQueueGroupSourceConfig = Omit< - SQSConsumerOptions, undefined>, - 'handlers' -> +export type SqsQueueGroupSourceConfig = SqsQueueSourceConfig export type SqsQueueGroupInvalidationSource = SqsQueueGroupSourceConfig & { messageTypeField?: string