@develit-io/backend-sdk 12.3.1 → 12.4.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.d.mts CHANGED
@@ -6,7 +6,7 @@ import { z as z$1, ZodType, ZodObject, ZodOptional } from 'zod';
6
6
  import * as z from 'zod/v4/core';
7
7
  import { ContentfulStatusCode, SuccessStatusCode } from 'hono/utils/http-status';
8
8
  export { ContentfulStatusCode as InternalResponseStatus } from 'hono/utils/http-status';
9
- import { Queue } from '@cloudflare/workers-types';
9
+ import { Queue, MessageBatch } from '@cloudflare/workers-types';
10
10
  import { BatchItem } from 'drizzle-orm/batch';
11
11
  import { DrizzleD1Database } from 'drizzle-orm/d1';
12
12
 
@@ -1254,6 +1254,89 @@ declare class DatabaseTransaction<TAuditAction = string> {
1254
1254
  getCommandsCount(): number;
1255
1255
  }
1256
1256
 
1257
+ /**
1258
+ * Reusable helpers for consuming Cloudflare dead-letter queues (`*-dlq`).
1259
+ *
1260
+ * A worker can consume both its main queues and their paired DLQs in the same
1261
+ * `queue()` handler; detect DLQ batches by name and route them to
1262
+ * `handleDlqBatch` (or compose the pure helpers yourself). Everything here is
1263
+ * runtime-agnostic — the project supplies the alert transport and alert-state
1264
+ * storage via callbacks, so the SDK stays free of notification/DO bindings.
1265
+ */
1266
+ /** Default thresholds, used when the matching option is omitted. */
1267
+ declare const DLQ_ALERT_DEFAULT_MIN_SIZE = 50;
1268
+ declare const DLQ_ALERT_DEFAULT_MAX_INTERVAL = 21600;
1269
+ /**
1270
+ * A DLQ is any queue whose name ends in `-dlq` or carries `-dlq` as a token
1271
+ * before the environment suffix Cloudflare appends (e.g. `...-dlq-production`).
1272
+ * Matches `-dlq` as a token rather than a bare substring, so names that merely
1273
+ * contain it (e.g. `orders-dlqx`) are not misclassified as DLQs.
1274
+ */
1275
+ declare function isDlqQueue(queueName: string): boolean;
1276
+ /**
1277
+ * Parses a comma-separated recipients string into a trimmed, de-duplicated list.
1278
+ */
1279
+ declare function parseDlqAlertRecipients(raw: string | undefined): string[];
1280
+ type ShouldAlertDlqParams = {
1281
+ batchSize: number;
1282
+ secondsSinceLastAlert: number;
1283
+ minSize: number;
1284
+ maxInterval: number;
1285
+ };
1286
+ /**
1287
+ * Rate-limit gate: suppress the alert only while the batch is small AND not
1288
+ * enough time has passed since the last alert. Otherwise alert.
1289
+ */
1290
+ declare function shouldAlertDlq({ batchSize, secondsSinceLastAlert, minSize, maxInterval, }: ShouldAlertDlqParams): boolean;
1291
+ type DlqAlert = {
1292
+ subject: string;
1293
+ text: string;
1294
+ };
1295
+ /**
1296
+ * Builds the alert subject + plain-text body summarizing the dead-lettered
1297
+ * messages. Bodies are stringified defensively — DLQ messages may come from any
1298
+ * consumed queue, so no shape is assumed.
1299
+ */
1300
+ declare function buildDlqAlert(queueName: string, messages: ReadonlyArray<{
1301
+ body: unknown;
1302
+ }>): DlqAlert;
1303
+ type HandleDlqBatchOptions = {
1304
+ /** Comma-separated recipient list (e.g. from an env var). */
1305
+ recipientsRaw: string | undefined;
1306
+ /** Returns the last-alert timestamp (ms) for a queue, or 0 if never alerted. */
1307
+ getLastAlertAt: (queueName: string) => Promise<number>;
1308
+ /** Persists the last-alert timestamp (ms) for a queue. */
1309
+ setLastAlertAt: (queueName: string, at: number) => Promise<void>;
1310
+ /** Delivers the alert. Return an object with `error` set on failure. */
1311
+ sendAlert: (input: {
1312
+ recipients: string[];
1313
+ subject: string;
1314
+ text: string;
1315
+ }) => Promise<{
1316
+ error?: unknown;
1317
+ }>;
1318
+ /** Alert immediately once a batch reaches this size. Default 50. */
1319
+ minSize?: number;
1320
+ /** Min seconds between alerts for the same queue while batches are small. Default 21600. */
1321
+ maxInterval?: number;
1322
+ /** Current time in ms. Defaults to `Date.now()` — override in tests. */
1323
+ now?: number;
1324
+ /** Optional logger for observability. */
1325
+ log?: (message: string) => void;
1326
+ };
1327
+ /**
1328
+ * Handles a batch from any consumed dead-letter queue: rate-limited alert then
1329
+ * ack. Mirrors the verified flow:
1330
+ * - rate-limited (already alerted this window) → `ackAll` (alert-only; retrying
1331
+ * would just churn until the DLQ's own max_retries drops the message);
1332
+ * - no recipients → log + `ackAll`;
1333
+ * - alert send fails → `retryAll` (try again next pull);
1334
+ * - success → persist last-alert + `ackAll`.
1335
+ *
1336
+ * NOTE: alert-only — acked messages are dropped (no persistence/replay).
1337
+ */
1338
+ declare function handleDlqBatch(batch: MessageBatch, opts: HandleDlqBatchOptions): Promise<void>;
1339
+
1257
1340
  declare function first<T>(rows: T[]): T | undefined;
