@lunora/queue 1.0.0-alpha.8 → 1.0.0-alpha.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.d.mts +233 -178
- package/dist/index.d.ts +233 -178
- package/dist/index.mjs +1 -6
- package/dist/packem_shared/createQueueCaptureSink-CSOPftvj.mjs +1 -0
- package/dist/packem_shared/createQueueContext-CZ3ZpUFb.mjs +1 -0
- package/dist/packem_shared/createQueueRunContext-kMii_-oH.mjs +1 -0
- package/dist/packem_shared/createQueues-BVwkHkeb.mjs +1 -0
- package/dist/packem_shared/defineQueue-TY4-i3nG.mjs +1 -0
- package/dist/packem_shared/dispatchQueueBatch-B8GbPNBs.mjs +1 -0
- package/dist/packem_shared/requeue-envelope-BjL4JftH.mjs +4 -0
- package/dist/packem_shared/run-context-wO-J6k6F.mjs +1 -0
- package/package.json +3 -2
- package/dist/packem_shared/createQueueCaptureSink-B8WE0eHf.mjs +0 -64
- package/dist/packem_shared/createQueueContext-D0XCdCsd.mjs +0 -14
- package/dist/packem_shared/createQueueRunContext-C8jboCk6.mjs +0 -94
- package/dist/packem_shared/createQueues-14-vSICK.mjs +0 -33
- package/dist/packem_shared/defineQueue-D40gREfg.mjs +0 -18
- package/dist/packem_shared/dispatchQueueBatch-DSEWhEy8.mjs +0 -132
package/README.md
CHANGED
|
@@ -26,13 +26,19 @@ import { api } from "./_generated/api";
|
|
|
26
26
|
export const emailQueue = defineQueue<{ to: string }>({
|
|
27
27
|
handler: async (ctx, batch) => {
|
|
28
28
|
for (const message of batch.messages) {
|
|
29
|
-
await
|
|
29
|
+
await message.run(api.email.send, { to: message.body.to });
|
|
30
30
|
message.ack();
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
`message.run(...)` is `ctx.run(...)` pinned to that message. Call it inside the
|
|
37
|
+
batch loop: when the dispatched function fails deterministically (`400`, `403`,
|
|
38
|
+
`404`, `422`), the consumer acks just that message and retries the rest instead
|
|
39
|
+
of letting one poison message retry — and eventually dead-letter — the whole
|
|
40
|
+
batch.
|
|
41
|
+
|
|
36
42
|
```ts
|
|
37
43
|
// inside a mutation/action
|
|
38
44
|
await ctx.queues.emailQueue.send({ to: user.email });
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { QueueBindingLike, MessageBatchLike, MessageLike, QueueSendOptions, MessageSendRequestLike, QueueSendBatchOptions } from '@lunora/platform';
|
|
2
|
+
export type { MessageBatchLike, MessageLike, MessageSendRequestLike, QueueBindingLike, QueueContentType, QueueRetryOptions, QueueSendBatchOptions, QueueSendOptions } from '@lunora/platform';
|
|
1
3
|
/**
|
|
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
|
-
*/
|
|
4
|
+
* Shared types for dispatching a Lunora function back into the worker from a
|
|
5
|
+
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
6
|
+
* Node-safe — no Cloudflare runtime imports — so the consumers stay unit-testable
|
|
7
|
+
* with plain-object doubles.
|
|
8
|
+
*/
|
|
7
9
|
/** Opaque generated function reference (`api.foo.bar`), carrying its dispatch id. */
|
|
8
10
|
interface FunctionReference {
|
|
9
11
|
__lunoraRef: string;
|
|
@@ -12,8 +14,45 @@ interface FunctionReference {
|
|
|
12
14
|
type ArgsOf<F> = F extends FunctionReference ? Record<string, unknown> : never;
|
|
13
15
|
/** Options for a function call made via a dispatch runner. */
|
|
14
16
|
interface RunFunctionOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The at-least-once dedup key for THIS ONE CALL, sent to the dispatch
|
|
19
|
+
* endpoint as the body's `id` and forwarded to the shard as the
|
|
20
|
+
* replay-dedup `mutationId` — so a redelivery that re-runs the same call
|
|
21
|
+
* applies it exactly once instead of twice.
|
|
22
|
+
*
|
|
23
|
+
* Must be unique per call and stable across redeliveries. It is NOT
|
|
24
|
+
* {@link RunFunctionOptions.messageId}: the shard's dedup table is keyed
|
|
25
|
+
* `(identity, mutationId)` with no function path in it, and every
|
|
26
|
+
* server-initiated dispatch shares the `"system:"` identity, so reusing
|
|
27
|
+
* one id across two calls makes the second return the FIRST call's cached
|
|
28
|
+
* result without ever executing. A per-message id is 1:N with the calls a
|
|
29
|
+
* handler makes; this is 1:1.
|
|
30
|
+
*
|
|
31
|
+
* `@lunora/queue`'s `message.run` derives one automatically as
|
|
32
|
+
* `<messageId>#<n>`, `n` counting that message's calls in order. That is
|
|
33
|
+
* stable across redeliveries because the handler replays from the start —
|
|
34
|
+
* it relies on the handler issuing its `run` calls in a DETERMINISTIC
|
|
35
|
+
* order, which at-least-once replay already assumes. A handler whose call
|
|
36
|
+
* order varies per attempt (branching on `Date.now()`, `Math.random()`,
|
|
37
|
+
* or unordered concurrent settles) must pass its own stable ids instead.
|
|
38
|
+
*
|
|
39
|
+
* Optional; when omitted the dispatch is at-least-once.
|
|
40
|
+
*/
|
|
41
|
+
dedupId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Correlate this call with a caller-defined message/item id (e.g. a queue
|
|
44
|
+
* message's `id`), for failure attribution only — never sent to the
|
|
45
|
+
* dispatch endpoint. Carried onto the `LunoraError` a deterministic
|
|
46
|
+
* dispatch failure throws, so a batching consumer (`@lunora/queue`'s push
|
|
47
|
+
* handler) can read it back and attribute the failure to the one item that
|
|
48
|
+
* caused it instead of the whole batch. Deliberately NOT the dedup key —
|
|
49
|
+
* see {@link RunFunctionOptions.dedupId}. Optional and inert when omitted.
|
|
50
|
+
*/
|
|
51
|
+
messageId?: string;
|
|
15
52
|
/** Route the call to a specific shard (defaults to the worker's root shard). */
|
|
16
53
|
shardKey?: string;
|
|
54
|
+
/** Abort the dispatch after this many ms; the abort is retryable. Overrides the runner's default. */
|
|
55
|
+
timeoutMs?: number;
|
|
17
56
|
}
|
|
18
57
|
/** Invoke a Lunora function (query/mutation/action) by reference. The shape of `ctx.run`. */
|
|
19
58
|
type DispatchRunFunction = <F extends FunctionReference>(function_: F, args?: ArgsOf<F>, options?: RunFunctionOptions) => Promise<unknown>;
|
|
@@ -24,66 +63,11 @@ interface DispatchLogger {
|
|
|
24
63
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
64
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
65
|
}
|
|
27
|
-
/** Build a {@link DispatchLogger} that prefixes every line with `prefix` (e.g. `[queue:email]`). */
|
|
28
|
-
/** How a queue message body is serialized on the wire (Cloudflare default `"json"`). */
|
|
29
|
-
type QueueContentType = "bytes" | "json" | "text" | "v8";
|
|
30
|
-
/** Options for a single `producer.send(body, options?)`. */
|
|
31
|
-
interface QueueSendOptions {
|
|
32
|
-
/** Wire serialization for this message (defaults to the queue's content type). */
|
|
33
|
-
contentType?: QueueContentType;
|
|
34
|
-
/** Per-message delivery delay in seconds (0–43200, i.e. up to 12 hours). */
|
|
35
|
-
delaySeconds?: number;
|
|
36
|
-
}
|
|
37
|
-
/** Options for a `producer.sendBatch(messages, options?)`. */
|
|
38
|
-
interface QueueSendBatchOptions {
|
|
39
|
-
/** Delivery delay applied to the whole batch, in seconds. */
|
|
40
|
-
delaySeconds?: number;
|
|
41
|
-
}
|
|
42
|
-
/** One entry in a `sendBatch` call — a body plus optional per-message overrides. */
|
|
43
|
-
interface MessageSendRequestLike<Body = unknown> {
|
|
44
|
-
body: Body;
|
|
45
|
-
contentType?: QueueContentType;
|
|
46
|
-
delaySeconds?: number;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
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
|
-
*/
|
|
53
|
-
interface QueueBindingLike<Body = unknown> {
|
|
54
|
-
send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
|
|
55
|
-
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
|
|
56
|
-
}
|
|
57
|
-
/** Options for retrying a message / batch (`message.retry({ delaySeconds })`). */
|
|
58
|
-
interface QueueRetryOptions {
|
|
59
|
-
delaySeconds?: number;
|
|
60
|
-
}
|
|
61
|
-
/** Structural mirror of workers-types' `Message<Body>` (one delivered message). */
|
|
62
|
-
interface MessageLike<Body = unknown> {
|
|
63
|
-
/** Acknowledge this message so it is not redelivered. */
|
|
64
|
-
ack: () => void;
|
|
65
|
-
readonly attempts: number;
|
|
66
|
-
readonly body: Body;
|
|
67
|
-
readonly id: string;
|
|
68
|
-
/** Explicitly retry this message (optionally after a delay). */
|
|
69
|
-
retry: (options?: QueueRetryOptions) => void;
|
|
70
|
-
readonly timestamp: Date;
|
|
71
|
-
}
|
|
72
|
-
/** Structural mirror of workers-types' `MessageBatch<Body>` handed to a consumer. */
|
|
73
|
-
interface MessageBatchLike<Body = unknown> {
|
|
74
|
-
/** Acknowledge every message in the batch. */
|
|
75
|
-
ackAll: () => void;
|
|
76
|
-
readonly messages: ReadonlyArray<MessageLike<Body>>;
|
|
77
|
-
/** The queue name this batch was delivered from (`batch.queue`), used to route. */
|
|
78
|
-
readonly queue: string;
|
|
79
|
-
/** Retry every message in the batch (optionally after a delay). */
|
|
80
|
-
retryAll: (options?: QueueRetryOptions) => void;
|
|
81
|
-
}
|
|
82
66
|
/**
|
|
83
|
-
* The typed producer bound to `ctx.queues
|
|
84
|
-
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
85
|
-
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
86
|
-
*/
|
|
67
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
68
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
69
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
70
|
+
*/
|
|
87
71
|
interface QueueProducer<Body = unknown> {
|
|
88
72
|
/** Enqueue one message. */
|
|
89
73
|
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
@@ -91,10 +75,10 @@ interface QueueProducer<Body = unknown> {
|
|
|
91
75
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
76
|
}
|
|
93
77
|
/**
|
|
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
|
-
*/
|
|
78
|
+
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
79
|
+
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
80
|
+
* `createQueues` stays schema-agnostic.
|
|
81
|
+
*/
|
|
98
82
|
interface Queues {
|
|
99
83
|
[exportName: string]: QueueProducer;
|
|
100
84
|
}
|
|
@@ -104,23 +88,49 @@ interface LunoraQueuesOptions {
|
|
|
104
88
|
bindings: Record<string, QueueBindingLike>;
|
|
105
89
|
}
|
|
106
90
|
/**
|
|
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
|
-
*/
|
|
91
|
+
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
92
|
+
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
93
|
+
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
94
|
+
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
95
|
+
*/
|
|
112
96
|
interface QueueRunContext {
|
|
113
97
|
/** The worker `env` (bindings + vars). */
|
|
114
98
|
readonly env: Record<string, unknown>;
|
|
115
99
|
/** Queue-name-prefixed logger. */
|
|
116
100
|
readonly log: DispatchLogger;
|
|
117
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Invoke a Lunora function (query/mutation/action) by reference.
|
|
103
|
+
*
|
|
104
|
+
* Batch-unaware: a failure it throws is attributed to nothing, so a
|
|
105
|
+
* deterministic failure retries the whole batch. Inside the `batch.messages`
|
|
106
|
+
* loop, prefer {@link QueueMessage.run}, which pins the call to its message.
|
|
107
|
+
*/
|
|
108
|
+
readonly run: DispatchRunFunction;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* One delivered message as the push handler sees it: the Cloudflare `Message`
|
|
112
|
+
* plus `run` — a {@link QueueRunContext.run} pinned to THIS message.
|
|
113
|
+
*
|
|
114
|
+
* Prefer `message.run(api.x.y, args)` over `ctx.run(...)` inside the batch
|
|
115
|
+
* loop. The pin is what lets the dispatcher attribute a deterministic dispatch
|
|
116
|
+
* failure (400/403/404/422) to the one message that caused it: that message is
|
|
117
|
+
* acked and every other one is retried, instead of the whole batch being
|
|
118
|
+
* re-delivered because of a single poison message. A plain `ctx.run` call
|
|
119
|
+
* carries no message id, so its failure stays unattributed and the whole batch
|
|
120
|
+
* retries.
|
|
121
|
+
*/
|
|
122
|
+
interface QueueMessage<Body = unknown> extends MessageLike<Body> {
|
|
123
|
+
/** {@link QueueRunContext.run}, pinned to this message for failure attribution. */
|
|
118
124
|
readonly run: DispatchRunFunction;
|
|
119
125
|
}
|
|
126
|
+
/** The delivered batch as the push handler sees it — {@link QueueMessage}s rather than bare `Message`s. */
|
|
127
|
+
interface QueueMessageBatch<Body = unknown> extends Omit<MessageBatchLike<Body>, "messages"> {
|
|
128
|
+
readonly messages: ReadonlyArray<QueueMessage<Body>>;
|
|
129
|
+
}
|
|
120
130
|
/** Whether a declared queue is consumed by this worker (push) or polled externally (pull). */
|
|
121
131
|
type QueueConsumerMode = "pull" | "push";
|
|
122
132
|
/** The handler body run for each delivered batch (push consumers only). */
|
|
123
|
-
type QueueHandler<Body = unknown> = (context: QueueRunContext, batch:
|
|
133
|
+
type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: QueueMessageBatch<Body>) => Promise<void> | void;
|
|
124
134
|
/** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
|
|
125
135
|
interface QueueConsumerTuning {
|
|
126
136
|
/** Name of the dead-letter queue messages land in after `maxRetries`. */
|
|
@@ -129,7 +139,7 @@ interface QueueConsumerTuning {
|
|
|
129
139
|
maxBatchSize?: number;
|
|
130
140
|
/** Max seconds to wait before delivering a partial batch (0–60, default 5). */
|
|
131
141
|
maxBatchTimeout?: number;
|
|
132
|
-
/**
|
|
142
|
+
/** Retries **after** the initial delivery, before a message is dropped / dead-lettered. Default 3, so up to 4 deliveries in total. */
|
|
133
143
|
maxRetries?: number;
|
|
134
144
|
/** Delay in seconds before a failed batch is retried. */
|
|
135
145
|
retryDelay?: number;
|
|
@@ -137,25 +147,25 @@ interface QueueConsumerTuning {
|
|
|
137
147
|
/** The config object passed to `defineQueue`. */
|
|
138
148
|
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
149
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
* The push-consumer body. Required for `mode: "push"` (the default); omit it
|
|
151
|
+
* for `mode: "pull"`, where an external worker polls the queue over HTTP.
|
|
152
|
+
*/
|
|
143
153
|
handler?: QueueHandler<Body>;
|
|
144
154
|
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
155
|
mode?: QueueConsumerMode;
|
|
146
156
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
158
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
159
|
+
*/
|
|
150
160
|
name?: string;
|
|
151
161
|
}
|
|
152
162
|
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
163
|
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
164
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
166
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
167
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
168
|
+
*/
|
|
159
169
|
readonly __lunoraBody?: Body;
|
|
160
170
|
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
171
|
isLunoraQueue: true;
|
|
@@ -172,13 +182,20 @@ interface QueueBindingSpec {
|
|
|
172
182
|
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
173
183
|
interface QueueRegistryEntry {
|
|
174
184
|
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
* The queue's own producer binding on `env` (`QUEUE_*`). A message whose
|
|
186
|
+
* dispatch is declined on its last delivery is re-enqueued through it (see
|
|
187
|
+
* {@link resolveDeclinedBatch}); without it that message is dead-lettered or
|
|
188
|
+
* dropped, and logged. Codegen always emits it.
|
|
189
|
+
*/
|
|
190
|
+
binding?: string;
|
|
191
|
+
/**
|
|
192
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
193
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
194
|
+
* queues carry different message bodies, and the handler param is
|
|
195
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
196
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
197
|
+
* batch straight through, so the erasure is type-only.
|
|
198
|
+
*/
|
|
182
199
|
definition: QueueDefinition<any>;
|
|
183
200
|
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
184
201
|
exportName: string;
|
|
@@ -188,17 +205,38 @@ type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
|
188
205
|
/** The disposition a consumer left one message in for a single delivery attempt. */
|
|
189
206
|
type QueueMessageOutcome = "ack" | "error" | "retry";
|
|
190
207
|
/**
|
|
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
|
-
*/
|
|
208
|
+
* One consumed message as captured by {@link dispatchQueueBatch} and handed to an
|
|
209
|
+
* {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
|
|
210
|
+
* `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
|
|
211
|
+
* the two packages share only this contract, so keep them in sync by hand.
|
|
212
|
+
*/
|
|
196
213
|
interface CapturedQueueMessage {
|
|
197
214
|
/** Delivery attempt number for this message (`message.attempts`). */
|
|
198
215
|
attempts: number;
|
|
199
216
|
/** The message body (JSON-encoded + capped by the catcher). */
|
|
200
217
|
body: unknown;
|
|
201
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* `true` when this failed delivery was the message's last (its retries are
|
|
220
|
+
* exhausted) AND the queue declares a `deadLetterQueue` for it to land in.
|
|
221
|
+
* Stays `false` for a queue with no DLQ, where the broker drops the
|
|
222
|
+
* exhausted message instead — `attempts > maxRetries` with
|
|
223
|
+
* `outcome !== "ack"` is what identifies that case.
|
|
224
|
+
*
|
|
225
|
+
* Read from the `defineQueue` DECLARATION, because a consumer has no
|
|
226
|
+
* runtime API that exposes its deployed `queues.consumers[]` settings. It is
|
|
227
|
+
* true of the deployed broker only as far as Lunora's binding reconcile (run
|
|
228
|
+
* by `lunora dev`, `deploy` and `prepare`) keeps the two in step: it writes
|
|
229
|
+
* every declared tuning field onto the consumer, including onto one that
|
|
230
|
+
* already exists, and takes a field back out once `defineQueue` drops it
|
|
231
|
+
* (it records what `defineQueue` declared in `package.json`
|
|
232
|
+
* `lunora.queueTuning`). The gaps that remain: a field changed by hand to
|
|
233
|
+
* another value than the declared one is
|
|
234
|
+
* kept when the declaration drops it, so a hand-set DLQ still reads
|
|
235
|
+
* `false` here; a `--env` deploy retunes the `env.<name>` consumers but
|
|
236
|
+
* never writes their `dead_letter_queue`, which that block names itself;
|
|
237
|
+
* and a bare `wrangler deploy` over a hand-edited consumer can disagree
|
|
238
|
+
* too. Nothing on this side can see any of it.
|
|
239
|
+
*/
|
|
202
240
|
deadLettered: boolean;
|
|
203
241
|
/** Handler error message when `outcome` is `error`; absent otherwise. */
|
|
204
242
|
error?: string;
|
|
@@ -214,31 +252,40 @@ interface CapturedQueueMessage {
|
|
|
214
252
|
timestamp: number;
|
|
215
253
|
}
|
|
216
254
|
/**
|
|
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
|
-
*/
|
|
255
|
+
* Persists a batch of consumed messages. The codegen worker wires this to POST the
|
|
256
|
+
* batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
|
|
257
|
+
* Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
|
|
258
|
+
* capture failure never changes delivery semantics.
|
|
259
|
+
*/
|
|
222
260
|
type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
|
|
223
261
|
interface DispatchOptions {
|
|
224
262
|
/**
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
* Optional capture sink. When set, every message's final disposition is
|
|
264
|
+
* turned into a record and handed to this sink after the handler runs.
|
|
265
|
+
* Omitted in production unless queue capture is enabled, so a consumer pays
|
|
266
|
+
* no record-building or sink cost by default. Delivery semantics — including
|
|
267
|
+
* poison-message isolation — do not depend on it.
|
|
268
|
+
*/
|
|
230
269
|
capture?: QueueCaptureSink;
|
|
231
270
|
/** Worker `env`, forwarded to the queue run context. */
|
|
232
271
|
env: Record<string, unknown>;
|
|
233
272
|
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
234
273
|
fetchImpl?: typeof fetch;
|
|
274
|
+
/**
|
|
275
|
+
* W3C `traceparent` of the consumer invocation's own trace, supplied by the
|
|
276
|
+
* runtime's `queue()` entry. Forwarded on every `ctx.run` dispatch so the
|
|
277
|
+
* functions a handler calls are CHILDREN of the queue span instead of a set of
|
|
278
|
+
* unrelated root traces. Absent (a hand-built dispatch, a unit test) keeps the
|
|
279
|
+
* prior behaviour: the callee mints its own trace.
|
|
280
|
+
*/
|
|
281
|
+
traceparent?: string;
|
|
235
282
|
}
|
|
236
283
|
/**
|
|
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
|
-
*/
|
|
284
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
285
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
286
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
287
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
288
|
+
*/
|
|
242
289
|
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
243
290
|
/** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
|
|
244
291
|
type QueueEnv = Record<string, unknown>;
|
|
@@ -250,81 +297,87 @@ interface QueueCaptureOptions {
|
|
|
250
297
|
rootShard?: string;
|
|
251
298
|
}
|
|
252
299
|
/**
|
|
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
|
-
*/
|
|
300
|
+
* Whether consumed queue messages should be captured into the studio's log.
|
|
301
|
+
* Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
|
|
302
|
+
* unset, capture is on only in a development environment. Mirrors
|
|
303
|
+
* `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
|
|
304
|
+
* same way.
|
|
305
|
+
*/
|
|
259
306
|
declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
|
|
260
307
|
/**
|
|
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
|
-
*/
|
|
308
|
+
* Build the {@link QueueCaptureSink} that records a processed batch into the
|
|
309
|
+
* studio's root-shard consumed-message log via the reserved `recordQueueMessage`
|
|
310
|
+
* admin RPC. Best-effort by contract: without the `SHARD` binding or
|
|
311
|
+
* `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
|
|
312
|
+
* rejection, so capture never changes delivery semantics.
|
|
313
|
+
*/
|
|
267
314
|
declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
|
|
268
315
|
/**
|
|
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
|
|
273
|
-
* the missing queue is actually used.
|
|
274
|
-
*/
|
|
316
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
317
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
318
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
319
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
320
|
+
* the missing queue is actually used.
|
|
321
|
+
*/
|
|
275
322
|
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
276
323
|
/**
|
|
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
|
-
*/
|
|
324
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
325
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
326
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
327
|
+
* queues (raised lazily on first use).
|
|
328
|
+
*/
|
|
282
329
|
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
283
330
|
/**
|
|
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
|
-
*/
|
|
331
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
332
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
333
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
334
|
+
* queue export can never collide with the built-in bindings.
|
|
335
|
+
*/
|
|
289
336
|
declare const queueBindingName: (exportName: string) => string;
|
|
290
337
|
/**
|
|
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
|
-
*/
|
|
338
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
339
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
340
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
341
|
+
*/
|
|
295
342
|
declare const queueDefaultName: (exportName: string) => string;
|
|
296
343
|
/**
|
|
297
|
-
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
298
|
-
* branding: codegen discovers the export, emits the typed `ctx.queues
|
|
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
|
|
309
|
-
* handler: async (ctx, batch) => {
|
|
310
|
-
* for (const message of batch.messages) {
|
|
311
|
-
* await
|
|
312
|
-
* message.ack();
|
|
313
|
-
* }
|
|
314
|
-
* },
|
|
315
|
-
* });
|
|
316
|
-
* ```
|
|
317
|
-
*
|
|
318
|
-
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
|
|
344
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
345
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
346
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
347
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
348
|
+
* entries from the same definition.
|
|
349
|
+
*
|
|
350
|
+
* ```ts
|
|
351
|
+
* // lunora/queues.ts
|
|
352
|
+
* import { defineQueue } from "@lunora/queue";
|
|
353
|
+
* import { api } from "./_generated/api";
|
|
354
|
+
*
|
|
355
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
356
|
+
* handler: async (ctx, batch) => {
|
|
357
|
+
* for (const message of batch.messages) {
|
|
358
|
+
* await message.run(api.email.send, { to: message.body.to });
|
|
359
|
+
* message.ack();
|
|
360
|
+
* }
|
|
361
|
+
* },
|
|
362
|
+
* });
|
|
363
|
+
* ```
|
|
364
|
+
*
|
|
365
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
366
|
+
*
|
|
367
|
+
* `message.run(...)` is `ctx.run(...)` pinned to that message — prefer it inside
|
|
368
|
+
* the batch loop. A deterministic failure (400/403/404/422) from a pinned call
|
|
369
|
+
* is attributed to its message: that one is acked and the rest are retried,
|
|
370
|
+
* instead of one poison message re-delivering (and eventually dead-lettering)
|
|
371
|
+
* the whole batch.
|
|
372
|
+
*
|
|
373
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
374
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
375
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
376
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
377
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
378
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
379
|
+
* unchecked body straight into a privileged mutation.
|
|
380
|
+
*/
|
|
328
381
|
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
329
382
|
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
330
383
|
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
@@ -332,7 +385,9 @@ interface RunContextOptions {
|
|
|
332
385
|
env: Record<string, unknown>;
|
|
333
386
|
exportName: string;
|
|
334
387
|
fetchImpl?: typeof fetch;
|
|
388
|
+
/** The consumer invocation's `traceparent`, so `ctx.run` joins the queue's trace. */
|
|
389
|
+
traceparent?: string;
|
|
335
390
|
}
|
|
336
391
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
337
392
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
338
|
-
export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type
|
|
393
|
+
export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueDefinition, type QueueEnv, type QueueHandler, type DispatchLogger as QueueLogger, type QueueMessage, type QueueMessageBatch, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type Queues, type RunFunctionOptions, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
|