Skip to content

feat(sqs): triggers own subscription config so spread needs no workarounds#405

Merged
kibertoad merged 2 commits into
mainfrom
feat/trigger-owns-subscription-config
Jun 17, 2026
Merged

feat(sqs): triggers own subscription config so spread needs no workarounds#405
kibertoad merged 2 commits into
mainfrom
feat/trigger-owns-subscription-config

Conversation

@kibertoad

@kibertoad kibertoad commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Problem

Spreading a pre-resolved consumer-options object (e.g. @lokalise/aws-config's resolveConsumerOptions(...)) into a trigger source forced callers to hand-unset fields the resolver had computed for a real consumer. Real-world call sites looked like this:

sources: [{
  creationConfig: { ... },
  ...resolvedOptions,
  // resolveConsumerOptions derives the filter policy from handlers, but the
  // trigger builds its own handlers from bindings. Empty handlers => reject-all,
  // so we reset Attributes here to accept all messages.
  subscriptionConfig: { updateAttributesIfExists: ... },
  // The resolver sets subscriptionDeadLetterQueue; we override it to undefined to
  // avoid a second Subscribe conflicting with the updated attributes.
  subscriptionDeadLetterQueue: undefined,
}]

Root cause: a trigger is not a plain consumer — it owns its handlers and its subscription filtering — but it blindly spread a full consumer's options, leaving callers to un-set everything that didn''t apply.

Changes

A. Fix the double-init in AbstractSqsTrigger. It called c.init() and c.start(), but the underlying message-queue-toolkit consumer''s start() already runs init() internally — so every consumer initted twice, and the second pass re-subscribed each queue, conflicting with the subscription attributes (filter policy, redrive policy) set on the first pass. Now it only calls start().

B. The trigger claims only what it truly owns, centrally. buildSnsTriggerConsumerOptions resets:

  • handlers — always rebuilt from bindings.
  • subscription filter policy (subscriptionConfig.Attributes.FilterPolicy / FilterPolicyScope) — dropped (the reject-all policy the resolver derives from empty handlers); defaults to accept-all.

Everything else flows through, including the subscription-level DLQ. Since fix A removes the double-init conflict, there''s no longer any reason to strip it: subscriptionConfig.Attributes.RedrivePolicy (the subscription DLQ in the installed toolkit) is preserved alongside any other attributes, and a spread-in subscriptionDeadLetterQueue (used by resolveConsumerOptions / newer toolkits) flows through to the consumer.

Shared by the flat and group SNS triggers, and exported. SQS-queue triggers were already lifecycle-only and gain fix A for free.

After this, the call site is just ...resolvedOptions + creationConfig + bindings — no subscriptionConfig patch, no subscriptionDeadLetterQueue override, no Attributes reset.

Tests

New test/triggerInternals.spec.ts (8 deterministic unit tests): the option sanitizer (handler injection, FilterPolicy/FilterPolicyScope drop, RedrivePolicy preserved, subscriptionDeadLetterQueue forwarded, updateAttributesIfExists default/override, pass-through of unrelated options) and the lifecycle fix (each consumer start()ed exactly once, init() never called separately; stop() closes all). Full suite: 42 passed, no type errors.

🤖 Generated with Claude Code

…ounds

A trigger is not a plain consumer: it owns its handlers, its subscription
filtering, and its lifecycle. Spreading a pre-resolved consumer-options object
(e.g. @lokalise/aws-config resolveConsumerOptions) into a trigger source forced
callers to hand-unset fields the resolver computed for a real consumer.

Two fixes remove those workarounds:

- AbstractSqsTrigger: only call the underlying consumer start() (which inits
  internally) instead of init() then start(). The previous double-init
  re-subscribed each queue, conflicting with subscription attributes (filter
  policy, redrive policy) applied on the first pass.

- buildSnsTriggerConsumerOptions: centrally strip the fields a trigger owns from
  the spread, instead of trusting callers to override them at every call site:
  the handler-derived subscription filter policy (subscriptionConfig.Attributes,
  defaults to accept-all) and a spread-in subscriptionDeadLetterQueue (trigger
  manages failures via the queue-level deadLetterQueue). Shared by the flat and
  group SNS triggers; exported for reuse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Extracts SNS→SQS consumer option normalization into a new exported buildSnsTriggerConsumerOptions helper that strips subscription-level DLQ, filter-policy Attributes, and forces updateAttributesIfExists: false. Removes the duplicate init() call from AbstractSqsTrigger.start(). Both flat and group invalidation triggers are refactored to use the helper. Adds unit tests and updates README docs.

Changes

SNS Trigger Consumer Options Refactor and Lifecycle Fix

Layer / File(s) Summary
AbstractSqsTrigger lifecycle fix
packages/sqs/lib/triggers/AbstractSqsTrigger.ts, packages/sqs/test/triggerInternals.spec.ts
Removes the explicit init() call per consumer from start(), relying on the consumer's own start() for initialization. Tests assert each handle's start() is called exactly once with no direct init(), and stop() closes all handles.
buildSnsTriggerConsumerOptions helper
packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts, packages/sqs/index.ts
New exported helper normalizes SNSSQSConsumerOptions: strips subscriptionDeadLetterQueue, removes subscriptionConfig.Attributes, forces updateAttributesIfExists: false, and injects bindings-derived handlers and messageTypeResolver. Re-exported from the package index.
Flat and group triggers refactored
packages/sqs/lib/triggers/SnsTopicInvalidationTrigger.ts, packages/sqs/lib/triggers/SnsTopicGroupInvalidationTrigger.ts
buildConsumer in both triggers delegates option assembly to buildSnsTriggerConsumerOptions, removing inline assembly of handlers, messageTypeResolver, and subscriptionConfig defaults. Group trigger imports updated accordingly.
Tests and docs
packages/sqs/test/triggerInternals.spec.ts, packages/sqs/README.md
triggerInternals.spec.ts covers all normalization behaviors of the helper. README clarifies which consumer options are trigger-owned versus passed through unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • kibertoad/layered-loader#389: Introduced the AbstractSqsTrigger and SNS-topic trigger plumbing that this PR's lifecycle fix and helper extraction directly build upon.
  • kibertoad/layered-loader#404: Added the "spread full consumer options" behavior in SnsTopicInvalidationTrigger.ts that this PR further normalizes by centralizing option assembly into buildSnsTriggerConsumerOptions.

Suggested labels

minor

Suggested reviewers

  • armababy
  • kosmos2uh
  • CarlosGamero

Poem

🐇 A rabbit refactored with glee,
Two triggers now share a helper, you see!
No double init() to fret,
The DLQ fields — stripped and unset,
Subscriptions flow cleanly and free! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: introducing a mechanism for triggers to own subscription configuration, eliminating the need for spread-based workarounds.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/trigger-owns-subscription-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The double-init fix means a subscription DLQ no longer conflicts with the
trigger lifecycle, so there is no reason to strip it. Narrow the trigger's
subscription ownership to just the handler-derived filter policy:

- Strip only Attributes.FilterPolicy / FilterPolicyScope (the reject-all policy
  resolveConsumerOptions derives from empty handlers), preserving every other
  subscription attribute — notably Attributes.RedrivePolicy, the SNS
  subscription-level DLQ in the installed toolkit.
- Stop stripping a spread-in subscriptionDeadLetterQueue; let it flow through to
  the consumer (used by resolveConsumerOptions / newer toolkit versions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kibertoad kibertoad merged commit f242037 into main Jun 17, 2026
6 checks passed
@kibertoad kibertoad deleted the feat/trigger-owns-subscription-config branch June 17, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants