@babelqueue/sqs 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,11 +54,37 @@ For LocalStack/ElasticMQ, point the `SQS` client's `endpoint` there.
54
54
  | `meta.created_at` | `MessageAttributes.bq-created-at` (Number, ms) |
55
55
  | `attempts` | reconciled to `ApproximateReceiveCount − 1` on receive |
56
56
  | reserve / ack | visibility timeout → `DeleteMessage` |
57
-
58
- Retry is **SQS-native**: a throwing handler leaves the message undeleted, so SQS
59
- redelivers it after the visibility timeout (at-least-once). The loop never stops on a
60
- bad message — use `onError` / `onUnknownUrn` to observe. The envelope is unchanged
61
- (`schema_version` stays `1`); SQS is purely additive.
57
+ | release (nack) | `ChangeMessageVisibility(ReceiptHandle, VisibilityTimeout = releaseDelay)` |
58
+
59
+ Retry is **SQS-native** (contract §3.5): a throwing handler's message is never deleted;
60
+ it is released with `ChangeMessageVisibility`, so SQS redelivers it after
61
+ `releaseDelay` seconds (**default `0`** — visible again immediately; a number or a
62
+ function of `attempts`, clamped to `0…43200`). If the release call itself fails, the
63
+ error goes to `onError` and the message is redelivered after the visibility timeout
64
+ (at-least-once). A `DeleteMessage` that fails *after* a successful handler is not a
65
+ handler failure: it reaches `onError` as an `SqsDeleteError` (broker error in `cause`)
66
+ and the message is **not** released — it returns only after its visibility timeout, so
67
+ dedupe on `meta.id` if the side effect must not repeat. The loop never stops on a bad message — use `onError` /
68
+ `onUnknownUrn` to observe. The envelope is unchanged (`schema_version` stays `1`); SQS
69
+ is purely additive.
70
+
71
+ > **Poison messages — configure a `RedrivePolicy`.** With the `0 s` default, a handler
72
+ > that always fails is retried back-to-back until the queue's native `RedrivePolicy`
73
+ > (`maxReceiveCount`) moves the message to its dead-letter queue. Without one, such a
74
+ > message loops forever. Give every consumed queue a `RedrivePolicy` (e.g.
75
+ > `maxReceiveCount` 3–5 → `<queue>.dlq`), and/or pass a backoff:
76
+ >
77
+ > ```ts
78
+ > new SqsConsumer(sqs, url, handlers, {
79
+ > releaseDelay: (attempts) => Math.min(2 ** attempts * 5, 900),
80
+ > });
81
+ > ```
82
+
83
+ An unmapped URN follows `unknownUrn`: `fail` (report, leave), `delete`, `release`
84
+ (`ChangeMessageVisibility` after `unknownUrnReleaseDelay`, default `0`) or
85
+ `dead_letter` (annotated envelope to the opt-in `deadLetterQueueUrl`, then delete; a
86
+ `.fifo` DLQ gets `MessageGroupId` + `MessageDeduplicationId`). Without `unknownUrn`,
87
+ `onUnknownUrn` is called and the message deleted, or — with no hook — reported and left.
62
88
 
63
89
  ## Test
64
90
 
@@ -69,6 +95,24 @@ The unit tests inject a fake SQS client (`SqsApi`), so they run without
69
95
  npm test