1258
1341
  declare function firstOrError<T>(rows: T[]): T;
1259
1342
  declare function derivePortFromId(id: string, base?: number, range?: number): number;
@@ -1386,9 +1469,24 @@ declare const action: (name: string) => MethodDecorator;
1386
1469
 
1387
1470
  interface WithRetryCounterOptions {
1388
1471
  baseDelay: number;
1472
+ /**
1473
+ * Cap after which a message is allowed to dead-letter instead of being
1474
+ * retried again. When a message's `attempts` reaches `maxRetries`, the
1475
+ * wrapped `retry()` becomes a no-op and — once the handler returns — the
1476
+ * decorator throws, so Cloudflare's native max-retries path routes the
1477
+ * message(s) to the configured DLQ.
1478
+ *
1479
+ * This is required because an explicit delayed `retry({ delaySeconds })`
1480
+ * issued at the cap is silently dropped by Cloudflare (never written to the
1481
+ * DLQ), and returning without a retry implicitly acks (also dropping it).
1482
+ *
1483
+ * Omit to keep the legacy behaviour (always retry with backoff). Set it to
1484
+ * the consumer's configured `max_retries` to get correct dead-lettering.
1485
+ */
1486
+ maxRetries?: number;
1389
1487
  }
1390
1488
  type AsyncMethod<TArgs extends unknown[] = unknown[], TResult = unknown> = (...args: TArgs) => Promise<TResult>;
1391
1489
  declare function cloudflareQueue<TArgs extends unknown[] = unknown[], TResult = unknown>(options: WithRetryCounterOptions): (target: unknown, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<AsyncMethod<TArgs, TResult>>) => void;
1392
1490
 
