@znight/messaging 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/async-CFh_bvex.mjs +216 -0
- package/dist/async-CFh_bvex.mjs.map +1 -0
- package/dist/codec-Dku-NXXU.mjs +83 -0
- package/dist/codec-Dku-NXXU.mjs.map +1 -0
- package/dist/compiler.d.mts +75 -0
- package/dist/compiler.mjs +1023 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/driver-ZxLqy_Ap.d.mts +216 -0
- package/dist/index.d.mts +459 -0
- package/dist/index.mjs +870 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kafka.d.mts +51 -0
- package/dist/kafka.mjs +517 -0
- package/dist/kafka.mjs.map +1 -0
- package/dist/memory.d.mts +124 -0
- package/dist/memory.mjs +429 -0
- package/dist/memory.mjs.map +1 -0
- package/dist/rabbitmq.d.mts +65 -0
- package/dist/rabbitmq.mjs +686 -0
- package/dist/rabbitmq.mjs.map +1 -0
- package/dist/shared-BJBpLS8V.mjs +52 -0
- package/dist/shared-BJBpLS8V.mjs.map +1 -0
- package/dist/sqs.d.mts +38 -0
- package/dist/sqs.mjs +421 -0
- package/dist/sqs.mjs.map +1 -0
- package/package.json +123 -4
- package/src/broker.ts +156 -0
- package/src/codec.ts +98 -0
- package/src/compiler/analyze.ts +800 -0
- package/src/compiler/constants.ts +73 -0
- package/src/compiler/data.ts +75 -0
- package/src/compiler/generate.ts +131 -0
- package/src/compiler/index.ts +52 -0
- package/src/compiler/patterns.ts +54 -0
- package/src/compiler/retry.ts +136 -0
- package/src/compiler/validate.ts +57 -0
- package/src/compiler/values.ts +76 -0
- package/src/consumers.ts +260 -0
- package/src/decorators.ts +131 -0
- package/src/driver.ts +142 -0
- package/src/drivers/kafka.ts +765 -0
- package/src/drivers/memory.ts +609 -0
- package/src/drivers/rabbitmq.ts +944 -0
- package/src/drivers/shared.ts +81 -0
- package/src/drivers/sqs.ts +583 -0
- package/src/errors.ts +95 -0
- package/src/headers.ts +70 -0
- package/src/hooks.ts +2 -0
- package/src/index.ts +89 -0
- package/src/internal/async.ts +115 -0
- package/src/message.ts +96 -0
- package/src/metadata.ts +65 -0
- package/src/plugin.ts +236 -0
- package/src/retry.ts +129 -0
- package/src/runtime.ts +424 -0
- package/src/standard-schema.ts +68 -0
- package/src/tokens.ts +14 -0
- package/src/validation.ts +61 -0
package/dist/sqs.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqs.mjs","names":[],"sources":["../src/drivers/sqs.ts"],"sourcesContent":["/**\n * `@znight/messaging/sqs`: Amazon SQS driver built on `@aws-sdk/client-sqs` v3 (optional peer\n * dependency). Standard and FIFO queues.\n *\n * - Long polling (`WaitTimeSeconds`, default 20 s), never more messages in flight than the consumer\n * concurrency (batches of up to 10).\n * - Attempts come from `ApproximateReceiveCount`.\n * - Retries change the visibility timeout of the message (`ChangeMessageVisibility`): the message\n * reappears after the delay (up to 12 h), no republication.\n * - The visibility of in-flight messages is extended while their handler runs (lease extension).\n * - Dead letters are sent to the dead-letter queue (`<queue>-dlq`, or the target of the queue\n * `RedrivePolicy`), then deleted from the queue. At subscribe time the driver verifies that the\n * dead-letter queue exists and that the redrive `maxReceiveCount` allows every `@Retry` attempt.\n * - Headers are message attributes (SQS allows 10 per message); binary payloads are sent base64\n * encoded (`x-znight-encoding: base64`).\n */\nimport type { Message, MessageAttributeValue, SendMessageCommandInput, SQSClientConfig } from '@aws-sdk/client-sqs'\nimport type {\n DeadLetterRequest,\n DeliveryHandler,\n DriverCapabilities,\n DriverContext,\n DriverDelivery,\n MessagingDriver,\n RetryRequest,\n SubscribeOptions,\n Subscription,\n} from '../driver.ts'\nimport type { OutboundMessage } from '../message.ts'\n\nimport {\n ChangeMessageVisibilityCommand,\n DeleteMessageCommand,\n GetQueueAttributesCommand,\n GetQueueUrlCommand,\n ReceiveMessageCommand,\n SendMessageCommand,\n SQSClient,\n} from '@aws-sdk/client-sqs'\n\nimport { bodyToBytes } from '../codec.ts'\nimport { brokerUnavailableError, consumerError } from '../errors.ts'\nimport { headerRecord, MessagingHeaders } from '../headers.ts'\nimport { errorMessage, sleep, truncate } from '../internal/async.ts'\nimport { durationOption, MAX_ERROR_HEADER_LENGTH } from './shared.ts'\n\n/** Options of {@link sqs}. */\nexport interface SqsDriverOptions {\n /** An existing client (you keep ownership: it is not destroyed on close). */\n readonly client?: SQSClient\n /** Configuration of the client created by the driver (region, endpoint, credentials...). */\n readonly clientConfig?: SQSClientConfig\n /** Queue URLs by queue name, to skip `GetQueueUrl`. Queue arguments may also be full URLs. */\n readonly queueUrls?: Readonly<Record<string, string>>\n /** Long polling duration, in seconds (0–20). Default: 20. */\n readonly waitTimeSeconds?: number\n /** Visibility timeout of received messages, in seconds. Default: the queue attribute. */\n readonly visibilityTimeout?: number\n /** Extend the visibility of messages while they are handled. Default: `true`. */\n readonly extendVisibility?: boolean\n /** Longest retry delay waited in-process. Default: 1000 ms (SQS delays have a 1 s granularity). */\n readonly maxInProcessDelayMs?: number\n /** Default dead-letter queue name. Default: `<queue>-dlq` (`<name>-dlq.fifo` for FIFO queues). */\n readonly deadLetterName?: (queue: string) => string\n}\n\n/** Longest visibility timeout (12 hours). */\nconst MAX_VISIBILITY_SECONDS = 43_200\n/** Longest `DelaySeconds` (15 minutes). */\nconst MAX_DELAY_SECONDS = 900\nconst MAX_ATTRIBUTES = 10\nconst ATTRIBUTE_NAME = /^(?!aws\\.|amazon\\.)(?!\\.)(?!.*\\.\\.)[\\w.-]{1,256}(?<!\\.)$/i\n/** Message attributes carrying transport fields (not exposed as headers). */\nconst TRANSPORT_ATTRIBUTES = new Set<string>([\n MessagingHeaders.Id,\n MessagingHeaders.Type,\n MessagingHeaders.ContentType,\n MessagingHeaders.Encoding,\n])\n\n/** Creates a {@link SqsDriver}. */\nexport function sqs(options: SqsDriverOptions = {}): SqsDriver {\n return new SqsDriver(options)\n}\n\ninterface QueueInfo {\n readonly name: string\n readonly url: string\n readonly fifo: boolean\n}\n\ninterface SqsSubscription {\n readonly queue: QueueInfo\n readonly handler: DeliveryHandler\n readonly options: SubscribeOptions\n readonly deadLetter: QueueInfo | undefined\n readonly visibilityTimeout: number\n readonly abort: AbortController\n /** Last delivery of each FIFO message group (serialised per group). */\n readonly groups: Map<string, Promise<void>>\n inflight: number\n active: boolean\n wake: (() => void) | undefined\n loop: Promise<void> | undefined\n}\n\n/** Amazon SQS {@link MessagingDriver}. See the module documentation. */\nexport class SqsDriver implements MessagingDriver {\n readonly name = 'sqs'\n readonly capabilities: DriverCapabilities\n readonly #options: SqsDriverOptions\n readonly #subscriptions = new Set<SqsSubscription>()\n readonly #queues = new Map<string, Promise<QueueInfo>>()\n #client: SQSClient | undefined\n #ownsClient = false\n #logger: DriverContext['logger'] = { debug() {}, info() {}, warn() {}, error() {} }\n #closed = false\n\n constructor(options: SqsDriverOptions = {}) {\n const wait = options.waitTimeSeconds\n if (wait !== undefined && !(Number.isInteger(wait) && wait >= 0 && wait <= 20)) {\n throw new TypeError(`[znight] sqs(): waitTimeSeconds must be an integer between 0 and 20, got ${String(wait)}`)\n }\n const visibility = options.visibilityTimeout\n if (\n visibility !== undefined &&\n !(Number.isInteger(visibility) && visibility >= 0 && visibility <= MAX_VISIBILITY_SECONDS)\n ) {\n throw new TypeError('[znight] sqs(): visibilityTimeout must be an integer number of seconds (0–43200)')\n }\n this.#options = options\n this.capabilities = Object.freeze({\n nativeDelay: true,\n maxNativeDelayMs: MAX_VISIBILITY_SECONDS * 1000,\n maxInProcessDelayMs: durationOption(options.maxInProcessDelayMs, 1000, 'sqs maxInProcessDelayMs'),\n deliveryCount: 'native',\n maxPublishDelayMs: MAX_DELAY_SECONDS * 1000,\n })\n }\n\n /** The SQS client (available once connected). */\n get client(): SQSClient | undefined {\n return this.#client\n }\n\n async connect(context: DriverContext): Promise<void> {\n await Promise.resolve()\n this.#logger = context.logger\n if (this.#closed) throw brokerUnavailableError('SQS: the driver is closed, create a new one')\n if (this.#client !== undefined) return\n if (this.#options.client === undefined) {\n this.#client = new SQSClient(this.#options.clientConfig ?? {})\n this.#ownsClient = true\n } else {\n this.#client = this.#options.client\n }\n }\n\n async close(): Promise<void> {\n if (this.#closed) return\n this.#closed = true\n const subscriptions = [...this.#subscriptions]\n this.#subscriptions.clear()\n for (const subscription of subscriptions) this.#stopPolling(subscription)\n await Promise.allSettled(subscriptions.map(subscription => subscription.loop))\n if (this.#ownsClient) this.#client?.destroy()\n this.#client = undefined\n }\n\n deadLetterName(queue: string): string {\n if (this.#options.deadLetterName !== undefined) return this.#options.deadLetterName(queue)\n const name = queueName(queue)\n return name.endsWith('.fifo') ? `${name.slice(0, -5)}-dlq.fifo` : `${name}-dlq`\n }\n\n #sqs(action: string): SQSClient {\n const client = this.#client\n if (client === undefined) {\n throw brokerUnavailableError(\n this.#closed ? `SQS: cannot ${action}, the driver is closed` : `SQS: cannot ${action} before connect()`,\n )\n }\n return client\n }\n\n /** Resolves (and caches) the URL of a queue name or URL. */\n #queue(queue: string): Promise<QueueInfo> {\n let info = this.#queues.get(queue)\n if (info === undefined) {\n info = this.#resolveQueue(queue)\n this.#queues.set(queue, info)\n info.catch(() => this.#queues.delete(queue))\n }\n return info\n }\n\n async #resolveQueue(queue: string): Promise<QueueInfo> {\n const name = queueName(queue)\n const configured = this.#options.queueUrls?.[queue]\n if (configured !== undefined) return { name, url: configured, fifo: name.endsWith('.fifo') }\n if (/^https?:\\/\\//.test(queue)) return { name, url: queue, fifo: name.endsWith('.fifo') }\n try {\n const result = await this.#sqs(`resolve the queue \"${queue}\"`).send(new GetQueueUrlCommand({ QueueName: queue }))\n if (result.QueueUrl === undefined) throw new Error('no QueueUrl in the response')\n return { name, url: result.QueueUrl, fifo: name.endsWith('.fifo') }\n } catch (error) {\n throw consumerError(`SQS: queue \"${queue}\" not found (${errorMessage(error)})`, [\n 'Create the queue, or map its URL with sqs({ queueUrls: { name: url } })',\n ])\n }\n }\n\n // -----------------------------------------------------------------------------------------------\n // Publishing\n // -----------------------------------------------------------------------------------------------\n\n async publish(queue: string, message: OutboundMessage): Promise<void> {\n const info = await this.#queue(queue)\n const binary = typeof message.body !== 'string'\n const attributes: Record<string, string> = headerRecord()\n attributes[MessagingHeaders.Id] = message.id\n if (message.type !== undefined) attributes[MessagingHeaders.Type] = message.type\n attributes[MessagingHeaders.ContentType] = message.contentType\n if (binary) attributes[MessagingHeaders.Encoding] = 'base64'\n for (const [name, value] of Object.entries(message.headers)) {\n if (TRANSPORT_ATTRIBUTES.has(name)) continue\n attributes[name] = value\n }\n const input: SendMessageCommandInput = {\n QueueUrl: info.url,\n MessageBody: encodeBody(message.body),\n MessageAttributes: toAttributes(attributes, queue),\n }\n if (message.delayMs > 0) {\n if (info.fifo) throw new RangeError(`[znight] SQS FIFO queue \"${queue}\" does not support per-message delays`)\n input.DelaySeconds = Math.min(MAX_DELAY_SECONDS, Math.ceil(message.delayMs / 1000))\n }\n if (info.fifo) {\n input.MessageGroupId = message.key ?? 'default'\n input.MessageDeduplicationId = message.id\n }\n await this.#sqs(`publish to \"${queue}\"`).send(new SendMessageCommand(input))\n }\n\n // -----------------------------------------------------------------------------------------------\n // Consuming\n // -----------------------------------------------------------------------------------------------\n\n async subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription> {\n const info = await this.#queue(queue)\n const client = this.#sqs(`subscribe to \"${queue}\"`)\n const { Attributes: attributes = {} } = await client.send(\n new GetQueueAttributesCommand({ QueueUrl: info.url, AttributeNames: ['VisibilityTimeout', 'RedrivePolicy'] }),\n )\n const visibilityTimeout = this.#options.visibilityTimeout ?? (Number(attributes.VisibilityTimeout) || 30)\n const redrive = parseRedrivePolicy(attributes.RedrivePolicy)\n if (redrive !== undefined && redrive.maxReceiveCount < options.maxAttempts) {\n throw consumerError(\n `SQS: queue \"${queue}\" moves messages to its dead-letter queue after ${redrive.maxReceiveCount} receive(s), but ${options.consumer} allows ${options.maxAttempts} attempt(s)`,\n [\n `Raise the RedrivePolicy maxReceiveCount of \"${queue}\" to at least ${options.maxAttempts}`,\n 'Or lower the @Retry attempts',\n ],\n )\n }\n let deadLetter: QueueInfo | undefined\n let dlqName = options.deadLetterQueue\n if (dlqName !== undefined && redrive !== undefined) {\n const target = queueName(redrive.deadLetterTargetArn)\n if (dlqName === this.deadLetterName(queue)) dlqName = target\n else if (dlqName !== target) {\n this.#logger.warn(\n `SQS: ${options.consumer} dead-letters to \"${dlqName}\" but the RedrivePolicy of \"${queue}\" targets \"${target}\"`,\n )\n }\n }\n if (dlqName !== undefined) deadLetter = await this.#queue(dlqName)\n\n const subscription: SqsSubscription = {\n queue: info,\n handler,\n options,\n deadLetter,\n visibilityTimeout,\n abort: new AbortController(),\n groups: new Map(),\n inflight: 0,\n active: true,\n wake: undefined,\n loop: undefined,\n }\n this.#subscriptions.add(subscription)\n subscription.loop = this.#poll(subscription)\n return {\n cancel: async (): Promise<void> => {\n this.#stopPolling(subscription)\n await subscription.loop\n },\n close: async (): Promise<void> => {\n this.#stopPolling(subscription)\n this.#subscriptions.delete(subscription)\n await subscription.loop\n },\n }\n }\n\n #stopPolling(subscription: SqsSubscription): void {\n subscription.active = false\n subscription.abort.abort()\n subscription.wake?.()\n }\n\n async #poll(subscription: SqsSubscription): Promise<void> {\n const { queue, options } = subscription\n const signal = subscription.abort.signal\n let errorDelay = 500\n while (subscription.active) {\n const free = options.concurrency - subscription.inflight\n if (free <= 0) {\n await new Promise<void>(resolve => {\n subscription.wake = resolve\n })\n subscription.wake = undefined\n continue\n }\n try {\n const result = await this.#sqs(`receive from \"${queue.name}\"`).send(\n new ReceiveMessageCommand({\n QueueUrl: queue.url,\n MaxNumberOfMessages: Math.min(10, free),\n WaitTimeSeconds: this.#options.waitTimeSeconds ?? 20,\n VisibilityTimeout: subscription.visibilityTimeout,\n MessageSystemAttributeNames: ['ApproximateReceiveCount', 'SentTimestamp', 'MessageGroupId'],\n MessageAttributeNames: ['All'],\n }),\n { abortSignal: signal },\n )\n errorDelay = 500\n for (const message of result.Messages ?? []) {\n if (subscription.active) this.#deliver(subscription, message)\n // stopped while receiving: the message becomes visible again after its visibility timeout\n }\n } catch (error) {\n if (!subscription.active) break\n this.#logger.warn(`SQS: receiving from \"${queue.name}\" failed, retrying in ${errorDelay} ms`, { err: error })\n try {\n await sleep(errorDelay, signal)\n } catch {\n break\n }\n errorDelay = Math.min(errorDelay * 2, 30_000)\n }\n }\n }\n\n #deliver(subscription: SqsSubscription, message: Message): void {\n const { queue } = subscription\n const client = this.#sqs('handle a message')\n const receiptHandle = message.ReceiptHandle as string\n const attributes = message.MessageAttributes ?? {}\n const transport = headerRecord()\n const headers = headerRecord()\n for (const [name, attribute] of Object.entries(attributes)) {\n const value = attributeToString(attribute)\n if (value === undefined) continue\n if (TRANSPORT_ATTRIBUTES.has(name)) transport[name] = value\n else headers[name] = value\n }\n const encoded = message.Body ?? ''\n const body = transport[MessagingHeaders.Encoding] === 'base64' ? Buffer.from(encoded, 'base64') : encoded\n const system = message.Attributes ?? {}\n const receiveCount = Number(system.ApproximateReceiveCount)\n const sentAt = Number(system.SentTimestamp)\n const id = transport[MessagingHeaders.Id] ?? message.MessageId ?? receiptHandle\n const groupId = system.MessageGroupId\n\n subscription.inflight++\n let lease: ReturnType<typeof setInterval> | undefined\n /** Lease extension in flight (one at a time). */\n let extending: Promise<void> | undefined\n const extendEvery = Math.floor((subscription.visibilityTimeout * 1000) / 2)\n if ((this.#options.extendVisibility ?? true) && extendEvery >= 1000) {\n lease = setInterval(() => {\n if (extending !== undefined) return\n extending = client\n .send(\n new ChangeMessageVisibilityCommand({\n QueueUrl: queue.url,\n ReceiptHandle: receiptHandle,\n VisibilityTimeout: subscription.visibilityTimeout,\n }),\n )\n .then(\n () => undefined,\n (error: unknown) => {\n this.#logger.warn(`SQS: failed to extend the visibility of message ${id}`, { err: error })\n },\n )\n .finally(() => {\n extending = undefined\n })\n }, extendEvery)\n }\n /**\n * Stops extending the lease before a settlement, once the extension in flight landed: otherwise it\n * could reach SQS after the settlement and override the visibility it set (retry delay, release).\n * When the settlement fails, the runtime releases the message (or its visibility timeout expires).\n */\n const stopLease = async (): Promise<void> => {\n if (lease !== undefined) clearInterval(lease)\n lease = undefined\n await extending\n }\n let settled = false\n const check = (action: string): void => {\n if (settled) throw new Error(`[sqs] message ${id} was already settled (${action})`)\n }\n const done = (): void => {\n settled = true\n }\n const changeVisibility = async (seconds: number): Promise<void> => {\n await client.send(\n new ChangeMessageVisibilityCommand({\n QueueUrl: queue.url,\n ReceiptHandle: receiptHandle,\n VisibilityTimeout: seconds,\n }),\n )\n }\n const remove = async (): Promise<void> => {\n await client.send(new DeleteMessageCommand({ QueueUrl: queue.url, ReceiptHandle: receiptHandle }))\n }\n const delivery: DriverDelivery = {\n id,\n body,\n contentType: transport[MessagingHeaders.ContentType],\n headers,\n type: transport[MessagingHeaders.Type],\n key: groupId,\n attempt: Number.isSafeInteger(receiveCount) && receiveCount > 0 ? receiveCount : 1,\n redelivered: receiveCount > 1,\n timestamp: Number.isFinite(sentAt) ? sentAt : undefined,\n raw: message,\n ack: async (): Promise<void> => {\n check('ack')\n await stopLease()\n await remove()\n done()\n },\n release: async (): Promise<void> => {\n check('release')\n await stopLease()\n await changeVisibility(0)\n done()\n },\n retry: async (request: RetryRequest): Promise<void> => {\n check('retry')\n await stopLease()\n await changeVisibility(Math.min(MAX_VISIBILITY_SECONDS, Math.ceil(request.delayMs / 1000)))\n done()\n },\n deadLetter: async (request: DeadLetterRequest): Promise<void> => {\n check('dead-letter')\n await stopLease()\n const target =\n request.queue === undefined ? undefined : (subscription.deadLetter ?? (await this.#queue(request.queue)))\n if (target !== undefined) {\n await client.send(\n new SendMessageCommand(\n this.#deadLetterInput(target, queue, encoded, transport, headers, request, groupId, id),\n ),\n )\n }\n await remove()\n done()\n },\n }\n // FIFO queues: messages of a group are handled one after the other, in order\n const serialKey = queue.fifo ? groupId : undefined\n const previous = serialKey === undefined ? undefined : subscription.groups.get(serialKey)\n const run = (): Promise<void> => subscription.handler(delivery)\n const handled = (previous === undefined ? run() : previous.then(run, run))\n .catch((error: unknown) => {\n this.#logger.error(`SQS: unexpected failure handling message ${id}`, { err: error })\n })\n .finally(() => {\n if (lease !== undefined) clearInterval(lease)\n subscription.inflight--\n if (serialKey !== undefined && subscription.groups.get(serialKey) === handled)\n subscription.groups.delete(serialKey)\n subscription.wake?.()\n })\n if (serialKey !== undefined) subscription.groups.set(serialKey, handled)\n }\n\n #deadLetterInput(\n target: QueueInfo,\n source: QueueInfo,\n body: string,\n transport: Readonly<Record<string, string>>,\n headers: Readonly<Record<string, string>>,\n request: DeadLetterRequest,\n groupId: string | undefined,\n id: string,\n ): SendMessageCommandInput {\n const attributes = headerRecord()\n Object.assign(attributes, transport)\n attributes[MessagingHeaders.DeadLetterReason] = request.reason\n attributes[MessagingHeaders.Error] =\n truncate(errorMessage(request.error), MAX_ERROR_HEADER_LENGTH) || 'unknown error'\n attributes[MessagingHeaders.OriginalQueue] = source.name\n attributes[MessagingHeaders.Attempts] = String(request.attempt)\n const dropped: string[] = []\n for (const [name, value] of Object.entries(headers)) {\n if (Object.keys(attributes).length >= MAX_ATTRIBUTES) dropped.push(name)\n else attributes[name] = value\n }\n if (dropped.length > 0) {\n this.#logger.warn(\n `SQS: dead-letter copy of message ${id} exceeds ${MAX_ATTRIBUTES} attributes, dropped: ${dropped.join(', ')}`,\n )\n }\n const input: SendMessageCommandInput = {\n QueueUrl: target.url,\n MessageBody: body,\n MessageAttributes: toAttributes(attributes, target.name),\n }\n if (target.fifo) {\n input.MessageGroupId = groupId ?? 'default'\n input.MessageDeduplicationId = `${id}-dead-letter`.slice(0, 128)\n }\n return input\n }\n}\n\n/** Queue name of a name, URL or ARN (last segment). */\nfunction queueName(queue: string): string {\n const segments = queue.split(/[/:]/)\n return segments[segments.length - 1] ?? queue\n}\n\nfunction encodeBody(body: Uint8Array | string): string {\n return typeof body === 'string' ? body : Buffer.from(bodyToBytes(body)).toString('base64')\n}\n\nfunction toAttributes(\n attributes: Readonly<Record<string, string>>,\n queue: string,\n): Record<string, MessageAttributeValue> {\n const entries = Object.entries(attributes)\n if (entries.length > MAX_ATTRIBUTES) {\n throw new RangeError(\n `[znight] SQS messages carry at most ${MAX_ATTRIBUTES} attributes (Znight uses up to 4): too many headers for \"${queue}\"`,\n )\n }\n const result: Record<string, MessageAttributeValue> = {}\n for (const [name, value] of entries) {\n if (!ATTRIBUTE_NAME.test(name)) throw new TypeError(`[znight] \"${name}\" is not a valid SQS message attribute name`)\n if (value === '') throw new TypeError(`[znight] SQS message attributes cannot be empty (header \"${name}\")`)\n result[name] = { DataType: 'String', StringValue: value }\n }\n return result\n}\n\nfunction attributeToString(attribute: MessageAttributeValue): string | undefined {\n if (attribute.StringValue !== undefined) return attribute.StringValue\n if (attribute.BinaryValue !== undefined) return Buffer.from(attribute.BinaryValue).toString('base64')\n return undefined\n}\n\nfunction parseRedrivePolicy(\n value: string | undefined,\n): { deadLetterTargetArn: string; maxReceiveCount: number } | undefined {\n if (value === undefined || value === '') return undefined\n try {\n const parsed = JSON.parse(value) as { deadLetterTargetArn?: unknown; maxReceiveCount?: unknown }\n const maxReceiveCount = Number(parsed.maxReceiveCount)\n if (typeof parsed.deadLetterTargetArn !== 'string' || !Number.isFinite(maxReceiveCount)) return undefined\n return { deadLetterTargetArn: parsed.deadLetterTargetArn, maxReceiveCount }\n } catch {\n return undefined\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,125 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@znight/messaging",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Znight messaging plugin: consumers, retries and dead-letter queues with RabbitMQ, SQS or Kafka",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ugho STEPHAN",
|
|
7
|
+
"homepage": "https://github.com/znight-org/znight/tree/main/packages/messaging#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/znight-org/znight.git",
|
|
11
|
+
"directory": "packages/messaging"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/znight-org/znight/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"znight",
|
|
16
|
+
"messaging",
|
|
17
|
+
"rabbitmq",
|
|
18
|
+
"amqp",
|
|
19
|
+
"sqs",
|
|
20
|
+
"kafka",
|
|
21
|
+
"consumer",
|
|
22
|
+
"queue",
|
|
23
|
+
"dead-letter-queue",
|
|
24
|
+
"retry"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"@znight/source": "./src/index.ts",
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"default": "./dist/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./memory": {
|
|
38
|
+
"@znight/source": "./src/drivers/memory.ts",
|
|
39
|
+
"types": "./dist/memory.d.mts",
|
|
40
|
+
"default": "./dist/memory.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./rabbitmq": {
|
|
43
|
+
"@znight/source": "./src/drivers/rabbitmq.ts",
|
|
44
|
+
"types": "./dist/rabbitmq.d.mts",
|
|
45
|
+
"default": "./dist/rabbitmq.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./sqs": {
|
|
48
|
+
"@znight/source": "./src/drivers/sqs.ts",
|
|
49
|
+
"types": "./dist/sqs.d.mts",
|
|
50
|
+
"default": "./dist/sqs.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./kafka": {
|
|
53
|
+
"@znight/source": "./src/drivers/kafka.ts",
|
|
54
|
+
"types": "./dist/kafka.d.mts",
|
|
55
|
+
"default": "./dist/kafka.mjs"
|
|
56
|
+
},
|
|
57
|
+
"./compiler": {
|
|
58
|
+
"@znight/source": "./src/compiler/index.ts",
|
|
59
|
+
"types": "./dist/compiler.d.mts",
|
|
60
|
+
"default": "./dist/compiler.mjs"
|
|
61
|
+
},
|
|
62
|
+
"./package.json": "./package.json"
|
|
63
|
+
},
|
|
64
|
+
"types": "./dist/index.d.mts",
|
|
65
|
+
"files": ["dist", "src"],
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsdown",
|
|
68
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p test",
|
|
69
|
+
"test": "bun test --conditions=@znight/source test",
|
|
70
|
+
"test:integration": "ZNIGHT_INTEGRATION=1 bun test --conditions=@znight/source --timeout 180000 test/integration"
|
|
71
|
+
},
|
|
72
|
+
"publishConfig": {
|
|
73
|
+
"access": "public"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@znight/core": "0.1.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependencies": {
|
|
79
|
+
"@aws-sdk/client-sqs": "^3.600.0",
|
|
80
|
+
"@platformatic/kafka": "^2.0.0",
|
|
81
|
+
"amqplib": "^2.0.0",
|
|
82
|
+
"@znight/compiler": "^0.1.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"@aws-sdk/client-sqs": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@platformatic/kafka": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"amqplib": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@znight/compiler": {
|
|
95
|
+
"optional": true
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"devDependencies": {
|
|
99
|
+
"@aws-sdk/client-sqs": "3.1156.0",
|
|
100
|
+
"@platformatic/kafka": "2.14.0",
|
|
101
|
+
"@testcontainers/kafka": "12.2.1",
|
|
102
|
+
"@testcontainers/localstack": "12.2.1",
|
|
103
|
+
"@testcontainers/rabbitmq": "12.2.1",
|
|
104
|
+
"amqplib": "2.2.0",
|
|
105
|
+
"testcontainers": "12.2.1",
|
|
106
|
+
"zod": "4.6.7",
|
|
107
|
+
"@znight/compiler": "0.1.0"
|
|
108
|
+
},
|
|
109
|
+
"znight": {
|
|
110
|
+
"apiVersion": 1,
|
|
111
|
+
"compiler": "@znight/messaging/compiler",
|
|
112
|
+
"provides": [
|
|
113
|
+
{
|
|
114
|
+
"export": "MessageBroker",
|
|
115
|
+
"plugin": "MessagingPlugin",
|
|
116
|
+
"scope": "singleton"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"export": "InboundMessage",
|
|
120
|
+
"plugin": "MessagingPlugin",
|
|
121
|
+
"scope": "request"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/broker.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Hooks } from '@znight/core'
|
|
2
|
+
import type { MessageCodec } from './codec.ts'
|
|
3
|
+
import type { MessagingDriver } from './driver.ts'
|
|
4
|
+
import type { MessagePublishEvent } from './hooks.ts'
|
|
5
|
+
import type { OutboundMessage, PublishOptions, PublishResult } from './message.ts'
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
import { brokerUnavailableError } from './errors.ts'
|
|
10
|
+
import { headerRecord } from './headers.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Publishes messages. Inject it in any provider (it is a singleton external provided by
|
|
14
|
+
* `MessagingPlugin`):
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* @Injectable()
|
|
18
|
+
* export class OrderService {
|
|
19
|
+
* constructor(private readonly broker: MessageBroker) {}
|
|
20
|
+
*
|
|
21
|
+
* async place(order: Order) {
|
|
22
|
+
* await this.broker.publish('orders', order, { type: 'order.placed', key: order.customerId })
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* `publish()` resolves once the broker confirmed the message (publisher confirms, SQS response,
|
|
28
|
+
* Kafka acknowledgement). It is usable from `onBootstrap` of the messaging plugin until the
|
|
29
|
+
* application is disposed (including providers' `onStart` / `onStop`).
|
|
30
|
+
*/
|
|
31
|
+
export abstract class MessageBroker {
|
|
32
|
+
/**
|
|
33
|
+
* Publishes `payload` to `queue` (a queue for RabbitMQ/SQS, a topic for Kafka). The payload is
|
|
34
|
+
* encoded by the plugin codec (JSON by default; `Uint8Array` is sent as binary).
|
|
35
|
+
*/
|
|
36
|
+
abstract publish(queue: string, payload: unknown, options?: PublishOptions): Promise<PublishResult>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const HEADER_NAME = /^[\x21-\x7e]+$/
|
|
40
|
+
|
|
41
|
+
/** The {@link MessageBroker} registered by `MessagingPlugin` (internal). */
|
|
42
|
+
export class DriverMessageBroker extends MessageBroker {
|
|
43
|
+
#driver: MessagingDriver | undefined
|
|
44
|
+
#codec: MessageCodec | undefined
|
|
45
|
+
#hooks: Hooks | undefined
|
|
46
|
+
#closed = false
|
|
47
|
+
|
|
48
|
+
/** Connects the broker to the driver (plugin `onBootstrap`). */
|
|
49
|
+
bind(driver: MessagingDriver, codec: MessageCodec, hooks: Hooks): void {
|
|
50
|
+
this.#driver = driver
|
|
51
|
+
this.#codec = codec
|
|
52
|
+
this.#hooks = hooks
|
|
53
|
+
this.#closed = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Rejects further publications (plugin `onDispose`). */
|
|
57
|
+
unbind(): void {
|
|
58
|
+
this.#closed = true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override async publish(queue: string, payload: unknown, options: PublishOptions = {}): Promise<PublishResult> {
|
|
62
|
+
const driver = this.#driver
|
|
63
|
+
const codec = this.#codec
|
|
64
|
+
if (driver === undefined || codec === undefined) {
|
|
65
|
+
throw brokerUnavailableError(
|
|
66
|
+
`MessageBroker.publish('${queue}'): the messaging plugin is not bootstrapped yet (publish from onStart or later)`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
if (this.#closed) {
|
|
70
|
+
throw brokerUnavailableError(`MessageBroker.publish('${queue}'): the messaging plugin has been disposed`)
|
|
71
|
+
}
|
|
72
|
+
const message = this.#prepare(queue, payload, options, codec, driver)
|
|
73
|
+
const hooks = this.#hooks
|
|
74
|
+
const wrapped = hooks?.hasWrappers('messaging:publish') ?? false
|
|
75
|
+
const observed = hooks?.hasListeners('messaging:publish') ?? false
|
|
76
|
+
if (hooks === undefined || (!wrapped && !observed)) {
|
|
77
|
+
await driver.publish(queue, message.outbound())
|
|
78
|
+
return { id: message.id }
|
|
79
|
+
}
|
|
80
|
+
const event: MessagePublishEvent = {
|
|
81
|
+
queue,
|
|
82
|
+
type: options.type,
|
|
83
|
+
messageId: message.id,
|
|
84
|
+
headers: message.headers,
|
|
85
|
+
startTime: performance.now(),
|
|
86
|
+
}
|
|
87
|
+
const send = async (): Promise<void> => {
|
|
88
|
+
try {
|
|
89
|
+
await driver.publish(queue, message.outbound())
|
|
90
|
+
} catch (error) {
|
|
91
|
+
event.error = error // visible to the wrappers before the rejection reaches them
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
if (wrapped) await hooks.run('messaging:publish', event, send)
|
|
97
|
+
else await send()
|
|
98
|
+
} finally {
|
|
99
|
+
if (observed) hooks.emit('messaging:publish', event)
|
|
100
|
+
}
|
|
101
|
+
return { id: message.id }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#prepare(
|
|
105
|
+
queue: string,
|
|
106
|
+
payload: unknown,
|
|
107
|
+
options: PublishOptions,
|
|
108
|
+
codec: MessageCodec,
|
|
109
|
+
driver: MessagingDriver,
|
|
110
|
+
): { readonly id: string; readonly headers: Record<string, string>; outbound(): OutboundMessage } {
|
|
111
|
+
if (typeof queue !== 'string' || queue === '') {
|
|
112
|
+
throw new TypeError(
|
|
113
|
+
`[znight] MessageBroker.publish(): the queue must be a non-empty string, got ${String(queue)}`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
const delayMs = options.delay ?? 0
|
|
117
|
+
if (typeof delayMs !== 'number' || !Number.isFinite(delayMs) || delayMs < 0) {
|
|
118
|
+
throw new TypeError(`[znight] MessageBroker.publish('${queue}'): "delay" must be a number of milliseconds >= 0`)
|
|
119
|
+
}
|
|
120
|
+
if (delayMs > driver.capabilities.maxPublishDelayMs) {
|
|
121
|
+
throw new RangeError(
|
|
122
|
+
driver.capabilities.maxPublishDelayMs === 0
|
|
123
|
+
? `[znight] MessageBroker.publish('${queue}'): the ${driver.name} driver does not support delayed publishing`
|
|
124
|
+
: `[znight] MessageBroker.publish('${queue}'): the ${driver.name} driver supports delays up to ${driver.capabilities.maxPublishDelayMs} ms (got ${delayMs})`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const headers = headerRecord()
|
|
128
|
+
if (options.headers !== undefined) {
|
|
129
|
+
for (const [name, value] of Object.entries(options.headers)) {
|
|
130
|
+
if (!HEADER_NAME.test(name))
|
|
131
|
+
throw new TypeError(`[znight] MessageBroker.publish('${queue}'): invalid header name "${name}"`)
|
|
132
|
+
if (typeof value !== 'string') {
|
|
133
|
+
throw new TypeError(`[znight] MessageBroker.publish('${queue}'): header "${name}" must be a string`)
|
|
134
|
+
}
|
|
135
|
+
headers[name] = value
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const id = options.id ?? randomUUID()
|
|
139
|
+
const encoded = codec.encode(payload, options.contentType)
|
|
140
|
+
const timestamp = Date.now()
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
headers,
|
|
144
|
+
outbound: () => ({
|
|
145
|
+
id,
|
|
146
|
+
type: options.type,
|
|
147
|
+
key: options.key,
|
|
148
|
+
headers: { ...headers },
|
|
149
|
+
body: encoded.body,
|
|
150
|
+
contentType: encoded.contentType,
|
|
151
|
+
timestamp,
|
|
152
|
+
delayMs,
|
|
153
|
+
}),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { MessageDecodeError } from './errors.ts'
|
|
2
|
+
|
|
3
|
+
/** An encoded body and its content type. */
|
|
4
|
+
export interface EncodedBody {
|
|
5
|
+
readonly body: Uint8Array | string
|
|
6
|
+
readonly contentType: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts payloads to bodies and back. The default {@link jsonCodec} handles JSON, text and binary
|
|
11
|
+
* payloads; provide your own (MessagePack, Avro...) with `MessagingPlugin.configure({ codec })`.
|
|
12
|
+
*/
|
|
13
|
+
export interface MessageCodec {
|
|
14
|
+
/**
|
|
15
|
+
* Encodes a payload for publication. `contentType` is the one requested by the publisher, if any
|
|
16
|
+
* (`publish(queue, payload, { contentType })`).
|
|
17
|
+
*/
|
|
18
|
+
encode(payload: unknown, contentType: string | undefined): EncodedBody
|
|
19
|
+
/**
|
|
20
|
+
* Decodes a received body. Throw a {@link MessageDecodeError} (a `NonRetryableError`) for malformed
|
|
21
|
+
* bodies: the message is dead-lettered with reason `decode`.
|
|
22
|
+
*/
|
|
23
|
+
decode(body: Uint8Array | string, contentType: string | undefined): unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const JSON_CONTENT_TYPE = 'application/json'
|
|
27
|
+
export const TEXT_CONTENT_TYPE = 'text/plain; charset=utf-8'
|
|
28
|
+
export const BINARY_CONTENT_TYPE = 'application/octet-stream'
|
|
29
|
+
|
|
30
|
+
const encoder = new TextEncoder()
|
|
31
|
+
const decoder = new TextDecoder('utf-8')
|
|
32
|
+
|
|
33
|
+
/** Body as text (UTF-8). */
|
|
34
|
+
export function bodyToText(body: Uint8Array | string): string {
|
|
35
|
+
return typeof body === 'string' ? body : decoder.decode(body)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Body as bytes (UTF-8 for strings). */
|
|
39
|
+
export function bodyToBytes(body: Uint8Array | string): Uint8Array {
|
|
40
|
+
return typeof body === 'string' ? encoder.encode(body) : body
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Lower-cased media type without parameters (`application/json; charset=utf-8` → `application/json`). */
|
|
44
|
+
export function mediaType(contentType: string | undefined): string | undefined {
|
|
45
|
+
if (contentType === undefined) return undefined
|
|
46
|
+
const value = contentType.split(';', 1)[0]?.trim().toLowerCase()
|
|
47
|
+
return value === '' ? undefined : value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isJson(type: string): boolean {
|
|
51
|
+
return type === JSON_CONTENT_TYPE || type.endsWith('+json')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The default codec.
|
|
56
|
+
*
|
|
57
|
+
* - encode: `Uint8Array` → `application/octet-stream`; a `string` with an explicit text/binary
|
|
58
|
+
* `contentType` is sent as is; everything else is JSON (`application/json`), including strings.
|
|
59
|
+
* `undefined` is an empty JSON body.
|
|
60
|
+
* - decode: JSON (`application/json`, `*+json`) is parsed (empty body → `undefined`; malformed →
|
|
61
|
+
* `MessageDecodeError`); `text/*` → string; other declared types → `Uint8Array`. Without content
|
|
62
|
+
* type, JSON is attempted and the text is returned when it is not valid JSON.
|
|
63
|
+
*/
|
|
64
|
+
export const jsonCodec: MessageCodec = Object.freeze({
|
|
65
|
+
encode(payload: unknown, contentType: string | undefined): EncodedBody {
|
|
66
|
+
const type = mediaType(contentType)
|
|
67
|
+
if (payload instanceof Uint8Array) return { body: payload, contentType: contentType ?? BINARY_CONTENT_TYPE }
|
|
68
|
+
if (typeof payload === 'string' && type !== undefined && !isJson(type))
|
|
69
|
+
return { body: payload, contentType: contentType as string }
|
|
70
|
+
if (payload === undefined) return { body: '', contentType: contentType ?? JSON_CONTENT_TYPE }
|
|
71
|
+
const json = JSON.stringify(payload)
|
|
72
|
+
if (json === undefined) throw new TypeError(`[znight] Cannot encode a ${typeof payload} payload as JSON`)
|
|
73
|
+
return { body: json, contentType: contentType ?? JSON_CONTENT_TYPE }
|
|
74
|
+
},
|
|
75
|
+
decode(body: Uint8Array | string, contentType: string | undefined): unknown {
|
|
76
|
+
const type = mediaType(contentType)
|
|
77
|
+
if (type === undefined) {
|
|
78
|
+
const text = bodyToText(body)
|
|
79
|
+
if (text === '') return undefined
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text)
|
|
82
|
+
} catch {
|
|
83
|
+
return text
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (isJson(type)) {
|
|
87
|
+
const text = bodyToText(body)
|
|
88
|
+
if (text.trim() === '') return undefined
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(text)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new MessageDecodeError(`Malformed JSON body: ${(error as Error).message}`, { cause: error })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (type.startsWith('text/')) return bodyToText(body)
|
|
96
|
+
return bodyToBytes(body)
|
|
97
|
+
},
|
|
98
|
+
})
|