@lunora/queue 1.0.0-alpha.1 → 1.0.0-alpha.10
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/LICENSE.md +6 -0
- package/dist/index.d.mts +175 -109
- package/dist/index.d.ts +175 -109
- package/dist/index.mjs +3 -2
- package/dist/packem_shared/createQueueCaptureSink-B8WE0eHf.mjs +64 -0
- package/dist/packem_shared/{createQueueRunContext-DVqG7Oyk.mjs → createQueueRunContext-C8jboCk6.mjs} +28 -5
- package/dist/packem_shared/dispatchQueueBatch-DSEWhEy8.mjs +132 -0
- package/package.json +4 -1
- package/dist/packem_shared/dispatchQueueBatch-DVMmBISH.mjs +0 -18
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
|
-
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
4
|
-
* Node-safe — no Cloudflare runtime imports — so the consumers stay unit-testable
|
|
5
|
-
* with plain-object doubles.
|
|
6
|
-
*/
|
|
2
|
+
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
|
+
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
4
|
+
* Node-safe — no Cloudflare runtime imports — so the consumers stay unit-testable
|
|
5
|
+
* with plain-object doubles.
|
|
6
|
+
*/
|
|
7
7
|
/** Opaque generated function reference (`api.foo.bar`), carrying its dispatch id. */
|
|
8
8
|
interface FunctionReference {
|
|
9
9
|
__lunoraRef: string;
|
|
@@ -24,7 +24,6 @@ interface DispatchLogger {
|
|
|
24
24
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
25
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
26
|
}
|
|
27
|
-
/** Build a {@link DispatchLogger} that prefixes every line with `prefix` (e.g. `[queue:email]`). */
|
|
28
27
|
/** How a queue message body is serialized on the wire (Cloudflare default `"json"`). */
|
|
29
28
|
type QueueContentType = "bytes" | "json" | "text" | "v8";
|
|
30
29
|
/** Options for a single `producer.send(body, options?)`. */
|
|
@@ -46,10 +45,10 @@ interface MessageSendRequestLike<Body = unknown> {
|
|
|
46
45
|
delaySeconds?: number;
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
|
-
* Minimal structural projection of workers-types' `Queue<Body>` (the producer
|
|
50
|
-
* binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
|
|
51
|
-
* we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
|
|
52
|
-
*/
|
|
48
|
+
* Minimal structural projection of workers-types' `Queue<Body>` (the producer
|
|
49
|
+
* binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
|
|
50
|
+
* we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
|
|
51
|
+
*/
|
|
53
52
|
interface QueueBindingLike<Body = unknown> {
|
|
54
53
|
send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
|
|
55
54
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
|
|
@@ -80,10 +79,10 @@ interface MessageBatchLike<Body = unknown> {
|
|
|
80
79
|
retryAll: (options?: QueueRetryOptions) => void;
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
|
-
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
84
|
-
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
85
|
-
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
86
|
-
*/
|
|
82
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
83
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
84
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
85
|
+
*/
|
|
87
86
|
interface QueueProducer<Body = unknown> {
|
|
88
87
|
/** Enqueue one message. */
|
|
89
88
|
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
@@ -91,10 +90,10 @@ interface QueueProducer<Body = unknown> {
|
|
|
91
90
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
|
-
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
95
|
-
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
96
|
-
* `createQueues` stays schema-agnostic.
|
|
97
|
-
*/
|
|
93
|
+
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
94
|
+
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
95
|
+
* `createQueues` stays schema-agnostic.
|
|
96
|
+
*/
|
|
98
97
|
interface Queues {
|
|
99
98
|
[exportName: string]: QueueProducer;
|
|
100
99
|
}
|
|
@@ -104,11 +103,11 @@ interface LunoraQueuesOptions {
|
|
|
104
103
|
bindings: Record<string, QueueBindingLike>;
|
|
105
104
|
}
|
|
106
105
|
/**
|
|
107
|
-
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
108
|
-
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
109
|
-
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
110
|
-
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
111
|
-
*/
|
|
106
|
+
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
107
|
+
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
108
|
+
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
109
|
+
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
110
|
+
*/
|
|
112
111
|
interface QueueRunContext {
|
|
113
112
|
/** The worker `env` (bindings + vars). */
|
|
114
113
|
readonly env: Record<string, unknown>;
|
|
@@ -137,25 +136,25 @@ interface QueueConsumerTuning {
|
|
|
137
136
|
/** The config object passed to `defineQueue`. */
|
|
138
137
|
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
138
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
* The push-consumer body. Required for `mode: "push"` (the default); omit it
|
|
140
|
+
* for `mode: "pull"`, where an external worker polls the queue over HTTP.
|
|
141
|
+
*/
|
|
143
142
|
handler?: QueueHandler<Body>;
|
|
144
143
|
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
144
|
mode?: QueueConsumerMode;
|
|
146
145
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
147
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
148
|
+
*/
|
|
150
149
|
name?: string;
|
|
151
150
|
}
|
|
152
151
|
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
152
|
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
153
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
155
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
156
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
157
|
+
*/
|
|
159
158
|
readonly __lunoraBody?: Body;
|
|
160
159
|
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
160
|
isLunoraQueue: true;
|
|
@@ -169,98 +168,165 @@ interface QueueBindingSpec {
|
|
|
169
168
|
/** The stable wrangler queue name, e.g. `email-queue`. */
|
|
170
169
|
name: string;
|
|
171
170
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
174
|
-
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
175
|
-
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
176
|
-
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
177
|
-
* the missing queue is actually used.
|
|
178
|
-
*/
|
|
179
|
-
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
180
|
-
/**
|
|
181
|
-
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
182
|
-
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
183
|
-
* export whose binding is absent throws a directed error naming the declared
|
|
184
|
-
* queues (raised lazily on first use).
|
|
185
|
-
*/
|
|
186
|
-
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
187
|
-
/**
|
|
188
|
-
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
189
|
-
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
190
|
-
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
191
|
-
* queue export can never collide with the built-in bindings.
|
|
192
|
-
*/
|
|
193
|
-
declare const queueBindingName: (exportName: string) => string;
|
|
194
|
-
/**
|
|
195
|
-
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
196
|
-
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
197
|
-
* deployed queue's identifier when no explicit `name` override is given.
|
|
198
|
-
*/
|
|
199
|
-
declare const queueDefaultName: (exportName: string) => string;
|
|
200
|
-
/**
|
|
201
|
-
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
202
|
-
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
203
|
-
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
204
|
-
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
205
|
-
* entries from the same definition.
|
|
206
|
-
*
|
|
207
|
-
* ```ts
|
|
208
|
-
* // lunora/queues.ts
|
|
209
|
-
* import { defineQueue } from "@lunora/queue";
|
|
210
|
-
* import { api } from "./_generated/api";
|
|
211
|
-
*
|
|
212
|
-
* export const emailQueue = defineQueue<{ to: string }>({
|
|
213
|
-
* handler: async (ctx, batch) => {
|
|
214
|
-
* for (const message of batch.messages) {
|
|
215
|
-
* await ctx.run(api.email.send, { to: message.body.to });
|
|
216
|
-
* message.ack();
|
|
217
|
-
* }
|
|
218
|
-
* },
|
|
219
|
-
* });
|
|
220
|
-
* ```
|
|
221
|
-
*
|
|
222
|
-
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
223
|
-
*
|
|
224
|
-
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
225
|
-
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
226
|
-
* trusted path the scheduler and workflows use), so those calls run with the
|
|
227
|
-
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
228
|
-
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
229
|
-
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
230
|
-
* unchecked body straight into a privileged mutation.
|
|
231
|
-
*/
|
|
232
|
-
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
233
|
-
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
234
|
-
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
235
171
|
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
236
172
|
interface QueueRegistryEntry {
|
|
237
173
|
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
174
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
175
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
176
|
+
* queues carry different message bodies, and the handler param is
|
|
177
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
178
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
179
|
+
* batch straight through, so the erasure is type-only.
|
|
180
|
+
*/
|
|
245
181
|
definition: QueueDefinition<any>;
|
|
246
182
|
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
247
183
|
exportName: string;
|
|
248
184
|
}
|
|
249
185
|
/** Map of stable wrangler queue name → registry entry, built by codegen. */
|
|
250
186
|
type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
187
|
+
/** The disposition a consumer left one message in for a single delivery attempt. */
|
|
188
|
+
type QueueMessageOutcome = "ack" | "error" | "retry";
|
|
189
|
+
/**
|
|
190
|
+
* One consumed message as captured by {@link dispatchQueueBatch} and handed to an
|
|
191
|
+
* {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
|
|
192
|
+
* `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
|
|
193
|
+
* the two packages share only this contract, so keep them in sync by hand.
|
|
194
|
+
*/
|
|
195
|
+
interface CapturedQueueMessage {
|
|
196
|
+
/** Delivery attempt number for this message (`message.attempts`). */
|
|
197
|
+
attempts: number;
|
|
198
|
+
/** The message body (JSON-encoded + capped by the catcher). */
|
|
199
|
+
body: unknown;
|
|
200
|
+
/** `true` when this failed delivery was the message's last (its retries are exhausted — the broker dead-letters it). */
|
|
201
|
+
deadLettered: boolean;
|
|
202
|
+
/** Handler error message when `outcome` is `error`; absent otherwise. */
|
|
203
|
+
error?: string;
|
|
204
|
+
/** The `lunora/queues.ts` export name that consumed it. */
|
|
205
|
+
exportName: string;
|
|
206
|
+
/** The delivered message id (`message.id`). */
|
|
207
|
+
messageId: string;
|
|
208
|
+
/** How the handler disposed of the message this attempt. */
|
|
209
|
+
outcome: QueueMessageOutcome;
|
|
210
|
+
/** The stable wrangler queue name the batch was delivered from (`batch.queue`). */
|
|
211
|
+
queue: string;
|
|
212
|
+
/** Original message timestamp in epoch-ms (`message.timestamp`). */
|
|
213
|
+
timestamp: number;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Persists a batch of consumed messages. The codegen worker wires this to POST the
|
|
217
|
+
* batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
|
|
218
|
+
* Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
|
|
219
|
+
* capture failure never changes delivery semantics.
|
|
220
|
+
*/
|
|
221
|
+
type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
|
|
251
222
|
interface DispatchOptions {
|
|
223
|
+
/**
|
|
224
|
+
* Optional capture sink. When set, the batch is instrumented and every
|
|
225
|
+
* message's final disposition is recorded and handed to this sink after the
|
|
226
|
+
* handler runs. Omitted in production unless queue capture is enabled, so a
|
|
227
|
+
* consumer pays no instrumentation cost by default.
|
|
228
|
+
*/
|
|
229
|
+
capture?: QueueCaptureSink;
|
|
252
230
|
/** Worker `env`, forwarded to the queue run context. */
|
|
253
231
|
env: Record<string, unknown>;
|
|
254
232
|
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
255
233
|
fetchImpl?: typeof fetch;
|
|
256
234
|
}
|
|
257
235
|
/**
|
|
258
|
-
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
259
|
-
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
260
|
-
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
261
|
-
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
262
|
-
*/
|
|
236
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
237
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
238
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
239
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
240
|
+
*/
|
|
263
241
|
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
242
|
+
/** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
|
|
243
|
+
type QueueEnv = Record<string, unknown>;
|
|
244
|
+
/** Options for {@link createQueueCaptureSink}. */
|
|
245
|
+
interface QueueCaptureOptions {
|
|
246
|
+
/** Pin the consumed-message log to a Cloudflare data-residency jurisdiction (match the worker's `jurisdiction`). */
|
|
247
|
+
jurisdiction?: string;
|
|
248
|
+
/** Shard the consumed-message log lives on; override if the worker sets a custom `defaultShardKey`. */
|
|
249
|
+
rootShard?: string;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Whether consumed queue messages should be captured into the studio's log.
|
|
253
|
+
* Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
|
|
254
|
+
* unset, capture is on only in a development environment. Mirrors
|
|
255
|
+
* `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
|
|
256
|
+
* same way.
|
|
257
|
+
*/
|
|
258
|
+
declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Build the {@link QueueCaptureSink} that records a processed batch into the
|
|
261
|
+
* studio's root-shard consumed-message log via the reserved `recordQueueMessage`
|
|
262
|
+
* admin RPC. Best-effort by contract: without the `SHARD` binding or
|
|
263
|
+
* `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
|
|
264
|
+
* rejection, so capture never changes delivery semantics.
|
|
265
|
+
*/
|
|
266
|
+
declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
|
|
267
|
+
/**
|
|
268
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
269
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
270
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
271
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
272
|
+
* the missing queue is actually used.
|
|
273
|
+
*/
|
|
274
|
+
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
275
|
+
/**
|
|
276
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
277
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
278
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
279
|
+
* queues (raised lazily on first use).
|
|
280
|
+
*/
|
|
281
|
+
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
282
|
+
/**
|
|
283
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
284
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
285
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
286
|
+
* queue export can never collide with the built-in bindings.
|
|
287
|
+
*/
|
|
288
|
+
declare const queueBindingName: (exportName: string) => string;
|
|
289
|
+
/**
|
|
290
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
291
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
292
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
293
|
+
*/
|
|
294
|
+
declare const queueDefaultName: (exportName: string) => string;
|
|
295
|
+
/**
|
|
296
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
297
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
298
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
299
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
300
|
+
* entries from the same definition.
|
|
301
|
+
*
|
|
302
|
+
* ```ts
|
|
303
|
+
* // lunora/queues.ts
|
|
304
|
+
* import { defineQueue } from "@lunora/queue";
|
|
305
|
+
* import { api } from "./_generated/api";
|
|
306
|
+
*
|
|
307
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
308
|
+
* handler: async (ctx, batch) => {
|
|
309
|
+
* for (const message of batch.messages) {
|
|
310
|
+
* await ctx.run(api.email.send, { to: message.body.to });
|
|
311
|
+
* message.ack();
|
|
312
|
+
* }
|
|
313
|
+
* },
|
|
314
|
+
* });
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
318
|
+
*
|
|
319
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
320
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
321
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
322
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
323
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
324
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
325
|
+
* unchecked body straight into a privileged mutation.
|
|
326
|
+
*/
|
|
327
|
+
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
328
|
+
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
329
|
+
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
264
330
|
interface RunContextOptions {
|
|
265
331
|
env: Record<string, unknown>;
|
|
266
332
|
exportName: string;
|
|
@@ -268,4 +334,4 @@ interface RunContextOptions {
|
|
|
268
334
|
}
|
|
269
335
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
270
336
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
271
|
-
export { type ArgsOf, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueHandler, type DispatchLogger as QueueLogger, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRetryOptions, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type QueueSendBatchOptions, type QueueSendOptions, type Queues, type RunFunctionOptions, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName };
|
|
337
|
+
export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueEnv, type QueueHandler, type DispatchLogger as QueueLogger, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRetryOptions, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type QueueSendBatchOptions, type QueueSendOptions, type Queues, type RunFunctionOptions, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
|
-
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
4
|
-
* Node-safe — no Cloudflare runtime imports — so the consumers stay unit-testable
|
|
5
|
-
* with plain-object doubles.
|
|
6
|
-
*/
|
|
2
|
+
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
|
+
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
4
|
+
* Node-safe — no Cloudflare runtime imports — so the consumers stay unit-testable
|
|
5
|
+
* with plain-object doubles.
|
|
6
|
+
*/
|
|
7
7
|
/** Opaque generated function reference (`api.foo.bar`), carrying its dispatch id. */
|
|
8
8
|
interface FunctionReference {
|
|
9
9
|
__lunoraRef: string;
|
|
@@ -24,7 +24,6 @@ interface DispatchLogger {
|
|
|
24
24
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
25
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
26
|
}
|
|
27
|
-
/** Build a {@link DispatchLogger} that prefixes every line with `prefix` (e.g. `[queue:email]`). */
|
|
28
27
|
/** How a queue message body is serialized on the wire (Cloudflare default `"json"`). */
|
|
29
28
|
type QueueContentType = "bytes" | "json" | "text" | "v8";
|
|
30
29
|
/** Options for a single `producer.send(body, options?)`. */
|
|
@@ -46,10 +45,10 @@ interface MessageSendRequestLike<Body = unknown> {
|
|
|
46
45
|
delaySeconds?: number;
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
|
-
* Minimal structural projection of workers-types' `Queue<Body>` (the producer
|
|
50
|
-
* binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
|
|
51
|
-
* we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
|
|
52
|
-
*/
|
|
48
|
+
* Minimal structural projection of workers-types' `Queue<Body>` (the producer
|
|
49
|
+
* binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
|
|
50
|
+
* we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
|
|
51
|
+
*/
|
|
53
52
|
interface QueueBindingLike<Body = unknown> {
|
|
54
53
|
send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
|
|
55
54
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
|
|
@@ -80,10 +79,10 @@ interface MessageBatchLike<Body = unknown> {
|
|
|
80
79
|
retryAll: (options?: QueueRetryOptions) => void;
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
|
-
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
84
|
-
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
85
|
-
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
86
|
-
*/
|
|
82
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
83
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
84
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
85
|
+
*/
|
|
87
86
|
interface QueueProducer<Body = unknown> {
|
|
88
87
|
/** Enqueue one message. */
|
|
89
88
|
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
@@ -91,10 +90,10 @@ interface QueueProducer<Body = unknown> {
|
|
|
91
90
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
|
-
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
95
|
-
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
96
|
-
* `createQueues` stays schema-agnostic.
|
|
97
|
-
*/
|
|
93
|
+
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
94
|
+
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
95
|
+
* `createQueues` stays schema-agnostic.
|
|
96
|
+
*/
|
|
98
97
|
interface Queues {
|
|
99
98
|
[exportName: string]: QueueProducer;
|
|
100
99
|
}
|
|
@@ -104,11 +103,11 @@ interface LunoraQueuesOptions {
|
|
|
104
103
|
bindings: Record<string, QueueBindingLike>;
|
|
105
104
|
}
|
|
106
105
|
/**
|
|
107
|
-
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
108
|
-
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
109
|
-
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
110
|
-
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
111
|
-
*/
|
|
106
|
+
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
107
|
+
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
108
|
+
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
109
|
+
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
110
|
+
*/
|
|
112
111
|
interface QueueRunContext {
|
|
113
112
|
/** The worker `env` (bindings + vars). */
|
|
114
113
|
readonly env: Record<string, unknown>;
|
|
@@ -137,25 +136,25 @@ interface QueueConsumerTuning {
|
|
|
137
136
|
/** The config object passed to `defineQueue`. */
|
|
138
137
|
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
138
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
* The push-consumer body. Required for `mode: "push"` (the default); omit it
|
|
140
|
+
* for `mode: "pull"`, where an external worker polls the queue over HTTP.
|
|
141
|
+
*/
|
|
143
142
|
handler?: QueueHandler<Body>;
|
|
144
143
|
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
144
|
mode?: QueueConsumerMode;
|
|
146
145
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
147
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
148
|
+
*/
|
|
150
149
|
name?: string;
|
|
151
150
|
}
|
|
152
151
|
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
152
|
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
153
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
155
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
156
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
157
|
+
*/
|
|
159
158
|
readonly __lunoraBody?: Body;
|
|
160
159
|
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
160
|
isLunoraQueue: true;
|
|
@@ -169,98 +168,165 @@ interface QueueBindingSpec {
|
|
|
169
168
|
/** The stable wrangler queue name, e.g. `email-queue`. */
|
|
170
169
|
name: string;
|
|
171
170
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
174
|
-
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
175
|
-
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
176
|
-
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
177
|
-
* the missing queue is actually used.
|
|
178
|
-
*/
|
|
179
|
-
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
180
|
-
/**
|
|
181
|
-
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
182
|
-
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
183
|
-
* export whose binding is absent throws a directed error naming the declared
|
|
184
|
-
* queues (raised lazily on first use).
|
|
185
|
-
*/
|
|
186
|
-
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
187
|
-
/**
|
|
188
|
-
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
189
|
-
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
190
|
-
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
191
|
-
* queue export can never collide with the built-in bindings.
|
|
192
|
-
*/
|
|
193
|
-
declare const queueBindingName: (exportName: string) => string;
|
|
194
|
-
/**
|
|
195
|
-
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
196
|
-
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
197
|
-
* deployed queue's identifier when no explicit `name` override is given.
|
|
198
|
-
*/
|
|
199
|
-
declare const queueDefaultName: (exportName: string) => string;
|
|
200
|
-
/**
|
|
201
|
-
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
202
|
-
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
203
|
-
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
204
|
-
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
205
|
-
* entries from the same definition.
|
|
206
|
-
*
|
|
207
|
-
* ```ts
|
|
208
|
-
* // lunora/queues.ts
|
|
209
|
-
* import { defineQueue } from "@lunora/queue";
|
|
210
|
-
* import { api } from "./_generated/api";
|
|
211
|
-
*
|
|
212
|
-
* export const emailQueue = defineQueue<{ to: string }>({
|
|
213
|
-
* handler: async (ctx, batch) => {
|
|
214
|
-
* for (const message of batch.messages) {
|
|
215
|
-
* await ctx.run(api.email.send, { to: message.body.to });
|
|
216
|
-
* message.ack();
|
|
217
|
-
* }
|
|
218
|
-
* },
|
|
219
|
-
* });
|
|
220
|
-
* ```
|
|
221
|
-
*
|
|
222
|
-
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
223
|
-
*
|
|
224
|
-
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
225
|
-
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
226
|
-
* trusted path the scheduler and workflows use), so those calls run with the
|
|
227
|
-
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
228
|
-
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
229
|
-
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
230
|
-
* unchecked body straight into a privileged mutation.
|
|
231
|
-
*/
|
|
232
|
-
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
233
|
-
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
234
|
-
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
235
171
|
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
236
172
|
interface QueueRegistryEntry {
|
|
237
173
|
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
174
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
175
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
176
|
+
* queues carry different message bodies, and the handler param is
|
|
177
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
178
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
179
|
+
* batch straight through, so the erasure is type-only.
|
|
180
|
+
*/
|
|
245
181
|
definition: QueueDefinition<any>;
|
|
246
182
|
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
247
183
|
exportName: string;
|
|
248
184
|
}
|
|
249
185
|
/** Map of stable wrangler queue name → registry entry, built by codegen. */
|
|
250
186
|
type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
187
|
+
/** The disposition a consumer left one message in for a single delivery attempt. */
|
|
188
|
+
type QueueMessageOutcome = "ack" | "error" | "retry";
|
|
189
|
+
/**
|
|
190
|
+
* One consumed message as captured by {@link dispatchQueueBatch} and handed to an
|
|
191
|
+
* {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
|
|
192
|
+
* `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
|
|
193
|
+
* the two packages share only this contract, so keep them in sync by hand.
|
|
194
|
+
*/
|
|
195
|
+
interface CapturedQueueMessage {
|
|
196
|
+
/** Delivery attempt number for this message (`message.attempts`). */
|
|
197
|
+
attempts: number;
|
|
198
|
+
/** The message body (JSON-encoded + capped by the catcher). */
|
|
199
|
+
body: unknown;
|
|
200
|
+
/** `true` when this failed delivery was the message's last (its retries are exhausted — the broker dead-letters it). */
|
|
201
|
+
deadLettered: boolean;
|
|
202
|
+
/** Handler error message when `outcome` is `error`; absent otherwise. */
|
|
203
|
+
error?: string;
|
|
204
|
+
/** The `lunora/queues.ts` export name that consumed it. */
|
|
205
|
+
exportName: string;
|
|
206
|
+
/** The delivered message id (`message.id`). */
|
|
207
|
+
messageId: string;
|
|
208
|
+
/** How the handler disposed of the message this attempt. */
|
|
209
|
+
outcome: QueueMessageOutcome;
|
|
210
|
+
/** The stable wrangler queue name the batch was delivered from (`batch.queue`). */
|
|
211
|
+
queue: string;
|
|
212
|
+
/** Original message timestamp in epoch-ms (`message.timestamp`). */
|
|
213
|
+
timestamp: number;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Persists a batch of consumed messages. The codegen worker wires this to POST the
|
|
217
|
+
* batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
|
|
218
|
+
* Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
|
|
219
|
+
* capture failure never changes delivery semantics.
|
|
220
|
+
*/
|
|
221
|
+
type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
|
|
251
222
|
interface DispatchOptions {
|
|
223
|
+
/**
|
|
224
|
+
* Optional capture sink. When set, the batch is instrumented and every
|
|
225
|
+
* message's final disposition is recorded and handed to this sink after the
|
|
226
|
+
* handler runs. Omitted in production unless queue capture is enabled, so a
|
|
227
|
+
* consumer pays no instrumentation cost by default.
|
|
228
|
+
*/
|
|
229
|
+
capture?: QueueCaptureSink;
|
|
252
230
|
/** Worker `env`, forwarded to the queue run context. */
|
|
253
231
|
env: Record<string, unknown>;
|
|
254
232
|
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
255
233
|
fetchImpl?: typeof fetch;
|
|
256
234
|
}
|
|
257
235
|
/**
|
|
258
|
-
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
259
|
-
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
260
|
-
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
261
|
-
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
262
|
-
*/
|
|
236
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
237
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
238
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
239
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
240
|
+
*/
|
|
263
241
|
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
242
|
+
/** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
|
|
243
|
+
type QueueEnv = Record<string, unknown>;
|
|
244
|
+
/** Options for {@link createQueueCaptureSink}. */
|
|
245
|
+
interface QueueCaptureOptions {
|
|
246
|
+
/** Pin the consumed-message log to a Cloudflare data-residency jurisdiction (match the worker's `jurisdiction`). */
|
|
247
|
+
jurisdiction?: string;
|
|
248
|
+
/** Shard the consumed-message log lives on; override if the worker sets a custom `defaultShardKey`. */
|
|
249
|
+
rootShard?: string;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Whether consumed queue messages should be captured into the studio's log.
|
|
253
|
+
* Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
|
|
254
|
+
* unset, capture is on only in a development environment. Mirrors
|
|
255
|
+
* `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
|
|
256
|
+
* same way.
|
|
257
|
+
*/
|
|
258
|
+
declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Build the {@link QueueCaptureSink} that records a processed batch into the
|
|
261
|
+
* studio's root-shard consumed-message log via the reserved `recordQueueMessage`
|
|
262
|
+
* admin RPC. Best-effort by contract: without the `SHARD` binding or
|
|
263
|
+
* `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
|
|
264
|
+
* rejection, so capture never changes delivery semantics.
|
|
265
|
+
*/
|
|
266
|
+
declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
|
|
267
|
+
/**
|
|
268
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
269
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
270
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
271
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
272
|
+
* the missing queue is actually used.
|
|
273
|
+
*/
|
|
274
|
+
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
275
|
+
/**
|
|
276
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
277
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
278
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
279
|
+
* queues (raised lazily on first use).
|
|
280
|
+
*/
|
|
281
|
+
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
282
|
+
/**
|
|
283
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
284
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
285
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
286
|
+
* queue export can never collide with the built-in bindings.
|
|
287
|
+
*/
|
|
288
|
+
declare const queueBindingName: (exportName: string) => string;
|
|
289
|
+
/**
|
|
290
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
291
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
292
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
293
|
+
*/
|
|
294
|
+
declare const queueDefaultName: (exportName: string) => string;
|
|
295
|
+
/**
|
|
296
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
297
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
298
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
299
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
300
|
+
* entries from the same definition.
|
|
301
|
+
*
|
|
302
|
+
* ```ts
|
|
303
|
+
* // lunora/queues.ts
|
|
304
|
+
* import { defineQueue } from "@lunora/queue";
|
|
305
|
+
* import { api } from "./_generated/api";
|
|
306
|
+
*
|
|
307
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
308
|
+
* handler: async (ctx, batch) => {
|
|
309
|
+
* for (const message of batch.messages) {
|
|
310
|
+
* await ctx.run(api.email.send, { to: message.body.to });
|
|
311
|
+
* message.ack();
|
|
312
|
+
* }
|
|
313
|
+
* },
|
|
314
|
+
* });
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
318
|
+
*
|
|
319
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
320
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
321
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
322
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
323
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
324
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
325
|
+
* unchecked body straight into a privileged mutation.
|
|
326
|
+
*/
|
|
327
|
+
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
328
|
+
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
329
|
+
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
264
330
|
interface RunContextOptions {
|
|
265
331
|
env: Record<string, unknown>;
|
|
266
332
|
exportName: string;
|
|
@@ -268,4 +334,4 @@ interface RunContextOptions {
|
|
|
268
334
|
}
|
|
269
335
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
270
336
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
271
|
-
export { type ArgsOf, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueHandler, type DispatchLogger as QueueLogger, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRetryOptions, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type QueueSendBatchOptions, type QueueSendOptions, type Queues, type RunFunctionOptions, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName };
|
|
337
|
+
export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueEnv, type QueueHandler, type DispatchLogger as QueueLogger, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRetryOptions, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type QueueSendBatchOptions, type QueueSendOptions, type Queues, type RunFunctionOptions, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { createQueueCaptureSink, shouldCaptureQueue } from './packem_shared/createQueueCaptureSink-B8WE0eHf.mjs';
|
|
1
2
|
export { createQueueContext } from './packem_shared/createQueueContext-D0XCdCsd.mjs';
|
|
2
3
|
export { default as createQueues } from './packem_shared/createQueues-14-vSICK.mjs';
|
|
3
4
|
export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName } from './packem_shared/defineQueue-D40gREfg.mjs';
|
|
4
|
-
export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-
|
|
5
|
-
export { createQueueRunContext } from './packem_shared/createQueueRunContext-
|
|
5
|
+
export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-DSEWhEy8.mjs';
|
|
6
|
+
export { createQueueRunContext } from './packem_shared/createQueueRunContext-C8jboCk6.mjs';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const RECORD_QUEUE_MESSAGE_OP = "__lunora_admin__:recordQueueMessage";
|
|
4
|
+
const DEFAULT_ROOT_SHARD = "__root__";
|
|
5
|
+
const DEV_ENVIRONMENT_PATTERN = /^(?:dev(?:elopment)?|local(?:host)?|test)$/iu;
|
|
6
|
+
const ENVIRONMENT_VARS = ["CF_ENV", "ENVIRONMENT", "NODE_ENV", "WORKER_ENV"];
|
|
7
|
+
const CAPTURE_FETCH_TIMEOUT_MS = 5e3;
|
|
8
|
+
const shouldCaptureQueue = (env) => {
|
|
9
|
+
const flag = env["LUNORA_QUEUE_CAPTURE"];
|
|
10
|
+
if (typeof flag === "string") {
|
|
11
|
+
return flag === "1" || flag.toLowerCase() === "true";
|
|
12
|
+
}
|
|
13
|
+
return ENVIRONMENT_VARS.some((key) => {
|
|
14
|
+
const value = env[key];
|
|
15
|
+
return typeof value === "string" && DEV_ENVIRONMENT_PATTERN.test(value);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const createQueueCaptureSink = (env, options = {}) => {
|
|
19
|
+
const rootShard = options.rootShard ?? DEFAULT_ROOT_SHARD;
|
|
20
|
+
return async (messages) => {
|
|
21
|
+
if (messages.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const binding = env["SHARD"];
|
|
25
|
+
const adminToken = typeof env["LUNORA_ADMIN_TOKEN"] === "string" ? env["LUNORA_ADMIN_TOKEN"] : void 0;
|
|
26
|
+
if (binding === void 0 || adminToken === void 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let namespace = binding;
|
|
30
|
+
if (options.jurisdiction !== void 0) {
|
|
31
|
+
if (typeof binding.jurisdiction !== "function") {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
`@lunora/queue: Durable Object namespace does not support jurisdiction("${options.jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
namespace = binding.jurisdiction(options.jurisdiction);
|
|
37
|
+
}
|
|
38
|
+
const stub = namespace.get(namespace.idFromName(rootShard));
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeout = setTimeout(() => {
|
|
41
|
+
controller.abort();
|
|
42
|
+
}, CAPTURE_FETCH_TIMEOUT_MS);
|
|
43
|
+
try {
|
|
44
|
+
const response = await stub.fetch("https://shard.internal/rpc", {
|
|
45
|
+
body: JSON.stringify({ args: { messages }, functionPath: RECORD_QUEUE_MESSAGE_OP }),
|
|
46
|
+
headers: { authorization: `Bearer ${adminToken}`, "content-type": "application/json" },
|
|
47
|
+
method: "POST",
|
|
48
|
+
signal: controller.signal
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const detail = await response.text().catch(() => "");
|
|
52
|
+
throw new LunoraError(
|
|
53
|
+
"INTERNAL",
|
|
54
|
+
`@lunora/queue: capture write to the root shard failed (${String(response.status)} ${response.statusText})${detail === "" ? "" : `: ${detail}`}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
await response.body?.cancel();
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { createQueueCaptureSink, shouldCaptureQueue };
|
package/dist/packem_shared/{createQueueRunContext-DVqG7Oyk.mjs → createQueueRunContext-C8jboCk6.mjs}
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const createDispatchLogger = (prefix) => {
|
|
2
4
|
return {
|
|
3
5
|
debug: (message, ...rest) => {
|
|
@@ -23,6 +25,18 @@ const trimTrailingSlashes = (value) => {
|
|
|
23
25
|
}
|
|
24
26
|
return value.slice(0, end);
|
|
25
27
|
};
|
|
28
|
+
const toDispatchError = (label, status, rawBody) => {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(rawBody);
|
|
31
|
+
const errorBody = parsed?.error;
|
|
32
|
+
if (typeof errorBody === "object" && errorBody !== null && typeof errorBody.code === "string") {
|
|
33
|
+
const { code, data, message } = errorBody;
|
|
34
|
+
return new LunoraError(code, typeof message === "string" ? message : void 0, { data, status });
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
return new LunoraError("INTERNAL", `${label}: function dispatch failed (${String(status)}): ${rawBody}`, { status });
|
|
39
|
+
};
|
|
26
40
|
const createDispatchRunner = (options) => {
|
|
27
41
|
const { label } = options;
|
|
28
42
|
const globalFetch = globalThis.fetch;
|
|
@@ -33,20 +47,27 @@ const createDispatchRunner = (options) => {
|
|
|
33
47
|
}
|
|
34
48
|
const origin = options.env.LUNORA_ORIGIN_URL;
|
|
35
49
|
if (typeof origin !== "string" || origin.length === 0) {
|
|
36
|
-
throw new
|
|
50
|
+
throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);
|
|
37
51
|
}
|
|
38
52
|
const token = options.env.LUNORA_ADMIN_TOKEN;
|
|
39
53
|
if (typeof token !== "string" || token.length === 0) {
|
|
40
|
-
throw new
|
|
54
|
+
throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);
|
|
41
55
|
}
|
|
42
56
|
const url = `${trimTrailingSlashes(origin)}${SCHEDULER_DISPATCH_PATH}`;
|
|
57
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
58
|
+
if (options.identity?.userId !== void 0) {
|
|
59
|
+
headers["x-lunora-userid"] = options.identity.userId;
|
|
60
|
+
}
|
|
61
|
+
if (options.identity?.claims !== void 0) {
|
|
62
|
+
headers["x-lunora-identity"] = JSON.stringify(options.identity.claims);
|
|
63
|
+
}
|
|
43
64
|
const response = await fetchImpl(url, {
|
|
44
65
|
body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
|
|
45
|
-
headers
|
|
66
|
+
headers,
|
|
46
67
|
method: "POST"
|
|
47
68
|
});
|
|
48
69
|
if (!response.ok) {
|
|
49
|
-
throw
|
|
70
|
+
throw toDispatchError(label, response.status, await response.text());
|
|
50
71
|
}
|
|
51
72
|
const text = await response.text();
|
|
52
73
|
if (text.length === 0) {
|
|
@@ -55,7 +76,9 @@ const createDispatchRunner = (options) => {
|
|
|
55
76
|
try {
|
|
56
77
|
return JSON.parse(text);
|
|
57
78
|
} catch {
|
|
58
|
-
|
|
79
|
+
throw new LunoraError("INTERNAL", `${label}: function dispatch returned a non-JSON body (${String(response.status)}): ${text}`, {
|
|
80
|
+
status: response.status
|
|
81
|
+
});
|
|
59
82
|
}
|
|
60
83
|
};
|
|
61
84
|
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { createQueueRunContext } from './createQueueRunContext-C8jboCk6.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
5
|
+
const timestampToMs = (value) => {
|
|
6
|
+
if (value instanceof Date) {
|
|
7
|
+
return value.getTime();
|
|
8
|
+
}
|
|
9
|
+
const asNumber = typeof value === "number" ? value : Number(value);
|
|
10
|
+
return Number.isFinite(asNumber) ? asNumber : 0;
|
|
11
|
+
};
|
|
12
|
+
const instrumentBatch = (batch) => {
|
|
13
|
+
const dispositions = /* @__PURE__ */ new Map();
|
|
14
|
+
const originals = batch.messages;
|
|
15
|
+
const wrappedMessages = originals.map((message) => {
|
|
16
|
+
return {
|
|
17
|
+
ack: () => {
|
|
18
|
+
dispositions.set(message, "ack");
|
|
19
|
+
message.ack();
|
|
20
|
+
},
|
|
21
|
+
get attempts() {
|
|
22
|
+
return message.attempts;
|
|
23
|
+
},
|
|
24
|
+
get body() {
|
|
25
|
+
return message.body;
|
|
26
|
+
},
|
|
27
|
+
get id() {
|
|
28
|
+
return message.id;
|
|
29
|
+
},
|
|
30
|
+
retry: (options) => {
|
|
31
|
+
dispositions.set(message, "retry");
|
|
32
|
+
message.retry(options);
|
|
33
|
+
},
|
|
34
|
+
get timestamp() {
|
|
35
|
+
return message.timestamp;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const fillUndecided = (outcome) => {
|
|
40
|
+
for (const message of originals) {
|
|
41
|
+
if (!dispositions.has(message)) {
|
|
42
|
+
dispositions.set(message, outcome);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const wrappedBatch = {
|
|
47
|
+
ackAll: () => {
|
|
48
|
+
fillUndecided("ack");
|
|
49
|
+
batch.ackAll();
|
|
50
|
+
},
|
|
51
|
+
messages: wrappedMessages,
|
|
52
|
+
queue: batch.queue,
|
|
53
|
+
retryAll: (options) => {
|
|
54
|
+
fillUndecided("retry");
|
|
55
|
+
batch.retryAll(options);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return { dispositions, originals, wrappedBatch };
|
|
59
|
+
};
|
|
60
|
+
const describeThrownError = (handlerError) => {
|
|
61
|
+
if (handlerError instanceof Error) {
|
|
62
|
+
return handlerError.message;
|
|
63
|
+
}
|
|
64
|
+
if (typeof handlerError === "string") {
|
|
65
|
+
return handlerError;
|
|
66
|
+
}
|
|
67
|
+
if (handlerError !== null && typeof handlerError === "object") {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.stringify(handlerError);
|
|
70
|
+
} catch {
|
|
71
|
+
return "[unserializable thrown value]";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return String(handlerError);
|
|
75
|
+
};
|
|
76
|
+
const buildCaptureRecords = (harness, entry, queue, threw, handlerError) => {
|
|
77
|
+
const errorMessage = threw ? describeThrownError(handlerError) : void 0;
|
|
78
|
+
const maxRetries = typeof entry.definition.maxRetries === "number" ? entry.definition.maxRetries : DEFAULT_MAX_RETRIES;
|
|
79
|
+
return harness.originals.map((message) => {
|
|
80
|
+
const decided = harness.dispositions.get(message);
|
|
81
|
+
const outcome = decided ?? (threw ? "error" : "ack");
|
|
82
|
+
const attempts = typeof message.attempts === "number" ? message.attempts : 1;
|
|
83
|
+
return {
|
|
84
|
+
attempts,
|
|
85
|
+
body: message.body,
|
|
86
|
+
deadLettered: outcome !== "ack" && attempts > maxRetries,
|
|
87
|
+
error: outcome === "error" ? errorMessage : void 0,
|
|
88
|
+
exportName: entry.exportName,
|
|
89
|
+
messageId: message.id,
|
|
90
|
+
outcome,
|
|
91
|
+
queue,
|
|
92
|
+
timestamp: timestampToMs(message.timestamp)
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const dispatchQueueBatch = async (batch, registry, options) => {
|
|
97
|
+
const entry = Object.hasOwn(registry, batch.queue) ? registry[batch.queue] : void 0;
|
|
98
|
+
if (entry === void 0) {
|
|
99
|
+
const known = Object.keys(registry);
|
|
100
|
+
const suffix = known.length === 0 ? "no push queues are declared" : `known push queues: ${known.join(", ")}`;
|
|
101
|
+
throw new LunoraError("INTERNAL", `@lunora/queue: received a batch for queue "${batch.queue}" but no push handler is registered (${suffix})`);
|
|
102
|
+
}
|
|
103
|
+
const { handler } = entry.definition;
|
|
104
|
+
if (typeof handler !== "function") {
|
|
105
|
+
throw new TypeError(`@lunora/queue: queue "${batch.queue}" (${entry.exportName}) has no push handler — it is declared as a pull consumer`);
|
|
106
|
+
}
|
|
107
|
+
const context = createQueueRunContext({ env: options.env, exportName: entry.exportName, fetchImpl: options.fetchImpl });
|
|
108
|
+
if (options.capture === void 0) {
|
|
109
|
+
await handler(context, batch);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const harness = instrumentBatch(batch);
|
|
113
|
+
let threw = false;
|
|
114
|
+
let handlerError;
|
|
115
|
+
try {
|
|
116
|
+
await handler(context, harness.wrappedBatch);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
threw = true;
|
|
119
|
+
handlerError = error;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const records = buildCaptureRecords(harness, entry, batch.queue, threw, handlerError);
|
|
123
|
+
await options.capture(records);
|
|
124
|
+
} catch (captureError) {
|
|
125
|
+
console.warn("@lunora/queue: capture sink failed (delivery unaffected):", captureError);
|
|
126
|
+
}
|
|
127
|
+
if (threw) {
|
|
128
|
+
throw handlerError;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export { dispatchQueueBatch };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/queue",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
4
4
|
"description": "Cloudflare Queues for Lunora: defineQueue producers + consumers, the ctx.queues surface, and the generated queue() worker handler",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"background-jobs",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@lunora/errors": "1.0.0-alpha.8"
|
|
48
|
+
},
|
|
46
49
|
"engines": {
|
|
47
50
|
"node": "^22.15.0 || >=24.11.0"
|
|
48
51
|
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { createQueueRunContext } from './createQueueRunContext-DVqG7Oyk.mjs';
|
|
2
|
-
|
|
3
|
-
const dispatchQueueBatch = async (batch, registry, options) => {
|
|
4
|
-
const entry = registry[batch.queue];
|
|
5
|
-
if (entry === void 0) {
|
|
6
|
-
const known = Object.keys(registry);
|
|
7
|
-
const suffix = known.length === 0 ? "no push queues are declared" : `known push queues: ${known.join(", ")}`;
|
|
8
|
-
throw new Error(`@lunora/queue: received a batch for queue "${batch.queue}" but no push handler is registered (${suffix})`);
|
|
9
|
-
}
|
|
10
|
-
const { handler } = entry.definition;
|
|
11
|
-
if (typeof handler !== "function") {
|
|
12
|
-
throw new TypeError(`@lunora/queue: queue "${batch.queue}" (${entry.exportName}) has no push handler — it is declared as a pull consumer`);
|
|
13
|
-
}
|
|
14
|
-
const context = createQueueRunContext({ env: options.env, exportName: entry.exportName, fetchImpl: options.fetchImpl });
|
|
15
|
-
await handler(context, batch);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export { dispatchQueueBatch };
|