70
96
  ```
71
97
 
98
+ ## OpenTelemetry tracing (ADR-0028)
99
+
100
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
101
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
102
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
103
+ String `MessageAttributes`, beside the contract `bq-*` attributes (the contract wins a key collision; bounded by SQS's 10-attribute cap). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
104
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
105
+
106
+ ```ts
107
+ import { trace } from "@opentelemetry/api";
108
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
109
+ import type { HeaderCarrier } from "@babelqueue/core";
110
+
111
+ const headers: HeaderCarrier = {};
112
+ await tracedPublish(trace.getTracer("orders"), urn, data,
113
+ () => adapterPublish(urn, data, { headers }), { headers });
114
+ ```
115
+
72
116
  ## License
73
117
 
74
118
  MIT
package/dist/index.cjs CHANGED
@@ -20,12 +20,32 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ MAX_VISIBILITY_TIMEOUT: () => MAX_VISIBILITY_TIMEOUT,
23
24
  SqsConsumer: () => SqsConsumer,
25
+ SqsDeleteError: () => SqsDeleteError,
24
26
  SqsPublisher: () => SqsPublisher,
27
+ headersOf: () => headersOf,
28
+ mergeAttributes: () => mergeAttributes,
25
29
  toMessageAttributes: () => toMessageAttributes
26
30
  });
27
31
  module.exports = __toCommonJS(index_exports);
28
32
  var import_core = require("@babelqueue/core");
33
+
34
+ // src/headers.ts
35
+ function sanitizeHeaders(headers) {
36
+ const out = {};
37
+ if (!headers) return out;
38
+ for (const key of Object.keys(headers)) {
39
+ const value = headers[key];
40
+ if (key === "" || value == null || value === "") continue;
41
+ out[key] = value;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ // src/index.ts
47
+ var MAX_ATTRIBUTES = 10;
48
+ var MAX_VISIBILITY_TIMEOUT = 43200;
29
49
  var str = (value) => ({
30
50
  DataType: "String",
31
51
  StringValue: String(value)
@@ -48,6 +68,25 @@ function toMessageAttributes(envelope) {
48
68
  }
49
69
  return attrs;
50
70
  }
71
+ function mergeAttributes(base, headers) {
72
+ const clean = sanitizeHeaders(headers);
73
+ for (const key of Object.keys(clean).sort()) {
74
+ if (key in base) continue;
75
+ if (Object.keys(base).length >= MAX_ATTRIBUTES) break;
76
+ base[key] = str(clean[key]);
77
+ }
78
+ return base;
79
+ }
80
+ function headersOf(message) {
81
+ const attrs = message.MessageAttributes;
82
+ const out = {};
83
+ if (!attrs) return out;
84
+ for (const key of Object.keys(attrs)) {
85
+ const value = attrs[key]?.StringValue;
86
+ if (value != null && value !== "") out[key] = value;
87
+ }
88
+ return out;
89
+ }
51
90
  function queueNameFromUrl(queueUrl) {
52
91
  const segments = queueUrl.split("/").filter(Boolean);
53
92
  return segments[segments.length - 1] ?? "default";
@@ -73,7 +112,7 @@ var SqsPublisher = class {
73
112
  const input = {
74
113
  QueueUrl: this.queueUrl,
75
114
  MessageBody: import_core.EnvelopeCodec.encode(envelope),
76
- MessageAttributes: toMessageAttributes(envelope)
115
+ MessageAttributes: mergeAttributes(toMessageAttributes(envelope), options.headers)
77
116
  };
78
117
  if (this.options.fifo) {
79
118
  input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);
@@ -85,12 +124,29 @@ var SqsPublisher = class {
85
124
  return envelope.meta.id;
86
125
  }
87
126
  };
127
+ var SqsDeleteError = class extends import_core.BabelQueueError {
128
+ constructor(cause) {
129
+ super("Failed to delete a processed SQS message; it will be redelivered after its visibility timeout.");
130
+ this.name = "SqsDeleteError";
131
+ this.cause = cause;
132
+ }
133
+ };
88
134
  var SqsConsumer = class {
89
135
  constructor(client, queueUrl, handlers, options = {}) {
90
136
  this.client = client;
91
137
  this.queueUrl = queueUrl;
92
138
  this.handlers = handlers;
93
139
  this.options = options;
140
+ const strategy = options.unknownUrn;
141
+ if (strategy !== void 0 && !Object.values(import_core.UnknownUrnStrategy).includes(strategy)) {
142
+ throw new import_core.BabelQueueError(`Unknown unknownUrn strategy "${strategy}".`);
143
+ }
144
+ const needsRelease = options.releaseDelay != null || strategy === import_core.UnknownUrnStrategy.RELEASE;
145
+ if (needsRelease && typeof client.changeMessageVisibility !== "function") {
146
+ throw new import_core.BabelQueueError(
147
+ "SqsConsumer release requires a client with changeMessageVisibility (contract \xA73.5)."
148
+ );
149
+ }
94
150
  }
95
151
  client;
96
152
  queueUrl;
@@ -142,6 +198,38 @@ var SqsConsumer = class {
142
198
  const urn = import_core.EnvelopeCodec.urn(envelope);
143
199
  const handler = this.handlers[urn];
144
200
  if (!handler) {
201
+ await this.unknown(urn, envelope, message);
202
+ return;
203
+ }
204
+ try {
205
+ await handler(envelope, message, headersOf(message));
206
+ } catch (error) {
207
+ this.options.onError?.(error, envelope, message);
208
+ await this.guard(envelope, message, async () => {
209
+ const delay = this.options.releaseDelay ?? 0;
210
+ const attempts = typeof envelope.attempts === "number" ? envelope.attempts : 0;
211
+ await this.release(message, typeof delay === "function" ? delay(attempts) : delay);
212
+ });
213
+ return;
214
+ }
215
+ await this.guard(envelope, message, () => this.deleteHandled(message));
216
+ }
217
+ /**
218
+ * Run a release / dead-letter step; a failure (e.g. `MessageNotInflight`, a missing IAM grant, a
219
+ * throwing `releaseDelay`) is reported to `onError` and the message is left undeleted, so
220
+ * visibility expiry still redelivers it. Never lets the error stop the consume loop.
221
+ */
222
+ async guard(envelope, message, step) {
223
+ try {
224
+ await step();
225
+ } catch (error) {
226
+ this.options.onError?.(error, envelope, message);
227
+ }
228
+ }
229
+ /** Apply the unknown-URN strategy (contract §3.5). */
230
+ async unknown(urn, envelope, message) {
231
+ const strategy = this.options.unknownUrn;
232
+ if (strategy === void 0) {
145
233
  if (this.options.onUnknownUrn) {
146
234
  await this.options.onUnknownUrn(envelope, message);
147
235
  await this.delete(message);
@@ -150,11 +238,61 @@ var SqsConsumer = class {
150
238
  }
151
239
  return;
152
240
  }
241
+ await this.guard(envelope, message, async () => {
242
+ await this.options.onUnknownUrn?.(envelope, message);
243
+ });
244
+ switch (strategy) {
245
+ case import_core.UnknownUrnStrategy.DELETE:
246
+ await this.guard(envelope, message, () => this.deleteHandled(message));
247
+ return;
248
+ case import_core.UnknownUrnStrategy.RELEASE:
249
+ await this.guard(envelope, message, () => this.release(message, this.options.unknownUrnReleaseDelay ?? 0));
250
+ return;
251
+ case import_core.UnknownUrnStrategy.DEAD_LETTER:
252
+ await this.guard(envelope, message, async () => {
253
+ await this.deadLetter(envelope, "unknown_urn");
254
+ await this.deleteHandled(message);
255
+ });
256
+ return;
257
+ default:
258
+ this.options.onError?.(new import_core.UnknownUrnError(urn), envelope, message);
259
+ }
260
+ }
261
+ /** Make the message visible again after `seconds` via `ChangeMessageVisibility`; never deletes. */
262
+ async release(message, seconds) {
263
+ if (!message.ReceiptHandle || !this.client.changeMessageVisibility) return;
264
+ const timeout = Number.isFinite(seconds) ? Math.min(MAX_VISIBILITY_TIMEOUT, Math.max(0, Math.floor(seconds))) : 0;
265
+ await this.client.changeMessageVisibility({
266
+ QueueUrl: this.queueUrl,
267
+ ReceiptHandle: message.ReceiptHandle,
268
+ VisibilityTimeout: timeout
269
+ });
270
+ }
271
+ /** Send the annotated envelope to the opt-in DLQ; a no-op (→ plain delete) when none is set. */
272
+ async deadLetter(envelope, reason) {
273
+ const dlq = this.options.deadLetterQueueUrl;
274
+ if (!dlq) return;
275
+ const source = envelope.meta?.queue ?? queueNameFromUrl(this.queueUrl);
276
+ const annotated = (0, import_core.annotate)(envelope, reason, source, {
277
+ attempts: envelope.attempts ?? 0
278
+ });
279
+ const input = {
280
+ QueueUrl: dlq,
281
+ MessageBody: import_core.EnvelopeCodec.encode(annotated),
282
+ MessageAttributes: toMessageAttributes(annotated)
283
+ };
284
+ if (dlq.endsWith(".fifo")) {
285
+ input.MessageGroupId = source;
286
+ if (annotated.meta?.id) input.MessageDeduplicationId = annotated.meta.id;
287
+ }
288
+ await this.client.sendMessage(input);
289
+ }
290
+ /** Delete a message whose processing already succeeded; a broker failure becomes an SqsDeleteError. */
291
+ async deleteHandled(message) {
153
292
  try {
154
- await handler(envelope, message);
155
293
  await this.delete(message);
156
294
  } catch (error) {
157
- this.options.onError?.(error, envelope, message);
295
+ throw new SqsDeleteError(error);
158
296
  }
159
297
  }
160
298
  async delete(message) {
@@ -167,8 +305,12 @@ var SqsConsumer = class {
167
305
  };
168
306
  // Annotate the CommonJS export names for ESM import in node:
169
307
  0 && (module.exports = {
308
+ MAX_VISIBILITY_TIMEOUT,
170
309
  SqsConsumer,
310
+ SqsDeleteError,
171
311
  SqsPublisher,
312
+ headersOf,
313
+ mergeAttributes,
172
314
  toMessageAttributes
173
315
  });
174
316
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Amazon SQS adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Amazon SQS,\n * so an SQS-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the PHP/Laravel, Python, Go, Java and .NET SDKs.\n *\n * import { SQS } from \"@aws-sdk/client-sqs\";\n * import { SqsPublisher, SqsConsumer } from \"@babelqueue/sqs\";\n *\n * const sqs = new SQS({ region: \"eu-central-1\" });\n * const url = \"https://sqs.eu-central-1.amazonaws.com/123456789012/orders\";\n *\n * await new SqsPublisher(sqs, url).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new SqsConsumer(sqs, url, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §3 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native SQS `MessageAttributes`. The envelope is\n * unchanged (`schema_version` stays 1); SQS is purely additive. Retry is SQS-native\n * (a failed handler leaves the message for visibility-timeout redelivery); the\n * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as\n * `attempts = count − 1`.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal SQS shapes (a structural subset of @aws-sdk/client-sqs) -----------\n\nexport interface SqsMessageAttributeValue {\n DataType: string;\n StringValue?: string;\n}\n\nexport interface SqsMessage {\n Body?: string;\n ReceiptHandle?: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n Attributes?: Record<string, string>;\n}\n\nexport interface SendMessageInput {\n QueueUrl: string;\n MessageBody: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n MessageGroupId?: string;\n MessageDeduplicationId?: string;\n}\n\nexport interface ReceiveMessageInput {\n QueueUrl: string;\n MaxNumberOfMessages?: number;\n WaitTimeSeconds?: number;\n VisibilityTimeout?: number;\n MessageAttributeNames?: string[];\n AttributeNames?: string[];\n}\n\n/**\n * The subset of the AWS SQS client this adapter calls. The aggregated `SQS` class\n * from `@aws-sdk/client-sqs` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface SqsApi {\n sendMessage(input: SendMessageInput): Promise<{ MessageId?: string } | unknown>;\n receiveMessage(input: ReceiveMessageInput): Promise<{ Messages?: SqsMessage[] }>;\n deleteMessage(input: { QueueUrl: string; ReceiptHandle: string }): Promise<unknown>;\n}\n\n// --- Attribute projection (contract §3.2) --------------------------------------\n\nconst str = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"String\",\n StringValue: String(value),\n});\nconst num = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"Number\",\n StringValue: String(value),\n});\n\n/**\n * Project the envelope's contract fields onto native SQS `MessageAttributes` — a\n * redundant, routable view of the body (the body stays authoritative).\n */\nexport function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue> {\n const attrs: Record<string, SqsMessageAttributeValue> = {};\n if (envelope.job) attrs[\"bq-job\"] = str(envelope.job);\n if (envelope.trace_id) attrs[\"bq-trace-id\"] = str(envelope.trace_id);\n if (envelope.meta.id) attrs[\"bq-message-id\"] = str(envelope.meta.id);\n if (envelope.meta.schema_version != null) {\n attrs[\"bq-schema-version\"] = num(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) attrs[\"bq-source-lang\"] = str(envelope.meta.lang);\n if (envelope.meta.created_at != null) {\n attrs[\"bq-created-at\"] = num(envelope.meta.created_at);\n }\n return attrs;\n}\n\nfunction queueNameFromUrl(queueUrl: string): string {\n const segments = queueUrl.split(\"/\").filter(Boolean);\n return segments[segments.length - 1] ?? \"default\";\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link SqsPublisher}. */\nexport interface SqsPublisherOptions {\n /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */\n fifo?: boolean;\n /** FIFO ordering group (default: the queue name from the URL). */\n messageGroupId?: string;\n /** Use the queue's content-based dedup instead of `meta.id` as the dedup id. */\n contentDedup?: boolean;\n}\n\n/** Options for {@link SqsPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n}\n\n/** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */\nexport class SqsPublisher {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly options: SqsPublisherOptions = {},\n ) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it as the message body with\n * the projected `MessageAttributes`, and return the message id (`meta.id`).\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queueNameFromUrl(this.queueUrl),\n traceId: options.traceId,\n });\n const input: SendMessageInput = {\n QueueUrl: this.queueUrl,\n MessageBody: EnvelopeCodec.encode(envelope),\n MessageAttributes: toMessageAttributes(envelope),\n };\n if (this.options.fifo) {\n input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);\n if (!this.options.contentDedup) {\n input.MessageDeduplicationId = envelope.meta.id;\n }\n }\n await this.client.sendMessage(input);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw SQS message. */\nexport type BabelHandler = (envelope: Envelope, message: SqsMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link SqsConsumer}. */\nexport interface SqsConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is deleted). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: SqsMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: SqsMessage) => void;\n /** Long-poll wait seconds (default 20). */\n waitTimeSeconds?: number;\n /** Reservation window applied on receive (seconds). */\n visibilityTimeout?: number;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n}\n\n/**\n * Polls an SQS queue, decodes + validates each message, routes it to the handler\n * registered for its URN, and deletes it on success. A throwing handler leaves the\n * message undeleted — SQS redelivers it after the visibility timeout (at-least-once);\n * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler.\n */\nexport class SqsConsumer {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly handlers: BabelHandlers,\n private readonly options: SqsConsumerOptions = {},\n ) {}\n\n /** Receive one batch, route each message, delete the ones handled. Returns the batch size. */\n async poll(): Promise<number> {\n const input: ReceiveMessageInput = {\n QueueUrl: this.queueUrl,\n MaxNumberOfMessages: this.options.maxMessages ?? 10,\n WaitTimeSeconds: this.options.waitTimeSeconds ?? 20,\n MessageAttributeNames: [\"All\"],\n AttributeNames: [\"ApproximateReceiveCount\"],\n };\n if (this.options.visibilityTimeout != null) {\n input.VisibilityTimeout = this.options.visibilityTimeout;\n }\n const result = await this.client.receiveMessage(input);\n const messages = result.Messages ?? [];\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\n }\n\n /** Poll until `signal` aborts (each poll long-polls, so this does not busy-loop). */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: SqsMessage): Promise<void> {\n const envelope = EnvelopeCodec.decode(message.Body ?? \"\");\n\n const receiveCount = message.Attributes?.[\"ApproximateReceiveCount\"];\n if (receiveCount !== undefined) {\n const native = Number.parseInt(receiveCount, 10) - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (Number.isFinite(native) && native > current) {\n envelope.attempts = native;\n }\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from SQS.\"),\n envelope,\n message,\n );\n return;\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.delete(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n }\n return;\n }\n\n try {\n await handler(envelope, message);\n await this.delete(message);\n } catch (error) {\n // Leave the message undeleted — SQS redelivers after the visibility timeout.\n this.options.onError?.(error, envelope, message);\n }\n }\n\n private async delete(message: SqsMessage): Promise<void> {\n if (!message.ReceiptHandle) return;\n await this.client.deleteMessage({\n QueueUrl: this.queueUrl,\n ReceiptHandle: message.ReceiptHandle,\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Amazon SQS adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Amazon SQS,\n * so an SQS-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the PHP/Laravel, Python, Go, Java and .NET SDKs.\n *\n * import { SQS } from \"@aws-sdk/client-sqs\";\n * import { SqsPublisher, SqsConsumer } from \"@babelqueue/sqs\";\n *\n * const sqs = new SQS({ region: \"eu-central-1\" });\n * const url = \"https://sqs.eu-central-1.amazonaws.com/123456789012/orders\";\n *\n * await new SqsPublisher(sqs, url).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new SqsConsumer(sqs, url, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §3 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native SQS `MessageAttributes`. The envelope is\n * unchanged (`schema_version` stays 1); SQS is purely additive. Retry is SQS-native\n * (a failed handler's message is released via `ChangeMessageVisibility` after\n * `releaseDelay`, default 0 s — pair the queue with a `RedrivePolicy`); the\n * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as\n * `attempts = count − 1`.\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional String `MessageAttributes` **beside** the contract `bq-*`\n * attributes where `bq-trace-id` already lives — the contract attributes win a key collision, and\n * the merged set is bounded by SQS's **10-attribute limit** (contract attributes are seeded first,\n * so a rider only lands while headroom remains). On consume the inbound `MessageAttributes` are\n * surfaced as a `Record<string,string>` so the core's `otel` extract sees the `traceparent` and\n * links the consumer span as a child. A header-less publish is byte-identical. This mirrors the Go\n * `/sqs` and PHP `SqsTransport` wiring. GR-1: the wire envelope body is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy, annotate } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n/** SQS allows at most 10 user message attributes per message. */\nconst MAX_ATTRIBUTES = 10;\n\n/** The SQS ceiling for a `VisibilityTimeout` (12 hours, in seconds). */\nexport const MAX_VISIBILITY_TIMEOUT = 43_200;\n\n// --- Minimal SQS shapes (a structural subset of @aws-sdk/client-sqs) -----------\n\nexport interface SqsMessageAttributeValue {\n DataType: string;\n StringValue?: string;\n}\n\nexport interface SqsMessage {\n Body?: string;\n ReceiptHandle?: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n Attributes?: Record<string, string>;\n}\n\nexport interface SendMessageInput {\n QueueUrl: string;\n MessageBody: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n MessageGroupId?: string;\n MessageDeduplicationId?: string;\n}\n\nexport interface ReceiveMessageInput {\n QueueUrl: string;\n MaxNumberOfMessages?: number;\n WaitTimeSeconds?: number;\n VisibilityTimeout?: number;\n MessageAttributeNames?: string[];\n AttributeNames?: string[];\n}\n\n/**\n * The subset of the AWS SQS client this adapter calls. The aggregated `SQS` class\n * from `@aws-sdk/client-sqs` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface SqsApi {\n sendMessage(input: SendMessageInput): Promise<{ MessageId?: string } | unknown>;\n receiveMessage(input: ReceiveMessageInput): Promise<{ Messages?: SqsMessage[] }>;\n deleteMessage(input: { QueueUrl: string; ReceiptHandle: string }): Promise<unknown>;\n /**\n * The §3.5 release primitive. Optional so existing structural clients keep compiling; it is\n * required (checked at construction) when a consumer sets `releaseDelay` or the `release`\n * unknown-URN strategy explicitly. Without it the default handler-failure release degrades to\n * leaving the message for visibility-timeout redelivery. The aggregated `SQS` class from\n * `@aws-sdk/client-sqs` provides it.\n */\n changeMessageVisibility?(input: ChangeMessageVisibilityInput): Promise<unknown>;\n}\n\nexport interface ChangeMessageVisibilityInput {\n QueueUrl: string;\n ReceiptHandle: string;\n VisibilityTimeout: number;\n}\n\n// --- Attribute projection (contract §3.2) --------------------------------------\n\nconst str = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"String\",\n StringValue: String(value),\n});\nconst num = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"Number\",\n StringValue: String(value),\n});\n\n/**\n * Project the envelope's contract fields onto native SQS `MessageAttributes` — a\n * redundant, routable view of the body (the body stays authoritative).\n */\nexport function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue> {\n const attrs: Record<string, SqsMessageAttributeValue> = {};\n if (envelope.job) attrs[\"bq-job\"] = str(envelope.job);\n if (envelope.trace_id) attrs[\"bq-trace-id\"] = str(envelope.trace_id);\n if (envelope.meta.id) attrs[\"bq-message-id\"] = str(envelope.meta.id);\n if (envelope.meta.schema_version != null) {\n attrs[\"bq-schema-version\"] = num(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) attrs[\"bq-source-lang\"] = str(envelope.meta.lang);\n if (envelope.meta.created_at != null) {\n attrs[\"bq-created-at\"] = num(envelope.meta.created_at);\n }\n return attrs;\n}\n\n/**\n * Overlay the out-of-band `headers` onto the contract attribute projection as String\n * `MessageAttributes`, without overwriting an existing `bq-*` attribute (the contract wins a key\n * collision) and skipping blanks. Keys are merged in sorted order and the merge stops at the\n * 10-attribute SQS ceiling, so unbounded riders can never push the message past the limit (SQS\n * rejects the whole send otherwise) — the contract attributes are always preserved first. Mirrors\n * Go's `mergeAttributes` / PHP's `SqsTransport::attributes`.\n */\nexport function mergeAttributes(\n base: Record<string, SqsMessageAttributeValue>,\n headers: HeaderCarrier | null | undefined,\n): Record<string, SqsMessageAttributeValue> {\n const clean = sanitizeHeaders(headers);\n for (const key of Object.keys(clean).sort()) {\n if (key in base) continue; // never clobber a contract bq-* attribute\n if (Object.keys(base).length >= MAX_ATTRIBUTES) break; // respect the SQS 10-attribute cap\n base[key] = str(clean[key]);\n }\n return base;\n}\n\n/**\n * Map inbound SQS `MessageAttributes` onto a flat {@link HeaderCarrier} (the consume-side\n * counterpart of {@link mergeAttributes}), reading each attribute's `StringValue`. Returns an empty\n * object when there are none. Both the contract `bq-*` attributes and any out-of-band rider (e.g.\n * `traceparent`) surface — the core's `otel` extract reads only the keys it knows.\n */\nexport function headersOf(message: SqsMessage): HeaderCarrier {\n const attrs = message.MessageAttributes;\n const out: HeaderCarrier = {};\n if (!attrs) return out;\n for (const key of Object.keys(attrs)) {\n const value = attrs[key]?.StringValue;\n if (value != null && value !== \"\") out[key] = value;\n }\n return out;\n}\n\nfunction queueNameFromUrl(queueUrl: string): string {\n const segments = queueUrl.split(\"/\").filter(Boolean);\n return segments[segments.length - 1] ?? \"default\";\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link SqsPublisher}. */\nexport interface SqsPublisherOptions {\n /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */\n fifo?: boolean;\n /** FIFO ordering group (default: the queue name from the URL). */\n messageGroupId?: string;\n /** Use the queue's content-based dedup instead of `meta.id` as the dedup id. */\n contentDedup?: boolean;\n}\n\n/** Options for {@link SqsPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /**\n * Out-of-band transport headers carried as String `MessageAttributes` beside the contract `bq-*`\n * attributes (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract attributes win a key collision and the merged set is capped at SQS's 10-attribute\n * limit; an empty/omitted carrier leaves the publish unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */\nexport class SqsPublisher {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly options: SqsPublisherOptions = {},\n ) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it as the message body with\n * the projected `MessageAttributes`, and return the message id (`meta.id`).\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queueNameFromUrl(this.queueUrl),\n traceId: options.traceId,\n });\n const input: SendMessageInput = {\n QueueUrl: this.queueUrl,\n MessageBody: EnvelopeCodec.encode(envelope),\n MessageAttributes: mergeAttributes(toMessageAttributes(envelope), options.headers),\n };\n if (this.options.fifo) {\n input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);\n if (!this.options.contentDedup) {\n input.MessageDeduplicationId = envelope.meta.id;\n }\n }\n await this.client.sendMessage(input);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw SQS message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `MessageAttributes` (empty when there are none).\n * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of\n * the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: SqsMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link SqsConsumer}. */\nexport interface SqsConsumerOptions {\n /**\n * Called when a message's URN has no handler. Without `unknownUrn` the message is then deleted\n * (the original behaviour); with `unknownUrn` set it is a notification hook and the strategy\n * decides the message's fate.\n */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: SqsMessage) => unknown | Promise<unknown>;\n /**\n * Unknown-URN strategy per contract §3.5 — `fail` | `delete` | `release` | `dead_letter`\n * ({@link UnknownUrnStrategy}). Omitted keeps the original behaviour (`onUnknownUrn` → delete,\n * otherwise report and leave for visibility-timeout redelivery, i.e. `fail`). `release` uses\n * `ChangeMessageVisibility` with {@link unknownUrnReleaseDelay}; `dead_letter` sends the annotated\n * envelope to {@link deadLetterQueueUrl} then deletes, degrading to `delete` when no DLQ is set.\n */\n unknownUrn?: string;\n /** Backoff (seconds) for the `release` unknown-URN strategy (default 0 = redeliver now). */\n unknownUrnReleaseDelay?: number;\n /**\n * Backoff before a failed message is redelivered. A failed handler always releases the message\n * via `ChangeMessageVisibility(ReceiptHandle, VisibilityTimeout)` (contract §3.5); this sets the\n * `VisibilityTimeout` — a number of seconds, or a function of the (reconciled) `attempts`\n * returning seconds (e.g. an exponential backoff). Clamped to `0…43200`. Default `0`: the message\n * is visible again immediately. A permanently failing handler therefore retries without pause\n * until the queue's `RedrivePolicy` (`maxReceiveCount`) moves it to the DLQ — configure one.\n */\n releaseDelay?: number | ((attempts: number) => number);\n /**\n * The cross-language `<queue>.dlq` URL for the `dead_letter` strategy (opt-in; default none). A\n * `.fifo` DLQ is sent with `MessageGroupId` (the source queue name) and `MessageDeduplicationId`\n * (`meta.id`).\n */\n deadLetterQueueUrl?: string | null;\n /**\n * Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), a throwing handler,\n * a throwing `onUnknownUrn` under an `unknownUrn` strategy, a failed release / dead-letter send, or\n * a failed delete after successful processing (an {@link SqsDeleteError} wrapping the broker\n * error; never released). The message is then left for visibility-timeout redelivery. The loop\n * never stops on these.\n */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: SqsMessage) => void;\n /** Long-poll wait seconds (default 20). */\n waitTimeSeconds?: number;\n /** Reservation window applied on receive (seconds). */\n visibilityTimeout?: number;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n}\n\n/**\n * Reported via `onError` when `DeleteMessage` fails for a message whose processing already succeeded\n * (a handler returned normally, or the unknown-URN strategy chose `delete` / `dead_letter`). The\n * broker error is `cause`. Distinct from a handler failure: the message is NOT released, so it is\n * redelivered only after its visibility timeout expires (at-least-once; dedupe on `meta.id` with the\n * idempotency helper if the side effect must not repeat).\n */\nexport class SqsDeleteError extends BabelQueueError {\n constructor(cause: unknown) {\n super(\"Failed to delete a processed SQS message; it will be redelivered after its visibility timeout.\");\n this.name = \"SqsDeleteError\";\n this.cause = cause;\n }\n}\n\n/**\n * Polls an SQS queue, decodes + validates each message, routes it to the handler\n * registered for its URN, and deletes it on success. A throwing handler's message is released via\n * `ChangeMessageVisibility` after `releaseDelay` (default 0 s, contract §3.5); if the release itself\n * fails the message is left for visibility-timeout redelivery (at-least-once);\n * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler. An unmapped URN\n * follows the `unknownUrn` strategy (`fail` | `delete` | `release` | `dead_letter`).\n */\nexport class SqsConsumer {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly handlers: BabelHandlers,\n private readonly options: SqsConsumerOptions = {},\n ) {\n const strategy = options.unknownUrn;\n if (strategy !== undefined && !(Object.values(UnknownUrnStrategy) as string[]).includes(strategy)) {\n throw new BabelQueueError(`Unknown unknownUrn strategy \"${strategy}\".`);\n }\n const needsRelease = options.releaseDelay != null || strategy === UnknownUrnStrategy.RELEASE;\n if (needsRelease && typeof client.changeMessageVisibility !== \"function\") {\n throw new BabelQueueError(\n \"SqsConsumer release requires a client with changeMessageVisibility (contract §3.5).\",\n );\n }\n }\n\n /** Receive one batch, route each message, delete the ones handled. Returns the batch size. */\n async poll(): Promise<number> {\n const input: ReceiveMessageInput = {\n QueueUrl: this.queueUrl,\n MaxNumberOfMessages: this.options.maxMessages ?? 10,\n WaitTimeSeconds: this.options.waitTimeSeconds ?? 20,\n MessageAttributeNames: [\"All\"],\n AttributeNames: [\"ApproximateReceiveCount\"],\n };\n if (this.options.visibilityTimeout != null) {\n input.VisibilityTimeout = this.options.visibilityTimeout;\n }\n const result = await this.client.receiveMessage(input);\n const messages = result.Messages ?? [];\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\n }\n\n /** Poll until `signal` aborts (each poll long-polls, so this does not busy-loop). */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: SqsMessage): Promise<void> {\n const envelope = EnvelopeCodec.decode(message.Body ?? \"\");\n\n const receiveCount = message.Attributes?.[\"ApproximateReceiveCount\"];\n if (receiveCount !== undefined) {\n const native = Number.parseInt(receiveCount, 10) - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (Number.isFinite(native) && native > current) {\n envelope.attempts = native;\n }\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from SQS.\"),\n envelope,\n message,\n );\n return;\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.unknown(urn, envelope as Envelope, message);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n } catch (error) {\n this.options.onError?.(error, envelope, message);\n await this.guard(envelope, message, async () => {\n const delay = this.options.releaseDelay ?? 0;\n const attempts = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n await this.release(message, typeof delay === \"function\" ? delay(attempts) : delay);\n });\n return;\n }\n // Outside the handler's try: a failed delete is not a handler failure. It is reported as an\n // SqsDeleteError and the message is NOT released — releasing at the (default 0 s) backoff would\n // redeliver an already-processed message immediately; it returns on visibility expiry instead.\n await this.guard(envelope, message, () => this.deleteHandled(message));\n }\n\n /**\n * Run a release / dead-letter step; a failure (e.g. `MessageNotInflight`, a missing IAM grant, a\n * throwing `releaseDelay`) is reported to `onError` and the message is left undeleted, so\n * visibility expiry still redelivers it. Never lets the error stop the consume loop.\n */\n private async guard(envelope: IncomingEnvelope, message: SqsMessage, step: () => Promise<void>): Promise<void> {\n try {\n await step();\n } catch (error) {\n this.options.onError?.(error, envelope, message);\n }\n }\n\n /** Apply the unknown-URN strategy (contract §3.5). */\n private async unknown(urn: string, envelope: Envelope, message: SqsMessage): Promise<void> {\n const strategy = this.options.unknownUrn;\n if (strategy === undefined) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.delete(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n }\n return;\n }\n\n await this.guard(envelope, message, async () => {\n await this.options.onUnknownUrn?.(envelope, message);\n });\n switch (strategy) {\n case UnknownUrnStrategy.DELETE:\n await this.guard(envelope, message, () => this.deleteHandled(message));\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.guard(envelope, message, () => this.release(message, this.options.unknownUrnReleaseDelay ?? 0));\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.guard(envelope, message, async () => {\n await this.deadLetter(envelope, \"unknown_urn\");\n await this.deleteHandled(message);\n });\n return;\n default:\n // FAIL: surface and do NOT delete — visibility expiry redelivers, native redrive quarantines.\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n }\n }\n\n /** Make the message visible again after `seconds` via `ChangeMessageVisibility`; never deletes. */\n private async release(message: SqsMessage, seconds: number): Promise<void> {\n if (!message.ReceiptHandle || !this.client.changeMessageVisibility) return;\n const timeout = Number.isFinite(seconds)\n ? Math.min(MAX_VISIBILITY_TIMEOUT, Math.max(0, Math.floor(seconds)))\n : 0;\n await this.client.changeMessageVisibility({\n QueueUrl: this.queueUrl,\n ReceiptHandle: message.ReceiptHandle,\n VisibilityTimeout: timeout,\n });\n }\n\n /** Send the annotated envelope to the opt-in DLQ; a no-op (→ plain delete) when none is set. */\n private async deadLetter(envelope: Envelope, reason: string): Promise<void> {\n const dlq = this.options.deadLetterQueueUrl;\n if (!dlq) return;\n const source = envelope.meta?.queue ?? queueNameFromUrl(this.queueUrl);\n const annotated = annotate(envelope, reason, source, {\n attempts: envelope.attempts ?? 0,\n });\n const input: SendMessageInput = {\n QueueUrl: dlq,\n MessageBody: EnvelopeCodec.encode(annotated),\n MessageAttributes: toMessageAttributes(annotated),\n };\n if (dlq.endsWith(\".fifo\")) {\n // A FIFO DLQ (`<queue>.dlq.fifo`) rejects a send without a group; dedup on meta.id (§3.2).\n input.MessageGroupId = source;\n if (annotated.meta?.id) input.MessageDeduplicationId = annotated.meta.id;\n }\n await this.client.sendMessage(input);\n }\n\n /** Delete a message whose processing already succeeded; a broker failure becomes an SqsDeleteError. */\n private async deleteHandled(message: SqsMessage): Promise<void> {\n try {\n await this.delete(message);\n } catch (error) {\n throw new SqsDeleteError(error);\n }\n }\n\n private async delete(message: SqsMessage): Promise<void> {\n if (!message.ReceiptHandle) return;\n await this.client.deleteMessage({\n QueueUrl: this.queueUrl,\n ReceiptHandle: message.ReceiptHandle,\n });\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives, so a caller can treat the result as \"no headers\". A nullish input yields an empty\n * object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsCA,kBAA8F;;;ACtBvF,SAAS,gBAAgB,SAA0D;AACxF,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,QAAQ,MAAM,SAAS,QAAQ,UAAU,GAAI;AACjD,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;;;ADmBA,IAAM,iBAAiB;AAGhB,IAAM,yBAAyB;AA2DtC,IAAM,MAAM,CAAC,WAA8C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa,OAAO,KAAK;AAC3B;AACA,IAAM,MAAM,CAAC,WAA8C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa,OAAO,KAAK;AAC3B;AAMO,SAAS,oBAAoB,UAA8D;AAChG,QAAM,QAAkD,CAAC;AACzD,MAAI,SAAS,IAAK,OAAM,QAAQ,IAAI,IAAI,SAAS,GAAG;AACpD,MAAI,SAAS,SAAU,OAAM,aAAa,IAAI,IAAI,SAAS,QAAQ;AACnE,MAAI,SAAS,KAAK,GAAI,OAAM,eAAe,IAAI,IAAI,SAAS,KAAK,EAAE;AACnE,MAAI,SAAS,KAAK,kBAAkB,MAAM;AACxC,UAAM,mBAAmB,IAAI,IAAI,SAAS,KAAK,cAAc;AAAA,EAC/D;AACA,MAAI,SAAS,KAAK,KAAM,OAAM,gBAAgB,IAAI,IAAI,SAAS,KAAK,IAAI;AACxE,MAAI,SAAS,KAAK,cAAc,MAAM;AACpC,UAAM,eAAe,IAAI,IAAI,SAAS,KAAK,UAAU;AAAA,EACvD;AACA,SAAO;AACT;AAUO,SAAS,gBACd,MACA,SAC0C;AAC1C,QAAM,QAAQ,gBAAgB,OAAO;AACrC,aAAW,OAAO,OAAO,KAAK,KAAK,EAAE,KAAK,GAAG;AAC3C,QAAI,OAAO,KAAM;AACjB,QAAI,OAAO,KAAK,IAAI,EAAE,UAAU,eAAgB;AAChD,SAAK,GAAG,IAAI,IAAI,MAAM,GAAG,CAAC;AAAA,EAC5B;AACA,SAAO;AACT;AAQO,SAAS,UAAU,SAAoC;AAC5D,QAAM,QAAQ,QAAQ;AACtB,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAM,QAAQ,MAAM,GAAG,GAAG;AAC1B,QAAI,SAAS,QAAQ,UAAU,GAAI,KAAI,GAAG,IAAI;AAAA,EAChD;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,UAA0B;AAClD,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,SAAO,SAAS,SAAS,SAAS,CAAC,KAAK;AAC1C;AA4BO,IAAM,eAAN,MAAmB;AAAA,EACxB,YACmB,QACA,UACA,UAA+B,CAAC,GACjD;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnB,MAAM,QACJ,KACA,MACA,UAA0B,CAAC,GACV;AACjB,UAAM,WAAW,0BAAc,KAAK,KAAK,MAAM;AAAA,MAC7C,OAAO,iBAAiB,KAAK,QAAQ;AAAA,MACrC,SAAS,QAAQ;AAAA,IACnB,CAAC;AACD,UAAM,QAA0B;AAAA,MAC9B,UAAU,KAAK;AAAA,MACf,aAAa,0BAAc,OAAO,QAAQ;AAAA,MAC1C,mBAAmB,gBAAgB,oBAAoB,QAAQ,GAAG,QAAQ,OAAO;AAAA,IACnF;AACA,QAAI,KAAK,QAAQ,MAAM;AACrB,YAAM,iBAAiB,KAAK,QAAQ,kBAAkB,iBAAiB,KAAK,QAAQ;AACpF,UAAI,CAAC,KAAK,QAAQ,cAAc;AAC9B,cAAM,yBAAyB,SAAS,KAAK;AAAA,MAC/C;AAAA,IACF;AACA,UAAM,KAAK,OAAO,YAAY,KAAK;AACnC,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;AAuEO,IAAM,iBAAN,cAA6B,4BAAgB;AAAA,EAClD,YAAY,OAAgB;AAC1B,UAAM,gGAAgG;AACtG,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;AAUO,IAAM,cAAN,MAAkB;AAAA,EACvB,YACmB,QACA,UACA,UACA,UAA8B,CAAC,GAChD;AAJiB;AACA;AACA;AACA;AAEjB,UAAM,WAAW,QAAQ;AACzB,QAAI,aAAa,UAAa,CAAE,OAAO,OAAO,8BAAkB,EAAe,SAAS,QAAQ,GAAG;AACjG,YAAM,IAAI,4BAAgB,gCAAgC,QAAQ,IAAI;AAAA,IACxE;AACA,UAAM,eAAe,QAAQ,gBAAgB,QAAQ,aAAa,+BAAmB;AACrF,QAAI,gBAAgB,OAAO,OAAO,4BAA4B,YAAY;AACxE,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAfmB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAenB,MAAM,OAAwB;AAC5B,UAAM,QAA6B;AAAA,MACjC,UAAU,KAAK;AAAA,MACf,qBAAqB,KAAK,QAAQ,eAAe;AAAA,MACjD,iBAAiB,KAAK,QAAQ,mBAAmB;AAAA,MACjD,uBAAuB,CAAC,KAAK;AAAA,MAC7B,gBAAgB,CAAC,yBAAyB;AAAA,IAC5C;AACA,QAAI,KAAK,QAAQ,qBAAqB,MAAM;AAC1C,YAAM,oBAAoB,KAAK,QAAQ;AAAA,IACzC;AACA,UAAM,SAAS,MAAM,KAAK,OAAO,eAAe,KAAK;AACrD,UAAM,WAAW,OAAO,YAAY,CAAC;AACrC,eAAW,WAAW,UAAU;AAC9B,YAAM,KAAK,OAAO,OAAO;AAAA,IAC3B;AACA,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,MAAM,IAAI,QAAqC;AAC7C,WAAO,QAAQ,YAAY,MAAM;AAC/B,YAAM,KAAK,KAAK;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,MAAc,OAAO,SAAoC;AACvD,UAAM,WAAW,0BAAc,OAAO,QAAQ,QAAQ,EAAE;AAExD,UAAM,eAAe,QAAQ,aAAa,yBAAyB;AACnE,QAAI,iBAAiB,QAAW;AAC9B,YAAM,SAAS,OAAO,SAAS,cAAc,EAAE,IAAI;AACnD,YAAM,UAAU,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AAC5E,UAAI,OAAO,SAAS,MAAM,KAAK,SAAS,SAAS;AAC/C,iBAAS,WAAW;AAAA,MACtB;AAAA,IACF;AAEA,QAAI,CAAC,0BAAc,QAAQ,QAAQ,GAAG;AACpC,WAAK,QAAQ;AAAA,QACX,IAAI,4BAAgB,yDAAyD;AAAA,QAC7E;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,MAAM,0BAAc,IAAI,QAAQ;AACtC,UAAM,UAAU,KAAK,SAAS,GAAG;AACjC,QAAI,CAAC,SAAS;AACZ,YAAM,KAAK,QAAQ,KAAK,UAAsB,OAAO;AACrD;AAAA,IACF;AAEA,QAAI;AACF,YAAM,QAAQ,UAAU,SAAS,UAAU,OAAO,CAAC;AAAA,IACrD,SAAS,OAAO;AACd,WAAK,QAAQ,UAAU,OAAO,UAAU,OAAO;AAC/C,YAAM,KAAK,MAAM,UAAU,SAAS,YAAY;AAC9C,cAAM,QAAQ,KAAK,QAAQ,gBAAgB;AAC3C,cAAM,WAAW,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AAC7E,cAAM,KAAK,QAAQ,SAAS,OAAO,UAAU,aAAa,MAAM,QAAQ,IAAI,KAAK;AAAA,MACnF,CAAC;AACD;AAAA,IACF;AAIA,UAAM,KAAK,MAAM,UAAU,SAAS,MAAM,KAAK,cAAc,OAAO,CAAC;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,MAAM,UAA4B,SAAqB,MAA0C;AAC7G,QAAI;AACF,YAAM,KAAK;AAAA,IACb,SAAS,OAAO;AACd,WAAK,QAAQ,UAAU,OAAO,UAAU,OAAO;AAAA,IACjD;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,QAAQ,KAAa,UAAoB,SAAoC;AACzF,UAAM,WAAW,KAAK,QAAQ;AAC9B,QAAI,aAAa,QAAW;AAC1B,UAAI,KAAK,QAAQ,cAAc;AAC7B,cAAM,KAAK,QAAQ,aAAa,UAAU,OAAO;AACjD,cAAM,KAAK,OAAO,OAAO;AAAA,MAC3B,OAAO;AACL,aAAK,QAAQ,UAAU,IAAI,4BAAgB,GAAG,GAAG,UAAU,OAAO;AAAA,MACpE;AACA;AAAA,IACF;AAEA,UAAM,KAAK,MAAM,UAAU,SAAS,YAAY;AAC9C,YAAM,KAAK,QAAQ,eAAe,UAAU,OAAO;AAAA,IACrD,CAAC;AACD,YAAQ,UAAU;AAAA,MAChB,KAAK,+BAAmB;AACtB,cAAM,KAAK,MAAM,UAAU,SAAS,MAAM,KAAK,cAAc,OAAO,CAAC;AACrE;AAAA,MACF,KAAK,+BAAmB;AACtB,cAAM,KAAK,MAAM,UAAU,SAAS,MAAM,KAAK,QAAQ,SAAS,KAAK,QAAQ,0BAA0B,CAAC,CAAC;AACzG;AAAA,MACF,KAAK,+BAAmB;AACtB,cAAM,KAAK,MAAM,UAAU,SAAS,YAAY;AAC9C,gBAAM,KAAK,WAAW,UAAU,aAAa;AAC7C,gBAAM,KAAK,cAAc,OAAO;AAAA,QAClC,CAAC;AACD;AAAA,MACF;AAEE,aAAK,QAAQ,UAAU,IAAI,4BAAgB,GAAG,GAAG,UAAU,OAAO;AAAA,IACtE;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,QAAQ,SAAqB,SAAgC;AACzE,QAAI,CAAC,QAAQ,iBAAiB,CAAC,KAAK,OAAO,wBAAyB;AACpE,UAAM,UAAU,OAAO,SAAS,OAAO,IACnC,KAAK,IAAI,wBAAwB,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,CAAC,IACjE;AACJ,UAAM,KAAK,OAAO,wBAAwB;AAAA,MACxC,UAAU,KAAK;AAAA,MACf,eAAe,QAAQ;AAAA,MACvB,mBAAmB;AAAA,IACrB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAc,WAAW,UAAoB,QAA+B;AAC1E,UAAM,MAAM,KAAK,QAAQ;AACzB,QAAI,CAAC,IAAK;AACV,UAAM,SAAS,SAAS,MAAM,SAAS,iBAAiB,KAAK,QAAQ;AACrE,UAAM,gBAAY,sBAAS,UAAU,QAAQ,QAAQ;AAAA,MACnD,UAAU,SAAS,YAAY;AAAA,IACjC,CAAC;AACD,UAAM,QAA0B;AAAA,MAC9B,UAAU;AAAA,MACV,aAAa,0BAAc,OAAO,SAAS;AAAA,MAC3C,mBAAmB,oBAAoB,SAAS;AAAA,IAClD;AACA,QAAI,IAAI,SAAS,OAAO,GAAG;AAEzB,YAAM,iBAAiB;AACvB,UAAI,UAAU,MAAM,GAAI,OAAM,yBAAyB,UAAU,KAAK;AAAA,IACxE;AACA,UAAM,KAAK,OAAO,YAAY,KAAK;AAAA,EACrC;AAAA;AAAA,EAGA,MAAc,cAAc,SAAoC;AAC9D,QAAI;AACF,YAAM,KAAK,OAAO,OAAO;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,IAAI,eAAe,KAAK;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,MAAc,OAAO,SAAoC;AACvD,QAAI,CAAC,QAAQ,cAAe;AAC5B,UAAM,KAAK,OAAO,cAAc;AAAA,MAC9B,UAAU,KAAK;AAAA,MACf,eAAe,QAAQ;AAAA,IACzB,CAAC;AAAA,EACH;AACF;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope, BabelQueueError } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Amazon SQS adapter for BabelQueue.
@@ -23,11 +23,23 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
23
23
  * This implements §3 of the broker-bindings contract: the canonical envelope is the
