@rdlabo/workers-hono-kit 0.9.6 → 0.9.7

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 CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Infrastructure toolkit for building APIs on [Hono](https://hono.dev) + [Cloudflare Workers](https://workers.cloudflare.com).
4
4
 
5
- It provides the building blocks a NestJS-style API needs but that don't run on `workerd` (no Node.js AWS SDK, no `firebase-admin`), plus middleware for common HTTP response concerns:
5
+ It provides Workers-oriented building blocks for a NestJS-style API, plus middleware for common HTTP response concerns:
6
6
 
7
7
  - **Firebase ID-token verification** on Workers via [`jose`](https://github.com/panva/jose) (RS256 against Google's securetoken JWKS), with optional Identity Toolkit REST for `getUser` / `deleteUser`.
8
- - **AWS Secrets Manager / STS AssumeRole / CloudFront signed URLs** via SigV4-signed `fetch` ([`aws4fetch`](https://github.com/mhart/aws4fetch)) or Web Crypto no AWS SDK.
8
+ - **AWS Secrets Manager / STS AssumeRole / CloudFront signed URLs** via focused SigV4-signed `fetch` ([`aws4fetch`](https://github.com/mhart/aws4fetch)) or Web Crypto, avoiding a broad SDK dependency when only a few AWS APIs are needed.
9
9
  - **Middleware**: `finalizeResponse` (weak ETag via `hono/etag`), `validate` (NestJS `ValidationPipe`-shaped 400), and zod number-coercion helpers.
10
10
  - **Standard API errors**: `createHttpErrorHandler` / `notFoundHandler` / `HttpStatus`.
11
11
  - **Deadlock retry** (`ER_LOCK_DEADLOCK` exponential backoff) and an optional **MySQL data layer** (`@rdlabo/workers-hono-kit/db`) for Hyperdrive + Drizzle.
@@ -106,7 +106,7 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
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
107
  | `processBatch(batch, handler, options?)` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency (consumer-side counterpart to `sendInChunks`). |
108
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). |
109
- | `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape (for `withMysqlConnections` in worker entry modules without importing `./db`). |
109
+ | `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape used by lifecycle-compatible APIs and deferred work helpers. |
110
110
 
111
111
  ### Data layer — `@rdlabo/workers-hono-kit/db`
112
112
 
@@ -114,12 +114,12 @@ Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via r
114
114
 
115
115
  | Export | Description |
116
116
  | --- | --- |
117
- | `createHyperdriveDatabase(options)` | `DisposableDatabase` that lazily opens primary/replica connections from Hyperdrive bindings per request; `dispose()` closes them. |
117
+ | `createHyperdriveDatabase(options)` | `DisposableDatabase` that lazily opens primary/replica connections from Hyperdrive bindings per request. Workers cleans them up at invocation end; the legacy `dispose()` is a no-op. |
118
118
  | `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
119
119
  | `databaseFrom(orm, replica)` | Build a `Database` from an existing Drizzle instance + replica handle. |
120
120
  | `Database` / `DisposableDatabase` / `QueryRunner` / `TxOf` | The `read` / `write` / `transaction` API and its supporting types. |
121
- | `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). `ExecutionContextLike` is the same type as the root export, re-exported here so `withMysqlConnections` callers don't need the root import. |
122
- | `withMysqlConnections(...)` | Open primary/replica connections, run a function, close them in `finally` (via `ctx.waitUntil`). |
121
+ | `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). `timezone` controls mysql2's JavaScript `Date` conversion; it does not change the MySQL session timezone. |
122
+ | `withMysqlConnections(...)` | Open primary/replica connections in parallel and run a function. Workers cleans them up at invocation end. |
123
123
  | `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
124
124
  | `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
125
125
  | `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization params (advanced use). |
@@ -588,12 +588,8 @@ const db = createHyperdriveDatabase({
588
588
  createOrm: (conn) => drizzle(conn, { ...DRIZZLE_ORM_OPTIONS, schema }),
589
589
  });
590
590
 
591
- try {
592
- const rows = await db.read('SELECT * FROM users WHERE id = ?', [id]); // replica, raw SQL
593
- await db.write((dz) => dz.insert(users).values({ name })); // primary, deadlock-retried
594
- } finally {
595
- await db.dispose();
596
- }
591
+ const rows = await db.read('SELECT * FROM users WHERE id = ?', [id]); // replica, raw SQL
592
+ await db.write((dz) => dz.insert(users).values({ name })); // primary, deadlock-retried
597
593
  ```
598
594
 
599
595
  ### KV cache
@@ -2,7 +2,7 @@
2
2
  * AWS credentials used to sign Secrets Manager requests.
3
3
  *
4
4
  * @remarks
5
- * Cloudflare Workers have neither the AWS SDK nor IAM role credentials, so static AWS keys are supplied
5
+ * Cloudflare Workers do not provide the AWS default credential chain, so AWS credentials are supplied
6
6
  * as Workers secrets and used to produce a SigV4 signature.
7
7
  */
8
8
  export interface AwsSecretsOptions {
@@ -19,7 +19,7 @@ export interface AwsSecretsOptions {
19
19
  * Fetch and parse a secret from AWS Secrets Manager, caching the result per isolate.
20
20
  *
21
21
  * Issues a `GetSecretValue` call to Secrets Manager via a SigV4-signed `fetch` (using aws4fetch), with no
22
- * AWS SDK involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
22
+ * broad AWS SDK dependency involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
23
23
  * secret ID; concurrent first-time callers share one in-flight request, and a rejected fetch clears the
24
24
  * cache entry so the next call retries.
25
25
  *
@@ -15,7 +15,7 @@ let cache = null;
15
15
  * Fetch and parse a secret from AWS Secrets Manager, caching the result per isolate.
16
16
  *
17
17
  * Issues a `GetSecretValue` call to Secrets Manager via a SigV4-signed `fetch` (using aws4fetch), with no
18
- * AWS SDK involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
18
+ * broad AWS SDK dependency involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
19
19
  * secret ID; concurrent first-time callers share one in-flight request, and a rejected fetch clears the
20
20
  * cache entry so the next call retries.
21
21
  *
@@ -31,11 +31,9 @@ export interface HyperdriveLike {
31
31
  * - `decimalNumbers: true` — return `DECIMAL`/`NEWDECIMAL` columns as JS `number` rather than
32
32
  * strings, so raw-SQL reads and Drizzle's inferred types align on a single numeric domain type.
33
33
  * This assumes no column's precision exceeds the JS safe-integer range.
34
- * - `timezone: '+09:00'` — set the driver's session timezone to JST. mysql2 defaults to `'local'`,
35
- * which is UTC in the Workers runtime; pinning the driver timezone keeps `datetime`/`timestamp`
36
- * round-trips independent of the database's session timezone (only the internally stored UTC
37
- * value differs, which is invisible to the application). Non-JST deployments can override this
38
- * via `extra: { timezone: '...' }`.
34
+ * - `timezone: '+09:00'` — interpret and serialize JavaScript `Date` values as JST. This is a
35
+ * mysql2 client-side conversion option; it does not issue `SET time_zone` or change the MySQL
36
+ * session timezone. Non-JST deployments can override it via `extra: { timezone: '...' }`.
39
37
  *
40
38
  * @param hyperdrive - the Hyperdrive binding to derive connection fields from.
41
39
  * @param extra - additional mysql2 options merged last, overriding the defaults above.
@@ -43,14 +41,15 @@ export interface HyperdriveLike {
43
41
  */
44
42
  export declare function hyperdriveConnectionOptions(hyperdrive: HyperdriveLike, extra?: Record<string, unknown>): Record<string, unknown>;
45
43
  /**
46
- * Open primary and replica connections, run `fn` with them, and close both afterwards.
44
+ * Open primary and replica connections in parallel and run `fn` with them.
47
45
  *
48
- * The connections are always closed in a `finally` block; closing is scheduled through
49
- * `ctx.waitUntil` so it can complete after the response has been returned, without blocking it.
46
+ * Cloudflare Workers automatically cleans up connections created during an invocation. Calling
47
+ * `Connection.end()` is unnecessary and can race work registered with `waitUntil`. The `ctx`
48
+ * parameter remains for API compatibility and is intentionally not used for connection teardown.
50
49
  *
51
50
  * @typeParam T - resolved value produced by `fn`.
52
51
  * @param hyperdrives - the primary and replica Hyperdrive bindings to connect to.
53
- * @param ctx - the execution context whose `waitUntil` defers connection teardown past the response.
52
+ * @param ctx - the request execution context, retained for API compatibility.
54
53
  * @param fn - callback invoked with the open `primary` and `replica` connections.
55
54
  * @param connectionOptions - extra mysql2 options forwarded to {@link hyperdriveConnectionOptions}.
56
55
  * @returns the value resolved by `fn`.
@@ -11,11 +11,9 @@ import { MYSQL_TIMEZONE } from './jst.js';
11
11
  * - `decimalNumbers: true` — return `DECIMAL`/`NEWDECIMAL` columns as JS `number` rather than
12
12
  * strings, so raw-SQL reads and Drizzle's inferred types align on a single numeric domain type.
13
13
  * This assumes no column's precision exceeds the JS safe-integer range.
14
- * - `timezone: '+09:00'` — set the driver's session timezone to JST. mysql2 defaults to `'local'`,
15
- * which is UTC in the Workers runtime; pinning the driver timezone keeps `datetime`/`timestamp`
16
- * round-trips independent of the database's session timezone (only the internally stored UTC
17
- * value differs, which is invisible to the application). Non-JST deployments can override this
18
- * via `extra: { timezone: '...' }`.
14
+ * - `timezone: '+09:00'` — interpret and serialize JavaScript `Date` values as JST. This is a
15
+ * mysql2 client-side conversion option; it does not issue `SET time_zone` or change the MySQL
16
+ * session timezone. Non-JST deployments can override it via `extra: { timezone: '...' }`.
19
17
  *
20
18
  * @param hyperdrive - the Hyperdrive binding to derive connection fields from.
21
19
  * @param extra - additional mysql2 options merged last, overriding the defaults above.
@@ -35,14 +33,15 @@ export function hyperdriveConnectionOptions(hyperdrive, extra) {
35
33
  };
36
34
  }
37
35
  /**
38
- * Open primary and replica connections, run `fn` with them, and close both afterwards.
36
+ * Open primary and replica connections in parallel and run `fn` with them.
39
37
  *
40
- * The connections are always closed in a `finally` block; closing is scheduled through
41
- * `ctx.waitUntil` so it can complete after the response has been returned, without blocking it.
38
+ * Cloudflare Workers automatically cleans up connections created during an invocation. Calling
39
+ * `Connection.end()` is unnecessary and can race work registered with `waitUntil`. The `ctx`
40
+ * parameter remains for API compatibility and is intentionally not used for connection teardown.
42
41
  *
43
42
  * @typeParam T - resolved value produced by `fn`.
44
43
  * @param hyperdrives - the primary and replica Hyperdrive bindings to connect to.
45
- * @param ctx - the execution context whose `waitUntil` defers connection teardown past the response.
44
+ * @param ctx - the request execution context, retained for API compatibility.
46
45
  * @param fn - callback invoked with the open `primary` and `replica` connections.
47
46
  * @param connectionOptions - extra mysql2 options forwarded to {@link hyperdriveConnectionOptions}.
48
47
  * @returns the value resolved by `fn`.
@@ -59,17 +58,10 @@ export function hyperdriveConnectionOptions(hyperdrive, extra) {
59
58
  * ```
60
59
  */
61
60
  export async function withMysqlConnections(hyperdrives, ctx, fn, connectionOptions) {
62
- let primary;
63
- let replica;
64
- try {
65
- primary = await createConnection(hyperdriveConnectionOptions(hyperdrives.primary, connectionOptions));
66
- replica = await createConnection(hyperdriveConnectionOptions(hyperdrives.replica, connectionOptions));
67
- return await fn({ primary, replica });
68
- }
69
- finally {
70
- const closing = [primary, replica].filter((c) => c !== undefined).map((c) => c.end());
71
- if (closing.length > 0) {
72
- ctx.waitUntil(Promise.allSettled(closing));
73
- }
74
- }
61
+ void ctx;
62
+ const [primary, replica] = await Promise.all([
63
+ createConnection(hyperdriveConnectionOptions(hyperdrives.primary, connectionOptions)),
64
+ createConnection(hyperdriveConnectionOptions(hyperdrives.replica, connectionOptions)),
65
+ ]);
66
+ return fn({ primary, replica });
75
67
  }
@@ -79,19 +79,22 @@ export interface Database<TDrizzle, TTx = TxOf<TDrizzle>> {
79
79
  transaction<T>(fn: (tx: TTx) => Promise<T>): Promise<T>;
80
80
  }
81
81
  /**
82
- * A {@link Database} that owns its connections and must be disposed.
82
+ * A {@link Database} that opens its own connections.
83
83
  *
84
84
  * @remarks
85
- * Used by the variants that open connections internally (Hyperdrive- or Pool-backed).
85
+ * Used by variants that open connections internally. Lifecycle behavior depends on the backing
86
+ * implementation: pool-backed databases close their pool, while Hyperdrive-backed databases leave
87
+ * invocation-scoped connection cleanup to the Workers runtime.
86
88
  *
87
89
  * @typeParam TDrizzle - the consumer's Drizzle ORM type.
88
90
  * @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
89
91
  */
90
92
  export interface DisposableDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends Database<TDrizzle, TTx> {
91
93
  /**
92
- * Close the connections opened by this database.
94
+ * Release resources owned by the implementation. Hyperdrive-backed databases keep this method as
95
+ * a compatibility no-op; pool-backed databases use it to close their pool.
93
96
  *
94
- * @returns a promise that settles once both connections are closed.
97
+ * @returns a promise that resolves after implementation-specific cleanup.
95
98
  */
96
99
  dispose(): Promise<void>;
97
100
  }
@@ -155,13 +158,15 @@ export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
155
158
  * Create a {@link DisposableDatabase} that lazily opens its connections from Hyperdrive bindings.
156
159
  *
157
160
  * @remarks
158
- * Construct one per request and call `dispose()` after the response to close the connections.
159
- * Connections and the ORM are created on first use and reused for the lifetime of the instance; the
160
- * read/write/transaction surface is identical to {@link createMysqlDatabase}.
161
+ * Construct one per request. Connections and the ORM are created on first use and reused for the
162
+ * lifetime of the instance; the read/write/transaction surface is identical to
163
+ * {@link createMysqlDatabase}. Workers automatically cleans up connections at the end of the
164
+ * invocation, so callers do not need to close them manually.
161
165
  *
162
166
  * @typeParam TDrizzle - the consumer's Drizzle ORM type.
163
167
  * @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
164
- * @returns a {@link DisposableDatabase} that must be disposed when done.
168
+ * @returns a {@link DisposableDatabase} whose compatibility `dispose()` method is a no-op; Workers
169
+ * cleans up its invocation-scoped connections automatically.
165
170
  * @example
166
171
  * ```ts
167
172
  * const db = createHyperdriveDatabase({
@@ -169,11 +174,7 @@ export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
169
174
  * replicaHyperdrive: env.REPLICA,
170
175
  * createOrm: (primary) => drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
171
176
  * });
172
- * try {
173
- * await db.write((dz) => dz.insert(users).values(user));
174
- * } finally {
175
- * await db.dispose();
176
- * }
177
+ * await db.write((dz) => dz.insert(users).values(user));
177
178
  * ```
178
179
  */
179
180
  export declare function createHyperdriveDatabase<TDrizzle>(options: CreateHyperdriveDatabaseOptions<TDrizzle>): DisposableDatabase<TDrizzle>;
@@ -27,13 +27,15 @@ export function createMysqlDatabase(options) {
27
27
  * Create a {@link DisposableDatabase} that lazily opens its connections from Hyperdrive bindings.
28
28
  *
29
29
  * @remarks
30
- * Construct one per request and call `dispose()` after the response to close the connections.
31
- * Connections and the ORM are created on first use and reused for the lifetime of the instance; the
32
- * read/write/transaction surface is identical to {@link createMysqlDatabase}.
30
+ * Construct one per request. Connections and the ORM are created on first use and reused for the
31
+ * lifetime of the instance; the read/write/transaction surface is identical to
32
+ * {@link createMysqlDatabase}. Workers automatically cleans up connections at the end of the
33
+ * invocation, so callers do not need to close them manually.
33
34
  *
34
35
  * @typeParam TDrizzle - the consumer's Drizzle ORM type.
35
36
  * @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
36
- * @returns a {@link DisposableDatabase} that must be disposed when done.
37
+ * @returns a {@link DisposableDatabase} whose compatibility `dispose()` method is a no-op; Workers
38
+ * cleans up its invocation-scoped connections automatically.
37
39
  * @example
38
40
  * ```ts
39
41
  * const db = createHyperdriveDatabase({
@@ -41,11 +43,7 @@ export function createMysqlDatabase(options) {
41
43
  * replicaHyperdrive: env.REPLICA,
42
44
  * createOrm: (primary) => drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
43
45
  * });
44
- * try {
45
- * await db.write((dz) => dz.insert(users).values(user));
46
- * } finally {
47
- * await db.dispose();
48
- * }
46
+ * await db.write((dz) => dz.insert(users).values(user));
49
47
  * ```
50
48
  */
51
49
  export function createHyperdriveDatabase(options) {
@@ -71,8 +69,9 @@ export function createHyperdriveDatabase(options) {
71
69
  const dz = (await ormFor());
72
70
  return retryWhenDeadlock(() => dz.transaction(fn));
73
71
  },
74
- async dispose() {
75
- await Promise.all([primaryConn?.then((c) => c.end()), replicaConn?.then((c) => c.end())]);
72
+ /** @deprecated Workers cleans up invocation-scoped connections automatically. */
73
+ dispose() {
74
+ return Promise.resolve();
76
75
  },
77
76
  };
78
77
  }
@@ -16,8 +16,8 @@ export interface ServiceAccount {
16
16
  * Minimal Google Identity Toolkit REST client for the user-management operations that token
17
17
  * verification does not cover: `accounts:lookup` (getUser) and `accounts:delete` (deleteUser).
18
18
  *
19
- * This replaces the parts of the `firebase-admin` Node SDK that cannot run on Cloudflare
20
- * Workers (workerd).
19
+ * This is a focused REST alternative for projects that only need a small subset of Firebase Auth
20
+ * administration and do not want to bundle the broader `firebase-admin` SDK.
21
21
  *
22
22
  * @remarks
23
23
  * Authentication follows the JWT-bearer flow: a JWT assertion is signed with the service
@@ -11,8 +11,8 @@ const LOOKUP_CHUNK_SIZE = 100;
11
11
  * Minimal Google Identity Toolkit REST client for the user-management operations that token
12
12
  * verification does not cover: `accounts:lookup` (getUser) and `accounts:delete` (deleteUser).
13
13
  *
14
- * This replaces the parts of the `firebase-admin` Node SDK that cannot run on Cloudflare
15
- * Workers (workerd).
14
+ * This is a focused REST alternative for projects that only need a small subset of Firebase Auth
15
+ * administration and do not want to bundle the broader `firebase-admin` SDK.
16
16
  *
17
17
  * @remarks
18
18
  * Authentication follows the JWT-bearer flow: a JWT assertion is signed with the service
@@ -152,6 +152,12 @@ export class IdentityToolkit {
152
152
  body: JSON.stringify({ localId: uid }),
153
153
  });
154
154
  if (!res.ok) {
155
+ const body = (await res.json().catch(() => undefined));
156
+ // Account deletion is an idempotent operation for callers. A retry after a lost response may
157
+ // legitimately find that the preceding request already removed the Firebase user.
158
+ if (body?.error?.message === 'USER_NOT_FOUND') {
159
+ return;
160
+ }
155
161
  throw new Error(`Identity Toolkit delete failed: ${res.status}`);
156
162
  }
157
163
  }
@@ -2,11 +2,13 @@
2
2
  * Producer-side helper for fanning a large list of items into a Cloudflare Queue without letting the
3
3
  * producer's own subrequest count scale linearly with the list.
4
4
  *
5
- * A Worker may issue at most 50 (free) / 1000 (paid) subrequests per invocation, and each
6
- * {@link QueueLike.send} counts as one subrequest. Enqueuing `N` items with per-item `send()` calls
7
- * therefore reintroduces the very unbounded fan-out that queues exist to remove. {@link sendInChunks}
8
- * instead groups items into batches and issues one {@link QueueLike.sendBatch} per batch, so the
9
- * producer spends `ceil(N / chunkSize)` subrequests regardless of how large `N` grows.
5
+ * Queue bindings call a Cloudflare internal service. The internal-service subrequest budget is
6
+ * 1,000 on Workers Free and matches the configured subrequest limit on Paid (10,000 by default,
7
+ * configurable up to 10 million). Each {@link QueueLike.send} counts as one subrequest. Enqueuing
8
+ * `N` items with per-item `send()` calls therefore reintroduces the very unbounded fan-out that
9
+ * queues exist to remove. {@link sendInChunks} instead groups items into batches and issues one
10
+ * {@link QueueLike.sendBatch} per batch, so the producer spends `ceil(N / chunkSize)` subrequests
11
+ * regardless of how large `N` grows.
10
12
  *
11
13
  * The heavy per-item work (external API calls, etc.) is expected to run in the queue *consumer*,
12
14
  * where each invocation processes only `max_batch_size` messages and thus enjoys its own bounded
@@ -2,11 +2,13 @@
2
2
  * Producer-side helper for fanning a large list of items into a Cloudflare Queue without letting the
3
3
  * producer's own subrequest count scale linearly with the list.
4
4
  *
5
- * A Worker may issue at most 50 (free) / 1000 (paid) subrequests per invocation, and each
6
- * {@link QueueLike.send} counts as one subrequest. Enqueuing `N` items with per-item `send()` calls
7
- * therefore reintroduces the very unbounded fan-out that queues exist to remove. {@link sendInChunks}
8
- * instead groups items into batches and issues one {@link QueueLike.sendBatch} per batch, so the
9
- * producer spends `ceil(N / chunkSize)` subrequests regardless of how large `N` grows.
5
+ * Queue bindings call a Cloudflare internal service. The internal-service subrequest budget is
6
+ * 1,000 on Workers Free and matches the configured subrequest limit on Paid (10,000 by default,
7
+ * configurable up to 10 million). Each {@link QueueLike.send} counts as one subrequest. Enqueuing
8
+ * `N` items with per-item `send()` calls therefore reintroduces the very unbounded fan-out that
9
+ * queues exist to remove. {@link sendInChunks} instead groups items into batches and issues one
10
+ * {@link QueueLike.sendBatch} per batch, so the producer spends `ceil(N / chunkSize)` subrequests
11
+ * regardless of how large `N` grows.
10
12
  *
11
13
  * The heavy per-item work (external API calls, etc.) is expected to run in the queue *consumer*,
12
14
  * where each invocation processes only `max_batch_size` messages and thus enjoys its own bounded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * check-subrequest-fanout — flag per-item external-call fan-outs that scale with data size.
4
4
  *
5
- * Cloudflare Workers cap subrequests per invocation (50 free / 1000 paid). Looping an external call
5
+ * Cloudflare Workers cap external subrequests per invocation (50 on Free; 10,000 by default on
6
+ * Paid, configurable up to 10 million). Looping an external call
6
7
  * (fetch / AI / Stripe / push / ES) once per row reintroduces an unbounded fan-out that eventually
7
8
  * exceeds the cap as the userbase/data grows. This gate greps for the concurrency-loop markers that
8
9
  * usually wrap such fan-outs and fails CI unless the site is explicitly annotated as safe.