@outbox-event-bus/dynamodb-aws-sdk-outbox 1.0.3 → 1.1.1

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/dist/index.cjs CHANGED
@@ -167,11 +167,11 @@ var DynamoDBAwsSdkOutbox = class {
167
167
  const status = isFinalFailure ? outbox_event_bus.EventStatus.FAILED : outbox_event_bus.EventStatus.CREATED;
168
168
  const errorMsg = (0, outbox_event_bus.formatErrorMessage)(error);
169
169
  const now = Date.now();
170
- const updateExpression = isFinalFailure ? "SET #status = :status, retryCount = :rc, lastError = :err, nextRetryAt = :now REMOVE gsiSortKey" : "SET #status = :status, retryCount = :rc, lastError = :err, gsiSortKey = :nextAttempt";
170
+ const updateExpression = isFinalFailure ? "SET #status = :status, retryCount = :rc, lastError = :error, nextRetryAt = :now REMOVE gsiSortKey" : "SET #status = :status, retryCount = :rc, lastError = :error, gsiSortKey = :nextAttempt";
171
171
  const expressionAttributeValues = {
172
172
  ":status": status,
173
173
  ":rc": retryCount,
174
- ":err": errorMsg
174
+ ":error": errorMsg
175
175
  };
176
176
  if (isFinalFailure) expressionAttributeValues[":now"] = now;
177
177
  else expressionAttributeValues[":nextAttempt"] = now + this.poller.calculateBackoff(retryCount);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["DynamoDBDocumentClient","PollingService","BatchSizeLimitError","EventStatus","TransactWriteCommand","DocQueryCommand","event: FailedBusEvent","UpdateCommand","event: BusEvent","ConditionalCheckFailedException","expressionAttributeValues: Record<string, any>","AsyncLocalStorage","items: TransactWriteItem[]","collector: DynamoDBAwsSdkTransactionCollector"],"sources":["../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import { ConditionalCheckFailedException, type DynamoDBClient } from \"@aws-sdk/client-dynamodb\"\nimport {\n QueryCommand as DocQueryCommand,\n DynamoDBDocumentClient,\n TransactWriteCommand,\n type TransactWriteCommandInput,\n UpdateCommand,\n} from \"@aws-sdk/lib-dynamodb\"\nimport {\n BatchSizeLimitError,\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\n// DynamoDB has a hard limit of 100 items per transaction\nconst DYNAMODB_TRANSACTION_LIMIT = 100\n\nexport type TransactWriteItem = NonNullable<TransactWriteCommandInput[\"TransactItems\"]>[number]\n\nexport type DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => void\n items?: TransactWriteItem[]\n}\n\nexport interface DynamoDBAwsSdkOutboxConfig extends OutboxConfig {\n client: DynamoDBClient\n tableName: string\n statusIndexName?: string\n processingTimeoutMs?: number // Time before a PROCESSING event is considered stuck\n getCollector?: (() => DynamoDBAwsSdkTransactionCollector | undefined) | undefined\n}\n\nexport class DynamoDBAwsSdkOutbox implements IOutbox<DynamoDBAwsSdkTransactionCollector> {\n private readonly config: Required<DynamoDBAwsSdkOutboxConfig>\n private readonly docClient: DynamoDBDocumentClient\n private readonly poller: PollingService\n\n constructor(config: DynamoDBAwsSdkOutboxConfig) {\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n tableName: config.tableName,\n statusIndexName: config.statusIndexName ?? \"status-gsiSortKey-index\",\n client: config.client,\n getCollector: config.getCollector,\n }\n\n this.docClient = DynamoDBDocumentClient.from(config.client, {\n marshallOptions: { removeUndefinedValues: true },\n })\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n performMaintenance: () => this.recoverStuckEvents(),\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n async publish(\n events: BusEvent[],\n transaction?: DynamoDBAwsSdkTransactionCollector\n ): Promise<void> {\n if (events.length === 0) return\n\n if (events.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, events.length)\n }\n\n const collector = transaction ?? this.config.getCollector?.()\n\n const items = events.map((event) => {\n return {\n Put: {\n TableName: this.config.tableName,\n Item: {\n id: event.id,\n type: event.type,\n payload: event.payload,\n occurredAt: event.occurredAt.toISOString(),\n status: EventStatus.CREATED,\n retryCount: 0,\n gsiSortKey: event.occurredAt.getTime(),\n },\n },\n }\n })\n\n if (collector) {\n const itemsInCollector = collector.items?.length ?? 0\n\n if (itemsInCollector + items.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, itemsInCollector + items.length)\n }\n\n for (const item of items) {\n collector.push(item)\n }\n } else {\n await this.docClient.send(\n new TransactWriteCommand({\n TransactItems: items,\n })\n )\n }\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: { \":status\": EventStatus.FAILED },\n Limit: 100,\n ScanIndexForward: false,\n })\n )\n\n if (!result.Items) return []\n\n return result.Items.map((item) => {\n const event: FailedBusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n retryCount: item.retryCount || 0,\n }\n if (item.lastError) event.error = item.lastError\n if (item.startedOn) event.lastAttemptAt = new Date(item.startedOn)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n await Promise.all(\n eventIds.map((id) =>\n this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression:\n \"SET #status = :pending, retryCount = :zero, gsiSortKey = :now REMOVE lastError, nextRetryAt, startedOn\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":pending\": EventStatus.CREATED,\n \":zero\": 0,\n \":now\": Date.now(),\n },\n })\n )\n )\n )\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: {\n \"#status\": \"status\",\n },\n ExpressionAttributeValues: {\n \":status\": EventStatus.CREATED,\n \":now\": now,\n },\n Limit: this.config.batchSize,\n })\n )\n\n if (!result.Items || result.Items.length === 0) return\n\n for (const item of result.Items) {\n const event: BusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n }\n\n try {\n await this.markEventAsProcessing(item.id, now)\n await handler(event)\n await this.markEventAsCompleted(event.id)\n } catch (error) {\n if (error instanceof ConditionalCheckFailedException) {\n continue\n }\n\n const newRetryCount = (item.retryCount || 0) + 1\n reportEventError(this.poller.onError, error, event, newRetryCount, this.config.maxRetries)\n\n await this.markEventAsFailed(item.id, newRetryCount, error)\n }\n }\n }\n\n private parseOccurredAt(occurredAt: unknown): Date {\n if (occurredAt instanceof Date) {\n return occurredAt\n }\n if (typeof occurredAt === \"string\") {\n return new Date(occurredAt)\n }\n return new Date()\n }\n\n private async markEventAsProcessing(id: string, now: number): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :processing, gsiSortKey = :timeoutAt, startedOn = :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":processing\": EventStatus.ACTIVE,\n \":timeoutAt\": now + this.config.processingTimeoutMs,\n \":now\": now,\n },\n })\n )\n }\n\n private async markEventAsCompleted(id: string): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :completed, completedOn = :now REMOVE gsiSortKey\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":completed\": EventStatus.COMPLETED,\n \":now\": Date.now(),\n },\n })\n )\n }\n\n private async markEventAsFailed(id: string, retryCount: number, error: unknown): Promise<void> {\n const isFinalFailure = retryCount >= this.config.maxRetries\n const status = isFinalFailure ? EventStatus.FAILED : EventStatus.CREATED\n const errorMsg = formatErrorMessage(error)\n const now = Date.now()\n\n const updateExpression = isFinalFailure\n ? \"SET #status = :status, retryCount = :rc, lastError = :err, nextRetryAt = :now REMOVE gsiSortKey\"\n : \"SET #status = :status, retryCount = :rc, lastError = :err, gsiSortKey = :nextAttempt\"\n\n const expressionAttributeValues: Record<string, any> = {\n \":status\": status,\n \":rc\": retryCount,\n \":err\": errorMsg,\n }\n\n if (isFinalFailure) {\n expressionAttributeValues[\":now\"] = now\n } else {\n const delay = this.poller.calculateBackoff(retryCount)\n expressionAttributeValues[\":nextAttempt\"] = now + delay\n }\n\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: expressionAttributeValues,\n })\n )\n }\n\n private async recoverStuckEvents() {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":status\": EventStatus.ACTIVE,\n \":now\": now,\n },\n })\n )\n\n if (result.Items && result.Items.length > 0) {\n await Promise.all(\n result.Items.map((item) =>\n this.markEventAsFailed(item.id, (item.retryCount || 0) + 1, \"Processing timeout\")\n )\n )\n }\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type {\n DynamoDBAwsSdkTransactionCollector,\n TransactWriteItem,\n} from \"./dynamodb-aws-sdk-outbox\"\n\nexport const dynamodbAwsSdkTransactionStorage =\n new AsyncLocalStorage<DynamoDBAwsSdkTransactionCollector>()\n\nexport async function withDynamoDBAwsSdkTransaction<T>(\n fn: (collector: DynamoDBAwsSdkTransactionCollector) => Promise<T>\n): Promise<T> {\n const items: TransactWriteItem[] = []\n const collector: DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => items.push(item),\n get items() {\n return items\n },\n }\n return dynamodbAwsSdkTransactionStorage.run(collector, () => fn(collector))\n}\n\nexport function getDynamoDBAwsSdkCollector(): () => DynamoDBAwsSdkTransactionCollector | undefined {\n return () => dynamodbAwsSdkTransactionStorage.getStore()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["DynamoDBDocumentClient","PollingService","BatchSizeLimitError","EventStatus","TransactWriteCommand","DocQueryCommand","event: FailedBusEvent","UpdateCommand","event: BusEvent","ConditionalCheckFailedException","expressionAttributeValues: Record<string, any>","AsyncLocalStorage","items: TransactWriteItem[]","collector: DynamoDBAwsSdkTransactionCollector"],"sources":["../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import { ConditionalCheckFailedException, type DynamoDBClient } from \"@aws-sdk/client-dynamodb\"\nimport {\n QueryCommand as DocQueryCommand,\n DynamoDBDocumentClient,\n TransactWriteCommand,\n type TransactWriteCommandInput,\n UpdateCommand,\n} from \"@aws-sdk/lib-dynamodb\"\nimport {\n BatchSizeLimitError,\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\n// DynamoDB has a hard limit of 100 items per transaction\nconst DYNAMODB_TRANSACTION_LIMIT = 100\n\nexport type TransactWriteItem = NonNullable<TransactWriteCommandInput[\"TransactItems\"]>[number]\n\nexport type DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => void\n items?: TransactWriteItem[]\n}\n\nexport interface DynamoDBAwsSdkOutboxConfig extends OutboxConfig {\n client: DynamoDBClient\n tableName: string\n statusIndexName?: string\n processingTimeoutMs?: number // Time before a PROCESSING event is considered stuck\n getCollector?: (() => DynamoDBAwsSdkTransactionCollector | undefined) | undefined\n}\n\nexport class DynamoDBAwsSdkOutbox implements IOutbox<DynamoDBAwsSdkTransactionCollector> {\n private readonly config: Required<DynamoDBAwsSdkOutboxConfig>\n private readonly docClient: DynamoDBDocumentClient\n private readonly poller: PollingService\n\n constructor(config: DynamoDBAwsSdkOutboxConfig) {\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n tableName: config.tableName,\n statusIndexName: config.statusIndexName ?? \"status-gsiSortKey-index\",\n client: config.client,\n getCollector: config.getCollector,\n }\n\n this.docClient = DynamoDBDocumentClient.from(config.client, {\n marshallOptions: { removeUndefinedValues: true },\n })\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n performMaintenance: () => this.recoverStuckEvents(),\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n async publish(\n events: BusEvent[],\n transaction?: DynamoDBAwsSdkTransactionCollector\n ): Promise<void> {\n if (events.length === 0) return\n\n if (events.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, events.length)\n }\n\n const collector = transaction ?? this.config.getCollector?.()\n\n const items = events.map((event) => {\n return {\n Put: {\n TableName: this.config.tableName,\n Item: {\n id: event.id,\n type: event.type,\n payload: event.payload,\n occurredAt: event.occurredAt.toISOString(),\n status: EventStatus.CREATED,\n retryCount: 0,\n gsiSortKey: event.occurredAt.getTime(),\n },\n },\n }\n })\n\n if (collector) {\n const itemsInCollector = collector.items?.length ?? 0\n\n if (itemsInCollector + items.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, itemsInCollector + items.length)\n }\n\n for (const item of items) {\n collector.push(item)\n }\n } else {\n await this.docClient.send(\n new TransactWriteCommand({\n TransactItems: items,\n })\n )\n }\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: { \":status\": EventStatus.FAILED },\n Limit: 100,\n ScanIndexForward: false,\n })\n )\n\n if (!result.Items) return []\n\n return result.Items.map((item) => {\n const event: FailedBusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n retryCount: item.retryCount || 0,\n }\n if (item.lastError) event.error = item.lastError\n if (item.startedOn) event.lastAttemptAt = new Date(item.startedOn)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n await Promise.all(\n eventIds.map((id) =>\n this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression:\n \"SET #status = :pending, retryCount = :zero, gsiSortKey = :now REMOVE lastError, nextRetryAt, startedOn\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":pending\": EventStatus.CREATED,\n \":zero\": 0,\n \":now\": Date.now(),\n },\n })\n )\n )\n )\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: {\n \"#status\": \"status\",\n },\n ExpressionAttributeValues: {\n \":status\": EventStatus.CREATED,\n \":now\": now,\n },\n Limit: this.config.batchSize,\n })\n )\n\n if (!result.Items || result.Items.length === 0) return\n\n for (const item of result.Items) {\n const event: BusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n }\n\n try {\n await this.markEventAsProcessing(item.id, now)\n await handler(event)\n await this.markEventAsCompleted(event.id)\n } catch (error) {\n if (error instanceof ConditionalCheckFailedException) {\n continue\n }\n\n const newRetryCount = (item.retryCount || 0) + 1\n reportEventError(this.poller.onError, error, event, newRetryCount, this.config.maxRetries)\n\n await this.markEventAsFailed(item.id, newRetryCount, error)\n }\n }\n }\n\n private parseOccurredAt(occurredAt: unknown): Date {\n if (occurredAt instanceof Date) {\n return occurredAt\n }\n if (typeof occurredAt === \"string\") {\n return new Date(occurredAt)\n }\n return new Date()\n }\n\n private async markEventAsProcessing(id: string, now: number): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :processing, gsiSortKey = :timeoutAt, startedOn = :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":processing\": EventStatus.ACTIVE,\n \":timeoutAt\": now + this.config.processingTimeoutMs,\n \":now\": now,\n },\n })\n )\n }\n\n private async markEventAsCompleted(id: string): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :completed, completedOn = :now REMOVE gsiSortKey\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":completed\": EventStatus.COMPLETED,\n \":now\": Date.now(),\n },\n })\n )\n }\n\n private async markEventAsFailed(id: string, retryCount: number, error: unknown): Promise<void> {\n const isFinalFailure = retryCount >= this.config.maxRetries\n const status = isFinalFailure ? EventStatus.FAILED : EventStatus.CREATED\n const errorMsg = formatErrorMessage(error)\n const now = Date.now()\n\n const updateExpression = isFinalFailure\n ? \"SET #status = :status, retryCount = :rc, lastError = :error, nextRetryAt = :now REMOVE gsiSortKey\"\n : \"SET #status = :status, retryCount = :rc, lastError = :error, gsiSortKey = :nextAttempt\"\n\n const expressionAttributeValues: Record<string, any> = {\n \":status\": status,\n \":rc\": retryCount,\n \":error\": errorMsg,\n }\n\n if (isFinalFailure) {\n expressionAttributeValues[\":now\"] = now\n } else {\n const delay = this.poller.calculateBackoff(retryCount)\n expressionAttributeValues[\":nextAttempt\"] = now + delay\n }\n\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: expressionAttributeValues,\n })\n )\n }\n\n private async recoverStuckEvents() {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":status\": EventStatus.ACTIVE,\n \":now\": now,\n },\n })\n )\n\n if (result.Items && result.Items.length > 0) {\n await Promise.all(\n result.Items.map((item) =>\n this.markEventAsFailed(item.id, (item.retryCount || 0) + 1, \"Processing timeout\")\n )\n )\n }\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type {\n DynamoDBAwsSdkTransactionCollector,\n TransactWriteItem,\n} from \"./dynamodb-aws-sdk-outbox\"\n\nexport const dynamodbAwsSdkTransactionStorage =\n new AsyncLocalStorage<DynamoDBAwsSdkTransactionCollector>()\n\nexport async function withDynamoDBAwsSdkTransaction<T>(\n fn: (collector: DynamoDBAwsSdkTransactionCollector) => Promise<T>\n): Promise<T> {\n const items: TransactWriteItem[] = []\n const collector: DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => items.push(item),\n get items() {\n return items\n },\n }\n return dynamodbAwsSdkTransactionStorage.run(collector, () => fn(collector))\n}\n\nexport function getDynamoDBAwsSdkCollector(): () => DynamoDBAwsSdkTransactionCollector | undefined {\n return () => dynamodbAwsSdkTransactionStorage.getStore()\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;;UAEiB,kBAAA;UACP,WAAW;;;AADrB;AAMsB,uBAAA,WAAW,CAAA,iBACd,kBADc,GACO,kBADP,CAAA,SAEvB,KAAA,CAFuB;EACd,OAAA,CAAA,EAEA,QAFA,GAAA,SAAA;EAAqB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAIe,QAJf;;;;KCL5B;;QAEJ;EDJS,OAAA,ECKN,CDLM;EAMK,UAAA,ECAR,IDAmB;EACd,QAAA,CAAA,ECAN,MDAM,CAAA,MAAA,EAAA,OAAA,CAAA;CAAqB;AAErB,KCCP,cDDO,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCCkD,QDDlD,CCC2D,CDD3D,ECC8D,CDD9D,CAAA,GAAA;EAEoC,KAAA,CAAA,EAAA,MAAA;EAH7C,UAAA,EAAA,MAAA;EAAK,aAAA,CAAA,ECKG,IDLH;;ACJP,KAuBI,YAAA,GAvBJ,CAAA,KAAA,EAuB2B,WAvB3B,EAAA,GAAA,IAAA;;;UCJS;oBACG,0BAA0B,iBAAiB;2BACpC,aAAa,wBAAwB;cAClD;EFHG,eAAA,EAAA,GAAA,GEIQ,OFJU,CEIF,cFHZ,EAAA,CAAA;EAKC,WAAA,EAAA,CAAW,QAAA,EAAA,MAAA,EAAA,EAAA,GEDM,OFCN,CAAA,IAAA,CAAA;;UEsDhB,YAAA;EA5DA,UAAO,CAAA,EAAA,MAAA;EACJ,aAAA,CAAA,EAAA,MAAA;EAA0B,cAAA,CAAA,EAAA,MAAA;EAAiB,SAAA,CAAA,EAAA,MAAA;EACpC,mBAAA,CAAA,EAAA,MAAA;EAAa,iBAAA,CAAA,EAAA,MAAA;;;;KCoB5B,iBAAA,GAAoB,YAAY;KAEhC,kCAAA;EHxBK,IAAA,EAAA,CAAA,IAAA,EGyBF,iBHzBoB,EAAA,GACzB,IAAA;EAKY,KAAA,CAAA,EGoBZ,iBHpBuB,EAAA;CACd;AAAqB,UGsBvB,0BAAA,SAAmC,YHtBZ,CAAA;EAErB,MAAA,EGqBT,cHrBS;EAEoC,SAAA,EAAA,MAAA;EAH7C,eAAA,CAAA,EAAA,MAAA;EAAK,mBAAA,CAAA,EAAA,MAAA;wBG0BS;;cAGX,oBAAA,YAAgC,QAAQ;EFnCzC,iBAAQ,MAAA;EAEZ,iBAAA,SAAA;EACG,iBAAA,MAAA;EACG,WAAA,CAAA,MAAA,EEoCQ,0BFpCR;EACD,OAAA,CAAA,MAAA,EE+DD,QF/DC,EAAA,EAAA,WAAA,CAAA,EEgEK,kCFhEL,CAAA,EEiER,OFjEQ,CAAA,IAAA,CAAA;EAAM,eAAA,CAAA,CAAA,EE8GQ,OF9GR,CE8GgB,cF9GhB,EAAA,CAAA;EAGP,WAAA,CAAA,QAAc,EAAA,MAAA,EAAA,CAAA,EEwIe,OFxIf,CAAA,IAAA,CAAA;EAAoD,KAAA,CAAA,OAAA,EAAA,CAAA,KAAA,EE+JrD,QF/JqD,EAAA,GE+JxC,OF/JwC,CAAA,IAAA,CAAA,EAAA,OAAA,EE+JhB,YF/JgB,CAAA,EAAA,IAAA;EAAG,IAAA,CAAA,CAAA,EEmKjE,OFnKiE,CAAA,IAAA,CAAA;EAAZ,QAAA,YAAA;EAGnD,QAAA,eAAA;EAAI,QAAA,qBAAA;EAcV,QAAA,oBAAuB;;;;;;cGvBtB,kCAAgC,kBAAA;iBAGvB,iDACJ,uCAAuC,QAAQ,KAC9D,QAAQ;iBAWK,0BAAA,CAAA,SAAoC"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;;UAEiB,kBAAA;UACP,WAAW;;;AADrB;AAMsB,uBAAA,WAAW,CAAA,iBACd,kBADc,GACO,kBADP,CAAA,SAEvB,KAAA,CAFuB;EACd,OAAA,CAAA,EAEA,QAFA,GAAA,SAAA;EAAqB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAIe,QAJf;;;;KCL5B;;QAEJ;EDJS,OAAA,ECKN,CDLM;EAMK,UAAA,ECAR,IDAmB;EACd,QAAA,CAAA,ECAN,MDAM,CAAA,MAAA,EAAA,OAAA,CAAA;CAAqB;AAErB,KCCP,cDDO,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCCkD,QDDlD,CCC2D,CDD3D,ECC8D,CDD9D,CAAA,GAAA;EAEoC,KAAA,CAAA,EAAA,MAAA;EAH7C,UAAA,EAAA,MAAA;EAAK,aAAA,CAAA,ECKG,IDLH;;ACJP,KAuBI,YAAA,GAvBJ,CAAA,KAAA,EAuB2B,WAvB3B,EAAA,GAAA,IAAA;;;UCHS;oBACG,0BAA0B,iBAAiB;2BACpC,aAAa,wBAAwB;EFH/C,IAAA,EAAA,GAAA,GEIH,OFJG,CAAA,IAAkB,CAAA;EAMb,eAAW,EAAA,GAAA,GEDR,OFCQ,CEDA,cFCA,EAAA,CAAA;EACd,WAAA,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,EAAA,GEDoB,OFCpB,CAAA,IAAA,CAAA;;AELC,UA4DH,YAAA,CA5DG;EAA0B,UAAA,CAAA,EAAA,MAAA;EAAiB,aAAA,CAAA,EAAA,MAAA;EACpC,cAAA,CAAA,EAAA,MAAA;EAAa,SAAA,CAAA,EAAA,MAAA;EAAwB,mBAAA,CAAA,EAAA,MAAA;EAClD,iBAAA,CAAA,EAAA,MAAA;;;;KCkBF,iBAAA,GAAoB,YAAY;KAEhC,kCAAA;EHxBK,IAAA,EAAA,CAAA,IAAA,EGyBF,iBHzBoB,EACzB,GAAA,IAAA;EAKY,KAAA,CAAA,EGoBZ,iBHpBuB,EAAA;CACd;AAAqB,UGsBvB,0BAAA,SAAmC,YHtBZ,CAAA;EAErB,MAAA,EGqBT,cHrBS;EAEoC,SAAA,EAAA,MAAA;EAH7C,eAAA,CAAA,EAAA,MAAA;EAAK,mBAAA,CAAA,EAAA,MAAA;wBG0BS;;cAGX,oBAAA,YAAgC,QAAQ;EFnCzC,iBAAQ,MAAA;EAEZ,iBAAA,SAAA;EACG,iBAAA,MAAA;EACG,WAAA,CAAA,MAAA,EEoCQ,0BFpCR;EACD,OAAA,CAAA,MAAA,EE+DD,QF/DC,EAAA,EAAA,WAAA,CAAA,EEgEK,kCFhEL,CAAA,EEiER,OFjEQ,CAAA,IAAA,CAAA;EAAM,eAAA,CAAA,CAAA,EE8GQ,OF9GR,CE8GgB,cF9GhB,EAAA,CAAA;EAGP,WAAA,CAAA,QAAc,EAAA,MAAA,EAAA,CAAA,EEwIe,OFxIf,CAAA,IAAA,CAAA;EAAoD,KAAA,CAAA,OAAA,EAAA,CAAA,KAAA,EE+JrD,QF/JqD,EAAA,GE+JxC,OF/JwC,CAAA,IAAA,CAAA,EAAA,OAAA,EE+JhB,YF/JgB,CAAA,EAAA,IAAA;EAAG,IAAA,CAAA,CAAA,EEmKjE,OFnKiE,CAAA,IAAA,CAAA;EAAZ,QAAA,YAAA;EAGnD,QAAA,eAAA;EAAI,QAAA,qBAAA;EAcV,QAAA,oBAAuB;;;;;;cGvBtB,kCAAgC,kBAAA;iBAGvB,iDACJ,uCAAuC,QAAQ,KAC9D,QAAQ;iBAWK,0BAAA,CAAA,SAAoC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;;UAEiB,kBAAA;UACP,WAAW;;;AADrB;AAMsB,uBAAA,WAAW,CAAA,iBACd,kBADc,GACO,kBADP,CAAA,SAEvB,KAAA,CAFuB;EACd,OAAA,CAAA,EAEA,QAFA,GAAA,SAAA;EAAqB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAIe,QAJf;;;;KCL5B;;QAEJ;EDJS,OAAA,ECKN,CDLM;EAMK,UAAA,ECAR,IDAmB;EACd,QAAA,CAAA,ECAN,MDAM,CAAA,MAAA,EAAA,OAAA,CAAA;CAAqB;AAErB,KCCP,cDDO,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCCkD,QDDlD,CCC2D,CDD3D,ECC8D,CDD9D,CAAA,GAAA;EAEoC,KAAA,CAAA,EAAA,MAAA;EAH7C,UAAA,EAAA,MAAA;EAAK,aAAA,CAAA,ECKG,IDLH;;ACJP,KAuBI,YAAA,GAvBJ,CAAA,KAAA,EAuB2B,WAvB3B,EAAA,GAAA,IAAA;;;UCJS;oBACG,0BAA0B,iBAAiB;2BACpC,aAAa,wBAAwB;cAClD;EFHG,eAAA,EAAA,GAAA,GEIQ,OFJU,CEIF,cFHZ,EAAA,CAAA;EAKC,WAAA,EAAA,CAAW,QAAA,EAAA,MAAA,EAAA,EAAA,GEDM,OFCN,CAAA,IAAA,CAAA;;UEsDhB,YAAA;EA5DA,UAAO,CAAA,EAAA,MAAA;EACJ,aAAA,CAAA,EAAA,MAAA;EAA0B,cAAA,CAAA,EAAA,MAAA;EAAiB,SAAA,CAAA,EAAA,MAAA;EACpC,mBAAA,CAAA,EAAA,MAAA;EAAa,iBAAA,CAAA,EAAA,MAAA;;;;KCoB5B,iBAAA,GAAoB,YAAY;KAEhC,kCAAA;EHxBK,IAAA,EAAA,CAAA,IAAA,EGyBF,iBHzBoB,EAAA,GACzB,IAAA;EAKY,KAAA,CAAA,EGoBZ,iBHpBuB,EAAA;CACd;AAAqB,UGsBvB,0BAAA,SAAmC,YHtBZ,CAAA;EAErB,MAAA,EGqBT,cHrBS;EAEoC,SAAA,EAAA,MAAA;EAH7C,eAAA,CAAA,EAAA,MAAA;EAAK,mBAAA,CAAA,EAAA,MAAA;wBG0BS;;cAGX,oBAAA,YAAgC,QAAQ;EFnCzC,iBAAQ,MAAA;EAEZ,iBAAA,SAAA;EACG,iBAAA,MAAA;EACG,WAAA,CAAA,MAAA,EEoCQ,0BFpCR;EACD,OAAA,CAAA,MAAA,EE+DD,QF/DC,EAAA,EAAA,WAAA,CAAA,EEgEK,kCFhEL,CAAA,EEiER,OFjEQ,CAAA,IAAA,CAAA;EAAM,eAAA,CAAA,CAAA,EE8GQ,OF9GR,CE8GgB,cF9GhB,EAAA,CAAA;EAGP,WAAA,CAAA,QAAc,EAAA,MAAA,EAAA,CAAA,EEwIe,OFxIf,CAAA,IAAA,CAAA;EAAoD,KAAA,CAAA,OAAA,EAAA,CAAA,KAAA,EE+JrD,QF/JqD,EAAA,GE+JxC,OF/JwC,CAAA,IAAA,CAAA,EAAA,OAAA,EE+JhB,YF/JgB,CAAA,EAAA,IAAA;EAAG,IAAA,CAAA,CAAA,EEmKjE,OFnKiE,CAAA,IAAA,CAAA;EAAZ,QAAA,YAAA;EAGnD,QAAA,eAAA;EAAI,QAAA,qBAAA;EAcV,QAAA,oBAAuB;;;;;;cGvBtB,kCAAgC,kBAAA;iBAGvB,iDACJ,uCAAuC,QAAQ,KAC9D,QAAQ;iBAWK,0BAAA,CAAA,SAAoC"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;;UAEiB,kBAAA;UACP,WAAW;;;AADrB;AAMsB,uBAAA,WAAW,CAAA,iBACd,kBADc,GACO,kBADP,CAAA,SAEvB,KAAA,CAFuB;EACd,OAAA,CAAA,EAEA,QAFA,GAAA,SAAA;EAAqB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAIe,QAJf;;;;KCL5B;;QAEJ;EDJS,OAAA,ECKN,CDLM;EAMK,UAAA,ECAR,IDAmB;EACd,QAAA,CAAA,ECAN,MDAM,CAAA,MAAA,EAAA,OAAA,CAAA;CAAqB;AAErB,KCCP,cDDO,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCCkD,QDDlD,CCC2D,CDD3D,ECC8D,CDD9D,CAAA,GAAA;EAEoC,KAAA,CAAA,EAAA,MAAA;EAH7C,UAAA,EAAA,MAAA;EAAK,aAAA,CAAA,ECKG,IDLH;;ACJP,KAuBI,YAAA,GAvBJ,CAAA,KAAA,EAuB2B,WAvB3B,EAAA,GAAA,IAAA;;;UCHS;oBACG,0BAA0B,iBAAiB;2BACpC,aAAa,wBAAwB;EFH/C,IAAA,EAAA,GAAA,GEIH,OFJG,CAAA,IAAkB,CAAA;EAMb,eAAW,EAAA,GAAA,GEDR,OFCQ,CEDA,cFCA,EAAA,CAAA;EACd,WAAA,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,EAAA,GEDoB,OFCpB,CAAA,IAAA,CAAA;;AELC,UA4DH,YAAA,CA5DG;EAA0B,UAAA,CAAA,EAAA,MAAA;EAAiB,aAAA,CAAA,EAAA,MAAA;EACpC,cAAA,CAAA,EAAA,MAAA;EAAa,SAAA,CAAA,EAAA,MAAA;EAAwB,mBAAA,CAAA,EAAA,MAAA;EAClD,iBAAA,CAAA,EAAA,MAAA;;;;KCkBF,iBAAA,GAAoB,YAAY;KAEhC,kCAAA;EHxBK,IAAA,EAAA,CAAA,IAAA,EGyBF,iBHzBoB,EAAA,GACzB,IAAA;EAKY,KAAA,CAAA,EGoBZ,iBHpBuB,EAAA;CACd;AAAqB,UGsBvB,0BAAA,SAAmC,YHtBZ,CAAA;EAErB,MAAA,EGqBT,cHrBS;EAEoC,SAAA,EAAA,MAAA;EAH7C,eAAA,CAAA,EAAA,MAAA;EAAK,mBAAA,CAAA,EAAA,MAAA;wBG0BS;;cAGX,oBAAA,YAAgC,QAAQ;EFnCzC,iBAAQ,MAAA;EAEZ,iBAAA,SAAA;EACG,iBAAA,MAAA;EACG,WAAA,CAAA,MAAA,EEoCQ,0BFpCR;EACD,OAAA,CAAA,MAAA,EE+DD,QF/DC,EAAA,EAAA,WAAA,CAAA,EEgEK,kCFhEL,CAAA,EEiER,OFjEQ,CAAA,IAAA,CAAA;EAAM,eAAA,CAAA,CAAA,EE8GQ,OF9GR,CE8GgB,cF9GhB,EAAA,CAAA;EAGP,WAAA,CAAA,QAAc,EAAA,MAAA,EAAA,CAAA,EEwIe,OFxIf,CAAA,IAAA,CAAA;EAAoD,KAAA,CAAA,OAAA,EAAA,CAAA,KAAA,EE+JrD,QF/JqD,EAAA,GE+JxC,OF/JwC,CAAA,IAAA,CAAA,EAAA,OAAA,EE+JhB,YF/JgB,CAAA,EAAA,IAAA;EAAG,IAAA,CAAA,CAAA,EEmKjE,OFnKiE,CAAA,IAAA,CAAA;EAAZ,QAAA,YAAA;EAGnD,QAAA,eAAA;EAAI,QAAA,qBAAA;EAcV,QAAA,oBAAuB;;;;;;cGvBtB,kCAAgC,kBAAA;iBAGvB,iDACJ,uCAAuC,QAAQ,KAC9D,QAAQ;iBAWK,0BAAA,CAAA,SAAoC"}
package/dist/index.mjs CHANGED
@@ -167,11 +167,11 @@ var DynamoDBAwsSdkOutbox = class {
167
167
  const status = isFinalFailure ? EventStatus.FAILED : EventStatus.CREATED;
168
168
  const errorMsg = formatErrorMessage(error);
169
169
  const now = Date.now();
170
- const updateExpression = isFinalFailure ? "SET #status = :status, retryCount = :rc, lastError = :err, nextRetryAt = :now REMOVE gsiSortKey" : "SET #status = :status, retryCount = :rc, lastError = :err, gsiSortKey = :nextAttempt";
170
+ const updateExpression = isFinalFailure ? "SET #status = :status, retryCount = :rc, lastError = :error, nextRetryAt = :now REMOVE gsiSortKey" : "SET #status = :status, retryCount = :rc, lastError = :error, gsiSortKey = :nextAttempt";
171
171
  const expressionAttributeValues = {
172
172
  ":status": status,
173
173
  ":rc": retryCount,
174
- ":err": errorMsg
174
+ ":error": errorMsg
175
175
  };
176
176
  if (isFinalFailure) expressionAttributeValues[":now"] = now;
177
177
  else expressionAttributeValues[":nextAttempt"] = now + this.poller.calculateBackoff(retryCount);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["DocQueryCommand","event: FailedBusEvent","event: BusEvent","expressionAttributeValues: Record<string, any>","items: TransactWriteItem[]","collector: DynamoDBAwsSdkTransactionCollector"],"sources":["../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import { ConditionalCheckFailedException, type DynamoDBClient } from \"@aws-sdk/client-dynamodb\"\nimport {\n QueryCommand as DocQueryCommand,\n DynamoDBDocumentClient,\n TransactWriteCommand,\n type TransactWriteCommandInput,\n UpdateCommand,\n} from \"@aws-sdk/lib-dynamodb\"\nimport {\n BatchSizeLimitError,\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\n// DynamoDB has a hard limit of 100 items per transaction\nconst DYNAMODB_TRANSACTION_LIMIT = 100\n\nexport type TransactWriteItem = NonNullable<TransactWriteCommandInput[\"TransactItems\"]>[number]\n\nexport type DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => void\n items?: TransactWriteItem[]\n}\n\nexport interface DynamoDBAwsSdkOutboxConfig extends OutboxConfig {\n client: DynamoDBClient\n tableName: string\n statusIndexName?: string\n processingTimeoutMs?: number // Time before a PROCESSING event is considered stuck\n getCollector?: (() => DynamoDBAwsSdkTransactionCollector | undefined) | undefined\n}\n\nexport class DynamoDBAwsSdkOutbox implements IOutbox<DynamoDBAwsSdkTransactionCollector> {\n private readonly config: Required<DynamoDBAwsSdkOutboxConfig>\n private readonly docClient: DynamoDBDocumentClient\n private readonly poller: PollingService\n\n constructor(config: DynamoDBAwsSdkOutboxConfig) {\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n tableName: config.tableName,\n statusIndexName: config.statusIndexName ?? \"status-gsiSortKey-index\",\n client: config.client,\n getCollector: config.getCollector,\n }\n\n this.docClient = DynamoDBDocumentClient.from(config.client, {\n marshallOptions: { removeUndefinedValues: true },\n })\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n performMaintenance: () => this.recoverStuckEvents(),\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n async publish(\n events: BusEvent[],\n transaction?: DynamoDBAwsSdkTransactionCollector\n ): Promise<void> {\n if (events.length === 0) return\n\n if (events.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, events.length)\n }\n\n const collector = transaction ?? this.config.getCollector?.()\n\n const items = events.map((event) => {\n return {\n Put: {\n TableName: this.config.tableName,\n Item: {\n id: event.id,\n type: event.type,\n payload: event.payload,\n occurredAt: event.occurredAt.toISOString(),\n status: EventStatus.CREATED,\n retryCount: 0,\n gsiSortKey: event.occurredAt.getTime(),\n },\n },\n }\n })\n\n if (collector) {\n const itemsInCollector = collector.items?.length ?? 0\n\n if (itemsInCollector + items.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, itemsInCollector + items.length)\n }\n\n for (const item of items) {\n collector.push(item)\n }\n } else {\n await this.docClient.send(\n new TransactWriteCommand({\n TransactItems: items,\n })\n )\n }\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: { \":status\": EventStatus.FAILED },\n Limit: 100,\n ScanIndexForward: false,\n })\n )\n\n if (!result.Items) return []\n\n return result.Items.map((item) => {\n const event: FailedBusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n retryCount: item.retryCount || 0,\n }\n if (item.lastError) event.error = item.lastError\n if (item.startedOn) event.lastAttemptAt = new Date(item.startedOn)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n await Promise.all(\n eventIds.map((id) =>\n this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression:\n \"SET #status = :pending, retryCount = :zero, gsiSortKey = :now REMOVE lastError, nextRetryAt, startedOn\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":pending\": EventStatus.CREATED,\n \":zero\": 0,\n \":now\": Date.now(),\n },\n })\n )\n )\n )\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: {\n \"#status\": \"status\",\n },\n ExpressionAttributeValues: {\n \":status\": EventStatus.CREATED,\n \":now\": now,\n },\n Limit: this.config.batchSize,\n })\n )\n\n if (!result.Items || result.Items.length === 0) return\n\n for (const item of result.Items) {\n const event: BusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n }\n\n try {\n await this.markEventAsProcessing(item.id, now)\n await handler(event)\n await this.markEventAsCompleted(event.id)\n } catch (error) {\n if (error instanceof ConditionalCheckFailedException) {\n continue\n }\n\n const newRetryCount = (item.retryCount || 0) + 1\n reportEventError(this.poller.onError, error, event, newRetryCount, this.config.maxRetries)\n\n await this.markEventAsFailed(item.id, newRetryCount, error)\n }\n }\n }\n\n private parseOccurredAt(occurredAt: unknown): Date {\n if (occurredAt instanceof Date) {\n return occurredAt\n }\n if (typeof occurredAt === \"string\") {\n return new Date(occurredAt)\n }\n return new Date()\n }\n\n private async markEventAsProcessing(id: string, now: number): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :processing, gsiSortKey = :timeoutAt, startedOn = :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":processing\": EventStatus.ACTIVE,\n \":timeoutAt\": now + this.config.processingTimeoutMs,\n \":now\": now,\n },\n })\n )\n }\n\n private async markEventAsCompleted(id: string): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :completed, completedOn = :now REMOVE gsiSortKey\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":completed\": EventStatus.COMPLETED,\n \":now\": Date.now(),\n },\n })\n )\n }\n\n private async markEventAsFailed(id: string, retryCount: number, error: unknown): Promise<void> {\n const isFinalFailure = retryCount >= this.config.maxRetries\n const status = isFinalFailure ? EventStatus.FAILED : EventStatus.CREATED\n const errorMsg = formatErrorMessage(error)\n const now = Date.now()\n\n const updateExpression = isFinalFailure\n ? \"SET #status = :status, retryCount = :rc, lastError = :err, nextRetryAt = :now REMOVE gsiSortKey\"\n : \"SET #status = :status, retryCount = :rc, lastError = :err, gsiSortKey = :nextAttempt\"\n\n const expressionAttributeValues: Record<string, any> = {\n \":status\": status,\n \":rc\": retryCount,\n \":err\": errorMsg,\n }\n\n if (isFinalFailure) {\n expressionAttributeValues[\":now\"] = now\n } else {\n const delay = this.poller.calculateBackoff(retryCount)\n expressionAttributeValues[\":nextAttempt\"] = now + delay\n }\n\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: expressionAttributeValues,\n })\n )\n }\n\n private async recoverStuckEvents() {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":status\": EventStatus.ACTIVE,\n \":now\": now,\n },\n })\n )\n\n if (result.Items && result.Items.length > 0) {\n await Promise.all(\n result.Items.map((item) =>\n this.markEventAsFailed(item.id, (item.retryCount || 0) + 1, \"Processing timeout\")\n )\n )\n }\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type {\n DynamoDBAwsSdkTransactionCollector,\n TransactWriteItem,\n} from \"./dynamodb-aws-sdk-outbox\"\n\nexport const dynamodbAwsSdkTransactionStorage =\n new AsyncLocalStorage<DynamoDBAwsSdkTransactionCollector>()\n\nexport async function withDynamoDBAwsSdkTransaction<T>(\n fn: (collector: DynamoDBAwsSdkTransactionCollector) => Promise<T>\n): Promise<T> {\n const items: TransactWriteItem[] = []\n const collector: DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => items.push(item),\n get items() {\n return items\n },\n }\n return dynamodbAwsSdkTransactionStorage.run(collector, () => fn(collector))\n}\n\nexport function getDynamoDBAwsSdkCollector(): () => DynamoDBAwsSdkTransactionCollector | undefined {\n return () => dynamodbAwsSdkTransactionStorage.getStore()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["DocQueryCommand","event: FailedBusEvent","event: BusEvent","expressionAttributeValues: Record<string, any>","items: TransactWriteItem[]","collector: DynamoDBAwsSdkTransactionCollector"],"sources":["../src/dynamodb-aws-sdk-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import { ConditionalCheckFailedException, type DynamoDBClient } from \"@aws-sdk/client-dynamodb\"\nimport {\n QueryCommand as DocQueryCommand,\n DynamoDBDocumentClient,\n TransactWriteCommand,\n type TransactWriteCommandInput,\n UpdateCommand,\n} from \"@aws-sdk/lib-dynamodb\"\nimport {\n BatchSizeLimitError,\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\n// DynamoDB has a hard limit of 100 items per transaction\nconst DYNAMODB_TRANSACTION_LIMIT = 100\n\nexport type TransactWriteItem = NonNullable<TransactWriteCommandInput[\"TransactItems\"]>[number]\n\nexport type DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => void\n items?: TransactWriteItem[]\n}\n\nexport interface DynamoDBAwsSdkOutboxConfig extends OutboxConfig {\n client: DynamoDBClient\n tableName: string\n statusIndexName?: string\n processingTimeoutMs?: number // Time before a PROCESSING event is considered stuck\n getCollector?: (() => DynamoDBAwsSdkTransactionCollector | undefined) | undefined\n}\n\nexport class DynamoDBAwsSdkOutbox implements IOutbox<DynamoDBAwsSdkTransactionCollector> {\n private readonly config: Required<DynamoDBAwsSdkOutboxConfig>\n private readonly docClient: DynamoDBDocumentClient\n private readonly poller: PollingService\n\n constructor(config: DynamoDBAwsSdkOutboxConfig) {\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n tableName: config.tableName,\n statusIndexName: config.statusIndexName ?? \"status-gsiSortKey-index\",\n client: config.client,\n getCollector: config.getCollector,\n }\n\n this.docClient = DynamoDBDocumentClient.from(config.client, {\n marshallOptions: { removeUndefinedValues: true },\n })\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n performMaintenance: () => this.recoverStuckEvents(),\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n async publish(\n events: BusEvent[],\n transaction?: DynamoDBAwsSdkTransactionCollector\n ): Promise<void> {\n if (events.length === 0) return\n\n if (events.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, events.length)\n }\n\n const collector = transaction ?? this.config.getCollector?.()\n\n const items = events.map((event) => {\n return {\n Put: {\n TableName: this.config.tableName,\n Item: {\n id: event.id,\n type: event.type,\n payload: event.payload,\n occurredAt: event.occurredAt.toISOString(),\n status: EventStatus.CREATED,\n retryCount: 0,\n gsiSortKey: event.occurredAt.getTime(),\n },\n },\n }\n })\n\n if (collector) {\n const itemsInCollector = collector.items?.length ?? 0\n\n if (itemsInCollector + items.length > DYNAMODB_TRANSACTION_LIMIT) {\n throw new BatchSizeLimitError(DYNAMODB_TRANSACTION_LIMIT, itemsInCollector + items.length)\n }\n\n for (const item of items) {\n collector.push(item)\n }\n } else {\n await this.docClient.send(\n new TransactWriteCommand({\n TransactItems: items,\n })\n )\n }\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: { \":status\": EventStatus.FAILED },\n Limit: 100,\n ScanIndexForward: false,\n })\n )\n\n if (!result.Items) return []\n\n return result.Items.map((item) => {\n const event: FailedBusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n retryCount: item.retryCount || 0,\n }\n if (item.lastError) event.error = item.lastError\n if (item.startedOn) event.lastAttemptAt = new Date(item.startedOn)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n await Promise.all(\n eventIds.map((id) =>\n this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression:\n \"SET #status = :pending, retryCount = :zero, gsiSortKey = :now REMOVE lastError, nextRetryAt, startedOn\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":pending\": EventStatus.CREATED,\n \":zero\": 0,\n \":now\": Date.now(),\n },\n })\n )\n )\n )\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: {\n \"#status\": \"status\",\n },\n ExpressionAttributeValues: {\n \":status\": EventStatus.CREATED,\n \":now\": now,\n },\n Limit: this.config.batchSize,\n })\n )\n\n if (!result.Items || result.Items.length === 0) return\n\n for (const item of result.Items) {\n const event: BusEvent = {\n id: item.id,\n type: item.type,\n payload: item.payload,\n occurredAt: this.parseOccurredAt(item.occurredAt),\n }\n\n try {\n await this.markEventAsProcessing(item.id, now)\n await handler(event)\n await this.markEventAsCompleted(event.id)\n } catch (error) {\n if (error instanceof ConditionalCheckFailedException) {\n continue\n }\n\n const newRetryCount = (item.retryCount || 0) + 1\n reportEventError(this.poller.onError, error, event, newRetryCount, this.config.maxRetries)\n\n await this.markEventAsFailed(item.id, newRetryCount, error)\n }\n }\n }\n\n private parseOccurredAt(occurredAt: unknown): Date {\n if (occurredAt instanceof Date) {\n return occurredAt\n }\n if (typeof occurredAt === \"string\") {\n return new Date(occurredAt)\n }\n return new Date()\n }\n\n private async markEventAsProcessing(id: string, now: number): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :processing, gsiSortKey = :timeoutAt, startedOn = :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":processing\": EventStatus.ACTIVE,\n \":timeoutAt\": now + this.config.processingTimeoutMs,\n \":now\": now,\n },\n })\n )\n }\n\n private async markEventAsCompleted(id: string): Promise<void> {\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: \"SET #status = :completed, completedOn = :now REMOVE gsiSortKey\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":completed\": EventStatus.COMPLETED,\n \":now\": Date.now(),\n },\n })\n )\n }\n\n private async markEventAsFailed(id: string, retryCount: number, error: unknown): Promise<void> {\n const isFinalFailure = retryCount >= this.config.maxRetries\n const status = isFinalFailure ? EventStatus.FAILED : EventStatus.CREATED\n const errorMsg = formatErrorMessage(error)\n const now = Date.now()\n\n const updateExpression = isFinalFailure\n ? \"SET #status = :status, retryCount = :rc, lastError = :error, nextRetryAt = :now REMOVE gsiSortKey\"\n : \"SET #status = :status, retryCount = :rc, lastError = :error, gsiSortKey = :nextAttempt\"\n\n const expressionAttributeValues: Record<string, any> = {\n \":status\": status,\n \":rc\": retryCount,\n \":error\": errorMsg,\n }\n\n if (isFinalFailure) {\n expressionAttributeValues[\":now\"] = now\n } else {\n const delay = this.poller.calculateBackoff(retryCount)\n expressionAttributeValues[\":nextAttempt\"] = now + delay\n }\n\n await this.docClient.send(\n new UpdateCommand({\n TableName: this.config.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: expressionAttributeValues,\n })\n )\n }\n\n private async recoverStuckEvents() {\n const now = Date.now()\n\n const result = await this.docClient.send(\n new DocQueryCommand({\n TableName: this.config.tableName,\n IndexName: this.config.statusIndexName,\n KeyConditionExpression: \"#status = :status AND gsiSortKey <= :now\",\n ExpressionAttributeNames: { \"#status\": \"status\" },\n ExpressionAttributeValues: {\n \":status\": EventStatus.ACTIVE,\n \":now\": now,\n },\n })\n )\n\n if (result.Items && result.Items.length > 0) {\n await Promise.all(\n result.Items.map((item) =>\n this.markEventAsFailed(item.id, (item.retryCount || 0) + 1, \"Processing timeout\")\n )\n )\n }\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type {\n DynamoDBAwsSdkTransactionCollector,\n TransactWriteItem,\n} from \"./dynamodb-aws-sdk-outbox\"\n\nexport const dynamodbAwsSdkTransactionStorage =\n new AsyncLocalStorage<DynamoDBAwsSdkTransactionCollector>()\n\nexport async function withDynamoDBAwsSdkTransaction<T>(\n fn: (collector: DynamoDBAwsSdkTransactionCollector) => Promise<T>\n): Promise<T> {\n const items: TransactWriteItem[] = []\n const collector: DynamoDBAwsSdkTransactionCollector = {\n push: (item: TransactWriteItem) => items.push(item),\n get items() {\n return items\n },\n }\n return dynamodbAwsSdkTransactionStorage.run(collector, () => fn(collector))\n}\n\nexport function getDynamoDBAwsSdkCollector(): () => DynamoDBAwsSdkTransactionCollector | undefined {\n return () => dynamodbAwsSdkTransactionStorage.getStore()\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@outbox-event-bus/dynamodb-aws-sdk-outbox",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/dunika/outbox-event-bus.git",
9
- "directory": "adapters/dynamodb-aws-sdk"
9
+ "directory": "packages/adapters/dynamodb-aws-sdk"
10
10
  },
11
- "homepage": "https://github.com/dunika/outbox-event-bus/tree/main/adapters/dynamodb-aws-sdk#readme",
11
+ "homepage": "https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/dynamodb-aws-sdk#readme",
12
12
  "type": "module",
13
13
  "main": "./dist/index.cjs",
14
14
  "module": "./dist/index.mjs",
@@ -60,7 +60,7 @@ describe("DynamoDBAwsSdkOutbox Unit Tests", () => {
60
60
 
61
61
  outbox.start(
62
62
  async () => {},
63
- (err) => console.error(err)
63
+ (error) => console.error(error)
64
64
  )
65
65
 
66
66
  // Wait for one poll cycle
@@ -91,7 +91,7 @@ describe("DynamoDBAwsSdkOutbox Unit Tests", () => {
91
91
  async () => {
92
92
  throw new Error("Failed")
93
93
  },
94
- (err) => console.error(err)
94
+ (error) => console.error(error)
95
95
  )
96
96
 
97
97
  await new Promise((res) => setTimeout(res, 200))
@@ -271,13 +271,13 @@ export class DynamoDBAwsSdkOutbox implements IOutbox<DynamoDBAwsSdkTransactionCo
271
271
  const now = Date.now()
272
272
 
273
273
  const updateExpression = isFinalFailure
274
- ? "SET #status = :status, retryCount = :rc, lastError = :err, nextRetryAt = :now REMOVE gsiSortKey"
275
- : "SET #status = :status, retryCount = :rc, lastError = :err, gsiSortKey = :nextAttempt"
274
+ ? "SET #status = :status, retryCount = :rc, lastError = :error, nextRetryAt = :now REMOVE gsiSortKey"
275
+ : "SET #status = :status, retryCount = :rc, lastError = :error, gsiSortKey = :nextAttempt"
276
276
 
277
277
  const expressionAttributeValues: Record<string, any> = {
278
278
  ":status": status,
279
279
  ":rc": retryCount,
280
- ":err": errorMsg,
280
+ ":error": errorMsg,
281
281
  }
282
282
 
283
283
  if (isFinalFailure) {
@@ -46,9 +46,9 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
46
46
  })
47
47
  )
48
48
  break
49
- } catch (err: any) {
50
- if (err.name === "ResourceInUseException") break
51
- if (i === maxRetries - 1) throw err
49
+ } catch (error: any) {
50
+ if (error.name === "ResourceInUseException") break
51
+ if (i === maxRetries - 1) throw error
52
52
  await new Promise((res) => setTimeout(res, delay))
53
53
  }
54
54
  }
@@ -94,7 +94,7 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
94
94
  pollIntervalMs: 100,
95
95
  })
