@lunora/queue 1.0.0-alpha.7 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts 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&lt;Body>` (the producer
50
- * binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
51
- * we widen the return to `Promise&lt;unknown>` so a plain-object fake satisfies it.
52
- */
48
+ * Minimal structural projection of workers-types' `Queue&lt;Body>` (the producer
49
+ * binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
50
+ * we widen the return to `Promise&lt;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.&lt;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.&lt;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
- * The push-consumer body. Required for `mode: "push"` (the default); omit it
141
- * for `mode: "pull"`, where an external worker polls the queue over HTTP.
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
- * Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
148
- * kebab-cased export name (`emailQueue` → `email-queue`).
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
- * Phantom carrier for the message body type, so codegen can type the
156
- * generated `ctx.queues.&lt;name>` producer as `QueueProducer&lt;Body>` from
157
- * `typeof &lt;export>`. Never assigned at runtime (type-only).
158
- */
154
+ * Phantom carrier for the message body type, so codegen can type the
155
+ * generated `ctx.queues.&lt;name>` producer as `QueueProducer&lt;Body>` from
156
+ * `typeof &lt;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;
@@ -172,13 +171,13 @@ interface QueueBindingSpec {
172
171
  /** One declared queue, keyed for batch routing by its stable wrangler name. */
173
172
  interface QueueRegistryEntry {
174
173
  /**
175
- * The `defineQueue` result (carries the push handler). The body type is
176
- * erased to `any` here because the registry is heterogeneous — different
177
- * queues carry different message bodies, and the handler param is
178
- * contravariant, so a precise `QueueDefinition&lt;Body>` would not be assignable
179
- * to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
180
- * batch straight through, so the erasure is type-only.
181
- */
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&lt;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
+ */
182
181
  definition: QueueDefinition<any>;
183
182
  /** The `lunora/queues.ts` export name, for log correlation. */
184
183
  exportName: string;
@@ -188,11 +187,11 @@ type QueueRegistry = Record<string, QueueRegistryEntry>;
188
187
  /** The disposition a consumer left one message in for a single delivery attempt. */
189
188
  type QueueMessageOutcome = "ack" | "error" | "retry";
190
189
  /**
191
- * One consumed message as captured by {@link dispatchQueueBatch} and handed to an
192
- * {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
193
- * `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
194
- * the two packages share only this contract, so keep them in sync by hand.
195
- */
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
+ */
196
195
  interface CapturedQueueMessage {
197
196
  /** Delivery attempt number for this message (`message.attempts`). */
198
197
  attempts: number;
@@ -214,19 +213,19 @@ interface CapturedQueueMessage {
214
213
  timestamp: number;
215
214
  }
216
215
  /**
217
- * Persists a batch of consumed messages. The codegen worker wires this to POST the
218
- * batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
219
- * Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
220
- * capture failure never changes delivery semantics.
221
- */
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
+ */
222
221
  type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
223
222
  interface DispatchOptions {
224
223
  /**
225
- * Optional capture sink. When set, the batch is instrumented and every
226
- * message's final disposition is recorded and handed to this sink after the
227
- * handler runs. Omitted in production unless queue capture is enabled, so a
228
- * consumer pays no instrumentation cost by default.
229
- */
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
+ */
230
229
  capture?: QueueCaptureSink;
231
230
  /** Worker `env`, forwarded to the queue run context. */
232
231
  env: Record<string, unknown>;
@@ -234,11 +233,11 @@ interface DispatchOptions {
234
233
  fetchImpl?: typeof fetch;
235
234
  }
236
235
  /**
237
- * Look up the handler for `batch.queue` and invoke it with a fresh
238
- * `QueueRunContext`. Throws a directed error when no push handler is registered
239
- * for the delivered queue (a misconfiguration — the consumer was declared
240
- * `pull`, or the queue name drifted from the `defineQueue` export).
241
- */
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
+ */
242
241
  declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
243
242
  /** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
244
243
  type QueueEnv = Record<string, unknown>;
@@ -250,81 +249,81 @@ interface QueueCaptureOptions {
250
249
  rootShard?: string;
251
250
  }
252
251
  /**
253
- * Whether consumed queue messages should be captured into the studio's log.
254
- * Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
255
- * unset, capture is on only in a development environment. Mirrors
256
- * `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
257
- * same way.
258
- */
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
+ */
259
258
  declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
260
259
  /**
261
- * Build the {@link QueueCaptureSink} that records a processed batch into the
262
- * studio's root-shard consumed-message log via the reserved `recordQueueMessage`
263
- * admin RPC. Best-effort by contract: without the `SHARD` binding or
264
- * `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
265
- * rejection, so capture never changes delivery semantics.
266
- */
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
+ */
267
266
  declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
268
267
  /**
269
- * Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
270
- * into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
271
- * A spec whose binding is absent from `env` is skipped here — the helpful "no
272
- * queue named …" error is raised lazily by `ctx.queues.&lt;name>.send(...)` when
273
- * the missing queue is actually used.
274
- */
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.&lt;name>.send(...)` when
272
+ * the missing queue is actually used.
273
+ */
275
274
  declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
276
275
  /**
277
- * Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
278
- * `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
279
- * export whose binding is absent throws a directed error naming the declared
280
- * queues (raised lazily on first use).
281
- */
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
+ */
282
281
  declare const createQueues: (options: LunoraQueuesOptions) => Queues;
283
282
  /**
284
- * The wrangler producer binding name for a queue export: `emailQueue` →
285
- * `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
286
- * these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
287
- * queue export can never collide with the built-in bindings.
288
- */
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
+ */
289
288
  declare const queueBindingName: (exportName: string) => string;
290
289
  /**
291
- * The stable queue name wrangler registers (`queues.producers[].queue` and
292
- * `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
293
- * deployed queue's identifier when no explicit `name` override is given.
294
- */
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
+ */
295
294
  declare const queueDefaultName: (exportName: string) => string;
296
295
  /**
297
- * Declare a Cloudflare Queue deployed alongside the app. Pure validation +
298
- * branding: codegen discovers the export, emits the typed `ctx.queues.&lt;name>`
299
- * producer and (for push consumers) the worker `queue()` dispatch; the config
300
- * layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
301
- * entries from the same definition.
302
- *
303
- * ```ts
304
- * // lunora/queues.ts
305
- * import { defineQueue } from "@lunora/queue";
306
- * import { api } from "./_generated/api";
307
- *
308
- * export const emailQueue = defineQueue&lt;{ to: string }>({
309
- * handler: async (ctx, batch) => {
310
- * for (const message of batch.messages) {
311
- * await ctx.run(api.email.send, { to: message.body.to });
312
- * message.ack();
313
- * }
314
- * },
315
- * });
316
- * ```
317
- *
318
- * Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
319
- *
320
- * ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
321
- * Lunora functions over the admin-authenticated dispatch endpoint (the same
322
- * trusted path the scheduler and workflows use), so those calls run with the
323
- * system identity — **end-user RLS is not applied**. Treat a queue handler as
324
- * trusted server code: validate `message.body` (it may be attacker-influenced if
325
- * anything user-facing can enqueue) before acting on it, and don't forward an
326
- * unchecked body straight into a privileged mutation.
327
- */
296
+ * Declare a Cloudflare Queue deployed alongside the app. Pure validation +
297
+ * branding: codegen discovers the export, emits the typed `ctx.queues.&lt;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&lt;{ 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
+ */
328
327
  declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
329
328
  /** True when a value is a `defineQueue` result (the runtime brand check). */
330
329
  declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
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&lt;Body>` (the producer
50
- * binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
51
- * we widen the return to `Promise&lt;unknown>` so a plain-object fake satisfies it.
52
- */
48
+ * Minimal structural projection of workers-types' `Queue&lt;Body>` (the producer
49
+ * binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
50
+ * we widen the return to `Promise&lt;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.&lt;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.&lt;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
- * The push-consumer body. Required for `mode: "push"` (the default); omit it
141
- * for `mode: "pull"`, where an external worker polls the queue over HTTP.
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
- * Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
148
- * kebab-cased export name (`emailQueue` → `email-queue`).
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
- * Phantom carrier for the message body type, so codegen can type the
156
- * generated `ctx.queues.&lt;name>` producer as `QueueProducer&lt;Body>` from
157
- * `typeof &lt;export>`. Never assigned at runtime (type-only).
158
- */
154
+ * Phantom carrier for the message body type, so codegen can type the
155
+ * generated `ctx.queues.&lt;name>` producer as `QueueProducer&lt;Body>` from
156
+ * `typeof &lt;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;
@@ -172,13 +171,13 @@ interface QueueBindingSpec {
172
171
  /** One declared queue, keyed for batch routing by its stable wrangler name. */
173
172
  interface QueueRegistryEntry {
174
173
  /**
175
- * The `defineQueue` result (carries the push handler). The body type is
176
- * erased to `any` here because the registry is heterogeneous — different
177
- * queues carry different message bodies, and the handler param is
178
- * contravariant, so a precise `QueueDefinition&lt;Body>` would not be assignable
179
- * to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
180
- * batch straight through, so the erasure is type-only.
181
- */
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&lt;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
+ */
182
181
  definition: QueueDefinition<any>;
183
182
  /** The `lunora/queues.ts` export name, for log correlation. */
184
183
  exportName: string;
@@ -188,11 +187,11 @@ type QueueRegistry = Record<string, QueueRegistryEntry>;
188
187
  /** The disposition a consumer left one message in for a single delivery attempt. */
189
188
  type QueueMessageOutcome = "ack" | "error" | "retry";
190
189
  /**
191
- * One consumed message as captured by {@link dispatchQueueBatch} and handed to an
192
- * {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
193
- * `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
194
- * the two packages share only this contract, so keep them in sync by hand.
195
- */
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
+ */
196
195
  interface CapturedQueueMessage {
197
196
  /** Delivery attempt number for this message (`message.attempts`). */
198
197
  attempts: number;
@@ -214,19 +213,19 @@ interface CapturedQueueMessage {
214
213
  timestamp: number;
215
214
  }
216
215
  /**
217
- * Persists a batch of consumed messages. The codegen worker wires this to POST the
218
- * batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
219
- * Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
220
- * capture failure never changes delivery semantics.
221
- */
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
+ */
222
221
  type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
223
222
  interface DispatchOptions {
224
223
  /**
225
- * Optional capture sink. When set, the batch is instrumented and every
226
- * message's final disposition is recorded and handed to this sink after the
227
- * handler runs. Omitted in production unless queue capture is enabled, so a
228
- * consumer pays no instrumentation cost by default.
229
- */
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
+ */
230
229
  capture?: QueueCaptureSink;
231
230
  /** Worker `env`, forwarded to the queue run context. */
232
231
  env: Record<string, unknown>;
@@ -234,11 +233,11 @@ interface DispatchOptions {
234
233
  fetchImpl?: typeof fetch;
235
234
  }
236
235
  /**
237
- * Look up the handler for `batch.queue` and invoke it with a fresh
238
- * `QueueRunContext`. Throws a directed error when no push handler is registered
239
- * for the delivered queue (a misconfiguration — the consumer was declared
240
- * `pull`, or the queue name drifted from the `defineQueue` export).
241
- */
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
+ */
242
241
  declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
243
242
  /** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
244
243
  type QueueEnv = Record<string, unknown>;
@@ -250,81 +249,81 @@ interface QueueCaptureOptions {
250
249
  rootShard?: string;
251
250
  }
252
251
  /**
253
- * Whether consumed queue messages should be captured into the studio's log.
254
- * Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
255
- * unset, capture is on only in a development environment. Mirrors
256
- * `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
257
- * same way.
258
- */
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
+ */
259
258
  declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
260
259
  /**
261
- * Build the {@link QueueCaptureSink} that records a processed batch into the
262
- * studio's root-shard consumed-message log via the reserved `recordQueueMessage`
263
- * admin RPC. Best-effort by contract: without the `SHARD` binding or
264
- * `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
265
- * rejection, so capture never changes delivery semantics.
266
- */
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
+ */
267
266
  declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
268
267
  /**
269
- * Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
270
- * into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
271
- * A spec whose binding is absent from `env` is skipped here — the helpful "no
272
- * queue named …" error is raised lazily by `ctx.queues.&lt;name>.send(...)` when
273
- * the missing queue is actually used.
274
- */
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.&lt;name>.send(...)` when
272
+ * the missing queue is actually used.
273
+ */
275
274
  declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
276
275
  /**
277
- * Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
278
- * `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
279
- * export whose binding is absent throws a directed error naming the declared
280
- * queues (raised lazily on first use).
281
- */
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
+ */
282
281
  declare const createQueues: (options: LunoraQueuesOptions) => Queues;
283
282
  /**
284
- * The wrangler producer binding name for a queue export: `emailQueue` →
285
- * `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
286
- * these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
287
- * queue export can never collide with the built-in bindings.
288
- */
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
+ */
289
288
  declare const queueBindingName: (exportName: string) => string;
290
289
  /**
291
- * The stable queue name wrangler registers (`queues.producers[].queue` and
292
- * `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
293
- * deployed queue's identifier when no explicit `name` override is given.
294
- */
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
+ */
295
294
  declare const queueDefaultName: (exportName: string) => string;
296
295
  /**
297
- * Declare a Cloudflare Queue deployed alongside the app. Pure validation +
298
- * branding: codegen discovers the export, emits the typed `ctx.queues.&lt;name>`
299
- * producer and (for push consumers) the worker `queue()` dispatch; the config
300
- * layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
301
- * entries from the same definition.
302
- *
303
- * ```ts
304
- * // lunora/queues.ts
305
- * import { defineQueue } from "@lunora/queue";
306
- * import { api } from "./_generated/api";
307
- *
308
- * export const emailQueue = defineQueue&lt;{ to: string }>({
309
- * handler: async (ctx, batch) => {
310
- * for (const message of batch.messages) {
311
- * await ctx.run(api.email.send, { to: message.body.to });
312
- * message.ack();
313
- * }
314
- * },
315
- * });
316
- * ```
317
- *
318
- * Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
319
- *
320
- * ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
321
- * Lunora functions over the admin-authenticated dispatch endpoint (the same
322
- * trusted path the scheduler and workflows use), so those calls run with the
323
- * system identity — **end-user RLS is not applied**. Treat a queue handler as
324
- * trusted server code: validate `message.body` (it may be attacker-influenced if
325
- * anything user-facing can enqueue) before acting on it, and don't forward an
326
- * unchecked body straight into a privileged mutation.
327
- */
296
+ * Declare a Cloudflare Queue deployed alongside the app. Pure validation +
297
+ * branding: codegen discovers the export, emits the typed `ctx.queues.&lt;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&lt;{ 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
+ */
328
327
  declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
329
328
  /** True when a value is a `defineQueue` result (the runtime brand check). */
330
329
  declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
package/dist/index.mjs CHANGED
@@ -2,5 +2,5 @@ export { createQueueCaptureSink, shouldCaptureQueue } from './packem_shared/crea
2
2
  export { createQueueContext } from './packem_shared/createQueueContext-D0XCdCsd.mjs';
3
3
  export { default as createQueues } from './packem_shared/createQueues-14-vSICK.mjs';
4
4
  export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName } from './packem_shared/defineQueue-D40gREfg.mjs';
5
- export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-DbA5vKBs.mjs';
6
- export { createQueueRunContext } from './packem_shared/createQueueRunContext-wScWyFao.mjs';
5
+ export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-DSEWhEy8.mjs';
6
+ export { createQueueRunContext } from './packem_shared/createQueueRunContext-C8jboCk6.mjs';
@@ -54,9 +54,16 @@ const createDispatchRunner = (options) => {
54
54
  throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);
55
55
  }
56
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
+ }
57
64
  const response = await fetchImpl(url, {
58
65
  body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
59
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
66
+ headers,
60
67
  method: "POST"
61
68
  });
62
69
  if (!response.ok) {
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { createQueueRunContext } from './createQueueRunContext-wScWyFao.mjs';
2
+ import { createQueueRunContext } from './createQueueRunContext-C8jboCk6.mjs';
3
3
 
4
4
  const DEFAULT_MAX_RETRIES = 3;
5
5
  const timestampToMs = (value) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/queue",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
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",
@@ -44,7 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@lunora/errors": "1.0.0-alpha.4"
47
+ "@lunora/errors": "1.0.0-alpha.6"
48
48
  },
49
49
  "engines": {
50
50
  "node": "^22.15.0 || >=24.11.0"