1393
- export { DatabaseTransaction, ENVIRONMENT, RPCResponse, USER_ROLES, action, asNonEmpty, bankAccount, bankAccountMetadataSchema, base, bicSchema, buildMultiFilterConditions, buildRangeFilterConditions, buildSearchConditions, calculateExponentialBackoff, chunkQueueMessages, cloudflareQueue, composeWranglerBase, createAuditLogWriter, createInsertSchema, createInternalError, createPatchSchema, createSelectSchema, createUpdateSchema, defineCommand, derivePortFromId, develitWorker, durableObjectNamespaceIdFromName, first, firstOrError, getD1Credentials, getDrizzleD1Config, getLocalD1DatabaseIdFromWrangler, getSecret, handleAction, ibanSchema, isInternalError, nullToOptional, optionalToNull, paginationQuerySchema, paginationSchema, pushToQueue, queryInChunks, redact, resolveColumn, service, structuredAddressSchema, useFetch, useResult, useResultSync, uuidv4, uuidv5, workflowInstanceStatusSchema };
1394
- export type { ActionExecution, ActionHandlerOptions, AuditLogWriter, BankAccountMetadata, BaseEvent, BuildSearchOptions, Command, CommandLogPayload, DevelitWorkerMethods, Environment, GatewayResponse, IRPCResponse, IdempotencyContextVariables, IdentityContextVariables, InternalError, InternalErrorResponseStatus, Project, RequestLog, ResponseLog, StructuredAddress, UserRole, ValidatedInput, WorkflowInstanceStatus };
1491
+ export { DLQ_ALERT_DEFAULT_MAX_INTERVAL, DLQ_ALERT_DEFAULT_MIN_SIZE, DatabaseTransaction, ENVIRONMENT, RPCResponse, USER_ROLES, action, asNonEmpty, bankAccount, bankAccountMetadataSchema, base, bicSchema, buildDlqAlert, buildMultiFilterConditions, buildRangeFilterConditions, buildSearchConditions, calculateExponentialBackoff, chunkQueueMessages, cloudflareQueue, composeWranglerBase, createAuditLogWriter, createInsertSchema, createInternalError, createPatchSchema, createSelectSchema, createUpdateSchema, defineCommand, derivePortFromId, develitWorker, durableObjectNamespaceIdFromName, first, firstOrError, getD1Credentials, getDrizzleD1Config, getLocalD1DatabaseIdFromWrangler, getSecret, handleAction, handleDlqBatch, ibanSchema, isDlqQueue, isInternalError, nullToOptional, optionalToNull, paginationQuerySchema, paginationSchema, parseDlqAlertRecipients, pushToQueue, queryInChunks, redact, resolveColumn, service, shouldAlertDlq, structuredAddressSchema, useFetch, useResult, useResultSync, uuidv4, uuidv5, workflowInstanceStatusSchema };
1492
+ export type { ActionExecution, ActionHandlerOptions, AuditLogWriter, BankAccountMetadata, BaseEvent, BuildSearchOptions, Command, CommandLogPayload, DevelitWorkerMethods, DlqAlert, Environment, GatewayResponse, HandleDlqBatchOptions, IRPCResponse, IdempotencyContextVariables, IdentityContextVariables, InternalError, InternalErrorResponseStatus, Project, RequestLog, ResponseLog, ShouldAlertDlqParams, StructuredAddress, UserRole, ValidatedInput, WorkflowInstanceStatus };
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ import { z as z$1, ZodType, ZodObject, ZodOptional } from 'zod';
6
6
  import * as z from 'zod/v4/core';
7
7
  import { ContentfulStatusCode, SuccessStatusCode } from 'hono/utils/http-status';
8
8
  export { ContentfulStatusCode as InternalResponseStatus } from 'hono/utils/http-status';
9
- import { Queue } from '@cloudflare/workers-types';
9
+ import { Queue, MessageBatch } from '@cloudflare/workers-types';
10
10
  import { BatchItem } from 'drizzle-orm/batch';
11
11
  import { DrizzleD1Database } from 'drizzle-orm/d1';
12
12
 
@@ -1254,6 +1254,89 @@ declare class DatabaseTransaction<TAuditAction = string> {
1254
1254
  getCommandsCount(): number;
1255
1255
  }
1256
1256
 