24
24
  * message body, projected onto native SQS `MessageAttributes`. The envelope is
25
25
  * unchanged (`schema_version` stays 1); SQS is purely additive. Retry is SQS-native
26
- * (a failed handler leaves the message for visibility-timeout redelivery); the
26
+ * (a failed handler's message is released via `ChangeMessageVisibility` after
27
+ * `releaseDelay`, default 0 s — pair the queue with a `RedrivePolicy`); the
27
28
  * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as
28
29
  * `attempts = count − 1`.
30
+ *
31
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
32
+ * span linkage) rides as additional String `MessageAttributes` **beside** the contract `bq-*`
33
+ * attributes where `bq-trace-id` already lives — the contract attributes win a key collision, and
34
+ * the merged set is bounded by SQS's **10-attribute limit** (contract attributes are seeded first,
35
+ * so a rider only lands while headroom remains). On consume the inbound `MessageAttributes` are
36
+ * surfaced as a `Record<string,string>` so the core's `otel` extract sees the `traceparent` and
37
+ * links the consumer span as a child. A header-less publish is byte-identical. This mirrors the Go
38
+ * `/sqs` and PHP `SqsTransport` wiring. GR-1: the wire envelope body is never touched.
29
39
  */
30
40
 
41
+ /** The SQS ceiling for a `VisibilityTimeout` (12 hours, in seconds). */
42
+ declare const MAX_VISIBILITY_TIMEOUT = 43200;
31
43
  interface SqsMessageAttributeValue {
32
44
  DataType: string;
33
45
  StringValue?: string;
@@ -68,12 +80,41 @@ interface SqsApi {
68
80
  QueueUrl: string;
69
81
  ReceiptHandle: string;
70
82
  }): Promise<unknown>;
