@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 +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/dynamodb-aws-sdk-outbox.test.ts +2 -2
- package/src/dynamodb-aws-sdk-outbox.ts +3 -3
- package/src/integration.e2e.ts +9 -9
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 = :
|
|
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
|
-
":
|
|
174
|
+
":error": errorMsg
|
|
175
175
|
};
|
|
176
176
|
if (isFinalFailure) expressionAttributeValues[":now"] = now;
|
|
177
177
|
else expressionAttributeValues[":nextAttempt"] = now + this.poller.calculateBackoff(retryCount);
|
package/dist/index.cjs.map
CHANGED
|
@@ -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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"}
|
package/dist/index.d.cts.map
CHANGED
|
@@ -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;;;
|
|
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"}
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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;;;
|
|
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 = :
|
|
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
|
-
":
|
|
174
|
+
":error": errorMsg
|
|
175
175
|
};
|
|
176
176
|
if (isFinalFailure) expressionAttributeValues[":now"] = now;
|
|
177
177
|
else expressionAttributeValues[":nextAttempt"] = now + this.poller.calculateBackoff(retryCount);
|
package/dist/index.mjs.map
CHANGED
|
@@ -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.
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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 = :
|
|
275
|
-
: "SET #status = :status, retryCount = :rc, lastError = :
|
|
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
|
-
":
|
|
280
|
+
":error": errorMsg,
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
if (isFinalFailure) {
|
package/src/integration.e2e.ts
CHANGED
|
@@ -46,9 +46,9 @@ describe("DynamoDBAwsSdkOutbox E2E", () => {
|
|
|
46
46
|
})
|
|
47
47
|
)
|
|
48
48
|
break
|
|
49
|
-
} catch (
|
|
50
|
-
if (
|
|
51
|
-
if (i === maxRetries - 1) throw
|
|
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, (
|
|
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, (
|
|
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
|
-
(
|
|
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, (
|
|
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
|
-
(
|
|
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
|
-
(
|
|
456
|
+
(error) => console.error(error)
|
|
457
457
|
)
|
|
458
458
|
|
|
459
459
|
await new Promise((r) => setTimeout(r, 1500))
|