1257
+ /**
1258
+ * Reusable helpers for consuming Cloudflare dead-letter queues (`*-dlq`).
1259
+ *
1260
+ * A worker can consume both its main queues and their paired DLQs in the same
1261
+ * `queue()` handler; detect DLQ batches by name and route them to
1262
+ * `handleDlqBatch` (or compose the pure helpers yourself). Everything here is
1263
+ * runtime-agnostic — the project supplies the alert transport and alert-state
1264
+ * storage via callbacks, so the SDK stays free of notification/DO bindings.
1265
+ */
1266
+ /** Default thresholds, used when the matching option is omitted. */
1267
+ declare const DLQ_ALERT_DEFAULT_MIN_SIZE = 50;
1268
+ declare const DLQ_ALERT_DEFAULT_MAX_INTERVAL = 21600;
1269
+ /**
1270
+ * A DLQ is any queue whose name ends in `-dlq` or carries `-dlq` as a token
1271
+ * before the environment suffix Cloudflare appends (e.g. `...-dlq-production`).
1272
+ * Matches `-dlq` as a token rather than a bare substring, so names that merely
1273
+ * contain it (e.g. `orders-dlqx`) are not misclassified as DLQs.
1274
+ */
1275
+ declare function isDlqQueue(queueName: string): boolean;
1276
+ /**
1277
+ * Parses a comma-separated recipients string into a trimmed, de-duplicated list.
1278
+ */
1279
+ declare function parseDlqAlertRecipients(raw: string | undefined): string[];
1280
+ type ShouldAlertDlqParams = {
1281
+ batchSize: number;
1282
+ secondsSinceLastAlert: number;
1283
+ minSize: number;
1284
+ maxInterval: number;
1285
+ };
1286
+ /**
1287
+ * Rate-limit gate: suppress the alert only while the batch is small AND not
1288
+ * enough time has passed since the last alert. Otherwise alert.
1289
+ */
1290
+ declare function shouldAlertDlq({ batchSize, secondsSinceLastAlert, minSize, maxInterval, }: ShouldAlertDlqParams): boolean;
1291
+ type DlqAlert = {
1292
+ subject: string;
1293
+ text: string;
1294
+ };
1295
+ /**
1296
+ * Builds the alert subject + plain-text body summarizing the dead-lettered
1297
+ * messages. Bodies are stringified defensively — DLQ messages may come from any
1298
+ * consumed queue, so no shape is assumed.
1299
+ */
1300
+ declare function buildDlqAlert(queueName: string, messages: ReadonlyArray<{
1301
+ body: unknown;
1302
+ }>): DlqAlert;
1303
+ type HandleDlqBatchOptions = {
1304
+ /** Comma-separated recipient list (e.g. from an env var). */
1305
+ recipientsRaw: string | undefined;
1306
+ /** Returns the last-alert timestamp (ms) for a queue, or 0 if never alerted. */
1307
+ getLastAlertAt: (queueName: string) => Promise<number>;
1308
+ /** Persists the last-alert timestamp (ms) for a queue. */
1309
+ setLastAlertAt: (queueName: string, at: number) => Promise<void>;
1310
+ /** Delivers the alert. Return an object with `error` set on failure. */
1311
+ sendAlert: (input: {
1312
+ recipients: string[];
1313
+ subject: string;
1314
+ text: string;
1315
+ }) => Promise<{
1316
+ error?: unknown;
1317
+ }>;
1318
+ /** Alert immediately once a batch reaches this size. Default 50. */
1319
+ minSize?: number;
1320
+ /** Min seconds between alerts for the same queue while batches are small. Default 21600. */
1321
+ maxInterval?: number;
1322
+ /** Current time in ms. Defaults to `Date.now()` — override in tests. */
1323
+ now?: number;
1324
+ /** Optional logger for observability. */
1325
+ log?: (message: string) => void;
1326
+ };
1327
+ /**
1328
+ * Handles a batch from any consumed dead-letter queue: rate-limited alert then
1329
+ * ack. Mirrors the verified flow:
1330
+ * - rate-limited (already alerted this window) → `ackAll` (alert-only; retrying
1331
+ * would just churn until the DLQ's own max_retries drops the message);
1332
+ * - no recipients → log + `ackAll`;
1333
+ * - alert send fails → `retryAll` (try again next pull);
1334
+ * - success → persist last-alert + `ackAll`.
1335
+ *
1336
+ * NOTE: alert-only — acked messages are dropped (no persistence/replay).
1337
+ */
1338
+ declare function handleDlqBatch(batch: MessageBatch, opts: HandleDlqBatchOptions): Promise<void>;
1339
+
1257
1340
  declare function first<T>(rows: T[]): T | undefined;
1258
1341
  declare function firstOrError<T>(rows: T[]): T;
1259
1342
  declare function derivePortFromId(id: string, base?: number, range?: number): number;
@@ -1386,9 +1469,24 @@ declare const action: (name: string) => MethodDecorator;
1386
1469
 