83
+ /**
84
+ * The §3.5 release primitive. Optional so existing structural clients keep compiling; it is
85
+ * required (checked at construction) when a consumer sets `releaseDelay` or the `release`
86
+ * unknown-URN strategy explicitly. Without it the default handler-failure release degrades to
87
+ * leaving the message for visibility-timeout redelivery. The aggregated `SQS` class from
88
+ * `@aws-sdk/client-sqs` provides it.
89
+ */
90
+ changeMessageVisibility?(input: ChangeMessageVisibilityInput): Promise<unknown>;
91
+ }
92
+ interface ChangeMessageVisibilityInput {
93
+ QueueUrl: string;
94
+ ReceiptHandle: string;
95
+ VisibilityTimeout: number;
71
96
  }
72
97
  /**
73
98
  * Project the envelope's contract fields onto native SQS `MessageAttributes` — a
74
99
  * redundant, routable view of the body (the body stays authoritative).
75
100
  */
76
101
  declare function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue>;
102
+ /**
103
+ * Overlay the out-of-band `headers` onto the contract attribute projection as String
104
+ * `MessageAttributes`, without overwriting an existing `bq-*` attribute (the contract wins a key
105
+ * collision) and skipping blanks. Keys are merged in sorted order and the merge stops at the
106
+ * 10-attribute SQS ceiling, so unbounded riders can never push the message past the limit (SQS
107
+ * rejects the whole send otherwise) — the contract attributes are always preserved first. Mirrors
108
+ * Go's `mergeAttributes` / PHP's `SqsTransport::attributes`.
109
+ */
110
+ declare function mergeAttributes(base: Record<string, SqsMessageAttributeValue>, headers: HeaderCarrier | null | undefined): Record<string, SqsMessageAttributeValue>;
111
+ /**
112
+ * Map inbound SQS `MessageAttributes` onto a flat {@link HeaderCarrier} (the consume-side
113
+ * counterpart of {@link mergeAttributes}), reading each attribute's `StringValue`. Returns an empty
114
+ * object when there are none. Both the contract `bq-*` attributes and any out-of-band rider (e.g.
115
+ * `traceparent`) surface — the core's `otel` extract reads only the keys it knows.
116
+ */
117
+ declare function headersOf(message: SqsMessage): HeaderCarrier;
77
118
  /** Options for {@link SqsPublisher}. */
