@babelqueue/sqs 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,24 @@ The unit tests inject a fake SQS client (`SqsApi`), so they run without
69
69
  npm test
70
70
  ```
71
71
 
72
+ ## OpenTelemetry tracing (ADR-0028)
73
+
74
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
75
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
76
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
77
+ 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),
78
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
79
+
80
+ ```ts
81
+ import { trace } from "@opentelemetry/api";
82
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
83
+ import type { HeaderCarrier } from "@babelqueue/core";
84
+
85
+ const headers: HeaderCarrier = {};
86
+ await tracedPublish(trace.getTracer("orders"), urn, data,
87
+ () => adapterPublish(urn, data, { headers }), { headers });
88
+ ```
89
+
72
90
  ## License
73
91
 
74
92
  MIT
package/dist/index.cjs CHANGED
@@ -22,10 +22,27 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  SqsConsumer: () => SqsConsumer,
24
24
  SqsPublisher: () => SqsPublisher,
25
+ headersOf: () => headersOf,
26
+ mergeAttributes: () => mergeAttributes,
25
27
  toMessageAttributes: () => toMessageAttributes
26
28
  });
27
29
  module.exports = __toCommonJS(index_exports);
28
30
  var import_core = require("@babelqueue/core");
31
+
32
+ // src/headers.ts
33
+ function sanitizeHeaders(headers) {
34
+ const out = {};
35
+ if (!headers) return out;
36
+ for (const key of Object.keys(headers)) {
37
+ const value = headers[key];
38
+ if (key === "" || value == null || value === "") continue;
39
+ out[key] = value;
40
+ }
41
+ return out;
42
+ }
43
+
44
+ // src/index.ts
45
+ var MAX_ATTRIBUTES = 10;
29
46
  var str = (value) => ({
30
47
  DataType: "String",
31
48
  StringValue: String(value)
@@ -48,6 +65,25 @@ function toMessageAttributes(envelope) {
48
65
  }
49
66
  return attrs;
50
67
  }
68
+ function mergeAttributes(base, headers) {
69
+ const clean = sanitizeHeaders(headers);
70
+ for (const key of Object.keys(clean).sort()) {
71
+ if (key in base) continue;
72
+ if (Object.keys(base).length >= MAX_ATTRIBUTES) break;
73
+ base[key] = str(clean[key]);
74
+ }
75
+ return base;
76
+ }
77
+ function headersOf(message) {
78
+ const attrs = message.MessageAttributes;
79
+ const out = {};
80
+ if (!attrs) return out;
81
+ for (const key of Object.keys(attrs)) {
82
+ const value = attrs[key]?.StringValue;
83
+ if (value != null && value !== "") out[key] = value;
84
+ }
85
+ return out;
86
+ }
51
87
  function queueNameFromUrl(queueUrl) {
52
88
  const segments = queueUrl.split("/").filter(Boolean);
53
89
  return segments[segments.length - 1] ?? "default";
@@ -73,7 +109,7 @@ var SqsPublisher = class {
73
109
  const input = {
74
110
  QueueUrl: this.queueUrl,
75
111
  MessageBody: import_core.EnvelopeCodec.encode(envelope),
76
- MessageAttributes: toMessageAttributes(envelope)
112
+ MessageAttributes: mergeAttributes(toMessageAttributes(envelope), options.headers)
77
113
  };
78
114
  if (this.options.fifo) {
79
115
  input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);
@@ -151,7 +187,7 @@ var SqsConsumer = class {
151
187
  return;
152
188
  }
153
189
  try {
154
- await handler(envelope, message);
190
+ await handler(envelope, message, headersOf(message));
155
191
  await this.delete(message);
156
192
  } catch (error) {
157
193
  this.options.onError?.(error, envelope, message);
@@ -169,6 +205,8 @@ var SqsConsumer = class {
169
205
  0 && (module.exports = {
170
206
  SqsConsumer,
171
207
  SqsPublisher,
208
+ headersOf,
209
+ mergeAttributes,
172
210
  toMessageAttributes
173
211
  });
174
212
  //# 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 leaves the message for visibility-timeout redelivery); 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 } 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// --- 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\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 /** 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, headersOf(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","/**\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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Amazon SQS adapter for BabelQueue.
@@ -26,6 +26,15 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
26
26
  * (a failed handler leaves the message for visibility-timeout redelivery); the
27
27
  * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as
28
28
  * `attempts = count − 1`.
29
+ *
30
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
31
+ * span linkage) rides as additional String `MessageAttributes` **beside** the contract `bq-*`
32
+ * attributes where `bq-trace-id` already lives — the contract attributes win a key collision, and
33
+ * the merged set is bounded by SQS's **10-attribute limit** (contract attributes are seeded first,
34
+ * so a rider only lands while headroom remains). On consume the inbound `MessageAttributes` are
35
+ * surfaced as a `Record<string,string>` so the core's `otel` extract sees the `traceparent` and
36
+ * links the consumer span as a child. A header-less publish is byte-identical. This mirrors the Go
37
+ * `/sqs` and PHP `SqsTransport` wiring. GR-1: the wire envelope body is never touched.
29
38
  */
30
39
 
31
40
  interface SqsMessageAttributeValue {
@@ -74,6 +83,22 @@ interface SqsApi {
74
83
  * redundant, routable view of the body (the body stays authoritative).
75
84
  */
76
85
  declare function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue>;
86
+ /**
87
+ * Overlay the out-of-band `headers` onto the contract attribute projection as String
88
+ * `MessageAttributes`, without overwriting an existing `bq-*` attribute (the contract wins a key
89
+ * collision) and skipping blanks. Keys are merged in sorted order and the merge stops at the
90
+ * 10-attribute SQS ceiling, so unbounded riders can never push the message past the limit (SQS
91
+ * rejects the whole send otherwise) — the contract attributes are always preserved first. Mirrors
92
+ * Go's `mergeAttributes` / PHP's `SqsTransport::attributes`.
93
+ */
94
+ declare function mergeAttributes(base: Record<string, SqsMessageAttributeValue>, headers: HeaderCarrier | null | undefined): Record<string, SqsMessageAttributeValue>;
95
+ /**
96
+ * Map inbound SQS `MessageAttributes` onto a flat {@link HeaderCarrier} (the consume-side
97
+ * counterpart of {@link mergeAttributes}), reading each attribute's `StringValue`. Returns an empty
98
+ * object when there are none. Both the contract `bq-*` attributes and any out-of-band rider (e.g.
99
+ * `traceparent`) surface — the core's `otel` extract reads only the keys it knows.
100
+ */
101
+ declare function headersOf(message: SqsMessage): HeaderCarrier;
77
102
  /** Options for {@link SqsPublisher}. */
78
103
  interface SqsPublisherOptions {
79
104
  /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */
@@ -87,6 +112,13 @@ interface SqsPublisherOptions {
87
112
  interface PublishOptions {
88
113
  /** Reuse an existing trace id (trace continuation). */
89
114
  traceId?: string;
115
+ /**
116
+ * Out-of-band transport headers carried as String `MessageAttributes` beside the contract `bq-*`
117
+ * attributes (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
118
+ * The contract attributes win a key collision and the merged set is capped at SQS's 10-attribute
119
+ * limit; an empty/omitted carrier leaves the publish unchanged.
120
+ */
121
+ headers?: HeaderCarrier;
90
122
  }
91
123
  /** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */
92
124
  declare class SqsPublisher {
@@ -100,8 +132,13 @@ declare class SqsPublisher {
100
132
  */
101
133
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
102
134
  }
103
- /** A URN handler. Receives the validated envelope and the raw SQS message. */
104
- type BabelHandler = (envelope: Envelope, message: SqsMessage) => unknown | Promise<unknown>;
135
+ /**
136
+ * A URN handler. Receives the validated envelope, the raw SQS message, and the out-of-band
137
+ * {@link HeaderCarrier} read from the message's `MessageAttributes` (empty when there are none).
138
+ * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of
139
+ * the producer span (ADR-0028).
140
+ */
141
+ type BabelHandler = (envelope: Envelope, message: SqsMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
105
142
  /** A map of URN → handler. */
106
143
  type BabelHandlers = Record<string, BabelHandler>;
107
144
  /** Options for {@link SqsConsumer}. */
@@ -137,4 +174,4 @@ declare class SqsConsumer {
137
174
  private delete;
138
175
  }
139
176
 
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 };
177
+ export { type BabelHandler, type BabelHandlers, type PublishOptions, type ReceiveMessageInput, type SendMessageInput, type SqsApi, SqsConsumer, type SqsConsumerOptions, 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 } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Amazon SQS adapter for BabelQueue.
@@ -26,6 +26,15 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
26
26
  * (a failed handler leaves the message for visibility-timeout redelivery); the
27
27
  * authoritative attempt count is `ApproximateReceiveCount`, surfaced to handlers as
28
28
  * `attempts = count − 1`.
29
+ *
30
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
31
+ * span linkage) rides as additional String `MessageAttributes` **beside** the contract `bq-*`
32
+ * attributes where `bq-trace-id` already lives — the contract attributes win a key collision, and
33
+ * the merged set is bounded by SQS's **10-attribute limit** (contract attributes are seeded first,
34
+ * so a rider only lands while headroom remains). On consume the inbound `MessageAttributes` are
35
+ * surfaced as a `Record<string,string>` so the core's `otel` extract sees the `traceparent` and
36
+ * links the consumer span as a child. A header-less publish is byte-identical. This mirrors the Go
37
+ * `/sqs` and PHP `SqsTransport` wiring. GR-1: the wire envelope body is never touched.
29
38
  */
30
39
 
31
40
  interface SqsMessageAttributeValue {
@@ -74,6 +83,22 @@ interface SqsApi {
74
83
  * redundant, routable view of the body (the body stays authoritative).
75
84
  */
76
85
  declare function toMessageAttributes(envelope: Envelope): Record<string, SqsMessageAttributeValue>;
86
+ /**
87
+ * Overlay the out-of-band `headers` onto the contract attribute projection as String
88
+ * `MessageAttributes`, without overwriting an existing `bq-*` attribute (the contract wins a key
89
+ * collision) and skipping blanks. Keys are merged in sorted order and the merge stops at the
90
+ * 10-attribute SQS ceiling, so unbounded riders can never push the message past the limit (SQS
91
+ * rejects the whole send otherwise) — the contract attributes are always preserved first. Mirrors
92
+ * Go's `mergeAttributes` / PHP's `SqsTransport::attributes`.
93
+ */
94
+ declare function mergeAttributes(base: Record<string, SqsMessageAttributeValue>, headers: HeaderCarrier | null | undefined): Record<string, SqsMessageAttributeValue>;
95
+ /**
96
+ * Map inbound SQS `MessageAttributes` onto a flat {@link HeaderCarrier} (the consume-side
97
+ * counterpart of {@link mergeAttributes}), reading each attribute's `StringValue`. Returns an empty
98
+ * object when there are none. Both the contract `bq-*` attributes and any out-of-band rider (e.g.
99
+ * `traceparent`) surface — the core's `otel` extract reads only the keys it knows.
100
+ */
101
+ declare function headersOf(message: SqsMessage): HeaderCarrier;
77
102
  /** Options for {@link SqsPublisher}. */
78
103
  interface SqsPublisherOptions {
79
104
  /** Treat the queue as FIFO: set `MessageGroupId` and (unless content dedup) `MessageDeduplicationId`. */
@@ -87,6 +112,13 @@ interface SqsPublisherOptions {
87
112
  interface PublishOptions {
88
113
  /** Reuse an existing trace id (trace continuation). */
89
114
  traceId?: string;
115
+ /**
116
+ * Out-of-band transport headers carried as String `MessageAttributes` beside the contract `bq-*`
117
+ * attributes (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
118
+ * The contract attributes win a key collision and the merged set is capped at SQS's 10-attribute
119
+ * limit; an empty/omitted carrier leaves the publish unchanged.
120
+ */
121
+ headers?: HeaderCarrier;
90
122
  }
91
123
  /** Sends canonical-envelope messages to one SQS queue with the §3 attribute projection. */
92
124
  declare class SqsPublisher {
@@ -100,8 +132,13 @@ declare class SqsPublisher {
100
132
  */
101
133
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
102
134
  }
103
- /** A URN handler. Receives the validated envelope and the raw SQS message. */
104
- type BabelHandler = (envelope: Envelope, message: SqsMessage) => unknown | Promise<unknown>;
135
+ /**
136
+ * A URN handler. Receives the validated envelope, the raw SQS message, and the out-of-band
137
+ * {@link HeaderCarrier} read from the message's `MessageAttributes` (empty when there are none).
138
+ * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of
139
+ * the producer span (ADR-0028).
140
+ */
141
+ type BabelHandler = (envelope: Envelope, message: SqsMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
105
142
  /** A map of URN → handler. */
106
143
  type BabelHandlers = Record<string, BabelHandler>;
107
144
  /** Options for {@link SqsConsumer}. */
@@ -137,4 +174,4 @@ declare class SqsConsumer {
137
174
  private delete;
138
175
  }
139
176
 
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 };
177
+ export { type BabelHandler, type BabelHandlers, type PublishOptions, type ReceiveMessageInput, type SendMessageInput, type SqsApi, SqsConsumer, type SqsConsumerOptions, type SqsMessage, type SqsMessageAttributeValue, SqsPublisher, type SqsPublisherOptions, headersOf, mergeAttributes, toMessageAttributes };
package/dist/index.js CHANGED
@@ -1,5 +1,20 @@
1
1
  // src/index.ts
2
2
  import { BabelQueueError, EnvelopeCodec, UnknownUrnError } 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;
3
18
  var str = (value) => ({
4
19
  DataType: "String",
5
20
  StringValue: String(value)
@@ -22,6 +37,25 @@ function toMessageAttributes(envelope) {
22
37
  }
23
38
  return attrs;
24
39
  }
40
+ function mergeAttributes(base, headers) {
41
+ const clean = sanitizeHeaders(headers);
42
+ for (const key of Object.keys(clean).sort()) {
43
+ if (key in base) continue;
44
+ if (Object.keys(base).length >= MAX_ATTRIBUTES) break;
45
+ base[key] = str(clean[key]);
46
+ }
47
+ return base;
48
+ }
49
+ function headersOf(message) {
50
+ const attrs = message.MessageAttributes;
51
+ const out = {};
52
+ if (!attrs) return out;
53
+ for (const key of Object.keys(attrs)) {
54
+ const value = attrs[key]?.StringValue;
55
+ if (value != null && value !== "") out[key] = value;
56
+ }
57
+ return out;
58
+ }
25
59
  function queueNameFromUrl(queueUrl) {
26
60
  const segments = queueUrl.split("/").filter(Boolean);
27
61
  return segments[segments.length - 1] ?? "default";
@@ -47,7 +81,7 @@ var SqsPublisher = class {
47
81
  const input = {
48
82
  QueueUrl: this.queueUrl,
49
83
  MessageBody: EnvelopeCodec.encode(envelope),
50
- MessageAttributes: toMessageAttributes(envelope)
84
+ MessageAttributes: mergeAttributes(toMessageAttributes(envelope), options.headers)
51
85
  };
52
86
  if (this.options.fifo) {
53
87
  input.MessageGroupId = this.options.messageGroupId ?? queueNameFromUrl(this.queueUrl);
@@ -125,7 +159,7 @@ var SqsConsumer = class {
125
159
  return;
126
160
  }
127
161
  try {
128
- await handler(envelope, message);
162
+ await handler(envelope, message, headersOf(message));
129
163
  await this.delete(message);
130
164
  } catch (error) {
131
165
  this.options.onError?.(error, envelope, message);
@@ -142,6 +176,8 @@ var SqsConsumer = class {
142
176
  export {
143
177
  SqsConsumer,
144
178
  SqsPublisher,
179
+ headersOf,
180
+ mergeAttributes,
145
181
  toMessageAttributes
146
182
  };
147
183
  //# 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 leaves the message for visibility-timeout redelivery); 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 } 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// --- 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\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 /** 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, headersOf(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","/**\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.1.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.4.0"
45
54
  },
46
55
  "peerDependencies": {
47
56
  "@aws-sdk/client-sqs": "^3.0.0"