1387
1470
  interface WithRetryCounterOptions {
1388
1471
  baseDelay: number;
1472
+ /**
1473
+ * Cap after which a message is allowed to dead-letter instead of being
1474
+ * retried again. When a message's `attempts` reaches `maxRetries`, the
1475
+ * wrapped `retry()` becomes a no-op and — once the handler returns — the
1476
+ * decorator throws, so Cloudflare's native max-retries path routes the
1477
+ * message(s) to the configured DLQ.
1478
+ *
1479
+ * This is required because an explicit delayed `retry({ delaySeconds })`
1480
+ * issued at the cap is silently dropped by Cloudflare (never written to the
1481
+ * DLQ), and returning without a retry implicitly acks (also dropping it).
1482
+ *
1483
+ * Omit to keep the legacy behaviour (always retry with backoff). Set it to
1484
+ * the consumer's configured `max_retries` to get correct dead-lettering.
1485
+ */
1486
+ maxRetries?: number;
1389
1487
  }
1390
1488
  type AsyncMethod<TArgs extends unknown[] = unknown[], TResult = unknown> = (...args: TArgs) => Promise<TResult>;
1391
1489
  declare function cloudflareQueue<TArgs extends unknown[] = unknown[], TResult = unknown>(options: WithRetryCounterOptions): (target: unknown, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<AsyncMethod<TArgs, TResult>>) => void;
1392
1490
 
1393
- export { DatabaseTransaction, ENVIRONMENT, RPCResponse, USER_ROLES, action, asNonEmpty, bankAccount, bankAccountMetadataSchema, base, bicSchema, buildMultiFilterConditions, buildRangeFilterConditions, buildSearchConditions, calculateExponentialBackoff, chunkQueueMessages, cloudflareQueue, composeWranglerBase, createAuditLogWriter, createInsertSchema, createInternalError, createPatchSchema, createSelectSchema, createUpdateSchema, defineCommand, derivePortFromId, develitWorker, durableObjectNamespaceIdFromName, first, firstOrError, getD1Credentials, getDrizzleD1Config, getLocalD1DatabaseIdFromWrangler, getSecret, handleAction, ibanSchema, isInternalError, nullToOptional, optionalToNull, paginationQuerySchema, paginationSchema, pushToQueue, queryInChunks, redact, resolveColumn, service, structuredAddressSchema, useFetch, useResult, useResultSync, uuidv4, uuidv5, workflowInstanceStatusSchema };
1394
- export type { ActionExecution, ActionHandlerOptions, AuditLogWriter, BankAccountMetadata, BaseEvent, BuildSearchOptions, Command, CommandLogPayload, DevelitWorkerMethods, Environment, GatewayResponse, IRPCResponse, IdempotencyContextVariables, IdentityContextVariables, InternalError, InternalErrorResponseStatus, Project, RequestLog, ResponseLog, StructuredAddress, UserRole, ValidatedInput, WorkflowInstanceStatus };
1491
+ export { DLQ_ALERT_DEFAULT_MAX_INTERVAL, DLQ_ALERT_DEFAULT_MIN_SIZE, DatabaseTransaction, ENVIRONMENT, RPCResponse, USER_ROLES, action, asNonEmpty, bankAccount, bankAccountMetadataSchema, base, bicSchema, buildDlqAlert, buildMultiFilterConditions, buildRangeFilterConditions, buildSearchConditions, calculateExponentialBackoff, chunkQueueMessages, cloudflareQueue, composeWranglerBase, createAuditLogWriter, createInsertSchema, createInternalError, createPatchSchema, createSelectSchema, createUpdateSchema, defineCommand, derivePortFromId, develitWorker, durableObjectNamespaceIdFromName, first, firstOrError, getD1Credentials, getDrizzleD1Config, getLocalD1DatabaseIdFromWrangler, getSecret, handleAction, handleDlqBatch, ibanSchema, isDlqQueue, isInternalError, nullToOptional, optionalToNull, paginationQuerySchema, paginationSchema, parseDlqAlertRecipients, pushToQueue, queryInChunks, redact, resolveColumn, service, shouldAlertDlq, structuredAddressSchema, useFetch, useResult, useResultSync, uuidv4, uuidv5, workflowInstanceStatusSchema };
1492
+ export type { ActionExecution, ActionHandlerOptions, AuditLogWriter, BankAccountMetadata, BaseEvent, BuildSearchOptions, Command, CommandLogPayload, DevelitWorkerMethods, DlqAlert, Environment, GatewayResponse, HandleDlqBatchOptions, IRPCResponse, IdempotencyContextVariables, IdentityContextVariables, InternalError, InternalErrorResponseStatus, Project, RequestLog, ResponseLog, ShouldAlertDlqParams, StructuredAddress, UserRole, ValidatedInput, WorkflowInstanceStatus };
package/dist/index.mjs CHANGED
@@ -697,6 +697,86 @@ const defineCommand = (handler) => {
697
697
  }));