78
119
  interface SqsPublisherOptions {
79
120
  /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */
@@ -87,6 +128,13 @@ interface SqsPublisherOptions {
87
128
  interface PublishOptions {
88
129
  /** Reuse an existing trace id (trace continuation). */
89
130
  traceId?: string;
131
+ /**
132
+ * Out-of-band transport headers carried as String `MessageAttributes` beside the contract `bq-*`
133
+ * attributes (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
134
+ * The contract attributes win a key collision and the merged set is capped at SQS's 10-attribute
135
+ * limit; an empty/omitted carrier leaves the publish unchanged.
136
+ */
137
+ headers?: HeaderCarrier;
90
138
  }
91
139
  /** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */
92
140
  declare class SqsPublisher {
@@ -100,15 +148,55 @@ declare class SqsPublisher {
100
148
  */
101
149
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
102
150
  }
103
- /** A URN handler. Receives the validated envelope and the raw SQS message. */
104
- type BabelHandler = (envelope: Envelope, message: SqsMessage) => unknown | Promise<unknown>;
151
+ /**
152
+ * A URN handler. Receives the validated envelope, the raw SQS message, and the out-of-band
153
+ * {@link HeaderCarrier} read from the message's `MessageAttributes` (empty when there are none).
154
+ * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of
155
+ * the producer span (ADR-0028).
156
+ */
157
+ type BabelHandler = (envelope: Envelope, message: SqsMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
105
158
  /** A map of URN → handler. */
106
159
  type BabelHandlers = Record<string, BabelHandler>;
107
160
  /** Options for {@link SqsConsumer}. */
108
161
  interface SqsConsumerOptions {
109
- /** Called instead of erroring when a message's URN has no handler (then the message is deleted). */
162
+ /**
163
+ * Called when a message's URN has no handler. Without `unknownUrn` the message is then deleted
164
+ * (the original behaviour); with `unknownUrn` set it is a notification hook and the strategy
165
+ * decides the message's fate.
166
+ */
110
167
  onUnknownUrn?: (envelope: IncomingEnvelope, message: SqsMessage) => unknown | Promise<unknown>;
111
- /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */
168
+ /**
169
+ * Unknown-URN strategy per contract §3.5 — `fail` | `delete` | `release` | `dead_letter`
170
+ * ({@link UnknownUrnStrategy}). Omitted keeps the original behaviour (`onUnknownUrn` → delete,
171
+ * otherwise report and leave for visibility-timeout redelivery, i.e. `fail`). `release` uses
172
+ * `ChangeMessageVisibility` with {@link unknownUrnReleaseDelay}; `dead_letter` sends the annotated
173
+ * envelope to {@link deadLetterQueueUrl} then deletes, degrading to `delete` when no DLQ is set.
174
+ */
175
+ unknownUrn?: string;
176
+ /** Backoff (seconds) for the `release` unknown-URN strategy (default 0 = redeliver now). */
177
+ unknownUrnReleaseDelay?: number;
178
+ /**
179
+ * Backoff before a failed message is redelivered. A failed handler always releases the message
180
+ * via `ChangeMessageVisibility(ReceiptHandle, VisibilityTimeout)` (contract §3.5); this sets the
181
+ * `VisibilityTimeout` — a number of seconds, or a function of the (reconciled) `attempts`
182
+ * returning seconds (e.g. an exponential backoff). Clamped to `0…43200`. Default `0`: the message
183
+ * is visible again immediately. A permanently failing handler therefore retries without pause
184
+ * until the queue's `RedrivePolicy` (`maxReceiveCount`) moves it to the DLQ — configure one.
185
+ */
186
+ releaseDelay?: number | ((attempts: number) => number);
187
+ /**
188
+ * The cross-language `<queue>.dlq` URL for the `dead_letter` strategy (opt-in; default none). A
189
+ * `.fifo` DLQ is sent with `MessageGroupId` (the source queue name) and `MessageDeduplicationId`
190
+ * (`meta.id`).
191
+ */
192
+ deadLetterQueueUrl?: string | null;
193
+ /**
194
+ * Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), a throwing handler,
195
+ * a throwing `onUnknownUrn` under an `unknownUrn` strategy, a failed release / dead-letter send, or
196
+ * a failed delete after successful processing (an {@link SqsDeleteError} wrapping the broker
197
+ * error; never released). The message is then left for visibility-timeout redelivery. The loop
198
+ * never stops on these.
199
+ */
112
200
  onError?: (error: unknown, envelope: IncomingEnvelope, message: SqsMessage) => void;
113
201
  /** Long-poll wait seconds (default 20). */
114
202
  waitTimeSeconds?: number;
@@ -117,11 +205,23 @@ interface SqsConsumerOptions {
117
205
  /** Max messages per receive (default 10). */
118
206
  maxMessages?: number;
119
207
  }
208
+ /**
209
+ * Reported via `onError` when `DeleteMessage` fails for a message whose processing already succeeded
210
+ * (a handler returned normally, or the unknown-URN strategy chose `delete` / `dead_letter`). The
211
+ * broker error is `cause`. Distinct from a handler failure: the message is NOT released, so it is
212
+ * redelivered only after its visibility timeout expires (at-least-once; dedupe on `meta.id` with the
213
+ * idempotency helper if the side effect must not repeat).
214
+ */
215
+ declare class SqsDeleteError extends BabelQueueError {
216
+ constructor(cause: unknown);
217
+ }
120
218
  /**
121
219
  * Polls an SQS queue, decodes + validates each message, routes it to the handler
122
- * registered for its URN, and deletes it on success. A throwing handler leaves the
123
- * message undeleted — SQS redelivers it after the visibility timeout (at-least-once);
124
- * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler.
220
+ * registered for its URN, and deletes it on success. A throwing handler's message is released via
221
+ * `ChangeMessageVisibility` after `releaseDelay` (default 0 s, contract §3.5); if the release itself
222
+ * fails the message is left for visibility-timeout redelivery (at-least-once);
223
+ * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler. An unmapped URN
224
+ * follows the `unknownUrn` strategy (`fail` | `delete` | `release` | `dead_letter`).
125
225
  */
126
226
  declare class SqsConsumer {
127
227
  private readonly client;
@@ -134,7 +234,21 @@ declare class SqsConsumer {
134
234
  /** Poll until `signal` aborts (each poll long-polls, so this does not busy-loop). */
135
235
  run(signal?: AbortSignal): Promise<void>;
136
236
  private handle;
237
+ /**
238
+ * Run a release / dead-letter step; a failure (e.g. `MessageNotInflight`, a missing IAM grant, a
239
+ * throwing `releaseDelay`) is reported to `onError` and the message is left undeleted, so
240
+ * visibility expiry still redelivers it. Never lets the error stop the consume loop.
241
+ */
242
+ private guard;
243
+ /** Apply the unknown-URN strategy (contract §3.5). */
244
+ private unknown;
245
+ /** Make the message visible again after `seconds` via `ChangeMessageVisibility`; never deletes. */
246
+ private release;
247
+ /** Send the annotated envelope to the opt-in DLQ; a no-op (→ plain delete) when none is set. */
248
+ private deadLetter;
249
+ /** Delete a message whose processing already succeeded; a broker failure becomes an SqsDeleteError. */
250
+ private deleteHandled;
137
251
  private delete;
138
252
  }
139
253
 
140
- export { type BabelHandler, type BabelHandlers, type PublishOptions, type ReceiveMessageInput, type SendMessageInput, type SqsApi, SqsConsumer, type SqsConsumerOptions, type SqsMessage, type SqsMessageAttributeValue, SqsPublisher, type SqsPublisherOptions, toMessageAttributes };
254
+ export { type BabelHandler, type BabelHandlers, type ChangeMessageVisibilityInput, MAX_VISIBILITY_TIMEOUT, type PublishOptions, type ReceiveMessageInput, type SendMessageInput, type SqsApi, SqsConsumer, type SqsConsumerOptions, SqsDeleteError, type SqsMessage, type SqsMessageAttributeValue, SqsPublisher, type SqsPublisherOptions, headersOf, mergeAttributes, toMessageAttributes };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope, BabelQueueError } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Amazon SQS adapter for BabelQueue.
@@ -23,11 +23,23 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
23
23
  * This implements §3 of the broker-bindings contract: the canonical envelope is the
24
24
  * message body, projected onto native SQS `MessageAttributes`. The envelope is
25
25
  * unchanged (`schema_version` stays 1); SQS is purely additive. Retry is SQS-native
26
- * (a failed handler leaves the message for visibility-timeout redelivery); the
26
+ * (a failed handler's message is released via `ChangeMessageVisibility` after
27
+ * `releaseDelay`, default 0 s — pair the queue with a `RedrivePolicy`); the
27
28
  * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as
28
29
  * `attempts = count − 1`.
30
+ *
31
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
32
+ * span linkage) rides as additional String `MessageAttributes` **beside** the contract `bq-*`
33
+ * attributes where `bq-trace-id` already lives — the contract attributes win a key collision, and
34
+ * the merged set is bounded by SQS's **10-attribute limit** (contract attributes are seeded first,
35
+ * so a rider only lands while headroom remains). On consume the inbound `MessageAttributes` are
36
+ * surfaced as a `Record<string,string>` so the core's `otel` extract sees the `traceparent` and
37
+ * links the consumer span as a child. A header-less publish is byte-identical. This mirrors the Go
38
+ * `/sqs` and PHP `SqsTransport` wiring. GR-1: the wire envelope body is never touched.
29
39
  */
30
40
 
41
+ /** The SQS ceiling for a `VisibilityTimeout` (12 hours, in seconds). */
42
+ declare const MAX_VISIBILITY_TIMEOUT = 43200;
31
43
  interface SqsMessageAttributeValue {
32
44
  DataType: string;
33
45
  StringValue?: string;
@@ -68,12 +80,41 @@ interface SqsApi {
68
80
  QueueUrl: string;
69
81
  ReceiptHandle: string;
70
82
  }): Promise<unknown>;
83
+ /**
84
+ * The §3.5 release primitive. Optional so existing structural clients keep compiling; it is
85
+ * required (checked at construction) when a consumer sets `releaseDelay` or the `release`
86
+ * unknown-URN strategy explicitly. Without it the default handler-failure release degrades to
87
+ * leaving the message for visibility-timeout redelivery. The aggregated `SQS` class from
88
+ * `@aws-sdk/client-sqs` provides it.
89
+ */
90
+ changeMessageVisibility?(input: ChangeMessageVisibilityInput): Promise<unknown>;
91
+ }
92
+ interface ChangeMessageVisibilityInput {
93
+ QueueUrl: string;
94
+ ReceiptHandle: string;
95
+ VisibilityTimeout: number;
71
96
  }
72
97
  /**
73
98
  * Project the envelope's contract fields onto native SQS `MessageAttributes` — a
74
99
  * redundant, routable view of the body (the body stays authoritative).
75
100
  */
76
101
  declare function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue>;
102
+ /**
103
+ * Overlay the out-of-band `headers` onto the contract attribute projection as String
104
+ * `MessageAttributes`, without overwriting an existing `bq-*` attribute (the contract wins a key
105
+ * collision) and skipping blanks. Keys are merged in sorted order and the merge stops at the
106
+ * 10-attribute SQS ceiling, so unbounded riders can never push the message past the limit (SQS
107
+ * rejects the whole send otherwise) — the contract attributes are always preserved first. Mirrors
108
+ * Go's `mergeAttributes` / PHP's `SqsTransport::attributes`.
109
+ */
110
+ declare function mergeAttributes(base: Record<string, SqsMessageAttributeValue>, headers: HeaderCarrier | null | undefined): Record<string, SqsMessageAttributeValue>;
111
+ /**
112
+ * Map inbound SQS `MessageAttributes` onto a flat {@link HeaderCarrier} (the consume-side
113
+ * counterpart of {@link mergeAttributes}), reading each attribute's `StringValue`. Returns an empty
114
+ * object when there are none. Both the contract `bq-*` attributes and any out-of-band rider (e.g.
115
+ * `traceparent`) surface — the core's `otel` extract reads only the keys it knows.
116
+ */
117
+ declare function headersOf(message: SqsMessage): HeaderCarrier;
77
118
  /** Options for {@link SqsPublisher}. */
78
119
  interface SqsPublisherOptions {
79
120
  /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */
@@ -87,6 +128,13 @@ interface SqsPublisherOptions {
87
128
  interface PublishOptions {
88
129
  /** Reuse an existing trace id (trace continuation). */
89
130
  traceId?: string;
131
+ /**
132
+ * Out-of-band transport headers carried as String `MessageAttributes` beside the contract `bq-*`
133
+ * attributes (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
134
+ * The contract attributes win a key collision and the merged set is capped at SQS's 10-attribute
135
+ * limit; an empty/omitted carrier leaves the publish unchanged.
136
+ */
137
+ headers?: HeaderCarrier;
90
138
  }
91
139
  /** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */
92
140
  declare class SqsPublisher {
@@ -100,15 +148,55 @@ declare class SqsPublisher {
100
148
  */
101
149
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
102
150
  }
103
- /** A URN handler. Receives the validated envelope and the raw SQS message. */
104
- type BabelHandler = (envelope: Envelope, message: SqsMessage) => unknown | Promise<unknown>;
151
+ /**
152
+ * A URN handler. Receives the validated envelope, the raw SQS message, and the out-of-band
153
+ * {@link HeaderCarrier} read from the message's `MessageAttributes` (empty when there are none).
154
+ * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of
155
+ * the producer span (ADR-0028).
156
+ */
157
+ type BabelHandler = (envelope: Envelope, message: SqsMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
105
158
  /** A map of URN → handler. */
106
159
  type BabelHandlers = Record<string, BabelHandler>;
107
160
  /** Options for {@link SqsConsumer}. */
108
161
  interface SqsConsumerOptions {
109
- /** Called instead of erroring when a message's URN has no handler (then the message is deleted). */
162
+ /**
163
+ * Called when a message's URN has no handler. Without `unknownUrn` the message is then deleted
164
+ * (the original behaviour); with `unknownUrn` set it is a notification hook and the strategy
165
+ * decides the message's fate.
166
+ */
110
167
  onUnknownUrn?: (envelope: IncomingEnvelope, message: SqsMessage) => unknown | Promise<unknown>;
111
- /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */
168
+ /**
169
+ * Unknown-URN strategy per contract §3.5 — `fail` | `delete` | `release` | `dead_letter`
170
+ * ({@link UnknownUrnStrategy}). Omitted keeps the original behaviour (`onUnknownUrn` → delete,
171
+ * otherwise report and leave for visibility-timeout redelivery, i.e. `fail`). `release` uses
172
+ * `ChangeMessageVisibility` with {@link unknownUrnReleaseDelay}; `dead_letter` sends the annotated
173
+ * envelope to {@link deadLetterQueueUrl} then deletes, degrading to `delete` when no DLQ is set.
174
+ */
175
+ unknownUrn?: string;
176
+ /** Backoff (seconds) for the `release` unknown-URN strategy (default 0 = redeliver now). */
177
+ unknownUrnReleaseDelay?: number;
178
+ /**
179
+ * Backoff before a failed message is redelivered. A failed handler always releases the message
180
+ * via `ChangeMessageVisibility(ReceiptHandle, VisibilityTimeout)` (contract §3.5); this sets the
181
+ * `VisibilityTimeout` — a number of seconds, or a function of the (reconciled) `attempts`
182
+ * returning seconds (e.g. an exponential backoff). Clamped to `0…43200`. Default `0`: the message
183
+ * is visible again immediately. A permanently failing handler therefore retries without pause
184
+ * until the queue's `RedrivePolicy` (`maxReceiveCount`) moves it to the DLQ — configure one.
185
+ */
186
+ releaseDelay?: number | ((attempts: number) => number);
187
+ /**
188
+ * The cross-language `<queue>.dlq` URL for the `dead_letter` strategy (opt-in; default none). A
189
+ * `.fifo` DLQ is sent with `MessageGroupId` (the source queue name) and `MessageDeduplicationId`
190
+ * (`meta.id`).
191
+ */
192
+ deadLetterQueueUrl?: string | null;
193
+ /**
194
+ * Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), a throwing handler,
195
+ * a throwing `onUnknownUrn` under an `unknownUrn` strategy, a failed release / dead-letter send, or
196
+ * a failed delete after successful processing (an {@link SqsDeleteError} wrapping the broker
197
+ * error; never released). The message is then left for visibility-timeout redelivery. The loop
198
+ * never stops on these.
199
+ */
112
200
  onError?: (error: unknown, envelope: IncomingEnvelope, message: SqsMessage) => void;
113
201
  /** Long-poll wait seconds (default 20). */
114
202
  waitTimeSeconds?: number;
@@ -117,11 +205,23 @@ interface SqsConsumerOptions {
117
205
  /** Max messages per receive (default 10). */
118
206
  maxMessages?: number;
119
207
  }
208
+ /**
209
+ * Reported via `onError` when `DeleteMessage` fails for a message whose processing already succeeded
210
+ * (a handler returned normally, or the unknown-URN strategy chose `delete` / `dead_letter`). The
211
+ * broker error is `cause`. Distinct from a handler failure: the message is NOT released, so it is
212
+ * redelivered only after its visibility timeout expires (at-least-once; dedupe on `meta.id` with the
213
+ * idempotency helper if the side effect must not repeat).
214
+ */
215
+ declare class SqsDeleteError extends BabelQueueError {
216
+ constructor(cause: unknown);
217
+ }
120
218
  /**
121
219
  * Polls an SQS queue, decodes + validates each message, routes it to the handler
122
- * registered for its URN, and deletes it on success. A throwing handler leaves the
123
- * message undeleted — SQS redelivers it after the visibility timeout (at-least-once);
124
- * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler.
220
+ * registered for its URN, and deletes it on success. A throwing handler's message is released via
221
+ * `ChangeMessageVisibility` after `releaseDelay` (default 0 s, contract §3.5); if the release itself
222
+ * fails the message is left for visibility-timeout redelivery (at-least-once);
223
+ * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler. An unmapped URN
224
+ * follows the `unknownUrn` strategy (`fail` | `delete` | `release` | `dead_letter`).
125
225
  */
126
226
  declare class SqsConsumer {
127
227
  private readonly client;
@@ -134,7 +234,21 @@ declare class SqsConsumer {
134
234
  /** Poll until `signal` aborts (each poll long-polls, so this does not busy-loop). */
135
235
  run(signal?: AbortSignal): Promise<void>;
136
236
  private handle;
237
+ /**
238
+ * Run a release / dead-letter step; a failure (e.g. `MessageNotInflight`, a missing IAM grant, a
239
+ * throwing `releaseDelay`) is reported to `onError` and the message is left undeleted, so
240
+ * visibility expiry still redelivers it. Never lets the error stop the consume loop.
241
+ */
242
+ private guard;
243
+ /** Apply the unknown-URN strategy (contract §3.5). */
244
+ private unknown;
245
+ /** Make the message visible again after `seconds` via `ChangeMessageVisibility`; never deletes. */
246
+ private release;
247
+ /** Send the annotated envelope to the opt-in DLQ; a no-op (→ plain delete) when none is set. */
248
+ private deadLetter;
249
+ /** Delete a message whose processing already succeeded; a broker failure becomes an SqsDeleteError. */
250
+ private deleteHandled;
137
251
  private delete;
138
252
  }
139
253
 
140
- export { type BabelHandler, type BabelHandlers, type PublishOptions, type ReceiveMessageInput, type SendMessageInput, type SqsApi, SqsConsumer, type SqsConsumerOptions, type SqsMessage, type SqsMessageAttributeValue, SqsPublisher, type SqsPublisherOptions, toMessageAttributes };
254
+ export { type BabelHandler, type BabelHandlers, type ChangeMessageVisibilityInput, MAX_VISIBILITY_TIMEOUT, type PublishOptions, type ReceiveMessageInput, type SendMessageInput, type SqsApi, SqsConsumer, type SqsConsumerOptions, SqsDeleteError, type SqsMessage, type SqsMessageAttributeValue, SqsPublisher, type SqsPublisherOptions, headersOf, mergeAttributes, toMessageAttributes };
package/dist/index.js CHANGED
@@ -1,5 +1,21 @@
1
1
  // src/index.ts
2
- import { BabelQueueError, EnvelopeCodec, UnknownUrnError } from "@babelqueue/core";
2
+ import { BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy, annotate } from "@babelqueue/core";
3
+
4
+ // src/headers.ts
5
+ function sanitizeHeaders(headers) {
6
+ const out = {};
7
+ if (!headers) return out;
8
+ for (const key of Object.keys(headers)) {
9
+ const value = headers[key];
10
+ if (key === "" || value == null || value === "") continue;
11
+ out[key] = value;
12
+ }
13
+ return out;
14
+ }
15
+
16
+ // src/index.ts
17
+ var MAX_ATTRIBUTES = 10;
18
+ var MAX_VISIBILITY_TIMEOUT = 43200;
3
19
  var str = (value) => ({
4
20
  DataType: "String",
5
21
  StringValue: String(value)
@@ -22,6 +38,25 @@ function toMessageAttributes(envelope) {
22
38
  }
23
39
  return attrs;
24
40
  }
41
+ function mergeAttributes(base, headers) {
42
+ const clean = sanitizeHeaders(headers);
43
+ for (const key of Object.keys(clean).sort()) {
44
+ if (key in base) continue;
45
+ if (Object.keys(base).length >= MAX_ATTRIBUTES) break;
46
+ base[key] = str(clean[key]);
47
+ }
48
+ return base;
49
+ }
50
+ function headersOf(message) {
51
+ const attrs = message.MessageAttributes;
52
+ const out = {};
53
+ if (!attrs) return out;
54
+ for (const key of Object.keys(attrs)) {
55
+ const value = attrs[key]?.StringValue;
56
+ if (value != null && value !== "") out[key] = value;
57
+ }
58
+ return out;
59
+ }
25
60
  function queueNameFromUrl(queueUrl) {
26
61
  const segments = queueUrl.split("/").filter(Boolean);
27
62
  return segments[segments.length - 1] ?? "default";
@@ -47,7 +82,7 @@ var SqsPublisher = class {
47
82
  const input = {
48
83
  QueueUrl: this.queueUrl,
49
84
  MessageBody: EnvelopeCodec.encode(envelope),
50
- MessageAttributes: toMessageAttributes(envelope)
85
+ MessageAttributes: mergeAttributes(toMessageAttributes(envelope), options.headers)
51
86
  };
52
87
  if (this.options.fifo) {
53
88
  input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);
@@ -59,12 +94,29 @@ var SqsPublisher = class {
59
94
  return envelope.meta.id;
60
95
  }
61
96
  };
97
+ var SqsDeleteError = class extends BabelQueueError {
98
+ constructor(cause) {
99
+ super("Failed to delete a processed SQS message; it will be redelivered after its visibility timeout.");
100
+ this.name = "SqsDeleteError";
101
+ this.cause = cause;
102
+ }
103
+ };
62
104
  var SqsConsumer = class {
63
105
  constructor(client, queueUrl, handlers, options = {}) {
64
106
  this.client = client;
65
107
  this.queueUrl = queueUrl;
66
108
  this.handlers = handlers;
67
109
  this.options = options;
110
+ const strategy = options.unknownUrn;
111
+ if (strategy !== void 0 && !Object.values(UnknownUrnStrategy).includes(strategy)) {
112
+ throw new BabelQueueError(`Unknown unknownUrn strategy "${strategy}".`);
113
+ }
114
+ const needsRelease = options.releaseDelay != null || strategy === UnknownUrnStrategy.RELEASE;
115
+ if (needsRelease && typeof client.changeMessageVisibility !== "function") {
116
+ throw new BabelQueueError(
117
+ "SqsConsumer release requires a client with changeMessageVisibility (contract \xA73.5)."
118
+ );
119
+ }
68
120
  }
69
121
  client;
70
122
  queueUrl;
@@ -116,6 +168,38 @@ var SqsConsumer = class {
116
168
  const urn = EnvelopeCodec.urn(envelope);
117
169
  const handler = this.handlers[urn];
118
170
  if (!handler) {
171
+ await this.unknown(urn, envelope, message);
172
+ return;
173
+ }
174
+ try {
175
+ await handler(envelope, message, headersOf(message));
176
+ } catch (error) {
177
+ this.options.onError?.(error, envelope, message);
178
+ await this.guard(envelope, message, async () => {
179
+ const delay = this.options.releaseDelay ?? 0;
180
+ const attempts = typeof envelope.attempts === "number" ? envelope.attempts : 0;
181
+ await this.release(message, typeof delay === "function" ? delay(attempts) : delay);
182
+ });
183
+ return;
184
+ }
185
+ await this.guard(envelope, message, () => this.deleteHandled(message));
186
+ }
187
+ /**
188
+ * Run a release / dead-letter step; a failure (e.g. `MessageNotInflight`, a missing IAM grant, a
189
+ * throwing `releaseDelay`) is reported to `onError` and the message is left undeleted, so
190
+ * visibility expiry still redelivers it. Never lets the error stop the consume loop.
191
+ */
192
+ async guard(envelope, message, step) {
193
+ try {
194
+ await step();
195
+ } catch (error) {
196
+ this.options.onError?.(error, envelope, message);
197
+ }
198
+ }
199
+ /** Apply the unknown-URN strategy (contract §3.5). */
200
+ async unknown(urn, envelope, message) {
201
+ const strategy = this.options.unknownUrn;
202
+ if (strategy === void 0) {
119
203
  if (this.options.onUnknownUrn) {
120
204
  await this.options.onUnknownUrn(envelope, message);
121
205
  await this.delete(message);
@@ -124,11 +208,61 @@ var SqsConsumer = class {
124
208
  }
125
209
  return;
126
210
  }
211
+ await this.guard(envelope, message, async () => {
212
+ await this.options.onUnknownUrn?.(envelope, message);
213
+ });
214
+ switch (strategy) {
215
+ case UnknownUrnStrategy.DELETE:
216
+ await this.guard(envelope, message, () => this.deleteHandled(message));
217
+ return;
218
+ case UnknownUrnStrategy.RELEASE:
219
+ await this.guard(envelope, message, () => this.release(message, this.options.unknownUrnReleaseDelay ?? 0));
220
+ return;
221
+ case UnknownUrnStrategy.DEAD_LETTER:
222
+ await this.guard(envelope, message, async () => {
223
+ await this.deadLetter(envelope, "unknown_urn");
224
+ await this.deleteHandled(message);
225
+ });
226
+ return;
227
+ default:
228
+ this.options.onError?.(new UnknownUrnError(urn), envelope, message);
229
+ }
230
+ }
231
+ /** Make the message visible again after `seconds` via `ChangeMessageVisibility`; never deletes. */
232
+ async release(message, seconds) {
233
+ if (!message.ReceiptHandle || !this.client.changeMessageVisibility) return;
234
+ const timeout = Number.isFinite(seconds) ? Math.min(MAX_VISIBILITY_TIMEOUT, Math.max(0, Math.floor(seconds))) : 0;
235
+ await this.client.changeMessageVisibility({
236
+ QueueUrl: this.queueUrl,
237
+ ReceiptHandle: message.ReceiptHandle,
238
+ VisibilityTimeout: timeout
239
+ });
240
+ }
241
+ /** Send the annotated envelope to the opt-in DLQ; a no-op (→ plain delete) when none is set. */
242
+ async deadLetter(envelope, reason) {
243
+ const dlq = this.options.deadLetterQueueUrl;
244
+ if (!dlq) return;
245
+ const source = envelope.meta?.queue ?? queueNameFromUrl(this.queueUrl);
246
+ const annotated = annotate(envelope, reason, source, {
247
+ attempts: envelope.attempts ?? 0
248
+ });
249
+ const input = {
250
+ QueueUrl: dlq,
251
+ MessageBody: EnvelopeCodec.encode(annotated),
252
+ MessageAttributes: toMessageAttributes(annotated)
253
+ };
254
+ if (dlq.endsWith(".fifo")) {
255
+ input.MessageGroupId = source;
256
+ if (annotated.meta?.id) input.MessageDeduplicationId = annotated.meta.id;
257
+ }
258
+ await this.client.sendMessage(input);
259
+ }
260
+ /** Delete a message whose processing already succeeded; a broker failure becomes an SqsDeleteError. */
261
+ async deleteHandled(message) {
127
262
  try {
128
- await handler(envelope, message);
129
263
  await this.delete(message);
130
264
  } catch (error) {
131
- this.options.onError?.(error, envelope, message);
265
+ throw new SqsDeleteError(error);
132
266
  }
133
267
  }
134
268
  async delete(message) {
@@ -140,8 +274,12 @@ var SqsConsumer = class {
140
274
  }
141
275
  };
142
276
  export {
277
+ MAX_VISIBILITY_TIMEOUT,
143
278
  SqsConsumer,
279
+ SqsDeleteError,
144
280
  SqsPublisher,
281
+ headersOf,
282
+ mergeAttributes,
145
283
  toMessageAttributes
146
284
  };
147
285
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Amazon SQS adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Amazon SQS,\n * so an SQS-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the PHP/Laravel, Python, Go, Java and .NET SDKs.\n *\n * import { SQS } from \"@aws-sdk/client-sqs\";\n * import { SqsPublisher, SqsConsumer } from \"@babelqueue/sqs\";\n *\n * const sqs = new SQS({ region: \"eu-central-1\" });\n * const url = \"https://sqs.eu-central-1.amazonaws.com/123456789012/orders\";\n *\n * await new SqsPublisher(sqs, url).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new SqsConsumer(sqs, url, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §3 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native SQS `MessageAttributes`. The envelope is\n * unchanged (`schema_version` stays 1); SQS is purely additive. Retry is SQS-native\n * (a failed handler leaves the message for visibility-timeout redelivery); the\n * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as\n * `attempts = count − 1`.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal SQS shapes (a structural subset of @aws-sdk/client-sqs) -----------\n\nexport interface SqsMessageAttributeValue {\n DataType: string;\n StringValue?: string;\n}\n\nexport interface SqsMessage {\n Body?: string;\n ReceiptHandle?: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n Attributes?: Record<string, string>;\n}\n\nexport interface SendMessageInput {\n QueueUrl: string;\n MessageBody: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n MessageGroupId?: string;\n MessageDeduplicationId?: string;\n}\n\nexport interface ReceiveMessageInput {\n QueueUrl: string;\n MaxNumberOfMessages?: number;\n WaitTimeSeconds?: number;\n VisibilityTimeout?: number;\n MessageAttributeNames?: string[];\n AttributeNames?: string[];\n}\n\n/**\n * The subset of the AWS SQS client this adapter calls. The aggregated `SQS` class\n * from `@aws-sdk/client-sqs` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface SqsApi {\n sendMessage(input: SendMessageInput): Promise<{ MessageId?: string } | unknown>;\n receiveMessage(input: ReceiveMessageInput): Promise<{ Messages?: SqsMessage[] }>;\n deleteMessage(input: { QueueUrl: string; ReceiptHandle: string }): Promise<unknown>;\n}\n\n// --- Attribute projection (contract §3.2) --------------------------------------\n\nconst str = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"String\",\n StringValue: String(value),\n});\nconst num = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"Number\",\n StringValue: String(value),\n});\n\n/**\n * Project the envelope's contract fields onto native SQS `MessageAttributes` — a\n * redundant, routable view of the body (the body stays authoritative).\n */\nexport function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue> {\n const attrs: Record<string, SqsMessageAttributeValue> = {};\n if (envelope.job) attrs[\"bq-job\"] = str(envelope.job);\n if (envelope.trace_id) attrs[\"bq-trace-id\"] = str(envelope.trace_id);\n if (envelope.meta.id) attrs[\"bq-message-id\"] = str(envelope.meta.id);\n if (envelope.meta.schema_version != null) {\n attrs[\"bq-schema-version\"] = num(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) attrs[\"bq-source-lang\"] = str(envelope.meta.lang);\n if (envelope.meta.created_at != null) {\n attrs[\"bq-created-at\"] = num(envelope.meta.created_at);\n }\n return attrs;\n}\n\nfunction queueNameFromUrl(queueUrl: string): string {\n const segments = queueUrl.split(\"/\").filter(Boolean);\n return segments[segments.length - 1] ?? \"default\";\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link SqsPublisher}. */\nexport interface SqsPublisherOptions {\n /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */\n fifo?: boolean;\n /** FIFO ordering group (default: the queue name from the URL). */\n messageGroupId?: string;\n /** Use the queue's content-based dedup instead of `meta.id` as the dedup id. */\n contentDedup?: boolean;\n}\n\n/** Options for {@link SqsPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n}\n\n/** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */\nexport class SqsPublisher {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly options: SqsPublisherOptions = {},\n ) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it as the message body with\n * the projected `MessageAttributes`, and return the message id (`meta.id`).\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queueNameFromUrl(this.queueUrl),\n traceId: options.traceId,\n });\n const input: SendMessageInput = {\n QueueUrl: this.queueUrl,\n MessageBody: EnvelopeCodec.encode(envelope),\n MessageAttributes: toMessageAttributes(envelope),\n };\n if (this.options.fifo) {\n input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);\n if (!this.options.contentDedup) {\n input.MessageDeduplicationId = envelope.meta.id;\n }\n }\n await this.client.sendMessage(input);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw SQS message. */\nexport type BabelHandler = (envelope: Envelope, message: SqsMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link SqsConsumer}. */\nexport interface SqsConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is deleted). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: SqsMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: SqsMessage) => void;\n /** Long-poll wait seconds (default 20). */\n waitTimeSeconds?: number;\n /** Reservation window applied on receive (seconds). */\n visibilityTimeout?: number;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n}\n\n/**\n * Polls an SQS queue, decodes + validates each message, routes it to the handler\n * registered for its URN, and deletes it on success. A throwing handler leaves the\n * message undeleted — SQS redelivers it after the visibility timeout (at-least-once);\n * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler.\n */\nexport class SqsConsumer {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly handlers: BabelHandlers,\n private readonly options: SqsConsumerOptions = {},\n ) {}\n\n /** Receive one batch, route each message, delete the ones handled. Returns the batch size. */\n async poll(): Promise<number> {\n const input: ReceiveMessageInput = {\n QueueUrl: this.queueUrl,\n MaxNumberOfMessages: this.options.maxMessages ?? 10,\n WaitTimeSeconds: this.options.waitTimeSeconds ?? 20,\n MessageAttributeNames: [\"All\"],\n AttributeNames: [\"ApproximateReceiveCount\"],\n };\n if (this.options.visibilityTimeout != null) {\n input.VisibilityTimeout = this.options.visibilityTimeout;\n }\n const result = await this.client.receiveMessage(input);\n const messages = result.Messages ?? [];\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\n }\n\n /** Poll until `signal` aborts (each poll long-polls, so this does not busy-loop). */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: SqsMessage): Promise<void> {\n const envelope = EnvelopeCodec.decode(message.Body ?? \"\");\n\n const receiveCount = message.Attributes?.[\"ApproximateReceiveCount\"];\n if (receiveCount !== undefined) {\n const native = Number.parseInt(receiveCount, 10) - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (Number.isFinite(native) && native > current) {\n envelope.attempts = native;\n }\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from SQS.\"),\n envelope,\n message,\n );\n return;\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.delete(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n }\n return;\n }\n\n try {\n await handler(envelope, message);\n await this.delete(message);\n } catch (error) {\n // Leave the message undeleted — SQS redelivers after the visibility timeout.\n this.options.onError?.(error, envelope, message);\n }\n }\n\n private async delete(message: SqsMessage): Promise<void> {\n if (!message.ReceiptHandle) return;\n await this.client.deleteMessage({\n QueueUrl: this.queueUrl,\n ReceiptHandle: message.ReceiptHandle,\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Amazon SQS adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Amazon SQS,\n * so an SQS-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the PHP/Laravel, Python, Go, Java and .NET SDKs.\n *\n * import { SQS } from \"@aws-sdk/client-sqs\";\n * import { SqsPublisher, SqsConsumer } from \"@babelqueue/sqs\";\n *\n * const sqs = new SQS({ region: \"eu-central-1\" });\n * const url = \"https://sqs.eu-central-1.amazonaws.com/123456789012/orders\";\n *\n * await new SqsPublisher(sqs, url).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new SqsConsumer(sqs, url, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §3 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native SQS `MessageAttributes`. The envelope is\n * unchanged (`schema_version` stays 1); SQS is purely additive. Retry is SQS-native\n * (a failed handler's message is released via `ChangeMessageVisibility` after\n * `releaseDelay`, default 0 s — pair the queue with a `RedrivePolicy`); the\n * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as\n * `attempts = count − 1`.\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional String `MessageAttributes` **beside** the contract `bq-*`\n * attributes where `bq-trace-id` already lives — the contract attributes win a key collision, and\n * the merged set is bounded by SQS's **10-attribute limit** (contract attributes are seeded first,\n * so a rider only lands while headroom remains). On consume the inbound `MessageAttributes` are\n * surfaced as a `Record<string,string>` so the core's `otel` extract sees the `traceparent` and\n * links the consumer span as a child. A header-less publish is byte-identical. This mirrors the Go\n * `/sqs` and PHP `SqsTransport` wiring. GR-1: the wire envelope body is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy, annotate } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n/** SQS allows at most 10 user message attributes per message. */\nconst MAX_ATTRIBUTES = 10;\n\n/** The SQS ceiling for a `VisibilityTimeout` (12 hours, in seconds). */\nexport const MAX_VISIBILITY_TIMEOUT = 43_200;\n\n// --- Minimal SQS shapes (a structural subset of @aws-sdk/client-sqs) -----------\n\nexport interface SqsMessageAttributeValue {\n DataType: string;\n StringValue?: string;\n}\n\nexport interface SqsMessage {\n Body?: string;\n ReceiptHandle?: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n Attributes?: Record<string, string>;\n}\n\nexport interface SendMessageInput {\n QueueUrl: string;\n MessageBody: string;\n MessageAttributes?: Record<string, SqsMessageAttributeValue>;\n MessageGroupId?: string;\n MessageDeduplicationId?: string;\n}\n\nexport interface ReceiveMessageInput {\n QueueUrl: string;\n MaxNumberOfMessages?: number;\n WaitTimeSeconds?: number;\n VisibilityTimeout?: number;\n MessageAttributeNames?: string[];\n AttributeNames?: string[];\n}\n\n/**\n * The subset of the AWS SQS client this adapter calls. The aggregated `SQS` class\n * from `@aws-sdk/client-sqs` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface SqsApi {\n sendMessage(input: SendMessageInput): Promise<{ MessageId?: string } | unknown>;\n receiveMessage(input: ReceiveMessageInput): Promise<{ Messages?: SqsMessage[] }>;\n deleteMessage(input: { QueueUrl: string; ReceiptHandle: string }): Promise<unknown>;\n /**\n * The §3.5 release primitive. Optional so existing structural clients keep compiling; it is\n * required (checked at construction) when a consumer sets `releaseDelay` or the `release`\n * unknown-URN strategy explicitly. Without it the default handler-failure release degrades to\n * leaving the message for visibility-timeout redelivery. The aggregated `SQS` class from\n * `@aws-sdk/client-sqs` provides it.\n */\n changeMessageVisibility?(input: ChangeMessageVisibilityInput): Promise<unknown>;\n}\n\nexport interface ChangeMessageVisibilityInput {\n QueueUrl: string;\n ReceiptHandle: string;\n VisibilityTimeout: number;\n}\n\n// --- Attribute projection (contract §3.2) --------------------------------------\n\nconst str = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"String\",\n StringValue: String(value),\n});\nconst num = (value: unknown): SqsMessageAttributeValue => ({\n DataType: \"Number\",\n StringValue: String(value),\n});\n\n/**\n * Project the envelope's contract fields onto native SQS `MessageAttributes` — a\n * redundant, routable view of the body (the body stays authoritative).\n */\nexport function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue> {\n const attrs: Record<string, SqsMessageAttributeValue> = {};\n if (envelope.job) attrs[\"bq-job\"] = str(envelope.job);\n if (envelope.trace_id) attrs[\"bq-trace-id\"] = str(envelope.trace_id);\n if (envelope.meta.id) attrs[\"bq-message-id\"] = str(envelope.meta.id);\n if (envelope.meta.schema_version != null) {\n attrs[\"bq-schema-version\"] = num(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) attrs[\"bq-source-lang\"] = str(envelope.meta.lang);\n if (envelope.meta.created_at != null) {\n attrs[\"bq-created-at\"] = num(envelope.meta.created_at);\n }\n return attrs;\n}\n\n/**\n * Overlay the out-of-band `headers` onto the contract attribute projection as String\n * `MessageAttributes`, without overwriting an existing `bq-*` attribute (the contract wins a key\n * collision) and skipping blanks. Keys are merged in sorted order and the merge stops at the\n * 10-attribute SQS ceiling, so unbounded riders can never push the message past the limit (SQS\n * rejects the whole send otherwise) — the contract attributes are always preserved first. Mirrors\n * Go's `mergeAttributes` / PHP's `SqsTransport::attributes`.\n */\nexport function mergeAttributes(\n base: Record<string, SqsMessageAttributeValue>,\n headers: HeaderCarrier | null | undefined,\n): Record<string, SqsMessageAttributeValue> {\n const clean = sanitizeHeaders(headers);\n for (const key of Object.keys(clean).sort()) {\n if (key in base) continue; // never clobber a contract bq-* attribute\n if (Object.keys(base).length >= MAX_ATTRIBUTES) break; // respect the SQS 10-attribute cap\n base[key] = str(clean[key]);\n }\n return base;\n}\n\n/**\n * Map inbound SQS `MessageAttributes` onto a flat {@link HeaderCarrier} (the consume-side\n * counterpart of {@link mergeAttributes}), reading each attribute's `StringValue`. Returns an empty\n * object when there are none. Both the contract `bq-*` attributes and any out-of-band rider (e.g.\n * `traceparent`) surface — the core's `otel` extract reads only the keys it knows.\n */\nexport function headersOf(message: SqsMessage): HeaderCarrier {\n const attrs = message.MessageAttributes;\n const out: HeaderCarrier = {};\n if (!attrs) return out;\n for (const key of Object.keys(attrs)) {\n const value = attrs[key]?.StringValue;\n if (value != null && value !== \"\") out[key] = value;\n }\n return out;\n}\n\nfunction queueNameFromUrl(queueUrl: string): string {\n const segments = queueUrl.split(\"/\").filter(Boolean);\n return segments[segments.length - 1] ?? \"default\";\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link SqsPublisher}. */\nexport interface SqsPublisherOptions {\n /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */\n fifo?: boolean;\n /** FIFO ordering group (default: the queue name from the URL). */\n messageGroupId?: string;\n /** Use the queue's content-based dedup instead of `meta.id` as the dedup id. */\n contentDedup?: boolean;\n}\n\n/** Options for {@link SqsPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /**\n * Out-of-band transport headers carried as String `MessageAttributes` beside the contract `bq-*`\n * attributes (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract attributes win a key collision and the merged set is capped at SQS's 10-attribute\n * limit; an empty/omitted carrier leaves the publish unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */\nexport class SqsPublisher {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly options: SqsPublisherOptions = {},\n ) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it as the message body with\n * the projected `MessageAttributes`, and return the message id (`meta.id`).\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queueNameFromUrl(this.queueUrl),\n traceId: options.traceId,\n });\n const input: SendMessageInput = {\n QueueUrl: this.queueUrl,\n MessageBody: EnvelopeCodec.encode(envelope),\n MessageAttributes: mergeAttributes(toMessageAttributes(envelope), options.headers),\n };\n if (this.options.fifo) {\n input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);\n if (!this.options.contentDedup) {\n input.MessageDeduplicationId = envelope.meta.id;\n }\n }\n await this.client.sendMessage(input);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw SQS message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `MessageAttributes` (empty when there are none).\n * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of\n * the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: SqsMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link SqsConsumer}. */\nexport interface SqsConsumerOptions {\n /**\n * Called when a message's URN has no handler. Without `unknownUrn` the message is then deleted\n * (the original behaviour); with `unknownUrn` set it is a notification hook and the strategy\n * decides the message's fate.\n */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: SqsMessage) => unknown | Promise<unknown>;\n /**\n * Unknown-URN strategy per contract §3.5 — `fail` | `delete` | `release` | `dead_letter`\n * ({@link UnknownUrnStrategy}). Omitted keeps the original behaviour (`onUnknownUrn` → delete,\n * otherwise report and leave for visibility-timeout redelivery, i.e. `fail`). `release` uses\n * `ChangeMessageVisibility` with {@link unknownUrnReleaseDelay}; `dead_letter` sends the annotated\n * envelope to {@link deadLetterQueueUrl} then deletes, degrading to `delete` when no DLQ is set.\n */\n unknownUrn?: string;\n /** Backoff (seconds) for the `release` unknown-URN strategy (default 0 = redeliver now). */\n unknownUrnReleaseDelay?: number;\n /**\n * Backoff before a failed message is redelivered. A failed handler always releases the message\n * via `ChangeMessageVisibility(ReceiptHandle, VisibilityTimeout)` (contract §3.5); this sets the\n * `VisibilityTimeout` — a number of seconds, or a function of the (reconciled) `attempts`\n * returning seconds (e.g. an exponential backoff). Clamped to `0…43200`. Default `0`: the message\n * is visible again immediately. A permanently failing handler therefore retries without pause\n * until the queue's `RedrivePolicy` (`maxReceiveCount`) moves it to the DLQ — configure one.\n */\n releaseDelay?: number | ((attempts: number) => number);\n /**\n * The cross-language `<queue>.dlq` URL for the `dead_letter` strategy (opt-in; default none). A\n * `.fifo` DLQ is sent with `MessageGroupId` (the source queue name) and `MessageDeduplicationId`\n * (`meta.id`).\n */\n deadLetterQueueUrl?: string | null;\n /**\n * Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), a throwing handler,\n * a throwing `onUnknownUrn` under an `unknownUrn` strategy, a failed release / dead-letter send, or\n * a failed delete after successful processing (an {@link SqsDeleteError} wrapping the broker\n * error; never released). The message is then left for visibility-timeout redelivery. The loop\n * never stops on these.\n */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: SqsMessage) => void;\n /** Long-poll wait seconds (default 20). */\n waitTimeSeconds?: number;\n /** Reservation window applied on receive (seconds). */\n visibilityTimeout?: number;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n}\n\n/**\n * Reported via `onError` when `DeleteMessage` fails for a message whose processing already succeeded\n * (a handler returned normally, or the unknown-URN strategy chose `delete` / `dead_letter`). The\n * broker error is `cause`. Distinct from a handler failure: the message is NOT released, so it is\n * redelivered only after its visibility timeout expires (at-least-once; dedupe on `meta.id` with the\n * idempotency helper if the side effect must not repeat).\n */\nexport class SqsDeleteError extends BabelQueueError {\n constructor(cause: unknown) {\n super(\"Failed to delete a processed SQS message; it will be redelivered after its visibility timeout.\");\n this.name = \"SqsDeleteError\";\n this.cause = cause;\n }\n}\n\n/**\n * Polls an SQS queue, decodes + validates each message, routes it to the handler\n * registered for its URN, and deletes it on success. A throwing handler's message is released via\n * `ChangeMessageVisibility` after `releaseDelay` (default 0 s, contract §3.5); if the release itself\n * fails the message is left for visibility-timeout redelivery (at-least-once);\n * `attempts` is reconciled to `ApproximateReceiveCount − 1` for the handler. An unmapped URN\n * follows the `unknownUrn` strategy (`fail` | `delete` | `release` | `dead_letter`).\n */\nexport class SqsConsumer {\n constructor(\n private readonly client: SqsApi,\n private readonly queueUrl: string,\n private readonly handlers: BabelHandlers,\n private readonly options: SqsConsumerOptions = {},\n ) {\n const strategy = options.unknownUrn;\n if (strategy !== undefined && !(Object.values(UnknownUrnStrategy) as string[]).includes(strategy)) {\n throw new BabelQueueError(`Unknown unknownUrn strategy \"${strategy}\".`);\n }\n const needsRelease = options.releaseDelay != null || strategy === UnknownUrnStrategy.RELEASE;\n if (needsRelease && typeof client.changeMessageVisibility !== \"function\") {\n throw new BabelQueueError(\n \"SqsConsumer release requires a client with changeMessageVisibility (contract §3.5).\",\n );\n }\n }\n\n /** Receive one batch, route each message, delete the ones handled. Returns the batch size. */\n async poll(): Promise<number> {\n const input: ReceiveMessageInput = {\n QueueUrl: this.queueUrl,\n MaxNumberOfMessages: this.options.maxMessages ?? 10,\n WaitTimeSeconds: this.options.waitTimeSeconds ?? 20,\n MessageAttributeNames: [\"All\"],\n AttributeNames: [\"ApproximateReceiveCount\"],\n };\n if (this.options.visibilityTimeout != null) {\n input.VisibilityTimeout = this.options.visibilityTimeout;\n }\n const result = await this.client.receiveMessage(input);\n const messages = result.Messages ?? [];\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\n }\n\n /** Poll until `signal` aborts (each poll long-polls, so this does not busy-loop). */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: SqsMessage): Promise<void> {\n const envelope = EnvelopeCodec.decode(message.Body ?? \"\");\n\n const receiveCount = message.Attributes?.[\"ApproximateReceiveCount\"];\n if (receiveCount !== undefined) {\n const native = Number.parseInt(receiveCount, 10) - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (Number.isFinite(native) && native > current) {\n envelope.attempts = native;\n }\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from SQS.\"),\n envelope,\n message,\n );\n return;\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.unknown(urn, envelope as Envelope, message);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n } catch (error) {\n this.options.onError?.(error, envelope, message);\n await this.guard(envelope, message, async () => {\n const delay = this.options.releaseDelay ?? 0;\n const attempts = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n await this.release(message, typeof delay === \"function\" ? delay(attempts) : delay);\n });\n return;\n }\n // Outside the handler's try: a failed delete is not a handler failure. It is reported as an\n // SqsDeleteError and the message is NOT released — releasing at the (default 0 s) backoff would\n // redeliver an already-processed message immediately; it returns on visibility expiry instead.\n await this.guard(envelope, message, () => this.deleteHandled(message));\n }\n\n /**\n * Run a release / dead-letter step; a failure (e.g. `MessageNotInflight`, a missing IAM grant, a\n * throwing `releaseDelay`) is reported to `onError` and the message is left undeleted, so\n * visibility expiry still redelivers it. Never lets the error stop the consume loop.\n */\n private async guard(envelope: IncomingEnvelope, message: SqsMessage, step: () => Promise<void>): Promise<void> {\n try {\n await step();\n } catch (error) {\n this.options.onError?.(error, envelope, message);\n }\n }\n\n /** Apply the unknown-URN strategy (contract §3.5). */\n private async unknown(urn: string, envelope: Envelope, message: SqsMessage): Promise<void> {\n const strategy = this.options.unknownUrn;\n if (strategy === undefined) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.delete(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n }\n return;\n }\n\n await this.guard(envelope, message, async () => {\n await this.options.onUnknownUrn?.(envelope, message);\n });\n switch (strategy) {\n case UnknownUrnStrategy.DELETE:\n await this.guard(envelope, message, () => this.deleteHandled(message));\n return;\n case UnknownUrnStrategy.RELEASE:\n await this.guard(envelope, message, () => this.release(message, this.options.unknownUrnReleaseDelay ?? 0));\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.guard(envelope, message, async () => {\n await this.deadLetter(envelope, \"unknown_urn\");\n await this.deleteHandled(message);\n });\n return;\n default:\n // FAIL: surface and do NOT delete — visibility expiry redelivers, native redrive quarantines.\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n }\n }\n\n /** Make the message visible again after `seconds` via `ChangeMessageVisibility`; never deletes. */\n private async release(message: SqsMessage, seconds: number): Promise<void> {\n if (!message.ReceiptHandle || !this.client.changeMessageVisibility) return;\n const timeout = Number.isFinite(seconds)\n ? Math.min(MAX_VISIBILITY_TIMEOUT, Math.max(0, Math.floor(seconds)))\n : 0;\n await this.client.changeMessageVisibility({\n QueueUrl: this.queueUrl,\n ReceiptHandle: message.ReceiptHandle,\n VisibilityTimeout: timeout,\n });\n }\n\n /** Send the annotated envelope to the opt-in DLQ; a no-op (→ plain delete) when none is set. */\n private async deadLetter(envelope: Envelope, reason: string): Promise<void> {\n const dlq = this.options.deadLetterQueueUrl;\n if (!dlq) return;\n const source = envelope.meta?.queue ?? queueNameFromUrl(this.queueUrl);\n const annotated = annotate(envelope, reason, source, {\n attempts: envelope.attempts ?? 0,\n });\n const input: SendMessageInput = {\n QueueUrl: dlq,\n MessageBody: EnvelopeCodec.encode(annotated),\n MessageAttributes: toMessageAttributes(annotated),\n };\n if (dlq.endsWith(\".fifo\")) {\n // A FIFO DLQ (`<queue>.dlq.fifo`) rejects a send without a group; dedup on meta.id (§3.2).\n input.MessageGroupId = source;\n if (annotated.meta?.id) input.MessageDeduplicationId = annotated.meta.id;\n }\n await this.client.sendMessage(input);\n }\n\n /** Delete a message whose processing already succeeded; a broker failure becomes an SqsDeleteError. */\n private async deleteHandled(message: SqsMessage): Promise<void> {\n try {\n await this.delete(message);\n } catch (error) {\n throw new SqsDeleteError(error);\n }\n }\n\n private async delete(message: SqsMessage): Promise<void> {\n if (!message.ReceiptHandle) return;\n await this.client.deleteMessage({\n QueueUrl: this.queueUrl,\n ReceiptHandle: message.ReceiptHandle,\n });\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives, so a caller can treat the result as \"no headers\". A nullish input yields an empty\n * object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@babelqueue/sqs",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Amazon SQS adapter for BabelQueue — canonical-envelope publisher and a URN-routed consumer.",
5
- "keywords": ["babelqueue", "sqs", "aws", "queue", "polyglot", "messaging"],
5
+ "keywords": [
6
+ "babelqueue",
7
+ "sqs",
8
+ "aws",
9
+ "queue",
10
+ "polyglot",
11
+ "messaging"
12
+ ],
6
13
  "homepage": "https://babelqueue.com",
7
14
  "bugs": {
8
15
  "url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
@@ -30,7 +37,9 @@
30
37
  "main": "./dist/index.cjs",
31
38
  "module": "./dist/index.js",
32
39
  "types": "./dist/index.d.ts",
33
- "files": ["dist"],
40
+ "files": [
41
+ "dist"
42
+ ],
34
43
  "sideEffects": false,
35
44
  "engines": {
36
45
  "node": ">=18"
@@ -41,7 +50,7 @@
41
50
  "test": "node --import tsx --test test/*.test.ts"
42
51
  },
43
52
  "dependencies": {
44
- "@babelqueue/core": "^1.0.0"
53
+ "@babelqueue/core": "^1.8.0"
45
54
  },
46
55
  "peerDependencies": {
47
56
  "@aws-sdk/client-sqs": "^3.0.0"