96
96
 
97
- const eventBus = new OutboxEventBus(outbox, (err) => console.error("Bus error:", err))
97
+ const eventBus = new OutboxEventBus(outbox, (error) => console.error("Bus error:", error))
98
98
 
99
99
  const received: any[] = []
100
100
  eventBus.subscribe(["test.event"], async (event) => {
@@ -204,7 +204,7 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
204
204
 
205
205
  await outbox.retryEvents([eventId])
206
206
 
207
- const eventBus = new OutboxEventBus(outbox, (err) => console.error("Bus error:", err))
207
+ const eventBus = new OutboxEventBus(outbox, (error) => console.error("Bus error:", error))
208
208
 
209
209
  const processed: any[] = []
210
210
  const _sub = eventBus.subscribe(["manual.retry"], async (event) => {
@@ -258,7 +258,7 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
258
258
  async (event) => {
259
259
  received.push(event)
260
260
  },
261
- (err) => console.error("Outbox error:", err)
261
+ (error) => console.error("Outbox error:", error)
262
262
  )
263
263
 
264
264
  await new Promise((resolve) => setTimeout(resolve, 1500))
@@ -316,7 +316,7 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
316
316
  batchSize: 5,
317
317
  })
318
318
  workers.push(worker)
319
- worker.start(handler, (err) => console.error(`Worker ${i} Error:`, err))
319
+ worker.start(handler, (error) => console.error(`Worker ${i} Error:`, error))
320
320
  }
321
321
 
322
322
  const maxWaitTime = 15000
@@ -391,7 +391,7 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
391
391
  async (e) => {
392
392
  processedEvents.push(e)
393
393
  },
394
- (err) => console.error(err)
394
+ (error) => console.error(error)
395
395
  )
396
396
 
397
397
  await new Promise((r) => setTimeout(r, 1000))
@@ -453,7 +453,7 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
453
453
  async (e) => {
454
454
  processedEvents.push(e)
455
455
  },
456
- (err) => console.error(err)
456
+ (error) => console.error(error)
457
457
  )
458
458
 
459
459
  await new Promise((r) => setTimeout(r, 1500))