698
698
  };
699
699
 
700
+ const DLQ_ALERT_DEFAULT_MIN_SIZE = 50;
701
+ const DLQ_ALERT_DEFAULT_MAX_INTERVAL = 21600;
702
+ function isDlqQueue(queueName) {
703
+ return /-dlq(?:-|$)/.test(queueName);
704
+ }
705
+ function parseDlqAlertRecipients(raw) {
706
+ if (!raw) return [];
707
+ return [
708
+ ...new Set(
709
+ raw.split(",").map((email) => email.trim()).filter(Boolean)
710
+ )
711
+ ];
712
+ }
713
+ function shouldAlertDlq({
714
+ batchSize,
715
+ secondsSinceLastAlert,
716
+ minSize,
717
+ maxInterval
718
+ }) {
719
+ const suppress = batchSize < minSize && secondsSinceLastAlert < maxInterval;
720
+ return !suppress;
721
+ }
722
+ function buildDlqAlert(queueName, messages) {
723
+ const summary = messages.map((message, index) => {
724
+ let serialized;
725
+ try {
726
+ serialized = JSON.stringify(message.body, null, 2);
727
+ } catch {
728
+ serialized = "[unserializable message body]";
729
+ }
730
+ return `#${index + 1}:
731
+ ${serialized}`;
732
+ }).join("\n\n");
733
+ return {
734
+ subject: `\u{1F534} DLQ events in ${queueName} (${messages.length})`,
735
+ text: `${messages.length} dead-lettered message(s) on queue "${queueName}".
736
+
737
+ ${summary}`
738
+ };
739
+ }
740
+ async function handleDlqBatch(batch, opts) {
741
+ const queueName = batch.queue;
742
+ const minSize = opts.minSize ?? DLQ_ALERT_DEFAULT_MIN_SIZE;
743
+ const maxInterval = opts.maxInterval ?? DLQ_ALERT_DEFAULT_MAX_INTERVAL;
744
+ const now = opts.now ?? Date.now();
745
+ opts.log?.(
746
+ `[dlq] Received ${batch.messages.length} dead-lettered message(s) on ${queueName}`
747
+ );
748
+ const lastAlertAt = await opts.getLastAlertAt(queueName);
749
+ const secondsSinceLastAlert = (now - lastAlertAt) / 1e3;
750
+ if (!shouldAlertDlq({
751
+ batchSize: batch.messages.length,
752
+ secondsSinceLastAlert,
753
+ minSize,
754
+ maxInterval
755
+ })) {
756
+ batch.ackAll();
757
+ return;
758
+ }
759
+ const recipients = parseDlqAlertRecipients(opts.recipientsRaw);
760
+ if (recipients.length === 0) {
761
+ opts.log?.(
762
+ `[dlq] No recipients configured; dropping ${batch.messages.length} message(s) from ${queueName}`
763
+ );
764
+ await opts.setLastAlertAt(queueName, now);
765
+ batch.ackAll();
766
+ return;
767
+ }
768
+ const { subject, text } = buildDlqAlert(queueName, batch.messages);
769
+ try {
770
+ const { error } = await opts.sendAlert({ recipients, subject, text });
771
+ if (error) throw error;
772
+ } catch {
773
+ batch.retryAll();
774
+ return;
775
+ }
776
+ await opts.setLastAlertAt(queueName, now);
777
+ batch.ackAll();
778
+ }
779
+
700
780
  async function useFetch(url, { parseAs = "json", ...options } = {}) {
701
781
  const [response, fetchError] = await useResult(fetch(url, options));
702
782
  if (fetchError || !response) {
@@ -855,9 +935,14 @@ function cloudflareQueue(options) {
855
935
  descriptor.value = async function(...args) {
856
936
  const batch = args[0];
857
937
  let retriedCount = 0;
938
+ let exhaustedCount = 0;
858
939
  batch.messages.forEach((msg) => {
859
940
  const originalRetry = msg.retry?.bind(msg);
860
941
  msg.retry = () => {
942
+ if (options.maxRetries !== void 0 && msg.attempts >= options.maxRetries) {
943
+ exhaustedCount++;
944
+ return;
945
+ }
861
946
  retriedCount++;
862
947
  return originalRetry({
863
948
  delaySeconds: calculateExponentialBackoff(
@@ -873,6 +958,11 @@ function cloudflareQueue(options) {
873
958
  message: `Retried ${retriedCount} out of ${batch.messages.length} messages`
874
959
  });
875
960
  }
961
+ if (exhaustedCount > 0) {
962
+ throw new Error(
963
+ `[cloudflareQueue] dead-lettering ${exhaustedCount} message(s) past maxRetries=${options.maxRetries}`
964
+ );
965
+ }
876
966
  return result;
877
967
  };
878
968
  };
@@ -968,4 +1058,4 @@ function develitWorker(Worker) {
968
1058
  return DevelitWorker;
969
1059
  }
970
1060
 
971
- export { DatabaseTransaction, ENVIRONMENT, RPCResponse, USER_ROLES, action, asNonEmpty, bankAccount, bankAccountMetadataSchema, base, bicSchema, buildMultiFilterConditions, buildRangeFilterConditions, buildSearchConditions, calculateExponentialBackoff, chunkQueueMessages, cloudflareQueue, composeWranglerBase, createAuditLogWriter, createInsertSchema, createInternalError, createPatchSchema, createSelectSchema, createUpdateSchema, defineCommand, derivePortFromId, develitWorker, durableObjectNamespaceIdFromName, first, firstOrError, getD1Credentials, getDrizzleD1Config, getLocalD1DatabaseIdFromWrangler, getSecret, handleAction, ibanSchema, isInternalError, nullToOptional, optionalToNull, paginationQuerySchema, paginationSchema, pushToQueue, queryInChunks, redact, resolveColumn, service, structuredAddressSchema, useFetch, useResult, useResultSync, workflowInstanceStatusSchema };
1061
+ export { DLQ_ALERT_DEFAULT_MAX_INTERVAL, DLQ_ALERT_DEFAULT_MIN_SIZE, DatabaseTransaction, ENVIRONMENT, RPCResponse, USER_ROLES, action, asNonEmpty, bankAccount, bankAccountMetadataSchema, base, bicSchema, buildDlqAlert, buildMultiFilterConditions, buildRangeFilterConditions, buildSearchConditions, calculateExponentialBackoff, chunkQueueMessages, cloudflareQueue, composeWranglerBase, createAuditLogWriter, createInsertSchema, createInternalError, createPatchSchema, createSelectSchema, createUpdateSchema, defineCommand, derivePortFromId, develitWorker, durableObjectNamespaceIdFromName, first, firstOrError, getD1Credentials, getDrizzleD1Config, getLocalD1DatabaseIdFromWrangler, getSecret, handleAction, handleDlqBatch, ibanSchema, isDlqQueue, isInternalError, nullToOptional, optionalToNull, paginationQuerySchema, paginationSchema, parseDlqAlertRecipients, pushToQueue, queryInChunks, redact, resolveColumn, service, shouldAlertDlq, structuredAddressSchema, useFetch, useResult, useResultSync, workflowInstanceStatusSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@develit-io/backend-sdk",
3
- "version": "12.3.1",
3
+ "version": "12.4.1",
4
4
  "description": "Develit Backend SDK",
5
5
  "author": "Develit.io",
6
6
  "license": "ISC",