@lunora/queue 1.0.0-alpha.9 → 1.0.0-alpha.91
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 +19 -1
- package/dist/index.d.mts +206 -72
- package/dist/index.d.ts +206 -72
- package/dist/index.mjs +1 -6
- package/dist/packem_shared/createQueueCaptureSink-CSOPftvj.mjs +1 -0
- package/dist/packem_shared/createQueueContext-aj6p-gxk.mjs +1 -0
- package/dist/packem_shared/createQueueRunContext-kMii_-oH.mjs +1 -0
- package/dist/packem_shared/createQueues-BVaXTnAh.mjs +1 -0
- package/dist/packem_shared/createTopicContext-nG16dV5W.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,18 +26,36 @@ 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 });
|
|
39
45
|
```
|
|
40
46
|
|
|
47
|
+
For one event with several consumers, declare a topic and subscribe to it.
|
|
48
|
+
Each subscription is its own queue, with its own retries and dead-letter queue:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// lunora/queues.ts
|
|
52
|
+
export const signups = defineTopic<{ userId: string }>();
|
|
53
|
+
export const welcomeEmail = defineSubscription(signups, { handler: async (ctx, batch) => {} });
|
|
54
|
+
|
|
55
|
+
// inside a mutation/action
|
|
56
|
+
await ctx.topics.signups.publish({ userId });
|
|
57
|
+
```
|
|
58
|
+
|
|
41
59
|
Codegen emits the typed `ctx.queues` producer and the worker `queue()` dispatch;
|
|
42
60
|
`@lunora/config` reconciles the wrangler `queues.producers[]` /
|
|
43
61
|
`queues.consumers[]` entries from the same definition. Scaffold a new queue with
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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
4
|
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
5
|
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
@@ -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,62 +63,8 @@ interface DispatchLogger {
|
|
|
24
63
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
64
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
65
|
}
|
|
27
|
-
/** How a queue message body is serialized on the wire (Cloudflare default `"json"`). */
|
|
28
|
-
type QueueContentType = "bytes" | "json" | "text" | "v8";
|
|
29
|
-
/** Options for a single `producer.send(body, options?)`. */
|
|
30
|
-
interface QueueSendOptions {
|
|
31
|
-
/** Wire serialization for this message (defaults to the queue's content type). */
|
|
32
|
-
contentType?: QueueContentType;
|
|
33
|
-
/** Per-message delivery delay in seconds (0–43200, i.e. up to 12 hours). */
|
|
34
|
-
delaySeconds?: number;
|
|
35
|
-
}
|
|
36
|
-
/** Options for a `producer.sendBatch(messages, options?)`. */
|
|
37
|
-
interface QueueSendBatchOptions {
|
|
38
|
-
/** Delivery delay applied to the whole batch, in seconds. */
|
|
39
|
-
delaySeconds?: number;
|
|
40
|
-
}
|
|
41
|
-
/** One entry in a `sendBatch` call — a body plus optional per-message overrides. */
|
|
42
|
-
interface MessageSendRequestLike<Body = unknown> {
|
|
43
|
-
body: Body;
|
|
44
|
-
contentType?: QueueContentType;
|
|
45
|
-
delaySeconds?: number;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
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
|
-
*/
|
|
52
|
-
interface QueueBindingLike<Body = unknown> {
|
|
53
|
-
send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
|
|
54
|
-
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
|
|
55
|
-
}
|
|
56
|
-
/** Options for retrying a message / batch (`message.retry({ delaySeconds })`). */
|
|
57
|
-
interface QueueRetryOptions {
|
|
58
|
-
delaySeconds?: number;
|
|
59
|
-
}
|
|
60
|
-
/** Structural mirror of workers-types' `Message<Body>` (one delivered message). */
|
|
61
|
-
interface MessageLike<Body = unknown> {
|
|
62
|
-
/** Acknowledge this message so it is not redelivered. */
|
|
63
|
-
ack: () => void;
|
|
64
|
-
readonly attempts: number;
|
|
65
|
-
readonly body: Body;
|
|
66
|
-
readonly id: string;
|
|
67
|
-
/** Explicitly retry this message (optionally after a delay). */
|
|
68
|
-
retry: (options?: QueueRetryOptions) => void;
|
|
69
|
-
readonly timestamp: Date;
|
|
70
|
-
}
|
|
71
|
-
/** Structural mirror of workers-types' `MessageBatch<Body>` handed to a consumer. */
|
|
72
|
-
interface MessageBatchLike<Body = unknown> {
|
|
73
|
-
/** Acknowledge every message in the batch. */
|
|
74
|
-
ackAll: () => void;
|
|
75
|
-
readonly messages: ReadonlyArray<MessageLike<Body>>;
|
|
76
|
-
/** The queue name this batch was delivered from (`batch.queue`), used to route. */
|
|
77
|
-
readonly queue: string;
|
|
78
|
-
/** Retry every message in the batch (optionally after a delay). */
|
|
79
|
-
retryAll: (options?: QueueRetryOptions) => void;
|
|
80
|
-
}
|
|
81
66
|
/**
|
|
82
|
-
* The typed producer bound to `ctx.queues
|
|
67
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
83
68
|
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
84
69
|
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
85
70
|
*/
|
|
@@ -113,13 +98,39 @@ interface QueueRunContext {
|
|
|
113
98
|
readonly env: Record<string, unknown>;
|
|
114
99
|
/** Queue-name-prefixed logger. */
|
|
115
100
|
readonly log: DispatchLogger;
|
|
116
|
-
/**
|
|
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. */
|
|
117
124
|
readonly run: DispatchRunFunction;
|
|
118
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
|
+
}
|
|
119
130
|
/** Whether a declared queue is consumed by this worker (push) or polled externally (pull). */
|
|
120
131
|
type QueueConsumerMode = "pull" | "push";
|
|
121
132
|
/** The handler body run for each delivered batch (push consumers only). */
|
|
122
|
-
type QueueHandler<Body = unknown> = (context: QueueRunContext, batch:
|
|
133
|
+
type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: QueueMessageBatch<Body>) => Promise<void> | void;
|
|
123
134
|
/** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
|
|
124
135
|
interface QueueConsumerTuning {
|
|
125
136
|
/** Name of the dead-letter queue messages land in after `maxRetries`. */
|
|
@@ -128,7 +139,7 @@ interface QueueConsumerTuning {
|
|
|
128
139
|
maxBatchSize?: number;
|
|
129
140
|
/** Max seconds to wait before delivering a partial batch (0–60, default 5). */
|
|
130
141
|
maxBatchTimeout?: number;
|
|
131
|
-
/**
|
|
142
|
+
/** Retries **after** the initial delivery, before a message is dropped / dead-lettered. Default 3, so up to 4 deliveries in total. */
|
|
132
143
|
maxRetries?: number;
|
|
133
144
|
/** Delay in seconds before a failed batch is retried. */
|
|
134
145
|
retryDelay?: number;
|
|
@@ -152,13 +163,71 @@ interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
|
152
163
|
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
153
164
|
/**
|
|
154
165
|
* Phantom carrier for the message body type, so codegen can type the
|
|
155
|
-
* generated `ctx.queues
|
|
156
|
-
* `typeof
|
|
166
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
167
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
157
168
|
*/
|
|
158
169
|
readonly __lunoraBody?: Body;
|
|
159
170
|
/** Runtime brand identifying a `defineQueue` result. */
|
|
160
171
|
isLunoraQueue: true;
|
|
161
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* The typed publisher bound to `ctx.topics.<name>`. A publish is one send to
|
|
175
|
+
* **each** subscription's queue, in parallel, so it is not atomic: when one send
|
|
176
|
+
* fails the call rejects, and a retry re-delivers to the subscriptions whose send
|
|
177
|
+
* already succeeded. Delivery is at-least-once per subscription — make handlers
|
|
178
|
+
* idempotent. A topic with no subscriptions publishes to nobody.
|
|
179
|
+
*/
|
|
180
|
+
interface TopicPublisher<Payload = unknown> {
|
|
181
|
+
/** Publish one message to every subscription. */
|
|
182
|
+
publish: (payload: Payload, options?: QueueSendOptions) => Promise<void>;
|
|
183
|
+
/** Publish a batch (at most 100) to every subscription in one send per subscription. */
|
|
184
|
+
publishBatch: (messages: Iterable<MessageSendRequestLike<Payload>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
185
|
+
}
|
|
186
|
+
/** `ctx.topics` — declared topic export names → typed publishers. Codegen narrows it to the exact names. */
|
|
187
|
+
interface Topics {
|
|
188
|
+
[exportName: string]: TopicPublisher;
|
|
189
|
+
}
|
|
190
|
+
/** The branded result of `defineTopic`, discovered by codegen. Carries no deploy config of its own. */
|
|
191
|
+
interface TopicDefinition<Payload = unknown> {
|
|
192
|
+
/**
|
|
193
|
+
* Phantom carrier for the payload type, so codegen can type `ctx.topics.<name>`.
|
|
194
|
+
* Named like {@link QueueDefinition}'s so one emitted helper reads both. Never
|
|
195
|
+
* assigned at runtime.
|
|
196
|
+
*/
|
|
197
|
+
readonly __lunoraBody?: Payload;
|
|
198
|
+
/** Runtime brand identifying a `defineTopic` result. */
|
|
199
|
+
isLunoraTopic: true;
|
|
200
|
+
}
|
|
201
|
+
/** The config passed to `defineSubscription`: a push consumer's handler plus its own retry/DLQ tuning. */
|
|
202
|
+
interface SubscriptionConfig<Payload = unknown> extends QueueConsumerTuning {
|
|
203
|
+
handler: QueueHandler<Payload>;
|
|
204
|
+
/**
|
|
205
|
+
* Stable wrangler queue name for this subscription. Defaults to the
|
|
206
|
+
* kebab-cased export name, exactly like a `defineQueue` export.
|
|
207
|
+
*/
|
|
208
|
+
name?: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The branded result of `defineSubscription`: a push {@link QueueDefinition}
|
|
212
|
+
* (so dispatch, retries, the DLQ, wrangler reconcile and the studio treat it as
|
|
213
|
+
* the queue it is) tagged with the topic it consumes.
|
|
214
|
+
*/
|
|
215
|
+
interface SubscriptionDefinition<Payload = unknown> extends QueueDefinition<Payload> {
|
|
216
|
+
handler: QueueHandler<Payload>;
|
|
217
|
+
mode: "push";
|
|
218
|
+
/** The topic this subscription consumes. */
|
|
219
|
+
topic: TopicDefinition<Payload>;
|
|
220
|
+
}
|
|
221
|
+
/** Wiring info for one declared topic, emitted by codegen into the generated shard. */
|
|
222
|
+
interface TopicBindingSpec {
|
|
223
|
+
/** The `lunora/queues.ts` topic export name, e.g. `signups`. */
|
|
224
|
+
exportName: string;
|
|
225
|
+
/** One entry per subscription: its export name and its `Queue` producer binding. */
|
|
226
|
+
subscriptions: ReadonlyArray<{
|
|
227
|
+
binding: string;
|
|
228
|
+
exportName: string;
|
|
229
|
+
}>;
|
|
230
|
+
}
|
|
162
231
|
/** Wiring info for one declared queue, emitted by codegen into the generated shard/handler. */
|
|
163
232
|
interface QueueBindingSpec {
|
|
164
233
|
/** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
|
|
@@ -170,11 +239,18 @@ interface QueueBindingSpec {
|
|
|
170
239
|
}
|
|
171
240
|
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
172
241
|
interface QueueRegistryEntry {
|
|
242
|
+
/**
|
|
243
|
+
* The queue's own producer binding on `env` (`QUEUE_*`). A message whose
|
|
244
|
+
* dispatch is declined on its last delivery is re-enqueued through it (see
|
|
245
|
+
* {@link resolveDeclinedBatch}); without it that message is dead-lettered or
|
|
246
|
+
* dropped, and logged. Codegen always emits it.
|
|
247
|
+
*/
|
|
248
|
+
binding?: string;
|
|
173
249
|
/**
|
|
174
250
|
* The `defineQueue` result (carries the push handler). The body type is
|
|
175
251
|
* erased to `any` here because the registry is heterogeneous — different
|
|
176
252
|
* queues carry different message bodies, and the handler param is
|
|
177
|
-
* contravariant, so a precise `QueueDefinition
|
|
253
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
178
254
|
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
179
255
|
* batch straight through, so the erasure is type-only.
|
|
180
256
|
*/
|
|
@@ -197,7 +273,28 @@ interface CapturedQueueMessage {
|
|
|
197
273
|
attempts: number;
|
|
198
274
|
/** The message body (JSON-encoded + capped by the catcher). */
|
|
199
275
|
body: unknown;
|
|
200
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* `true` when this failed delivery was the message's last (its retries are
|
|
278
|
+
* exhausted) AND the queue declares a `deadLetterQueue` for it to land in.
|
|
279
|
+
* Stays `false` for a queue with no DLQ, where the broker drops the
|
|
280
|
+
* exhausted message instead — `attempts > maxRetries` with
|
|
281
|
+
* `outcome !== "ack"` is what identifies that case.
|
|
282
|
+
*
|
|
283
|
+
* Read from the `defineQueue` DECLARATION, because a consumer has no
|
|
284
|
+
* runtime API that exposes its deployed `queues.consumers[]` settings. It is
|
|
285
|
+
* true of the deployed broker only as far as Lunora's binding reconcile (run
|
|
286
|
+
* by `lunora dev`, `deploy` and `prepare`) keeps the two in step: it writes
|
|
287
|
+
* every declared tuning field onto the consumer, including onto one that
|
|
288
|
+
* already exists, and takes a field back out once `defineQueue` drops it
|
|
289
|
+
* (it records what `defineQueue` declared in `package.json`
|
|
290
|
+
* `lunora.queueTuning`). The gaps that remain: a field changed by hand to
|
|
291
|
+
* another value than the declared one is
|
|
292
|
+
* kept when the declaration drops it, so a hand-set DLQ still reads
|
|
293
|
+
* `false` here; a `--env` deploy retunes the `env.<name>` consumers but
|
|
294
|
+
* never writes their `dead_letter_queue`, which that block names itself;
|
|
295
|
+
* and a bare `wrangler deploy` over a hand-edited consumer can disagree
|
|
296
|
+
* too. Nothing on this side can see any of it.
|
|
297
|
+
*/
|
|
201
298
|
deadLettered: boolean;
|
|
202
299
|
/** Handler error message when `outcome` is `error`; absent otherwise. */
|
|
203
300
|
error?: string;
|
|
@@ -221,16 +318,25 @@ interface CapturedQueueMessage {
|
|
|
221
318
|
type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
|
|
222
319
|
interface DispatchOptions {
|
|
223
320
|
/**
|
|
224
|
-
* Optional capture sink. When set,
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
321
|
+
* Optional capture sink. When set, every message's final disposition is
|
|
322
|
+
* turned into a record and handed to this sink after the handler runs.
|
|
323
|
+
* Omitted in production unless queue capture is enabled, so a consumer pays
|
|
324
|
+
* no record-building or sink cost by default. Delivery semantics — including
|
|
325
|
+
* poison-message isolation — do not depend on it.
|
|
228
326
|
*/
|
|
229
327
|
capture?: QueueCaptureSink;
|
|
230
328
|
/** Worker `env`, forwarded to the queue run context. */
|
|
231
329
|
env: Record<string, unknown>;
|
|
232
330
|
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
233
331
|
fetchImpl?: typeof fetch;
|
|
332
|
+
/**
|
|
333
|
+
* W3C `traceparent` of the consumer invocation's own trace, supplied by the
|
|
334
|
+
* runtime's `queue()` entry. Forwarded on every `ctx.run` dispatch so the
|
|
335
|
+
* functions a handler calls are CHILDREN of the queue span instead of a set of
|
|
336
|
+
* unrelated root traces. Absent (a hand-built dispatch, a unit test) keeps the
|
|
337
|
+
* prior behaviour: the callee mints its own trace.
|
|
338
|
+
*/
|
|
339
|
+
traceparent?: string;
|
|
234
340
|
}
|
|
235
341
|
/**
|
|
236
342
|
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
@@ -268,7 +374,7 @@ declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOpti
|
|
|
268
374
|
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
269
375
|
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
270
376
|
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
271
|
-
* queue named …" error is raised lazily by `ctx.queues
|
|
377
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
272
378
|
* the missing queue is actually used.
|
|
273
379
|
*/
|
|
274
380
|
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
@@ -282,7 +388,7 @@ declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
|
282
388
|
/**
|
|
283
389
|
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
284
390
|
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
285
|
-
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`
|
|
391
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`CONTAINER_*` so a
|
|
286
392
|
* queue export can never collide with the built-in bindings.
|
|
287
393
|
*/
|
|
288
394
|
declare const queueBindingName: (exportName: string) => string;
|
|
@@ -294,7 +400,7 @@ declare const queueBindingName: (exportName: string) => string;
|
|
|
294
400
|
declare const queueDefaultName: (exportName: string) => string;
|
|
295
401
|
/**
|
|
296
402
|
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
297
|
-
* branding: codegen discovers the export, emits the typed `ctx.queues
|
|
403
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
298
404
|
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
299
405
|
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
300
406
|
* entries from the same definition.
|
|
@@ -304,10 +410,10 @@ declare const queueDefaultName: (exportName: string) => string;
|
|
|
304
410
|
* import { defineQueue } from "@lunora/queue";
|
|
305
411
|
* import { api } from "./_generated/api";
|
|
306
412
|
*
|
|
307
|
-
* export const emailQueue = defineQueue
|
|
413
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
308
414
|
* handler: async (ctx, batch) => {
|
|
309
415
|
* for (const message of batch.messages) {
|
|
310
|
-
* await
|
|
416
|
+
* await message.run(api.email.send, { to: message.body.to });
|
|
311
417
|
* message.ack();
|
|
312
418
|
* }
|
|
313
419
|
* },
|
|
@@ -316,6 +422,12 @@ declare const queueDefaultName: (exportName: string) => string;
|
|
|
316
422
|
*
|
|
317
423
|
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
318
424
|
*
|
|
425
|
+
* `message.run(...)` is `ctx.run(...)` pinned to that message — prefer it inside
|
|
426
|
+
* the batch loop. A deterministic failure (400/403/404/422) from a pinned call
|
|
427
|
+
* is attributed to its message: that one is acked and the rest are retried,
|
|
428
|
+
* instead of one poison message re-delivering (and eventually dead-lettering)
|
|
429
|
+
* the whole batch.
|
|
430
|
+
*
|
|
319
431
|
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
320
432
|
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
321
433
|
* trusted path the scheduler and workflows use), so those calls run with the
|
|
@@ -331,7 +443,29 @@ interface RunContextOptions {
|
|
|
331
443
|
env: Record<string, unknown>;
|
|
332
444
|
exportName: string;
|
|
333
445
|
fetchImpl?: typeof fetch;
|
|
446
|
+
/** The consumer invocation's `traceparent`, so `ctx.run` joins the queue's trace. */
|
|
447
|
+
traceparent?: string;
|
|
334
448
|
}
|
|
335
449
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
336
450
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
337
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Declare a topic in `lunora/queues.ts`. The payload type is the only thing a
|
|
453
|
+
* topic carries; delivery config lives on each subscription.
|
|
454
|
+
*/
|
|
455
|
+
declare const defineTopic: <Payload = unknown>() => TopicDefinition<Payload>;
|
|
456
|
+
/**
|
|
457
|
+
* Subscribe to a topic. The result is a push queue definition — export it from
|
|
458
|
+
* `lunora/queues.ts` and it deploys, retries and dead-letters exactly like a
|
|
459
|
+
* `defineQueue` export of the same name.
|
|
460
|
+
*
|
|
461
|
+
* ⚠️ Same trust model as a queue handler: `message.run(...)` dispatches with the
|
|
462
|
+
* system identity, so validate `message.body` before acting on it.
|
|
463
|
+
*/
|
|
464
|
+
declare const defineSubscription: <Payload>(topic: TopicDefinition<Payload>, config: SubscriptionConfig<Payload>) => SubscriptionDefinition<Payload>;
|
|
465
|
+
/**
|
|
466
|
+
* Build `ctx.topics` for a request from the Worker `env` and the codegen-emitted
|
|
467
|
+
* specs. A topic with no subscriptions publishes to nobody; a topic with an
|
|
468
|
+
* unbound subscription rejects every publish (see {@link unboundPublisher}).
|
|
469
|
+
*/
|
|
470
|
+
declare const createTopicContext: (env: Record<string, unknown>, specs: ReadonlyArray<TopicBindingSpec>) => Topics;
|
|
471
|
+
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, type SubscriptionConfig, type SubscriptionDefinition, type TopicBindingSpec, type TopicDefinition, type TopicPublisher, type Topics, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, createTopicContext, defineQueue, defineSubscription, defineTopic, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
|