{"_id":"@layered-loader/sqs","_rev":"5-1c5a297665bdb9b1dbdfd382b6275e93","name":"@layered-loader/sqs","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@layered-loader/sqs","version":"1.0.0","keywords":["layered-loader","sqs","sns","aws","cache","invalidation","notification","pub-sub"],"license":"MIT","_id":"@layered-loader/sqs@1.0.0","maintainers":[{"name":"kibertoad","email":"iselwin@gmail.com"}],"homepage":"https://github.com/kibertoad/layered-loader#readme","bugs":{"url":"https://github.com/kibertoad/layered-loader/issues"},"dist":{"shasum":"86451b3637fb51da7746f8193a1497063962ee50","tarball":"https://registry.npmjs.org/@layered-loader/sqs/-/sqs-1.0.0.tgz","fileCount":48,"integrity":"sha512-KayQrCgrgMpw2wCrT3FhDBvYYlv61MVN68luhCsvhHVrQ6ELUPFPx6N1+9gtJ/DEKlEzntA282rBfaHITeoeiw==","signatures":[{"sig":"MEQCIEQk2KRlb/SILRmGsOZe6EeKil9NsAa8YP4VBrcRu1gPAiAJ1EqNhzN1j5bDrd7FpsqIw/HwDhka/l94tG9JXrl2jQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":110483},"main":"dist/index.js","type":"module","_from":"file:layered-loader-sqs-1.0.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"kibertoad","email":"iselwin@gmail.com"},"_resolved":"C:\\Users\\kiber\\AppData\\Local\\Temp\\35f49309520ff8f97926d0f3cf3ae709\\layered-loader-sqs-1.0.0.tgz","_integrity":"sha512-KayQrCgrgMpw2wCrT3FhDBvYYlv61MVN68luhCsvhHVrQ6ELUPFPx6N1+9gtJ/DEKlEzntA282rBfaHITeoeiw==","repository":{"url":"git://github.com/kibertoad/layered-loader.git","type":"git","directory":"packages/sqs"},"_npmVersion":"11.12.1","description":"SNS/SQS remote invalidation adapter for layered-loader","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","fauxqs":"^2.5.0","vitest":"^4.1.0","typescript":"^5.9.3","@types/node":"^22.19.15","layered-loader":"^14.3.1","@aws-sdk/client-sns":"^3.926.0","@aws-sdk/client-sqs":"^3.926.0","@aws-sdk/client-sts":"^3.926.0","@lokalise/node-core":"^14.8.1","@vitest/coverage-v8":"^4.1.0","@message-queue-toolkit/sns":"^24.6.1","@message-queue-toolkit/sqs":"^24.2.1","@message-queue-toolkit/core":"^25.4.0"},"peerDependencies":{"zod":"^4.0.0","layered-loader":"^14.3.1","@aws-sdk/client-sns":"^3.632.0","@aws-sdk/client-sqs":"^3.632.0","@aws-sdk/client-sts":"^3.632.0","@lokalise/node-core":"^14.0.0","@message-queue-toolkit/sns":">=24.0.0","@message-queue-toolkit/sqs":">=23.0.0","@message-queue-toolkit/core":">=24.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sqs_1.0.0_1778494950196_0.5910224319157069","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@layered-loader/sqs","version":"1.2.0","keywords":["layered-loader","sqs","sns","aws","cache","invalidation","notification","pub-sub"],"license":"MIT","_id":"@layered-loader/sqs@1.2.0","maintainers":[{"name":"kibertoad","email":"iselwin@gmail.com"}],"dist":{"shasum":"76100bfd7a19fb51937720a03e8adcee087e7952","tarball":"https://registry.npmjs.org/@layered-loader/sqs/-/sqs-1.2.0.tgz","fileCount":57,"integrity":"sha512-jt2zyqniKXI3g87+dFMz/xEuZHpqdGHq1fh4s+eOkL5Fg3wDG9y2jIquAav4p5i3cGZMC/SVS84I/DcmEeEVpg==","signatures":[{"sig":"MEUCIFjI45VeLhphUJG7rV8SWXZKn4PIE+fy7LZwnGMe5sycAiEA+uiVO5weEevjmBav/Z1ioF6Iuhh74TAj7OQFgDBv7zY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@layered-loader%2fsqs@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":143301},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dd6aba6a-4ba5-48f7-b787-ab244c4726ff"}},"repository":{"url":"git://github.com/kibertoad/layered-loader.git","type":"git","directory":"packages/sqs"},"description":"SNS/SQS remote invalidation adapter for layered-loader","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","fauxqs":"^2.5.0","vitest":"^4.1.6","typescript":"^6.0.3","@types/node":"^25.8.0","layered-loader":"^14.4.0","@aws-sdk/client-sns":"^3.1048.0","@aws-sdk/client-sqs":"^3.1048.0","@aws-sdk/client-sts":"^3.1047.0","@lokalise/node-core":"^14.8.1","@vitest/coverage-v8":"^4.1.6","@message-queue-toolkit/sns":"^24.6.1","@message-queue-toolkit/sqs":"^24.2.1","@message-queue-toolkit/core":"^25.5.0"},"peerDependencies":{"zod":"^4.0.0","layered-loader":"^14.4.0","@aws-sdk/client-sns":"^3.632.0","@aws-sdk/client-sqs":"^3.632.0","@aws-sdk/client-sts":"^3.632.0","@lokalise/node-core":"^14.0.0","@message-queue-toolkit/sns":">=24.0.0","@message-queue-toolkit/sqs":">=23.0.0","@message-queue-toolkit/core":">=24.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sqs_1.2.0_1780612230204_0.22346021836850527","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@layered-loader/sqs","version":"1.3.0","keywords":["layered-loader","sqs","sns","aws","cache","invalidation","notification","pub-sub"],"license":"MIT","_id":"@layered-loader/sqs@1.3.0","maintainers":[{"name":"kibertoad","email":"iselwin@gmail.com"}],"dist":{"shasum":"18e52b726887000286d2262e4f7b03378d907563","tarball":"https://registry.npmjs.org/@layered-loader/sqs/-/sqs-1.3.0.tgz","fileCount":57,"integrity":"sha512-jFHWgjm+f/U0yAuYdXKjh0C/NCy1c+lAIPLOev76yOolabVnh9IKbta+zG8m2BqpKoxwrDDWmMI+fTh2tZZXyw==","signatures":[{"sig":"MEUCIQDOtI/9zvQH01mf8zS3BxIJSj90iGJjSjHEzk7NuYU0NgIgBf3HqOa1o2Lj9c6DOz+CeJ6oDLhHk0QaBzgtAe0LEsY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@layered-loader%2fsqs@1.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":153130},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dd6aba6a-4ba5-48f7-b787-ab244c4726ff"}},"repository":{"url":"git://github.com/kibertoad/layered-loader.git","type":"git","directory":"packages/sqs"},"description":"SNS/SQS remote invalidation adapter for layered-loader","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","fauxqs":"^2.5.0","vitest":"^4.1.6","typescript":"^6.0.3","@types/node":"^25.8.0","layered-loader":"^14.4.0","@aws-sdk/client-sns":"^3.1048.0","@aws-sdk/client-sqs":"^3.1048.0","@aws-sdk/client-sts":"^3.1047.0","@lokalise/node-core":"^14.8.1","@vitest/coverage-v8":"^4.1.6","@message-queue-toolkit/sns":"^24.6.1","@message-queue-toolkit/sqs":"^24.2.1","@message-queue-toolkit/core":"^25.5.0"},"peerDependencies":{"zod":"^4.0.0","layered-loader":"^14.4.0","@aws-sdk/client-sns":"^3.632.0","@aws-sdk/client-sqs":"^3.632.0","@aws-sdk/client-sts":"^3.632.0","@lokalise/node-core":"^14.0.0","@message-queue-toolkit/sns":">=24.0.0","@message-queue-toolkit/sqs":">=23.0.0","@message-queue-toolkit/core":">=24.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sqs_1.3.0_1781608034245_0.23401928543535178","host":"s3://npm-registry-packages-npm-production"}},"1.3.1":{"name":"@layered-loader/sqs","version":"1.3.1","keywords":["layered-loader","sqs","sns","aws","cache","invalidation","notification","pub-sub"],"license":"MIT","_id":"@layered-loader/sqs@1.3.1","maintainers":[{"name":"kibertoad","email":"iselwin@gmail.com"}],"dist":{"shasum":"daf34be5ad453a00b193f3abff749dbcc4a0b64f","tarball":"https://registry.npmjs.org/@layered-loader/sqs/-/sqs-1.3.1.tgz","fileCount":57,"integrity":"sha512-dAYJS9aq1RUPDKe4npjl2uTkLSmx13mwuX65We5/iRrR6SFf94mKPi4fDNV+qWD+K84asGtH8jcPD4LbhJrPKA==","signatures":[{"sig":"MEUCIQD22VAv3N/utDnKAAmm+jfELiCyQyPEa2lorrGbyX6/xAIgNUEhyZBaSJK+m9WdiNvzLRt/tBgn+lY+E30IbhkQy4M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@layered-loader%2fsqs@1.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":157645},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dd6aba6a-4ba5-48f7-b787-ab244c4726ff"}},"repository":{"url":"git://github.com/kibertoad/layered-loader.git","type":"git","directory":"packages/sqs"},"description":"SNS/SQS remote invalidation adapter for layered-loader","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","fauxqs":"^2.5.0","vitest":"^4.1.6","typescript":"^6.0.3","@types/node":"^25.8.0","layered-loader":"^14.4.0","@aws-sdk/client-sns":"^3.1048.0","@aws-sdk/client-sqs":"^3.1048.0","@aws-sdk/client-sts":"^3.1047.0","@lokalise/node-core":"^14.8.1","@vitest/coverage-v8":"^4.1.6","@message-queue-toolkit/sns":"^24.6.1","@message-queue-toolkit/sqs":"^24.2.1","@message-queue-toolkit/core":"^25.5.0"},"peerDependencies":{"zod":"^4.0.0","layered-loader":"^14.4.0","@aws-sdk/client-sns":"^3.632.0","@aws-sdk/client-sqs":"^3.632.0","@aws-sdk/client-sts":"^3.632.0","@lokalise/node-core":"^14.0.0","@message-queue-toolkit/sns":">=24.0.0","@message-queue-toolkit/sqs":">=23.0.0","@message-queue-toolkit/core":">=24.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sqs_1.3.1_1781684746553_0.08807384867962353","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@layered-loader/sqs","version":"2.0.0","description":"SNS/SQS remote invalidation adapter for layered-loader","license":"MIT","type":"module","main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"engines":{"node":">=22"},"repository":{"type":"git","url":"git+https://github.com/kibertoad/layered-loader.git","directory":"packages/sqs"},"keywords":["layered-loader","sqs","sns","aws","cache","invalidation","notification","pub-sub"],"peerDependencies":{"@aws-sdk/client-sns":"^3.632.0","@aws-sdk/client-sqs":"^3.632.0","@aws-sdk/client-sts":"^3.632.0","@lokalise/node-core":"^14.0.0","@message-queue-toolkit/core":">=26.0.0","@message-queue-toolkit/sns":">=26.0.0","@message-queue-toolkit/sqs":">=26.0.0","zod":"^4.0.0","layered-loader":"^16.0.0"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.5","@aws-sdk/client-sns":"^3.1101.0","@aws-sdk/client-sqs":"^3.1101.0","@aws-sdk/client-sts":"^3.1101.0","@lokalise/node-core":"^14.8.1","@message-queue-toolkit/core":"^26.1.1","@message-queue-toolkit/sns":"^26.2.1","@message-queue-toolkit/sqs":"^26.1.1","@types/node":"^26.1.2","@vitest/coverage-v8":"^4.1.10","fauxqs":"^2.8.0","publint":"^0.3.22","typescript":"^7.0.2","vitest":"^4.1.10","zod":"^4.4.3","layered-loader":"^16.0.0"},"scripts":{"build":"tsc","test":"vitest run","test:coverage":"vitest run --coverage","lint":"tsc --noEmit","publint":"publint","attw":"attw --pack . --profile esm-only","lint:packaging":"pnpm run publint && pnpm run attw"},"_nodeVersion":"24.18.0","_id":"@layered-loader/sqs@2.0.0","dist":{"integrity":"sha512-p7HKtpIkGoLaqOBeN4G7q4t7Zgb3lf8ZCk/+Jpv05EUvn74SWfteoZUTvy4gxbBiLDZe5hNunLsW22eUq5D3Bg==","shasum":"f6350449a7812d74cd3c0558e37c9b2876207eb5","tarball":"https://registry.npmjs.org/@layered-loader/sqs/-/sqs-2.0.0.tgz","fileCount":57,"unpackedSize":158096,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@layered-loader%2fsqs@2.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIECyz/zrXl/HplKgKr8ZcqhEW57l2Xyndi1kJaHL6dw8AiBMzWeBL4TCTpcOeBRuZAoKvM8DA5PmmpV+aCe0ssAIUw=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dd6aba6a-4ba5-48f7-b787-ab244c4726ff"}},"directories":{},"maintainers":[{"name":"kibertoad","email":"iselwin@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sqs_2.0.0_1785697628858_0.005140169398222927"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-11T10:22:30.078Z","modified":"2026-08-02T19:07:09.454Z","1.0.0":"2026-05-11T10:22:30.429Z","1.2.0":"2026-06-04T22:30:30.342Z","1.3.0":"2026-06-16T11:07:14.396Z","1.3.1":"2026-06-17T08:25:46.692Z","2.0.0":"2026-08-02T19:07:09.002Z"},"license":"MIT","keywords":["layered-loader","sqs","sns","aws","cache","invalidation","notification","pub-sub"],"repository":{"type":"git","url":"git+https://github.com/kibertoad/layered-loader.git","directory":"packages/sqs"},"description":"SNS/SQS remote invalidation adapter for layered-loader","maintainers":[{"name":"kibertoad","email":"iselwin@gmail.com"}],"readme":"# @layered-loader/sqs\n \nSNS/SQS remote-invalidation adapter for [`layered-loader`](https://github.com/kibertoad/layered-loader).\n\nThis package provides:\n\n- **Notification publishers and consumers** that fan cache invalidations out across a cluster via an SNS topic and per-instance SQS queues — a drop-in alternative to the built-in Redis adapter for AWS-native deployments.\n- **Flexible invalidation triggers** that subscribe to an *existing* upstream SNS topic or SQS queue (one that knows nothing about the caching layer) and translate domain events such as `user.updated` into cache invalidations applied to your `Loader` / `GroupLoader`.\n\nThe implementation is built on top of [`@message-queue-toolkit/sns`](https://github.com/kibertoad/message-queue-toolkit) and is tested against [fauxqs](https://github.com/kibertoad/fauxqs), an in-process SNS/SQS emulator.\n\n> **Prefer Redis pub/sub when you can.** This package exists to support AWS-native deployments and upstream-event consumption, but operationally Redis pub/sub is simpler — no per-instance queues, no lifecycle management, no AWS quotas to track. If your only reason to be here is \"I have an upstream SNS topic to consume\", the **[recommended hybrid pattern](#recommended-pattern-redis-fanout--sqs-trigger)** is Redis publisher in the Loader + SQS trigger reading the upstream topic with a shared queue. You skip the queue-lifecycle problem entirely.\n\n## Contents\n\n- [Installation](#installation)\n- [Picking your shape](#picking-your-shape)\n- [Quick start: notification pair](#quick-start-notification-pair)\n- [Group notification pair](#group-notification-pair)\n- [Locator vs creation config](#locator-vs-creation-config)\n- [How invalidation flows through SNS/SQS](#how-invalidation-flows-through-snssqs)\n- [Self-message filtering and `serverUuid`](#self-message-filtering-and-serveruuid)\n- [Flexible invalidation triggers](#flexible-invalidation-triggers)\n  - [Recommended pattern: Redis fanout + SQS trigger](#recommended-pattern-redis-fanout--sqs-trigger)\n  - [Triggering from an existing SNS topic](#triggering-from-an-existing-sns-topic)\n  - [Triggering from an existing SQS queue](#triggering-from-an-existing-sqs-queue)\n  - [Group triggers](#group-triggers)\n  - [Multiple sources and event types](#multiple-sources-and-event-types)\n  - [Mixing source kinds with `composeTriggers`](#mixing-source-kinds-with-composetriggers)\n  - [Resolver semantics](#resolver-semantics)\n  - [Error handling and retries](#error-handling-and-retries)\n  - [Dead-letter queues](#dead-letter-queues)\n  - [Explicit vs spread configuration](#explicit-vs-spread-configuration)\n- [Testing with fauxqs](#testing-with-fauxqs)\n- [API reference](#api-reference)\n\n## Installation\n\n```bash\nnpm install @layered-loader/sqs layered-loader\n# Plus the AWS SDK clients and message-queue-toolkit (peer deps):\nnpm install \\\n  @aws-sdk/client-sns @aws-sdk/client-sqs @aws-sdk/client-sts \\\n  @lokalise/node-core \\\n  @message-queue-toolkit/core @message-queue-toolkit/sns @message-queue-toolkit/sqs \\\n  zod\n```\n\nNode 22+ is required.\n\n## Picking your shape\n\nThere are three deployment shapes you can build with this package, and they have very different operational profiles. Pick deliberately:\n\n| Shape | Publisher | Trigger source | Queue churn | When to use |\n| --- | --- | --- | --- | --- |\n| **A. Pure Redis** (use [`layered-loader`](https://github.com/kibertoad/layered-loader#update-notifications) directly, not this package) | Redis pub/sub | (none, or Redis-driven) | None | Default. Use this whenever Redis is available and you have no upstream AWS events to consume. |\n| **B. Pure SNS/SQS** | `SqsNotificationPublisher` | per-instance SQS queues | **Yes** — needs careful queue naming or external cleanup | Only when Redis is not available at all. |\n| **C. Hybrid: Redis fanout + SQS trigger** ⭐ | Redis pub/sub | `SnsTopicInvalidationTrigger` on a **shared** queue, applying to a `Loader` whose publisher is Redis | None | When you have AWS upstream events to consume but Redis is available for cluster fanout. **Recommended for the upstream-events use case.** |\n\nShape **C** is almost always the right answer if you have AWS upstream events and Redis. It gives you AWS-native event ingestion *and* the operational simplicity of Redis fanout. See [Recommended pattern: Redis fanout + SQS trigger](#recommended-pattern-redis-fanout--sqs-trigger).\n\nIf you settle on shape **B** (no Redis at all), use stable queue names (StatefulSet ordinals, ECS fixed slot IDs) wherever your deployment topology allows — every restart with a random `HOSTNAME` leaks an SQS queue + SNS subscription, and SQS does not have AMQP-style auto-delete queues.\n\n## Quick start: notification pair\n\nThe simplest setup mirrors the built-in Redis pair: each application instance gets a `publisher` (sends invalidations to a shared SNS topic) and a `consumer` (reads its own SQS queue subscribed to that topic and applies invalidations to its in-memory cache).\n\n```ts\nimport { SNSClient } from '@aws-sdk/client-sns'\nimport { SQSClient } from '@aws-sdk/client-sqs'\nimport { STSClient } from '@aws-sdk/client-sts'\nimport { globalLogger, NoopObservabilityManager } from '@lokalise/node-core'\nimport { SnsConsumerErrorResolver } from '@message-queue-toolkit/sns'\nimport { Loader } from 'layered-loader/core'\nimport { createNotificationPair } from '@layered-loader/sqs'\nimport type { User } from './types'\n\nconst region = 'us-east-1'\nconst snsClient = new SNSClient({ region })\nconst sqsClient = new SQSClient({ region })\nconst stsClient = new STSClient({ region })\n\nconst errorReporter = { report: () => {} }\n\nconst { publisher: notificationPublisher, consumer: notificationConsumer } =\n  createNotificationPair<User>({\n    publisher: {\n      dependencies: { snsClient, stsClient, logger: globalLogger, errorReporter },\n      creationConfig: { topic: { Name: 'user-cache-invalidations' } },\n    },\n    consumer: {\n      dependencies: {\n        snsClient,\n        sqsClient,\n        stsClient,\n        logger: globalLogger,\n        errorReporter,\n        consumerErrorResolver: new SnsConsumerErrorResolver(),\n        transactionObservabilityManager: new NoopObservabilityManager(),\n      },\n      creationConfig: {\n        topic: { Name: 'user-cache-invalidations' },\n        // Each instance MUST use a unique queue name (e.g. include the host id):\n        queue: { QueueName: `user-cache-invalidations-${process.env.HOSTNAME}` },\n      },\n    },\n  })\n\nconst userLoader = new Loader<User>({\n  inMemoryCache: { ttlInMsecs: 1000 * 60 * 5 },\n  asyncCache: yourAsyncCache,\n  notificationConsumer,\n  notificationPublisher,\n})\n\nawait userLoader.init()\nawait userLoader.invalidateCacheFor('123') // fans out to every other instance\n```\n\nEach consumer needs its **own** SQS queue subscribed to the shared topic; SNS handles the fan-out. If two instances share a queue, only one of them will receive each invalidation message.\n\n## Group notification pair\n\nFor `GroupLoader`, use `createGroupNotificationPair` with the same shape:\n\n```ts\nimport { GroupLoader } from 'layered-loader/core'\nimport { createGroupNotificationPair } from '@layered-loader/sqs'\n\nconst { publisher: notificationPublisher, consumer: notificationConsumer } =\n  createGroupNotificationPair<User>({\n    publisher: { dependencies, creationConfig: { topic: { Name: 'tenant-cache-invalidations' } } },\n    consumer: {\n      dependencies: consumerDependencies,\n      creationConfig: {\n        topic: { Name: 'tenant-cache-invalidations' },\n        queue: { QueueName: `tenant-cache-invalidations-${process.env.HOSTNAME}` },\n      },\n    },\n  })\n\nconst userLoader = new GroupLoader<User>({\n  inMemoryCache: { ttlInMsecs: 1000 * 60 * 5 },\n  asyncCache: yourAsyncCache,\n  notificationConsumer,\n  notificationPublisher,\n})\n\nawait userLoader.invalidateCacheFor('user-1', 'tenant-A')\n```\n\n## Locator vs creation config\n\nBoth publisher and consumer accept a discriminated config:\n\n| Field | Behaviour |\n| --- | --- |\n| `creationConfig` | Auto-creates the resource (`topic`, `queue`, `subscription`) on `subscribe()` if it does not exist. |\n| `locatorConfig`  | Reuses pre-provisioned resources. Throws if they are missing. |\n\nFor a consumer in `locatorConfig` mode, you must supply enough information to resolve the topic, queue, and subscription:\n\n```ts\nconsumer: {\n  dependencies: consumerDependencies,\n  locatorConfig: {\n    topicArn: 'arn:aws:sns:us-east-1:000000000000:user-cache-invalidations',\n    queueUrl: 'https://sqs.us-east-1.amazonaws.com/000000000000/cache-q-host-1',\n    subscriptionArn: 'arn:aws:sns:...:subscription/...',\n  },\n}\n```\n\nYou can grab those identifiers off a previously-initialised consumer/publisher:\n\n```ts\nawait pair.publisher.subscribe()\nawait pair.consumer.subscribe()\nconsole.log(pair.publisher.topicArn)\nconsole.log(pair.consumer.subscriptionArn, pair.consumer.queueUrl)\n```\n\nIf you need to override defaults of the SNS subscription (filter policy, raw delivery, etc.), pass `subscriptionConfig: SqsSubscriptionOptions` to the consumer config.\n\n## How invalidation flows through SNS/SQS\n\n```\n┌──────────────────┐                    SNS topic                     ┌──────────────────┐\n│  Instance A      │   publisher ────────────────────────────▶  ┌──── │   Instance B     │\n│  Loader          │                                              │ ───┴── consumer.delete(key)\n│                  │   consumer ──── (own queue, self-skip)       │\n└──────────────────┘                                              │ ───┬── consumer.delete(key)\n                                                                  └──── │   Instance C    │\n                                                                        └──────────────────┘\n```\n\nEach `Loader.invalidateCacheFor(...)` call publishes a JSON command (`DELETE`, `DELETE_MANY`, `SET`, `CLEAR`) to the SNS topic. SNS fan-outs to every subscribed SQS queue, and each consumer applies the command to its local in-memory cache.\n\n## Self-message filtering and `serverUuid`\n\nEvery published command carries an `originUuid`. A consumer skips a command whose `originUuid` matches its own `serverUuid`, preventing instance A from re-applying its own invalidations bouncing back through SNS.\n\n`createNotificationPair` (and `createGroupNotificationPair`) generate one `serverUuid` shared by both the publisher and the consumer it returns. You can override it with the `serverUuid` field if you need stable identifiers across restarts (e.g. when locating an existing subscription).\n\n## Flexible invalidation triggers\n\nA *trigger* lets you treat any upstream messaging system as a source of cache-invalidation events without that system knowing the cache exists. The trigger:\n\n1. Subscribes to one or more queues or topics you do not own.\n2. Validates each message with a Zod schema.\n3. Runs your **resolver** to extract entity ids (and optionally a group).\n4. Applies the resulting actions directly to your `Loader` / `GroupLoader` — which handles local in-memory and async-cache invalidation and, if you configured a notification pair, also fans the invalidation out to peer instances.\n\nThe actions and resolver shape are transport-agnostic — the same `InvalidationResolver`, `InvalidationAction`, and `InvalidationTrigger` interface can power future RabbitMQ / Kafka / Pub/Sub adapters. The SNS/SQS adapters live in this package.\n\n### Recommended pattern: Redis fanout + SQS trigger\n\nThis is shape **C** from [Picking your shape](#picking-your-shape) and the recommended setup whenever you have both AWS upstream events to consume and Redis available. The `Loader` uses Redis for cluster fanout, and the trigger queue can be **shared across every instance** (no per-instance queues to manage):\n\n```ts\nimport { z } from 'zod'\nimport Redis from 'ioredis'\nimport { Loader } from 'layered-loader/core'\nimport { createNotificationPair } from 'layered-loader/redis'\nimport { SnsTopicInvalidationTrigger } from '@layered-loader/sqs'\n\nconst USER_EVENT_SCHEMA = z.object({\n  type: z.literal('user.updated'),\n  userId: z.string(),\n})\n\nconst redisOptions = { host: 'redis', port: 6379 }\n\n// 1. The cache cluster's own invalidation pair — pure Redis pub/sub.\nconst { publisher: notificationPublisher, consumer: notificationConsumer } =\n  createNotificationPair<User>({\n    channel: 'user-cache-invalidations',\n    publisherRedis: new Redis(redisOptions),\n    consumerRedis: new Redis(redisOptions),\n  })\n\nconst userLoader = new Loader<User>({\n  inMemoryCache: { ttlInMsecs: 1000 * 60 * 5 },\n  asyncCache: yourAsyncCache,\n  notificationConsumer,\n  notificationPublisher,\n})\nawait userLoader.init()\n\n// 2. The trigger applies invalidations directly to the loader. Each instance\n//    runs this code, but the SQS queue is SHARED (no ${HOSTNAME} suffix) —\n//    competing-consumer semantics deliver each upstream event to exactly one\n//    instance. That instance's loader propagates via Redis pub/sub.\nconst trigger = new SnsTopicInvalidationTrigger({\n  target: userLoader,\n  dependencies: consumerDeps,\n  sources: [\n    {\n      creationConfig: {\n        topic: { Name: 'domain-events.users' },                  // upstream service's topic\n        queue: { QueueName: 'user-cache-invalidation-trigger' }, // SHARED across all instances\n      },\n      bindings: [\n        {\n          messageSchema: USER_EVENT_SCHEMA,\n          resolver: (msg) => ({ kind: 'delete', key: msg.userId }),\n        },\n      ],\n    },\n  ],\n})\n\nawait trigger.start()\n```\n\nOperational properties:\n\n- **One** SQS queue regardless of pod count → no churn, no lifecycle plumbing needed.\n- AWS-native ingestion of upstream domain events.\n- Redis pub/sub for cluster fanout — sub-ms latency, no per-instance setup.\n- Natural failure isolation: if a pod crashes mid-message, SQS visibility timeout returns the message to the queue and another pod picks it up.\n\nIf your upstream events have meaningful per-entity ordering, use an SQS FIFO queue (`QueueName: 'user-cache-invalidation-trigger.fifo'` plus `FifoQueue: 'true'` in the queue attributes). Otherwise a standard queue is appropriate.\n\nThe remainder of this section describes the trigger classes themselves — they are used identically whether the loader's publisher is Redis (shape **C**, recommended) or `SqsNotificationPublisher` (shape **B**).\n\nFour trigger classes ship for SNS/SQS:\n\n| Class | Source kind | Target |\n| --- | --- | --- |\n| `SnsTopicInvalidationTrigger`         | SNS topic (subscribes a dedicated SQS queue) | flat `Loader` |\n| `SqsQueueInvalidationTrigger`         | Existing SQS queue                           | flat `Loader` |\n| `SnsTopicGroupInvalidationTrigger`    | SNS topic                                    | `GroupLoader` |\n| `SqsQueueGroupInvalidationTrigger`    | Existing SQS queue                           | `GroupLoader` |\n\nEach trigger is homogeneous in source kind and takes a single `dependencies` block (shared across every source it consumes). For deployments that need to mix source kinds, `composeTriggers(...)` bundles multiple triggers under one `start()` / `stop()`.\n\n### Triggering from an existing SNS topic\n\nThe trigger creates (or reuses) an SQS queue and subscribes it to each upstream topic.\n\n```ts\nimport { z } from 'zod'\nimport { Loader } from 'layered-loader/core'\nimport { createNotificationPair, SnsTopicInvalidationTrigger } from '@layered-loader/sqs'\n\nconst USER_EVENT_SCHEMA = z.object({\n  type: z.enum(['user.updated', 'user.deleted', 'user.bulk-updated']),\n  userId: z.string().optional(),\n  userIds: z.array(z.string()).optional(),\n})\n\n// 1. The cache cluster's own invalidation pair (same as any deployment):\nconst cachePair = createNotificationPair<User>({\n  publisher: { dependencies: pubDeps, creationConfig: { topic: { Name: 'user-cache-invalidations' } } },\n  consumer:  {\n    dependencies: consumerDeps,\n    creationConfig: {\n      topic: { Name: 'user-cache-invalidations' },\n      queue: { QueueName: `user-cache-invalidations-${process.env.HOSTNAME}` },\n    },\n  },\n})\n\nconst userLoader = new Loader<User>({\n  inMemoryCache: { ttlInMsecs: 1000 * 60 * 5 },\n  asyncCache: yourAsyncCache,\n  notificationConsumer: cachePair.consumer,\n  notificationPublisher: cachePair.publisher,\n})\n\n// 2. The trigger consumes domain events and applies them to the Loader.\n//    The Loader's own publisher takes care of fanning out to peers.\nconst trigger = new SnsTopicInvalidationTrigger({\n  target: userLoader,\n  dependencies: consumerDeps,\n  sources: [\n    {\n      creationConfig: {\n        topic: { Name: 'domain-events.users' }, // owned by an upstream service\n        queue: { QueueName: `cache-trigger-${process.env.HOSTNAME}` },\n      },\n      bindings: [\n        {\n          messageSchema: USER_EVENT_SCHEMA,\n          resolver: (msg) => {\n            switch (msg.type) {\n              case 'user.updated':\n              case 'user.deleted':\n                return msg.userId ? { kind: 'delete', key: msg.userId } : null\n              case 'user.bulk-updated':\n                return msg.userIds?.length\n                  ? { kind: 'deleteMany', keys: msg.userIds }\n                  : null\n            }\n          },\n        },\n      ],\n    },\n  ],\n})\n\nawait trigger.start()\n```\n\n### Triggering from an existing SQS queue\n\nIf the upstream system writes directly to an SQS queue (no SNS topic in the middle), use `SqsQueueInvalidationTrigger`:\n\n```ts\nimport { SqsQueueInvalidationTrigger } from '@layered-loader/sqs'\n\nconst trigger = new SqsQueueInvalidationTrigger({\n  target: userLoader,\n  dependencies: sqsConsumerDeps, // only needs SQS clients, not SNS/STS\n  sources: [\n    {\n      locatorConfig: {\n        queueUrl: 'https://sqs.us-east-1.amazonaws.com/000000000000/domain-events',\n      },\n      bindings: [\n        { messageSchema: DOMAIN_EVENT_SCHEMA, resolver: (msg) => /* ... */ },\n      ],\n    },\n  ],\n})\n\nawait trigger.start()\n```\n\n`SqsQueueInvalidationTrigger` uses `@message-queue-toolkit/sqs`'s consumer directly and does not require SNS / STS clients in its dependencies.\n\n### Group triggers\n\nThe group counterparts emit `GroupInvalidationAction`s and target a `GroupLoader`:\n\n```ts\nimport { SnsTopicGroupInvalidationTrigger } from '@layered-loader/sqs'\n\nconst trigger = new SnsTopicGroupInvalidationTrigger({\n  target: tenantLoader, // a GroupLoader with its own notification pair\n  dependencies: consumerDeps,\n  sources: [\n    {\n      creationConfig: {\n        topic: { Name: 'tenant-events' },\n        queue: { QueueName: `tenant-trigger-${process.env.HOSTNAME}` },\n      },\n      bindings: [\n        {\n          messageSchema: TENANT_EVENT_SCHEMA,\n          resolver: (msg) => {\n            if (msg.type === 'tenant.purged') return { kind: 'deleteGroup', group: msg.tenantId }\n            if (msg.type === 'tenant.user.updated' && msg.userId) {\n              return { kind: 'deleteFromGroup', key: msg.userId, group: msg.tenantId }\n            }\n            return null\n          },\n        },\n      ],\n    },\n  ],\n})\n```\n\n`SqsQueueGroupInvalidationTrigger` is the SQS-queue counterpart.\n\n### Multiple sources and event types\n\nA trigger can subscribe to **multiple sources** at once (each spun up as an independent consumer) and can route **multiple event types from the same source** to different resolvers via a `messageTypeField` discriminator.\n\n```ts\nconst USER_UPDATED = z.object({ type: z.literal('user.updated'), userId: z.string() })\nconst USER_BULK    = z.object({ type: z.literal('user.bulk'),    userIds: z.array(z.string()) })\n\nconst trigger = new SqsQueueInvalidationTrigger({\n  target: userLoader,\n  dependencies: sqsConsumerDeps,\n  sources: [\n    {\n      locatorConfig: { queueUrl: process.env.UPSTREAM_QUEUE_A_URL! },\n      bindings: [\n        { messageSchema: USER_UPDATED, resolver: (m) => ({ kind: 'delete', key: m.userId }) },\n      ],\n    },\n    // Same trigger, different queue, multiple event types\n    {\n      locatorConfig: { queueUrl: process.env.UPSTREAM_QUEUE_B_URL! },\n      messageTypeField: 'type', // path on the message body that selects a binding\n      bindings: [\n        {\n          messageType: 'user.updated',\n          messageSchema: USER_UPDATED,\n          resolver: (m) => ({ kind: 'delete', key: m.userId }),\n        },\n        {\n          messageType: 'user.bulk',\n          messageSchema: USER_BULK,\n          resolver: (m) => ({ kind: 'deleteMany', keys: m.userIds }),\n        },\n      ],\n    },\n  ],\n})\n```\n\nRules:\n\n- Each source must declare at least one binding.\n- If a source has only one binding, `messageType` and `messageTypeField` are optional — the binding handles every message.\n- If a source has two or more bindings, the source must specify `messageTypeField` and every binding must specify `messageType`. `messageTypeField` is a dotted path (e.g. `'metadata.eventId'`).\n\n#### One binding over a union of event types\n\nWhen several event types should all flow through a **single** resolver, you do not need one binding per type. Bind a single `z.union(...)` (or `z.discriminatedUnion(...)`) schema instead and branch inside the resolver. This is the natural shape when you already have message-queue-toolkit message definitions and want to react to a couple of their `consumerSchema`s:\n\n```ts\nconst PROJECT_LANGUAGE_EVENT_SCHEMA = z.union([\n  ExpertProjectLanguageEvent['project_language.added'].consumerSchema,\n  ExpertProjectLanguageEvent['project_language.removed'].consumerSchema,\n])\n\nconst trigger = new SnsTopicInvalidationTrigger({\n  target: projectLoader,\n  dependencies: consumerDeps,\n  sources: [\n    {\n      creationConfig: {\n        topic: { Name: 'domain-events.project-languages' },\n        queue: { QueueName: `project-language-trigger-${process.env.HOSTNAME}` },\n      },\n      // No messageTypeField: this is still a single binding.\n      bindings: [\n        {\n          messageSchema: PROJECT_LANGUAGE_EVENT_SCHEMA,\n          resolver: (msg) => ({ kind: 'delete', key: msg.projectId }),\n        },\n      ],\n    },\n  ],\n})\n```\n\nHow routing behaves with this shape:\n\n- **Every member of the union is routed.** Because there is one binding, every message is validated against the union schema and, on success, handed to the resolver — regardless of which union member it matched.\n- **Event types outside the union are dropped.** A message that matches *neither* union member fails schema validation and is rejected **before** the resolver runs (it goes back to the queue / DLQ as a validation error — see [Error handling and retries](#error-handling-and-retries)). It never reaches your resolver and never invalidates anything.\n- **Do not set `messageTypeField` for the union shape.** `messageTypeField` switches the source into the multi-binding routing mode above, where the extracted type must match a binding's `messageType`. With a single union binding there is no per-member `messageType` to match, so setting it would cause every message to be treated as an unknown type and dropped. Leave it unset so all messages route to the one binding and the union schema does the filtering.\n\nReach for the multi-binding `messageTypeField` form when each event type needs a **different** resolver; reach for the single union binding when one resolver handles them all and you simply want everything outside the union ignored.\n\n### Mixing source kinds with `composeTriggers`\n\nA single trigger class is homogeneous (only SNS topics, or only SQS queues). When a deployment needs both, build one of each and wrap them:\n\n```ts\nimport { composeTriggers, SnsTopicInvalidationTrigger, SqsQueueInvalidationTrigger } from '@layered-loader/sqs'\n\nconst snsTrigger = new SnsTopicInvalidationTrigger({ target: userLoader, dependencies: snsSqsDeps, sources: [...] })\nconst sqsTrigger = new SqsQueueInvalidationTrigger({ target: userLoader, dependencies: sqsDeps,    sources: [...] })\n\nconst triggers = composeTriggers(snsTrigger, sqsTrigger)\nawait triggers.start()\n// later\nawait triggers.stop()\n```\n\n`composeTriggers` returns a plain `InvalidationTrigger` whose `start()` / `stop()` fan out to every wrapped trigger in parallel.\n\n### Resolver semantics\n\nA resolver receives the validated `TMessage` and returns:\n\n- A single `InvalidationAction` / `GroupInvalidationAction` — applied immediately.\n- An array of actions — applied sequentially, preserving emission order.\n- `null` or `undefined` — skip the message (the source treats it as successfully processed).\n\nFlat actions:\n\n```ts\ntype InvalidationAction =\n  | { kind: 'delete'; key: string }\n  | { kind: 'deleteMany'; keys: readonly string[] }\n  | { kind: 'clear' }\n```\n\nGroup actions:\n\n```ts\ntype GroupInvalidationAction =\n  | { kind: 'deleteFromGroup'; key: string; group: string }\n  | { kind: 'deleteGroup'; group: string }\n  | { kind: 'clear' }\n```\n\nA resolver may be **synchronous or asynchronous** — both are fully supported. Return the action(s) directly for a simple field-to-key mapping, or return a `Promise` (e.g. an `async` function) when deriving the affected keys requires I/O such as a database or service lookup. The trigger always `await`s the resolver before applying actions, so the two forms behave identically:\n\n```ts\n// Synchronous: map a field straight to a key.\nresolver: (msg) => ({ kind: 'delete', key: msg.userId })\n\n// Asynchronous: look up the affected keys first.\nresolver: async (msg) => {\n  const memberIds = await membership.listMemberIds(msg.teamId)\n  return { kind: 'deleteMany', keys: memberIds }\n}\n```\n\n### Error handling and retries\n\nIf the resolver or apply step throws, the trigger:\n\n1. Invokes the optional `errorHandler(err, channel)` for observability.\n2. Re-throws so `message-queue-toolkit` can apply its standard SQS retry / dead-letter behaviour.\n\nFor 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).\n\n### Dead-letter queues\n\nTo 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:\n\n```ts\nconst trigger = new SnsTopicInvalidationTrigger({\n  target: userLoader,\n  dependencies: consumerDeps,\n  sources: [\n    {\n      creationConfig: {\n        topic: { Name: 'domain-events.users' },\n        queue: { QueueName: `cache-trigger-${process.env.HOSTNAME}` },\n      },\n      deadLetterQueue: {\n        // Move a message to the DLQ after this many failed receives.\n        redrivePolicy: { maxReceiveCount: 3 },\n        // Auto-create the DLQ. Use `locatorConfig` instead to point at an existing one.\n        creationConfig: { queue: { QueueName: `cache-trigger-${process.env.HOSTNAME}-dlq` } },\n      },\n      bindings: [/* ... */],\n    },\n  ],\n})\n```\n\nThe `deadLetterQueue` field is the same shape `message-queue-toolkit` exposes on its consumers. It is available on all four trigger source types.\n\n### Explicit vs spread configuration\n\nA 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:\n\n- **Explicit** — spell options out inline (`creationConfig`/`locatorConfig`, `deadLetterQueue`, `subscriptionConfig`, `concurrentConsumersAmount`, `consumerOverrides`, ...). An unknown key or a wrong-typed value is a compile error.\n- **Spread** — resolve options elsewhere — e.g. with `@lokalise/aws-config`'s `getSnsMqtOptionsResolver()` — and spread the result straight in.\n\nA trigger is not a plain consumer — it owns its handlers and its subscription *filtering* — so just those two things are claimed by the trigger and everything else flows through untouched. You can spread a resolved options object straight in without un-setting anything:\n\n- **`handlers`** — always rebuilt from `bindings`.\n- **subscription filter policy** (`subscriptionConfig.Attributes.FilterPolicy` / `FilterPolicyScope`) — `resolveConsumerOptions` derives a `FilterPolicy` from the consumer's handlers. The trigger has its own handlers (from `bindings`), so the resolver's policy — built from an empty handler list — would reject every message. The trigger drops just those keys and defaults the subscription to accept-all.\n\nEverything else flows through as-is, including `creationConfig`/`locatorConfig`, `deadLetterQueue`, `concurrentConsumersAmount`, `consumerOverrides`, and the **subscription-level dead-letter queue** — both `subscriptionConfig.Attributes.RedrivePolicy` and a spread-in `subscriptionDeadLetterQueue` are preserved.\n\n```ts\nconst resolver = getSnsMqtOptionsResolver({ appEnv: 'production' })\nconst options = resolver.resolveConsumerOptions(topicName, queueName, {\n  /* awsConfig, logger, deadLetterQueue, ... */\n})\n\nconst trigger = new SnsTopicInvalidationTrigger({\n  target,\n  dependencies,\n  sources: [\n    {\n      ...options,\n      bindings: [\n        { messageSchema: PROJECT_LANGUAGE_EVENT_SCHEMA, resolver: async (message) => { /* ... */ } },\n      ],\n    },\n  ],\n})\n```\n\n### Lifecycle\n\n```ts\nconst trigger = new SnsTopicInvalidationTrigger({ ... }) // or SqsQueueInvalidationTrigger, plus the group variants\n\nawait trigger.start() // idempotent; concurrent calls share one start\nawait trigger.stop()  // idempotent; awaits any in-flight start\nawait trigger.start() // restart is supported\n```\n\n## Testing with fauxqs\n\nFor local development and tests, swap the AWS SDK endpoint for [fauxqs](https://github.com/kibertoad/fauxqs). Two modes are useful in practice:\n\n### In-process (recommended for tests)\n\nSub-second startup, no daemon required, isolated per test process. This package's own integration tests run this way — see `packages/sqs/test/globalSetup.ts`.\n\n```ts\nimport { startFauxqs } from 'fauxqs'\nimport { SQSClient } from '@aws-sdk/client-sqs'\n\nconst server = await startFauxqs({ port: 0, logger: false })\nconst credentials = { accessKeyId: 'test', secretAccessKey: 'test' }\nconst region = 'us-east-1'\n\nconst sqsClient = new SQSClient({ endpoint: server.address, region, credentials })\n// ...\nawait server.stop()\n```\n\n### Docker-compose (for app smoke testing)\n\nThe repository root's `docker-compose.yml` includes a `fauxqs` service on port `4566` for local app runs against a stable endpoint:\n\n```bash\ndocker compose up fauxqs\n```\n\nThen point your AWS SDK clients at `http://localhost:4566`.\n\n## API reference\n\n### Notification pair\n\n| Symbol | Purpose |\n| --- | --- |\n| `createNotificationPair<T>(config)` | Returns `{ publisher, consumer }` for flat-cache invalidation. |\n| `createGroupNotificationPair<T>(config)` | Same, but for group caches. |\n| `SqsNotificationPublisher<T>` | Lower-level constructor for direct notification publishing when not using `createNotificationPair`. |\n| `SqsNotificationConsumer<T>` | Lower-level constructor; rarely used directly. |\n| `SqsGroupNotificationPublisher<T>` / `SqsGroupNotificationConsumer<T>` | Group-cache equivalents. |\n| `SqsSubscriptionOptions` | Type for `subscriptionConfig` overrides. |\n\n### Triggers\n\n| Symbol | Purpose |\n| --- | --- |\n| `SnsTopicInvalidationTrigger` | Flat-cache trigger consuming from upstream SNS topics. |\n| `SqsQueueInvalidationTrigger` | Flat-cache trigger consuming directly from upstream SQS queues. |\n| `SnsTopicGroupInvalidationTrigger` / `SqsQueueGroupInvalidationTrigger` | `GroupLoader` counterparts. |\n| `composeTriggers(...triggers)` | Wraps multiple `InvalidationTrigger`s into one combined `start()` / `stop()`. |\n| `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. |\n| `FlatBinding<TMessage>` / `GroupBinding<TMessage>` | One `(messageSchema, resolver, messageType?)` triple. |\n| `InvalidationTarget` / `GroupInvalidationTarget` | Structural interfaces that `Loader` / `GroupLoader` satisfy. |\n| `InvalidationAction` / `GroupInvalidationAction` | Action ADTs returned by resolvers. |\n| `InvalidationResolver<TMessage, TAction>` | Resolver signature. |\n| `InvalidationTrigger` | `start()` / `stop()` lifecycle interface. |\n| `runFlatPipeline` / `runGroupPipeline` | Reusable resolver + dispatch helpers (transport-agnostic). |\n| `applyFlatAction` / `applyGroupAction` | Apply a single resolved action to a target. |\n| `buildFlatBindings` / `buildGroupBindings` | Helpers that turn a binding array into `MessageHandlerConfig`s — useful when writing a custom trigger. |\n| `AbstractSqsTrigger` | Lifecycle-only base class for building custom SQS-based triggers. |\n| `SqsQueueTriggerConsumer` / `SnsTopicTriggerConsumer` | Concrete subclasses of `@message-queue-toolkit` consumers, exposed for advanced custom triggers. |\n| `deriveSqsQueueChannelName` / `deriveSnsTopicChannelName` (+ group variants) | Derive a logical channel name from a source config. |\n","readmeFilename":""}