@rdlabo/workers-hono-kit 0.9.7 → 0.10.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/README.md +12 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/queue/consumer.d.ts +27 -7
- package/dist/queue/consumer.js +24 -8
- package/dist/queue/error-handler.d.ts +4 -2
- package/dist/queue/error-handler.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,10 +104,20 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
104
104
|
| `verifyAppleReceipt(receipt, opts)` / `classifyAppleRenewal(verify, now)` / `AppleRenewalClassification` / `AppleRenewalState` / `AppleVerifyReceiptResponse` / `ApplePendingRenewalInfo` / `AppleLatestReceiptInfo` | Verify an App Store receipt (production → sandbox fallback; inject `password` / `fetchImpl`) and classify it into `billing_retry` / `lapsed` / `active` / `unknown` plus the raw fields used (`statusCode` / `billingRetryStatus` / `autoRenewStatus`, latest `original_transaction_id` / `expires_date_ms`). |
|
|
105
105
|
| `googleAccessToken(creds, fetch?)` / `getGoogleSubscription(opts)` / `classifyGoogleSubscription(purchase, now)` / `GoogleSubscriptionClassification` / `GoogleSubscriptionState` / `GoogleSubscriptionPurchase` / `GoogleOAuthCredentials` | Exchange a refresh token for an Android Publisher access token (throws on `invalid_grant`), fetch a subscription purchase, and classify it into `canceled` / `gone` / `active` / `unknown` plus raw `statusCode` / `cancelReason`. |
|
|
106
106
|
| `sendInChunks(queue, messages, options?)` / `QueueLike` / `QueueSendMessage` | Send queue messages in bounded chunks to stay under the Workers subrequest cap per invocation. `options.chunkSize` sets the per-batch size (defaults to and is capped at 100). |
|
|
107
|
-
| `processBatch(batch, handler, options?)` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency
|
|
108
|
-
| `createQueueErrorHandler(options)` / `CreateQueueErrorHandlerOptions` | Factory for `processBatch`'s `onError`: logs every failure; optional Sentry capture with queue/message context; optional `maxRetries` gate (report only on final attempt). |
|
|
107
|
+
| `processBatch(batch, handler, options?)` / `isNonRetryableQueueError(error)` / `NonRetryableQueueErrorLike` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency. Errors explicitly tagged with `queueDisposition: 'discard'` are reported and acked as permanent failures; all other errors are retried. |
|
|
108
|
+
| `createQueueErrorHandler(options)` / `CreateQueueErrorHandlerOptions` | Factory for `processBatch`'s `onError`: logs every failure; optional Sentry capture with queue/message context; optional `maxRetries` gate (report only on final attempt, except permanent failures which are reported immediately). |
|
|
109
109
|
| `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape used by lifecycle-compatible APIs and deferred work helpers. |
|
|
110
110
|
|
|
111
|
+
Permanent Queue failures must opt in with the Queue-specific marker; unrelated `retryable` fields are ignored:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import type { NonRetryableQueueErrorLike } from '@rdlabo/workers-hono-kit';
|
|
115
|
+
|
|
116
|
+
class CustomerLinkMissingError extends Error implements NonRetryableQueueErrorLike {
|
|
117
|
+
readonly queueDisposition = 'discard' as const;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
111
121
|
### Data layer — `@rdlabo/workers-hono-kit/db`
|
|
112
122
|
|
|
113
123
|
Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via raw SQL; writes/transactions run against the primary through the Drizzle ORM with deadlock retry. The kit deliberately does not depend on the ORM's type identity — you pass the Drizzle instance in.
|
package/dist/index.d.ts
CHANGED
|
@@ -78,8 +78,8 @@ export type { GoogleSubscriptionClassification, GoogleSubscriptionState, GoogleS
|
|
|
78
78
|
export { retryWhenDeadlock } from './db/retry.js';
|
|
79
79
|
export { sendInChunks } from './queue/send.js';
|
|
80
80
|
export type { QueueLike, QueueSendMessage } from './queue/send.js';
|
|
81
|
-
export { processBatch } from './queue/consumer.js';
|
|
82
|
-
export type { QueueMessageLike, MessageBatchLike, ProcessBatchOptions, ProcessBatchResult } from './queue/consumer.js';
|
|
81
|
+
export { isNonRetryableQueueError, processBatch } from './queue/consumer.js';
|
|
82
|
+
export type { QueueMessageLike, MessageBatchLike, NonRetryableQueueErrorLike, ProcessBatchOptions, ProcessBatchResult, } from './queue/consumer.js';
|
|
83
83
|
export { createQueueErrorHandler } from './queue/error-handler.js';
|
|
84
84
|
export type { CreateQueueErrorHandlerOptions } from './queue/error-handler.js';
|
|
85
85
|
export { createAiGatewayProvider } from './ai/gateway.js';
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ export { classifyGoogleSubscription, getGoogleSubscription, googleAccessToken }
|
|
|
59
59
|
export { retryWhenDeadlock } from './db/retry.js';
|
|
60
60
|
// queue
|
|
61
61
|
export { sendInChunks } from './queue/send.js';
|
|
62
|
-
export { processBatch } from './queue/consumer.js';
|
|
62
|
+
export { isNonRetryableQueueError, processBatch } from './queue/consumer.js';
|
|
63
63
|
export { createQueueErrorHandler } from './queue/error-handler.js';
|
|
64
64
|
// ai
|
|
65
65
|
export { createAiGatewayProvider } from './ai/gateway.js';
|
package/dist/queue/consumer.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Consumer-side helper for processing a Cloudflare Queues `MessageBatch` with per-message success and
|
|
3
|
-
* failure handling.
|
|
3
|
+
* failure handling, including explicit acknowledgement of permanent failures.
|
|
4
4
|
*
|
|
5
5
|
* A queue consumer invocation receives at most `max_batch_size` messages (configured in
|
|
6
6
|
* `wrangler.toml`), which is precisely the mechanism that bounds its subrequest budget: with a small
|
|
7
7
|
* `max_batch_size`, each invocation performs a fixed, small number of external calls no matter how
|
|
8
8
|
* many messages are backed up in the queue. {@link processBatch} applies the standard
|
|
9
9
|
* ack-on-success / retry-on-failure discipline so one poison message does not fail its whole batch.
|
|
10
|
+
* Errors explicitly tagged with `queueDisposition: 'discard'` are reported and acknowledged because
|
|
11
|
+
* delivering the same payload again cannot make them converge.
|
|
10
12
|
*
|
|
11
13
|
* Messages are processed sequentially. This keeps the number of *simultaneously open* subrequests at
|
|
12
14
|
* one, staying well clear of the Workers concurrent-connection ceiling, and makes the per-invocation
|
|
@@ -66,8 +68,10 @@ export interface MessageBatchLike<Body = unknown> {
|
|
|
66
68
|
*/
|
|
67
69
|
export interface ProcessBatchOptions<Body = unknown> {
|
|
68
70
|
/**
|
|
69
|
-
* Invoked when `handler` throws for a message,
|
|
70
|
-
* Use it to log or report; it must not throw. Defaults to `console.error`.
|
|
71
|
+
* Invoked when `handler` throws for a message, before the message is retried or acknowledged as a
|
|
72
|
+
* permanent failure. Use it to log or report; it must not throw. Defaults to `console.error`.
|
|
73
|
+
* If a custom hook does throw, `processBatch` emits a console fallback and still applies the
|
|
74
|
+
* original error's disposition so a telemetry outage cannot turn a poison message into retries.
|
|
71
75
|
*/
|
|
72
76
|
onError?: (error: unknown, message: QueueMessageLike<Body>) => void;
|
|
73
77
|
/**
|
|
@@ -82,14 +86,30 @@ export interface ProcessBatchOptions<Body = unknown> {
|
|
|
82
86
|
export interface ProcessBatchResult {
|
|
83
87
|
/** Messages whose handler completed successfully and were acked. */
|
|
84
88
|
processed: number;
|
|
89
|
+
/** Messages whose handler failed permanently and were acked instead of retried. */
|
|
90
|
+
discarded: number;
|
|
85
91
|
/** Messages whose handler threw and were marked for retry. */
|
|
86
92
|
failed: number;
|
|
87
93
|
}
|
|
88
94
|
/**
|
|
89
|
-
*
|
|
95
|
+
* Error contract for failures that cannot converge by redelivering the same queue message.
|
|
90
96
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
97
|
+
* Domain packages should use a named error class with this flag. {@link processBatch} owns the
|
|
98
|
+
* transport decision: it reports the failure through `onError`, acknowledges the message, and does
|
|
99
|
+
* not spend retries or dead-letter capacity on it.
|
|
100
|
+
*/
|
|
101
|
+
export interface NonRetryableQueueErrorLike {
|
|
102
|
+
readonly queueDisposition: 'discard';
|
|
103
|
+
}
|
|
104
|
+
/** Return whether an unknown thrown value explicitly opts out of queue retry. */
|
|
105
|
+
export declare function isNonRetryableQueueError(error: unknown): error is NonRetryableQueueErrorLike;
|
|
106
|
+
/**
|
|
107
|
+
* Process every message in `batch` sequentially, acking successes and permanent failures while
|
|
108
|
+
* retrying transient or unclassified failures.
|
|
109
|
+
*
|
|
110
|
+
* Each message is passed to `handler`; if it resolves the message is acked. A thrown error is routed
|
|
111
|
+
* to {@link ProcessBatchOptions.onError}; errors tagged with `queueDisposition: 'discard'` are then
|
|
112
|
+
* acked and counted as discarded, while all other errors are marked for retry (honoring
|
|
93
113
|
* {@link ProcessBatchOptions.retryDelaySeconds}). One failing message never affects the others, and
|
|
94
114
|
* the returned counts let tests assert that the per-invocation workload — and therefore the
|
|
95
115
|
* subrequest count — stayed bounded by the batch size.
|
|
@@ -99,7 +119,7 @@ export interface ProcessBatchResult {
|
|
|
99
119
|
* @param handler - Async work for a single message; performs the bounded external call(s). Receives
|
|
100
120
|
* the decoded `body` and the raw message (for `attempts`, `id`, etc.).
|
|
101
121
|
* @param options - Error reporting and retry tuning; see {@link ProcessBatchOptions}.
|
|
102
|
-
* @returns The number of processed and failed messages.
|
|
122
|
+
* @returns The number of processed, discarded, and retryable-failed messages.
|
|
103
123
|
* @example
|
|
104
124
|
* ```ts
|
|
105
125
|
* const { processed, failed } = await processBatch(
|
package/dist/queue/consumer.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Consumer-side helper for processing a Cloudflare Queues `MessageBatch` with per-message success and
|
|
3
|
-
* failure handling.
|
|
3
|
+
* failure handling, including explicit acknowledgement of permanent failures.
|
|
4
4
|
*
|
|
5
5
|
* A queue consumer invocation receives at most `max_batch_size` messages (configured in
|
|
6
6
|
* `wrangler.toml`), which is precisely the mechanism that bounds its subrequest budget: with a small
|
|
7
7
|
* `max_batch_size`, each invocation performs a fixed, small number of external calls no matter how
|
|
8
8
|
* many messages are backed up in the queue. {@link processBatch} applies the standard
|
|
9
9
|
* ack-on-success / retry-on-failure discipline so one poison message does not fail its whole batch.
|
|
10
|
+
* Errors explicitly tagged with `queueDisposition: 'discard'` are reported and acknowledged because
|
|
11
|
+
* delivering the same payload again cannot make them converge.
|
|
10
12
|
*
|
|
11
13
|
* Messages are processed sequentially. This keeps the number of *simultaneously open* subrequests at
|
|
12
14
|
* one, staying well clear of the Workers concurrent-connection ceiling, and makes the per-invocation
|
|
@@ -27,11 +29,17 @@
|
|
|
27
29
|
*
|
|
28
30
|
* @packageDocumentation
|
|
29
31
|
*/
|
|
32
|
+
/** Return whether an unknown thrown value explicitly opts out of queue retry. */
|
|
33
|
+
export function isNonRetryableQueueError(error) {
|
|
34
|
+
return (typeof error === 'object' && error !== null && 'queueDisposition' in error && error.queueDisposition === 'discard');
|
|
35
|
+
}
|
|
30
36
|
/**
|
|
31
|
-
* Process every message in `batch` sequentially, acking
|
|
37
|
+
* Process every message in `batch` sequentially, acking successes and permanent failures while
|
|
38
|
+
* retrying transient or unclassified failures.
|
|
32
39
|
*
|
|
33
|
-
* Each message is passed to `handler`; if it resolves the message is acked
|
|
34
|
-
*
|
|
40
|
+
* Each message is passed to `handler`; if it resolves the message is acked. A thrown error is routed
|
|
41
|
+
* to {@link ProcessBatchOptions.onError}; errors tagged with `queueDisposition: 'discard'` are then
|
|
42
|
+
* acked and counted as discarded, while all other errors are marked for retry (honoring
|
|
35
43
|
* {@link ProcessBatchOptions.retryDelaySeconds}). One failing message never affects the others, and
|
|
36
44
|
* the returned counts let tests assert that the per-invocation workload — and therefore the
|
|
37
45
|
* subrequest count — stayed bounded by the batch size.
|
|
@@ -41,7 +49,7 @@
|
|
|
41
49
|
* @param handler - Async work for a single message; performs the bounded external call(s). Receives
|
|
42
50
|
* the decoded `body` and the raw message (for `attempts`, `id`, etc.).
|
|
43
51
|
* @param options - Error reporting and retry tuning; see {@link ProcessBatchOptions}.
|
|
44
|
-
* @returns The number of processed and failed messages.
|
|
52
|
+
* @returns The number of processed, discarded, and retryable-failed messages.
|
|
45
53
|
* @example
|
|
46
54
|
* ```ts
|
|
47
55
|
* const { processed, failed } = await processBatch(
|
|
@@ -58,6 +66,7 @@ export async function processBatch(batch, handler, options) {
|
|
|
58
66
|
});
|
|
59
67
|
const retryOptions = options?.retryDelaySeconds === undefined ? undefined : { delaySeconds: options.retryDelaySeconds };
|
|
60
68
|
let processed = 0;
|
|
69
|
+
let discarded = 0;
|
|
61
70
|
let failed = 0;
|
|
62
71
|
for (const message of batch.messages) {
|
|
63
72
|
try {
|
|
@@ -69,12 +78,19 @@ export async function processBatch(batch, handler, options) {
|
|
|
69
78
|
try {
|
|
70
79
|
onError(error, message);
|
|
71
80
|
}
|
|
72
|
-
catch {
|
|
73
|
-
//
|
|
81
|
+
catch (reportingError) {
|
|
82
|
+
// Reporting is best-effort. Preserve the domain error's disposition, but never let a broken
|
|
83
|
+
// custom reporter make a permanent failure disappear without any local trace.
|
|
84
|
+
console.error(`[queue:${batch.queue}] onError failed for message ${message.id}`, reportingError, 'original error:', error);
|
|
85
|
+
}
|
|
86
|
+
if (isNonRetryableQueueError(error)) {
|
|
87
|
+
message.ack();
|
|
88
|
+
discarded++;
|
|
89
|
+
continue;
|
|
74
90
|
}
|
|
75
91
|
message.retry(retryOptions);
|
|
76
92
|
failed++;
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
|
-
return { processed, failed };
|
|
95
|
+
return { processed, discarded, failed };
|
|
80
96
|
}
|
|
@@ -8,8 +8,10 @@ export interface CreateQueueErrorHandlerOptions {
|
|
|
8
8
|
queue: string;
|
|
9
9
|
/**
|
|
10
10
|
* When set, `captureException` is called only after the final delivery attempt
|
|
11
|
-
* (`message.attempts > maxRetries`).
|
|
12
|
-
*
|
|
11
|
+
* (`message.attempts > maxRetries`). Errors tagged with `queueDisposition: 'discard'` are captured
|
|
12
|
+
* on their first delivery because {@link processBatch} acknowledges them immediately. Cloudflare
|
|
13
|
+
* Queues uses 1-based `attempts`; the last delivery before the dead-letter queue has
|
|
14
|
+
* `attempts === maxRetries + 1`.
|
|
13
15
|
*/
|
|
14
16
|
maxRetries?: number;
|
|
15
17
|
/** Optional Sentry client. Omit for console-only reporting (e.g. airlec). */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNonRetryableQueueError } from './consumer.js';
|
|
1
2
|
/**
|
|
2
3
|
* Factory for {@link processBatch}'s `onError` hook: logs every failure and optionally reports to
|
|
3
4
|
* Sentry (or another sink) with queue / message id / attempts / body context.
|
|
@@ -10,11 +11,15 @@ export function createQueueErrorHandler(options) {
|
|
|
10
11
|
if (!capture) {
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
|
-
if (maxRetries !== undefined && message.attempts <= maxRetries) {
|
|
14
|
+
if (!isNonRetryableQueueError(error) && maxRetries !== undefined && message.attempts <= maxRetries) {
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
17
|
capture(error, {
|
|
17
|
-
tags: {
|
|
18
|
+
tags: {
|
|
19
|
+
queue,
|
|
20
|
+
queue_message_id: message.id,
|
|
21
|
+
queue_disposition: isNonRetryableQueueError(error) ? 'discard' : 'retry',
|
|
22
|
+
},
|
|
18
23
|
extra: { attempts: message.attempts, body: message.body },
|
|
19
24
|
});
|
|
20
25
|
};
|