@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 +101 -3
- package/dist/index.d.ts +101 -3
- package/dist/index.mjs +91 -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,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 };
|