@develit-io/backend-sdk 12.3.0 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.mjs +92 -1
- package/package.json +1 -1
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,87 @@ 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 contains `-dlq`. Robust to the environment
|
|
1271
|
+
* suffix Cloudflare appends in deployed envs (e.g. `...-dlq-production`).
|
|
1272
|
+
*/
|
|
1273
|
+
declare function isDlqQueue(queueName: string): boolean;
|
|
1274
|
+
/**
|
|
1275
|
+
* Parses a comma-separated recipients string into a trimmed, de-duplicated list.
|
|
1276
|
+
*/
|
|
1277
|
+
declare function parseDlqAlertRecipients(raw: string | undefined): string[];
|
|
1278
|
+
type ShouldAlertDlqParams = {
|
|
1279
|
+
batchSize: number;
|
|
1280
|
+
secondsSinceLastAlert: number;
|
|
1281
|
+
minSize: number;
|
|
1282
|
+
maxInterval: number;
|
|
1283
|
+
};
|
|
1284
|
+
/**
|
|
1285
|
+
* Rate-limit gate: suppress the alert only while the batch is small AND not
|
|
1286
|
+
* enough time has passed since the last alert. Otherwise alert.
|
|
1287
|
+
*/
|
|
1288
|
+
declare function shouldAlertDlq({ batchSize, secondsSinceLastAlert, minSize, maxInterval, }: ShouldAlertDlqParams): boolean;
|
|
1289
|
+
type DlqAlert = {
|
|
1290
|
+
subject: string;
|
|
1291
|
+
text: string;
|
|
1292
|
+
};
|
|
1293
|
+
/**
|
|
1294
|
+
* Builds the alert subject + plain-text body summarizing the dead-lettered
|
|
1295
|
+
* messages. Bodies are stringified defensively — DLQ messages may come from any
|
|
1296
|
+
* consumed queue, so no shape is assumed.
|
|
1297
|
+
*/
|
|
1298
|
+
declare function buildDlqAlert(queueName: string, messages: ReadonlyArray<{
|
|
1299
|
+
body: unknown;
|
|
1300
|
+
}>): DlqAlert;
|
|
1301
|
+
type HandleDlqBatchOptions = {
|
|
1302
|
+
/** Comma-separated recipient list (e.g. from an env var). */
|
|
1303
|
+
recipientsRaw: string | undefined;
|
|
1304
|
+
/** Returns the last-alert timestamp (ms) for a queue, or 0 if never alerted. */
|
|
1305
|
+
getLastAlertAt: (queueName: string) => Promise<number>;
|
|
1306
|
+
/** Persists the last-alert timestamp (ms) for a queue. */
|
|
1307
|
+
setLastAlertAt: (queueName: string, at: number) => Promise<void>;
|
|
1308
|
+
/** Delivers the alert. Return an object with `error` set on failure. */
|
|
1309
|
+
sendAlert: (input: {
|
|
1310
|
+
recipients: string[];
|
|
1311
|
+
subject: string;
|
|
1312
|
+
text: string;
|
|
1313
|
+
}) => Promise<{
|
|
1314
|
+
error?: unknown;
|
|
1315
|
+
}>;
|
|
1316
|
+
/** Alert immediately once a batch reaches this size. Default 50. */
|
|
1317
|
+
minSize?: number;
|
|
1318
|
+
/** Min seconds between alerts for the same queue while batches are small. Default 21600. */
|
|
1319
|
+
maxInterval?: number;
|
|
1320
|
+
/** Current time in ms. Defaults to `Date.now()` — override in tests. */
|
|
1321
|
+
now?: number;
|
|
1322
|
+
/** Optional logger for observability. */
|
|
1323
|
+
log?: (message: string) => void;
|
|
1324
|
+
};
|
|
1325
|
+
/**
|
|
1326
|
+
* Handles a batch from any consumed dead-letter queue: rate-limited alert then
|
|
1327
|
+
* ack. Mirrors the verified flow:
|
|
1328
|
+
* - rate-limited (already alerted this window) → `ackAll` (alert-only; retrying
|
|
1329
|
+
* would just churn until the DLQ's own max_retries drops the message);
|
|
1330
|
+
* - no recipients → log + `ackAll`;
|
|
1331
|
+
* - alert send fails → `retryAll` (try again next pull);
|
|
1332
|
+
* - success → persist last-alert + `ackAll`.
|
|
1333
|
+
*
|
|
1334
|
+
* NOTE: alert-only — acked messages are dropped (no persistence/replay).
|
|
1335
|
+
*/
|
|
1336
|
+
declare function handleDlqBatch(batch: MessageBatch, opts: HandleDlqBatchOptions): Promise<void>;
|
|
1337
|
+
|
|
1257
1338
|
declare function first<T>(rows: T[]): T | undefined;
|
|
1258
1339
|
declare function firstOrError<T>(rows: T[]): T;
|
|
1259
1340
|
declare function derivePortFromId(id: string, base?: number, range?: number): number;
|
|
@@ -1386,9 +1467,24 @@ declare const action: (name: string) => MethodDecorator;
|
|
|
1386
1467
|
|
|
1387
1468
|
interface WithRetryCounterOptions {
|
|
1388
1469
|
baseDelay: number;
|
|
1470
|
+
/**
|
|
1471
|
+
* Cap after which a message is allowed to dead-letter instead of being
|
|
1472
|
+
* retried again. When a message's `attempts` reaches `maxRetries`, the
|
|
1473
|
+
* wrapped `retry()` becomes a no-op and — once the handler returns — the
|
|
1474
|
+
* decorator throws, so Cloudflare's native max-retries path routes the
|
|
1475
|
+
* message(s) to the configured DLQ.
|
|
1476
|
+
*
|
|
1477
|
+
* This is required because an explicit delayed `retry({ delaySeconds })`
|
|
1478
|
+
* issued at the cap is silently dropped by Cloudflare (never written to the
|
|
1479
|
+
* DLQ), and returning without a retry implicitly acks (also dropping it).
|
|
1480
|
+
*
|
|
1481
|
+
* Omit to keep the legacy behaviour (always retry with backoff). Set it to
|
|
1482
|
+
* the consumer's configured `max_retries` to get correct dead-lettering.
|
|
1483
|
+
*/
|
|
1484
|
+
maxRetries?: number;
|
|
1389
1485
|
}
|
|
1390
1486
|
type AsyncMethod<TArgs extends unknown[] = unknown[], TResult = unknown> = (...args: TArgs) => Promise<TResult>;
|
|
1391
1487
|
declare function cloudflareQueue<TArgs extends unknown[] = unknown[], TResult = unknown>(options: WithRetryCounterOptions): (target: unknown, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<AsyncMethod<TArgs, TResult>>) => void;
|
|
1392
1488
|
|
|
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 };
|
|
1489
|
+
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 };
|
|
1490
|
+
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,87 @@ 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 contains `-dlq`. Robust to the environment
|
|
1271
|
+
* suffix Cloudflare appends in deployed envs (e.g. `...-dlq-production`).
|
|
1272
|
+
*/
|
|
1273
|
+
declare function isDlqQueue(queueName: string): boolean;
|
|
1274
|
+
/**
|
|
1275
|
+
* Parses a comma-separated recipients string into a trimmed, de-duplicated list.
|
|
1276
|
+
*/
|
|
1277
|
+
declare function parseDlqAlertRecipients(raw: string | undefined): string[];
|
|
1278
|
+
type ShouldAlertDlqParams = {
|
|
1279
|
+
batchSize: number;
|
|
1280
|
+
secondsSinceLastAlert: number;
|
|
1281
|
+
minSize: number;
|
|
1282
|
+
maxInterval: number;
|
|
1283
|
+
};
|
|
1284
|
+
/**
|
|
1285
|
+
* Rate-limit gate: suppress the alert only while the batch is small AND not
|
|
1286
|
+
* enough time has passed since the last alert. Otherwise alert.
|
|
1287
|
+
*/
|
|
1288
|
+
declare function shouldAlertDlq({ batchSize, secondsSinceLastAlert, minSize, maxInterval, }: ShouldAlertDlqParams): boolean;
|
|
1289
|
+
type DlqAlert = {
|
|
1290
|
+
subject: string;
|
|
1291
|
+
text: string;
|
|
1292
|
+
};
|
|
1293
|
+
/**
|
|
1294
|
+
* Builds the alert subject + plain-text body summarizing the dead-lettered
|
|
1295
|
+
* messages. Bodies are stringified defensively — DLQ messages may come from any
|
|
1296
|
+
* consumed queue, so no shape is assumed.
|
|
1297
|
+
*/
|
|
1298
|
+
declare function buildDlqAlert(queueName: string, messages: ReadonlyArray<{
|
|
1299
|
+
body: unknown;
|
|
1300
|
+
}>): DlqAlert;
|
|
1301
|
+
type HandleDlqBatchOptions = {
|
|
1302
|
+
/** Comma-separated recipient list (e.g. from an env var). */
|
|
1303
|
+
recipientsRaw: string | undefined;
|
|
1304
|
+
/** Returns the last-alert timestamp (ms) for a queue, or 0 if never alerted. */
|
|
1305
|
+
getLastAlertAt: (queueName: string) => Promise<number>;
|
|
1306
|
+
/** Persists the last-alert timestamp (ms) for a queue. */
|
|
1307
|
+
setLastAlertAt: (queueName: string, at: number) => Promise<void>;
|
|
1308
|
+
/** Delivers the alert. Return an object with `error` set on failure. */
|
|
1309
|
+
sendAlert: (input: {
|
|
1310
|
+
recipients: string[];
|
|
1311
|
+
subject: string;
|
|
1312
|
+
text: string;
|
|
1313
|
+
}) => Promise<{
|
|
1314
|
+
error?: unknown;
|
|
1315
|
+
}>;
|
|
1316
|
+
/** Alert immediately once a batch reaches this size. Default 50. */
|
|
1317
|
+
minSize?: number;
|
|
1318
|
+
/** Min seconds between alerts for the same queue while batches are small. Default 21600. */
|
|
1319
|
+
maxInterval?: number;
|
|
1320
|
+
/** Current time in ms. Defaults to `Date.now()` — override in tests. */
|
|
1321
|
+
now?: number;
|
|
1322
|
+
/** Optional logger for observability. */
|
|
1323
|
+
log?: (message: string) => void;
|
|
1324
|
+
};
|
|
1325
|
+
/**
|
|
1326
|
+
* Handles a batch from any consumed dead-letter queue: rate-limited alert then
|
|
1327
|
+
* ack. Mirrors the verified flow:
|
|
1328
|
+
* - rate-limited (already alerted this window) → `ackAll` (alert-only; retrying
|
|
1329
|
+
* would just churn until the DLQ's own max_retries drops the message);
|
|
1330
|
+
* - no recipients → log + `ackAll`;
|
|
1331
|
+
* - alert send fails → `retryAll` (try again next pull);
|
|
1332
|
+
* - success → persist last-alert + `ackAll`.
|
|
1333
|
+
*
|
|
1334
|
+
* NOTE: alert-only — acked messages are dropped (no persistence/replay).
|
|
1335
|
+
*/
|
|
1336
|
+
declare function handleDlqBatch(batch: MessageBatch, opts: HandleDlqBatchOptions): Promise<void>;
|
|
1337
|
+
|
|
1257
1338
|
declare function first<T>(rows: T[]): T | undefined;
|
|
1258
1339
|
declare function firstOrError<T>(rows: T[]): T;
|
|
1259
1340
|
declare function derivePortFromId(id: string, base?: number, range?: number): number;
|
|
@@ -1386,9 +1467,24 @@ declare const action: (name: string) => MethodDecorator;
|
|
|
1386
1467
|
|
|
1387
1468
|
interface WithRetryCounterOptions {
|
|
1388
1469
|
baseDelay: number;
|
|
1470
|
+
/**
|
|
1471
|
+
* Cap after which a message is allowed to dead-letter instead of being
|
|
1472
|
+
* retried again. When a message's `attempts` reaches `maxRetries`, the
|
|
1473
|
+
* wrapped `retry()` becomes a no-op and — once the handler returns — the
|
|
1474
|
+
* decorator throws, so Cloudflare's native max-retries path routes the
|
|
1475
|
+
* message(s) to the configured DLQ.
|
|
1476
|
+
*
|
|
1477
|
+
* This is required because an explicit delayed `retry({ delaySeconds })`
|
|
1478
|
+
* issued at the cap is silently dropped by Cloudflare (never written to the
|
|
1479
|
+
* DLQ), and returning without a retry implicitly acks (also dropping it).
|
|
1480
|
+
*
|
|
1481
|
+
* Omit to keep the legacy behaviour (always retry with backoff). Set it to
|
|
1482
|
+
* the consumer's configured `max_retries` to get correct dead-lettering.
|
|
1483
|
+
*/
|
|
1484
|
+
maxRetries?: number;
|
|
1389
1485
|
}
|
|
1390
1486
|
type AsyncMethod<TArgs extends unknown[] = unknown[], TResult = unknown> = (...args: TArgs) => Promise<TResult>;
|
|
1391
1487
|
declare function cloudflareQueue<TArgs extends unknown[] = unknown[], TResult = unknown>(options: WithRetryCounterOptions): (target: unknown, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<AsyncMethod<TArgs, TResult>>) => void;
|
|
1392
1488
|
|
|
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 };
|
|
1489
|
+
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 };
|
|
1490
|
+
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 queueName.includes("-dlq");
|
|
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) {
|
|
@@ -734,6 +814,7 @@ function nullToOptional(obj) {
|
|
|
734
814
|
return Object.fromEntries(
|
|
735
815
|
Object.entries(obj).map(([k, v]) => {
|
|
736
816
|
if (v === null) return [k, void 0];
|
|
817
|
+
if (typeof v === "string") return [k, v.trim() === "" ? void 0 : v];
|
|
737
818
|
if (Array.isArray(v))
|
|
738
819
|
return [
|
|
739
820
|
k,
|
|
@@ -854,9 +935,14 @@ function cloudflareQueue(options) {
|
|
|
854
935
|
descriptor.value = async function(...args) {
|
|
855
936
|
const batch = args[0];
|
|
856
937
|
let retriedCount = 0;
|
|
938
|
+
let exhaustedCount = 0;
|
|
857
939
|
batch.messages.forEach((msg) => {
|
|
858
940
|
const originalRetry = msg.retry?.bind(msg);
|
|
859
941
|
msg.retry = () => {
|
|
942
|
+
if (options.maxRetries !== void 0 && msg.attempts >= options.maxRetries) {
|
|
943
|
+
exhaustedCount++;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
860
946
|
retriedCount++;
|
|
861
947
|
return originalRetry({
|
|
862
948
|
delaySeconds: calculateExponentialBackoff(
|
|
@@ -872,6 +958,11 @@ function cloudflareQueue(options) {
|
|
|
872
958
|
message: `Retried ${retriedCount} out of ${batch.messages.length} messages`
|
|
873
959
|
});
|
|
874
960
|
}
|
|
961
|
+
if (exhaustedCount > 0) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`[cloudflareQueue] dead-lettering ${exhaustedCount} message(s) past maxRetries=${options.maxRetries}`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
875
966
|
return result;
|
|
876
967
|
};
|
|
877
968
|
};
|
|
@@ -967,4 +1058,4 @@ function develitWorker(Worker) {
|
|
|
967
1058
|
return DevelitWorker;
|
|
968
1059
|
}
|
|
969
1060
|
|
|
970
|
-
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 };
|