@lunora/queue 0.0.0 → 1.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +105 -0
- package/README.md +47 -1
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.mjs +5 -0
- package/dist/packem_shared/createQueueContext-D0XCdCsd.mjs +14 -0
- package/dist/packem_shared/createQueueRunContext-DVqG7Oyk.mjs +71 -0
- package/dist/packem_shared/createQueues-14-vSICK.mjs +33 -0
- package/dist/packem_shared/defineQueue-D40gREfg.mjs +18 -0
- package/dist/packem_shared/dispatchQueueBatch-DVMmBISH.mjs +18 -0
- package/package.json +34 -4
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
*/
|
|
7
|
+
/** Opaque generated function reference (`api.foo.bar`), carrying its dispatch id. */
|
|
8
|
+
interface FunctionReference {
|
|
9
|
+
__lunoraRef: string;
|
|
10
|
+
}
|
|
11
|
+
/** Infer the args object a {@link FunctionReference} expects (loose at the boundary). */
|
|
12
|
+
type ArgsOf<F> = F extends FunctionReference ? Record<string, unknown> : never;
|
|
13
|
+
/** Options for a function call made via a dispatch runner. */
|
|
14
|
+
interface RunFunctionOptions {
|
|
15
|
+
/** Route the call to a specific shard (defaults to the worker's root shard). */
|
|
16
|
+
shardKey?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Invoke a Lunora function (query/mutation/action) by reference. The shape of `ctx.run`. */
|
|
19
|
+
type DispatchRunFunction = <F extends FunctionReference>(function_: F, args?: ArgsOf<F>, options?: RunFunctionOptions) => Promise<unknown>;
|
|
20
|
+
/** Console-style logger prefixed for log correlation, routed to wrangler tail / Studio. */
|
|
21
|
+
interface DispatchLogger {
|
|
22
|
+
debug: (message: unknown, ...rest: unknown[]) => void;
|
|
23
|
+
error: (message: unknown, ...rest: unknown[]) => void;
|
|
24
|
+
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
|
+
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
|
+
}
|
|
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
|
+
/**
|
|
83
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
84
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
85
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
86
|
+
*/
|
|
87
|
+
interface QueueProducer<Body = unknown> {
|
|
88
|
+
/** Enqueue one message. */
|
|
89
|
+
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
90
|
+
/** Enqueue a batch of messages in one call. */
|
|
91
|
+
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
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
|
+
*/
|
|
98
|
+
interface Queues {
|
|
99
|
+
[exportName: string]: QueueProducer;
|
|
100
|
+
}
|
|
101
|
+
/** Options the package-level `createQueues` factory takes. */
|
|
102
|
+
interface LunoraQueuesOptions {
|
|
103
|
+
/** Map of `lunora/queues.ts` export name → Cloudflare `Queue` producer binding. */
|
|
104
|
+
bindings: Record<string, QueueBindingLike>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
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
|
+
*/
|
|
112
|
+
interface QueueRunContext {
|
|
113
|
+
/** The worker `env` (bindings + vars). */
|
|
114
|
+
readonly env: Record<string, unknown>;
|
|
115
|
+
/** Queue-name-prefixed logger. */
|
|
116
|
+
readonly log: DispatchLogger;
|
|
117
|
+
/** Invoke a Lunora function (query/mutation/action) by reference. */
|
|
118
|
+
readonly run: DispatchRunFunction;
|
|
119
|
+
}
|
|
120
|
+
/** Whether a declared queue is consumed by this worker (push) or polled externally (pull). */
|
|
121
|
+
type QueueConsumerMode = "pull" | "push";
|
|
122
|
+
/** The handler body run for each delivered batch (push consumers only). */
|
|
123
|
+
type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: MessageBatchLike<Body>) => Promise<void> | void;
|
|
124
|
+
/** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
|
|
125
|
+
interface QueueConsumerTuning {
|
|
126
|
+
/** Name of the dead-letter queue messages land in after `maxRetries`. */
|
|
127
|
+
deadLetterQueue?: string;
|
|
128
|
+
/** Max messages per batch (1–100, Cloudflare default 10). */
|
|
129
|
+
maxBatchSize?: number;
|
|
130
|
+
/** Max seconds to wait before delivering a partial batch (0–60, default 5). */
|
|
131
|
+
maxBatchTimeout?: number;
|
|
132
|
+
/** Max delivery attempts before a message is dropped / dead-lettered (default 3). */
|
|
133
|
+
maxRetries?: number;
|
|
134
|
+
/** Delay in seconds before a failed batch is retried. */
|
|
135
|
+
retryDelay?: number;
|
|
136
|
+
}
|
|
137
|
+
/** The config object passed to `defineQueue`. */
|
|
138
|
+
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
|
+
/**
|
|
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
|
+
*/
|
|
143
|
+
handler?: QueueHandler<Body>;
|
|
144
|
+
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
|
+
mode?: QueueConsumerMode;
|
|
146
|
+
/**
|
|
147
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
148
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
149
|
+
*/
|
|
150
|
+
name?: string;
|
|
151
|
+
}
|
|
152
|
+
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
|
+
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
|
+
/**
|
|
155
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
156
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
157
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
158
|
+
*/
|
|
159
|
+
readonly __lunoraBody?: Body;
|
|
160
|
+
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
|
+
isLunoraQueue: true;
|
|
162
|
+
}
|
|
163
|
+
/** Wiring info for one declared queue, emitted by codegen into the generated shard/handler. */
|
|
164
|
+
interface QueueBindingSpec {
|
|
165
|
+
/** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
|
|
166
|
+
binding: string;
|
|
167
|
+
/** The `lunora/queues.ts` export name, e.g. `emailQueue`. */
|
|
168
|
+
exportName: string;
|
|
169
|
+
/** The stable wrangler queue name, e.g. `email-queue`. */
|
|
170
|
+
name: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
174
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
175
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
176
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
177
|
+
* the missing queue is actually used.
|
|
178
|
+
*/
|
|
179
|
+
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
180
|
+
/**
|
|
181
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
182
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
183
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
184
|
+
* queues (raised lazily on first use).
|
|
185
|
+
*/
|
|
186
|
+
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
187
|
+
/**
|
|
188
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
189
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
190
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
191
|
+
* queue export can never collide with the built-in bindings.
|
|
192
|
+
*/
|
|
193
|
+
declare const queueBindingName: (exportName: string) => string;
|
|
194
|
+
/**
|
|
195
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
196
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
197
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
198
|
+
*/
|
|
199
|
+
declare const queueDefaultName: (exportName: string) => string;
|
|
200
|
+
/**
|
|
201
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
202
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
203
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
204
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
205
|
+
* entries from the same definition.
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* // lunora/queues.ts
|
|
209
|
+
* import { defineQueue } from "@lunora/queue";
|
|
210
|
+
* import { api } from "./_generated/api";
|
|
211
|
+
*
|
|
212
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
213
|
+
* handler: async (ctx, batch) => {
|
|
214
|
+
* for (const message of batch.messages) {
|
|
215
|
+
* await ctx.run(api.email.send, { to: message.body.to });
|
|
216
|
+
* message.ack();
|
|
217
|
+
* }
|
|
218
|
+
* },
|
|
219
|
+
* });
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
223
|
+
*
|
|
224
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
225
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
226
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
227
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
228
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
229
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
230
|
+
* unchecked body straight into a privileged mutation.
|
|
231
|
+
*/
|
|
232
|
+
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
233
|
+
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
234
|
+
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
235
|
+
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
236
|
+
interface QueueRegistryEntry {
|
|
237
|
+
/**
|
|
238
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
239
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
240
|
+
* queues carry different message bodies, and the handler param is
|
|
241
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
242
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
243
|
+
* batch straight through, so the erasure is type-only.
|
|
244
|
+
*/
|
|
245
|
+
definition: QueueDefinition<any>;
|
|
246
|
+
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
247
|
+
exportName: string;
|
|
248
|
+
}
|
|
249
|
+
/** Map of stable wrangler queue name → registry entry, built by codegen. */
|
|
250
|
+
type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
251
|
+
interface DispatchOptions {
|
|
252
|
+
/** Worker `env`, forwarded to the queue run context. */
|
|
253
|
+
env: Record<string, unknown>;
|
|
254
|
+
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
255
|
+
fetchImpl?: typeof fetch;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
259
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
260
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
261
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
262
|
+
*/
|
|
263
|
+
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
264
|
+
interface RunContextOptions {
|
|
265
|
+
env: Record<string, unknown>;
|
|
266
|
+
exportName: string;
|
|
267
|
+
fetchImpl?: typeof fetch;
|
|
268
|
+
}
|
|
269
|
+
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
270
|
+
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
271
|
+
export { type ArgsOf, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueHandler, type DispatchLogger as QueueLogger, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRetryOptions, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type QueueSendBatchOptions, type QueueSendOptions, type Queues, type RunFunctionOptions, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
*/
|
|
7
|
+
/** Opaque generated function reference (`api.foo.bar`), carrying its dispatch id. */
|
|
8
|
+
interface FunctionReference {
|
|
9
|
+
__lunoraRef: string;
|
|
10
|
+
}
|
|
11
|
+
/** Infer the args object a {@link FunctionReference} expects (loose at the boundary). */
|
|
12
|
+
type ArgsOf<F> = F extends FunctionReference ? Record<string, unknown> : never;
|
|
13
|
+
/** Options for a function call made via a dispatch runner. */
|
|
14
|
+
interface RunFunctionOptions {
|
|
15
|
+
/** Route the call to a specific shard (defaults to the worker's root shard). */
|
|
16
|
+
shardKey?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Invoke a Lunora function (query/mutation/action) by reference. The shape of `ctx.run`. */
|
|
19
|
+
type DispatchRunFunction = <F extends FunctionReference>(function_: F, args?: ArgsOf<F>, options?: RunFunctionOptions) => Promise<unknown>;
|
|
20
|
+
/** Console-style logger prefixed for log correlation, routed to wrangler tail / Studio. */
|
|
21
|
+
interface DispatchLogger {
|
|
22
|
+
debug: (message: unknown, ...rest: unknown[]) => void;
|
|
23
|
+
error: (message: unknown, ...rest: unknown[]) => void;
|
|
24
|
+
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
|
+
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
|
+
}
|
|
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
|
+
/**
|
|
83
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
84
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
85
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
86
|
+
*/
|
|
87
|
+
interface QueueProducer<Body = unknown> {
|
|
88
|
+
/** Enqueue one message. */
|
|
89
|
+
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
90
|
+
/** Enqueue a batch of messages in one call. */
|
|
91
|
+
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
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
|
+
*/
|
|
98
|
+
interface Queues {
|
|
99
|
+
[exportName: string]: QueueProducer;
|
|
100
|
+
}
|
|
101
|
+
/** Options the package-level `createQueues` factory takes. */
|
|
102
|
+
interface LunoraQueuesOptions {
|
|
103
|
+
/** Map of `lunora/queues.ts` export name → Cloudflare `Queue` producer binding. */
|
|
104
|
+
bindings: Record<string, QueueBindingLike>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
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
|
+
*/
|
|
112
|
+
interface QueueRunContext {
|
|
113
|
+
/** The worker `env` (bindings + vars). */
|
|
114
|
+
readonly env: Record<string, unknown>;
|
|
115
|
+
/** Queue-name-prefixed logger. */
|
|
116
|
+
readonly log: DispatchLogger;
|
|
117
|
+
/** Invoke a Lunora function (query/mutation/action) by reference. */
|
|
118
|
+
readonly run: DispatchRunFunction;
|
|
119
|
+
}
|
|
120
|
+
/** Whether a declared queue is consumed by this worker (push) or polled externally (pull). */
|
|
121
|
+
type QueueConsumerMode = "pull" | "push";
|
|
122
|
+
/** The handler body run for each delivered batch (push consumers only). */
|
|
123
|
+
type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: MessageBatchLike<Body>) => Promise<void> | void;
|
|
124
|
+
/** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
|
|
125
|
+
interface QueueConsumerTuning {
|
|
126
|
+
/** Name of the dead-letter queue messages land in after `maxRetries`. */
|
|
127
|
+
deadLetterQueue?: string;
|
|
128
|
+
/** Max messages per batch (1–100, Cloudflare default 10). */
|
|
129
|
+
maxBatchSize?: number;
|
|
130
|
+
/** Max seconds to wait before delivering a partial batch (0–60, default 5). */
|
|
131
|
+
maxBatchTimeout?: number;
|
|
132
|
+
/** Max delivery attempts before a message is dropped / dead-lettered (default 3). */
|
|
133
|
+
maxRetries?: number;
|
|
134
|
+
/** Delay in seconds before a failed batch is retried. */
|
|
135
|
+
retryDelay?: number;
|
|
136
|
+
}
|
|
137
|
+
/** The config object passed to `defineQueue`. */
|
|
138
|
+
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
|
+
/**
|
|
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
|
+
*/
|
|
143
|
+
handler?: QueueHandler<Body>;
|
|
144
|
+
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
|
+
mode?: QueueConsumerMode;
|
|
146
|
+
/**
|
|
147
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
148
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
149
|
+
*/
|
|
150
|
+
name?: string;
|
|
151
|
+
}
|
|
152
|
+
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
|
+
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
|
+
/**
|
|
155
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
156
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
157
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
158
|
+
*/
|
|
159
|
+
readonly __lunoraBody?: Body;
|
|
160
|
+
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
|
+
isLunoraQueue: true;
|
|
162
|
+
}
|
|
163
|
+
/** Wiring info for one declared queue, emitted by codegen into the generated shard/handler. */
|
|
164
|
+
interface QueueBindingSpec {
|
|
165
|
+
/** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
|
|
166
|
+
binding: string;
|
|
167
|
+
/** The `lunora/queues.ts` export name, e.g. `emailQueue`. */
|
|
168
|
+
exportName: string;
|
|
169
|
+
/** The stable wrangler queue name, e.g. `email-queue`. */
|
|
170
|
+
name: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
174
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
175
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
176
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
177
|
+
* the missing queue is actually used.
|
|
178
|
+
*/
|
|
179
|
+
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
180
|
+
/**
|
|
181
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
182
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
183
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
184
|
+
* queues (raised lazily on first use).
|
|
185
|
+
*/
|
|
186
|
+
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
187
|
+
/**
|
|
188
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
189
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
190
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
191
|
+
* queue export can never collide with the built-in bindings.
|
|
192
|
+
*/
|
|
193
|
+
declare const queueBindingName: (exportName: string) => string;
|
|
194
|
+
/**
|
|
195
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
196
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
197
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
198
|
+
*/
|
|
199
|
+
declare const queueDefaultName: (exportName: string) => string;
|
|
200
|
+
/**
|
|
201
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
202
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
203
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
204
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
205
|
+
* entries from the same definition.
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* // lunora/queues.ts
|
|
209
|
+
* import { defineQueue } from "@lunora/queue";
|
|
210
|
+
* import { api } from "./_generated/api";
|
|
211
|
+
*
|
|
212
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
213
|
+
* handler: async (ctx, batch) => {
|
|
214
|
+
* for (const message of batch.messages) {
|
|
215
|
+
* await ctx.run(api.email.send, { to: message.body.to });
|
|
216
|
+
* message.ack();
|
|
217
|
+
* }
|
|
218
|
+
* },
|
|
219
|
+
* });
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
223
|
+
*
|
|
224
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
225
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
226
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
227
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
228
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
229
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
230
|
+
* unchecked body straight into a privileged mutation.
|
|
231
|
+
*/
|
|
232
|
+
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
233
|
+
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
234
|
+
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
235
|
+
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
236
|
+
interface QueueRegistryEntry {
|
|
237
|
+
/**
|
|
238
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
239
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
240
|
+
* queues carry different message bodies, and the handler param is
|
|
241
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
242
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
243
|
+
* batch straight through, so the erasure is type-only.
|
|
244
|
+
*/
|
|
245
|
+
definition: QueueDefinition<any>;
|
|
246
|
+
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
247
|
+
exportName: string;
|
|
248
|
+
}
|
|
249
|
+
/** Map of stable wrangler queue name → registry entry, built by codegen. */
|
|
250
|
+
type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
251
|
+
interface DispatchOptions {
|
|
252
|
+
/** Worker `env`, forwarded to the queue run context. */
|
|
253
|
+
env: Record<string, unknown>;
|
|
254
|
+
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
255
|
+
fetchImpl?: typeof fetch;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
259
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
260
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
261
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
262
|
+
*/
|
|
263
|
+
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
264
|
+
interface RunContextOptions {
|
|
265
|
+
env: Record<string, unknown>;
|
|
266
|
+
exportName: string;
|
|
267
|
+
fetchImpl?: typeof fetch;
|
|
268
|
+
}
|
|
269
|
+
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
270
|
+
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
271
|
+
export { type ArgsOf, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueHandler, type DispatchLogger as QueueLogger, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRetryOptions, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type QueueSendBatchOptions, type QueueSendOptions, type Queues, type RunFunctionOptions, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createQueueContext } from './packem_shared/createQueueContext-D0XCdCsd.mjs';
|
|
2
|
+
export { default as createQueues } from './packem_shared/createQueues-14-vSICK.mjs';
|
|
3
|
+
export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName } from './packem_shared/defineQueue-D40gREfg.mjs';
|
|
4
|
+
export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-DVMmBISH.mjs';
|
|
5
|
+
export { createQueueRunContext } from './packem_shared/createQueueRunContext-DVqG7Oyk.mjs';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import createQueues from './createQueues-14-vSICK.mjs';
|
|
2
|
+
|
|
3
|
+
const createQueueContext = (env, specs) => {
|
|
4
|
+
const bindings = {};
|
|
5
|
+
for (const spec of specs) {
|
|
6
|
+
const binding = env[spec.binding];
|
|
7
|
+
if (binding && typeof binding.send === "function" && typeof binding.sendBatch === "function") {
|
|
8
|
+
bindings[spec.exportName] = binding;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return createQueues({ bindings });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { createQueueContext };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const createDispatchLogger = (prefix) => {
|
|
2
|
+
return {
|
|
3
|
+
debug: (message, ...rest) => {
|
|
4
|
+
console.debug(prefix, message, ...rest);
|
|
5
|
+
},
|
|
6
|
+
error: (message, ...rest) => {
|
|
7
|
+
console.error(prefix, message, ...rest);
|
|
8
|
+
},
|
|
9
|
+
info: (message, ...rest) => {
|
|
10
|
+
console.info(prefix, message, ...rest);
|
|
11
|
+
},
|
|
12
|
+
warn: (message, ...rest) => {
|
|
13
|
+
console.warn(prefix, message, ...rest);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
|
|
19
|
+
const trimTrailingSlashes = (value) => {
|
|
20
|
+
let end = value.length;
|
|
21
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
22
|
+
end -= 1;
|
|
23
|
+
}
|
|
24
|
+
return value.slice(0, end);
|
|
25
|
+
};
|
|
26
|
+
const createDispatchRunner = (options) => {
|
|
27
|
+
const { label } = options;
|
|
28
|
+
const globalFetch = globalThis.fetch;
|
|
29
|
+
const fetchImpl = options.fetchImpl ?? (typeof globalFetch === "function" ? globalFetch.bind(globalThis) : void 0);
|
|
30
|
+
return async (function_, args, runOptions = {}) => {
|
|
31
|
+
if (typeof fetchImpl !== "function") {
|
|
32
|
+
throw new TypeError(`${label}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);
|
|
33
|
+
}
|
|
34
|
+
const origin = options.env.LUNORA_ORIGIN_URL;
|
|
35
|
+
if (typeof origin !== "string" || origin.length === 0) {
|
|
36
|
+
throw new Error(`${label}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);
|
|
37
|
+
}
|
|
38
|
+
const token = options.env.LUNORA_ADMIN_TOKEN;
|
|
39
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
40
|
+
throw new Error(`${label}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);
|
|
41
|
+
}
|
|
42
|
+
const url = `${trimTrailingSlashes(origin)}${SCHEDULER_DISPATCH_PATH}`;
|
|
43
|
+
const response = await fetchImpl(url, {
|
|
44
|
+
body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
|
|
45
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
46
|
+
method: "POST"
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`${label}: function dispatch failed (${String(response.status)}): ${await response.text()}`);
|
|
50
|
+
}
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
if (text.length === 0) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(text);
|
|
57
|
+
} catch {
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const createQueueRunContext = (options) => {
|
|
64
|
+
return {
|
|
65
|
+
env: options.env,
|
|
66
|
+
log: createDispatchLogger(`[queue:${options.exportName}]`),
|
|
67
|
+
run: createDispatchRunner({ env: options.env, fetchImpl: options.fetchImpl, label: "@lunora/queue" })
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { createQueueRunContext };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const producerFor = (binding) => {
|
|
2
|
+
return {
|
|
3
|
+
send: async (body, options) => {
|
|
4
|
+
await binding.send(body, options);
|
|
5
|
+
},
|
|
6
|
+
sendBatch: async (messages, options) => {
|
|
7
|
+
await binding.sendBatch(messages, options);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
const createQueues = (options) => {
|
|
12
|
+
const bindings = options.bindings ?? {};
|
|
13
|
+
const producers = /* @__PURE__ */ Object.create(null);
|
|
14
|
+
for (const [exportName, binding] of Object.entries(bindings)) {
|
|
15
|
+
producers[exportName] = producerFor(binding);
|
|
16
|
+
}
|
|
17
|
+
const known = Object.keys(producers);
|
|
18
|
+
const missing = (name) => {
|
|
19
|
+
const suffix = known.length === 0 ? "no queues are declared" : `known queues: ${known.join(", ")}`;
|
|
20
|
+
const error = () => Promise.reject(new Error(`@lunora/queue: no queue named "${name}" (${suffix})`));
|
|
21
|
+
return { send: error, sendBatch: error };
|
|
22
|
+
};
|
|
23
|
+
return /* @__PURE__ */ new Proxy(producers, {
|
|
24
|
+
get(target, property) {
|
|
25
|
+
if (typeof property !== "string") {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
return Object.hasOwn(target, property) ? target[property] : missing(property);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { createQueues as default };
|