@lunora/queue 1.0.0-alpha.2 → 1.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/dist/index.d.mts +173 -159
- package/dist/index.d.ts +173 -159
- package/dist/index.mjs +1 -5
- package/dist/packem_shared/createQueueCaptureSink-saac8Ywl.mjs +1 -0
- package/dist/packem_shared/createQueueContext-jtkVvBM-.mjs +1 -0
- package/dist/packem_shared/createQueueRunContext-Dg_fCCaC.mjs +1 -0
- package/dist/packem_shared/createQueues-CrQQIHtU.mjs +1 -0
- package/dist/packem_shared/defineQueue-DJYVNfQY.mjs +1 -0
- package/dist/packem_shared/dispatchQueueBatch-BYb3OFCX.mjs +1 -0
- package/package.json +5 -1
- package/dist/packem_shared/createQueueContext-D0XCdCsd.mjs +0 -14
- package/dist/packem_shared/createQueueRunContext-DVqG7Oyk.mjs +0 -71
- package/dist/packem_shared/createQueues-14-vSICK.mjs +0 -33
- package/dist/packem_shared/defineQueue-D40gREfg.mjs +0 -18
- package/dist/packem_shared/dispatchQueueBatch-DVMmBISH.mjs +0 -18
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { QueueBindingLike, MessageBatchLike, 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;
|
|
@@ -24,66 +26,11 @@ interface DispatchLogger {
|
|
|
24
26
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
27
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
28
|
}
|
|
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
29
|
/**
|
|
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
|
-
*/
|
|
30
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
31
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
32
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
33
|
+
*/
|
|
87
34
|
interface QueueProducer<Body = unknown> {
|
|
88
35
|
/** Enqueue one message. */
|
|
89
36
|
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
@@ -91,10 +38,10 @@ interface QueueProducer<Body = unknown> {
|
|
|
91
38
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
39
|
}
|
|
93
40
|
/**
|
|
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
|
-
*/
|
|
41
|
+
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
42
|
+
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
43
|
+
* `createQueues` stays schema-agnostic.
|
|
44
|
+
*/
|
|
98
45
|
interface Queues {
|
|
99
46
|
[exportName: string]: QueueProducer;
|
|
100
47
|
}
|
|
@@ -104,11 +51,11 @@ interface LunoraQueuesOptions {
|
|
|
104
51
|
bindings: Record<string, QueueBindingLike>;
|
|
105
52
|
}
|
|
106
53
|
/**
|
|
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
|
-
*/
|
|
54
|
+
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
55
|
+
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
56
|
+
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
57
|
+
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
58
|
+
*/
|
|
112
59
|
interface QueueRunContext {
|
|
113
60
|
/** The worker `env` (bindings + vars). */
|
|
114
61
|
readonly env: Record<string, unknown>;
|
|
@@ -137,25 +84,25 @@ interface QueueConsumerTuning {
|
|
|
137
84
|
/** The config object passed to `defineQueue`. */
|
|
138
85
|
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
86
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
87
|
+
* The push-consumer body. Required for `mode: "push"` (the default); omit it
|
|
88
|
+
* for `mode: "pull"`, where an external worker polls the queue over HTTP.
|
|
89
|
+
*/
|
|
143
90
|
handler?: QueueHandler<Body>;
|
|
144
91
|
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
92
|
mode?: QueueConsumerMode;
|
|
146
93
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
94
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
95
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
96
|
+
*/
|
|
150
97
|
name?: string;
|
|
151
98
|
}
|
|
152
99
|
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
100
|
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
101
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
102
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
103
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
104
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
105
|
+
*/
|
|
159
106
|
readonly __lunoraBody?: Body;
|
|
160
107
|
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
108
|
isLunoraQueue: true;
|
|
@@ -169,98 +116,165 @@ interface QueueBindingSpec {
|
|
|
169
116
|
/** The stable wrangler queue name, e.g. `email-queue`. */
|
|
170
117
|
name: string;
|
|
171
118
|
}
|
|
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
119
|
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
236
120
|
interface QueueRegistryEntry {
|
|
237
121
|
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
122
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
123
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
124
|
+
* queues carry different message bodies, and the handler param is
|
|
125
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
126
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
127
|
+
* batch straight through, so the erasure is type-only.
|
|
128
|
+
*/
|
|
245
129
|
definition: QueueDefinition<any>;
|
|
246
130
|
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
247
131
|
exportName: string;
|
|
248
132
|
}
|
|
249
133
|
/** Map of stable wrangler queue name → registry entry, built by codegen. */
|
|
250
134
|
type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
135
|
+
/** The disposition a consumer left one message in for a single delivery attempt. */
|
|
136
|
+
type QueueMessageOutcome = "ack" | "error" | "retry";
|
|
137
|
+
/**
|
|
138
|
+
* One consumed message as captured by {@link dispatchQueueBatch} and handed to an
|
|
139
|
+
* {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
|
|
140
|
+
* `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
|
|
141
|
+
* the two packages share only this contract, so keep them in sync by hand.
|
|
142
|
+
*/
|
|
143
|
+
interface CapturedQueueMessage {
|
|
144
|
+
/** Delivery attempt number for this message (`message.attempts`). */
|
|
145
|
+
attempts: number;
|
|
146
|
+
/** The message body (JSON-encoded + capped by the catcher). */
|
|
147
|
+
body: unknown;
|
|
148
|
+
/** `true` when this failed delivery was the message's last (its retries are exhausted — the broker dead-letters it). */
|
|
149
|
+
deadLettered: boolean;
|
|
150
|
+
/** Handler error message when `outcome` is `error`; absent otherwise. */
|
|
151
|
+
error?: string;
|
|
152
|
+
/** The `lunora/queues.ts` export name that consumed it. */
|
|
153
|
+
exportName: string;
|
|
154
|
+
/** The delivered message id (`message.id`). */
|
|
155
|
+
messageId: string;
|
|
156
|
+
/** How the handler disposed of the message this attempt. */
|
|
157
|
+
outcome: QueueMessageOutcome;
|
|
158
|
+
/** The stable wrangler queue name the batch was delivered from (`batch.queue`). */
|
|
159
|
+
queue: string;
|
|
160
|
+
/** Original message timestamp in epoch-ms (`message.timestamp`). */
|
|
161
|
+
timestamp: number;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Persists a batch of consumed messages. The codegen worker wires this to POST the
|
|
165
|
+
* batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
|
|
166
|
+
* Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
|
|
167
|
+
* capture failure never changes delivery semantics.
|
|
168
|
+
*/
|
|
169
|
+
type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
|
|
251
170
|
interface DispatchOptions {
|
|
171
|
+
/**
|
|
172
|
+
* Optional capture sink. When set, the batch is instrumented and every
|
|
173
|
+
* message's final disposition is recorded and handed to this sink after the
|
|
174
|
+
* handler runs. Omitted in production unless queue capture is enabled, so a
|
|
175
|
+
* consumer pays no instrumentation cost by default.
|
|
176
|
+
*/
|
|
177
|
+
capture?: QueueCaptureSink;
|
|
252
178
|
/** Worker `env`, forwarded to the queue run context. */
|
|
253
179
|
env: Record<string, unknown>;
|
|
254
180
|
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
255
181
|
fetchImpl?: typeof fetch;
|
|
256
182
|
}
|
|
257
183
|
/**
|
|
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
|
-
*/
|
|
184
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
185
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
186
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
187
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
188
|
+
*/
|
|
263
189
|
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
190
|
+
/** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
|
|
191
|
+
type QueueEnv = Record<string, unknown>;
|
|
192
|
+
/** Options for {@link createQueueCaptureSink}. */
|
|
193
|
+
interface QueueCaptureOptions {
|
|
194
|
+
/** Pin the consumed-message log to a Cloudflare data-residency jurisdiction (match the worker's `jurisdiction`). */
|
|
195
|
+
jurisdiction?: string;
|
|
196
|
+
/** Shard the consumed-message log lives on; override if the worker sets a custom `defaultShardKey`. */
|
|
197
|
+
rootShard?: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether consumed queue messages should be captured into the studio's log.
|
|
201
|
+
* Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
|
|
202
|
+
* unset, capture is on only in a development environment. Mirrors
|
|
203
|
+
* `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
|
|
204
|
+
* same way.
|
|
205
|
+
*/
|
|
206
|
+
declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Build the {@link QueueCaptureSink} that records a processed batch into the
|
|
209
|
+
* studio's root-shard consumed-message log via the reserved `recordQueueMessage`
|
|
210
|
+
* admin RPC. Best-effort by contract: without the `SHARD` binding or
|
|
211
|
+
* `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
|
|
212
|
+
* rejection, so capture never changes delivery semantics.
|
|
213
|
+
*/
|
|
214
|
+
declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
|
|
215
|
+
/**
|
|
216
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
217
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
218
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
219
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
220
|
+
* the missing queue is actually used.
|
|
221
|
+
*/
|
|
222
|
+
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
223
|
+
/**
|
|
224
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
225
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
226
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
227
|
+
* queues (raised lazily on first use).
|
|
228
|
+
*/
|
|
229
|
+
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
230
|
+
/**
|
|
231
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
232
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
233
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
234
|
+
* queue export can never collide with the built-in bindings.
|
|
235
|
+
*/
|
|
236
|
+
declare const queueBindingName: (exportName: string) => string;
|
|
237
|
+
/**
|
|
238
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
239
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
240
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
241
|
+
*/
|
|
242
|
+
declare const queueDefaultName: (exportName: string) => string;
|
|
243
|
+
/**
|
|
244
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
245
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
246
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
247
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
248
|
+
* entries from the same definition.
|
|
249
|
+
*
|
|
250
|
+
* ```ts
|
|
251
|
+
* // lunora/queues.ts
|
|
252
|
+
* import { defineQueue } from "@lunora/queue";
|
|
253
|
+
* import { api } from "./_generated/api";
|
|
254
|
+
*
|
|
255
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
256
|
+
* handler: async (ctx, batch) => {
|
|
257
|
+
* for (const message of batch.messages) {
|
|
258
|
+
* await ctx.run(api.email.send, { to: message.body.to });
|
|
259
|
+
* message.ack();
|
|
260
|
+
* }
|
|
261
|
+
* },
|
|
262
|
+
* });
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
266
|
+
*
|
|
267
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
268
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
269
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
270
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
271
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
272
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
273
|
+
* unchecked body straight into a privileged mutation.
|
|
274
|
+
*/
|
|
275
|
+
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
276
|
+
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
277
|
+
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
264
278
|
interface RunContextOptions {
|
|
265
279
|
env: Record<string, unknown>;
|
|
266
280
|
exportName: string;
|
|
@@ -268,4 +282,4 @@ interface RunContextOptions {
|
|
|
268
282
|
}
|
|
269
283
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
270
284
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
271
|
-
export { type ArgsOf, type
|
|
285
|
+
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 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { QueueBindingLike, MessageBatchLike, 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;
|
|
@@ -24,66 +26,11 @@ interface DispatchLogger {
|
|
|
24
26
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
27
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
28
|
}
|
|
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
29
|
/**
|
|
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
|
-
*/
|
|
30
|
+
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
31
|
+
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
32
|
+
* the deterministic `QueryCtx`), mirroring `ctx.scheduler` / `ctx.workflows`.
|
|
33
|
+
*/
|
|
87
34
|
interface QueueProducer<Body = unknown> {
|
|
88
35
|
/** Enqueue one message. */
|
|
89
36
|
send: (body: Body, options?: QueueSendOptions) => Promise<void>;
|
|
@@ -91,10 +38,10 @@ interface QueueProducer<Body = unknown> {
|
|
|
91
38
|
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<void>;
|
|
92
39
|
}
|
|
93
40
|
/**
|
|
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
|
-
*/
|
|
41
|
+
* `ctx.queues` — the map of declared queue export names → typed producers.
|
|
42
|
+
* Codegen narrows this to the exact export names; the package keeps it open so
|
|
43
|
+
* `createQueues` stays schema-agnostic.
|
|
44
|
+
*/
|
|
98
45
|
interface Queues {
|
|
99
46
|
[exportName: string]: QueueProducer;
|
|
100
47
|
}
|
|
@@ -104,11 +51,11 @@ interface LunoraQueuesOptions {
|
|
|
104
51
|
bindings: Record<string, QueueBindingLike>;
|
|
105
52
|
}
|
|
106
53
|
/**
|
|
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
|
-
*/
|
|
54
|
+
* The context handed to a `defineQueue` handler. Decoupled from `@lunora/server`
|
|
55
|
+
* (like the workflow run context): to touch data, call a Lunora mutation/action
|
|
56
|
+
* via `ctx.run(api.x.y, args)` — the dispatch goes through the same
|
|
57
|
+
* `/_lunora/scheduler/dispatch` path the SchedulerDO and workflows use.
|
|
58
|
+
*/
|
|
112
59
|
interface QueueRunContext {
|
|
113
60
|
/** The worker `env` (bindings + vars). */
|
|
114
61
|
readonly env: Record<string, unknown>;
|
|
@@ -137,25 +84,25 @@ interface QueueConsumerTuning {
|
|
|
137
84
|
/** The config object passed to `defineQueue`. */
|
|
138
85
|
interface QueueConfig<Body = unknown> extends QueueConsumerTuning {
|
|
139
86
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
87
|
+
* The push-consumer body. Required for `mode: "push"` (the default); omit it
|
|
88
|
+
* for `mode: "pull"`, where an external worker polls the queue over HTTP.
|
|
89
|
+
*/
|
|
143
90
|
handler?: QueueHandler<Body>;
|
|
144
91
|
/** How this queue is consumed. Defaults to `"push"`. */
|
|
145
92
|
mode?: QueueConsumerMode;
|
|
146
93
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
94
|
+
* Stable wrangler queue name (`queues.producers[].queue`). Defaults to the
|
|
95
|
+
* kebab-cased export name (`emailQueue` → `email-queue`).
|
|
96
|
+
*/
|
|
150
97
|
name?: string;
|
|
151
98
|
}
|
|
152
99
|
/** The branded result of `defineQueue`, discovered by codegen + config. */
|
|
153
100
|
interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
|
|
154
101
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
102
|
+
* Phantom carrier for the message body type, so codegen can type the
|
|
103
|
+
* generated `ctx.queues.<name>` producer as `QueueProducer<Body>` from
|
|
104
|
+
* `typeof <export>`. Never assigned at runtime (type-only).
|
|
105
|
+
*/
|
|
159
106
|
readonly __lunoraBody?: Body;
|
|
160
107
|
/** Runtime brand identifying a `defineQueue` result. */
|
|
161
108
|
isLunoraQueue: true;
|
|
@@ -169,98 +116,165 @@ interface QueueBindingSpec {
|
|
|
169
116
|
/** The stable wrangler queue name, e.g. `email-queue`. */
|
|
170
117
|
name: string;
|
|
171
118
|
}
|
|
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
119
|
/** One declared queue, keyed for batch routing by its stable wrangler name. */
|
|
236
120
|
interface QueueRegistryEntry {
|
|
237
121
|
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
122
|
+
* The `defineQueue` result (carries the push handler). The body type is
|
|
123
|
+
* erased to `any` here because the registry is heterogeneous — different
|
|
124
|
+
* queues carry different message bodies, and the handler param is
|
|
125
|
+
* contravariant, so a precise `QueueDefinition<Body>` would not be assignable
|
|
126
|
+
* to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
|
|
127
|
+
* batch straight through, so the erasure is type-only.
|
|
128
|
+
*/
|
|
245
129
|
definition: QueueDefinition<any>;
|
|
246
130
|
/** The `lunora/queues.ts` export name, for log correlation. */
|
|
247
131
|
exportName: string;
|
|
248
132
|
}
|
|
249
133
|
/** Map of stable wrangler queue name → registry entry, built by codegen. */
|
|
250
134
|
type QueueRegistry = Record<string, QueueRegistryEntry>;
|
|
135
|
+
/** The disposition a consumer left one message in for a single delivery attempt. */
|
|
136
|
+
type QueueMessageOutcome = "ack" | "error" | "retry";
|
|
137
|
+
/**
|
|
138
|
+
* One consumed message as captured by {@link dispatchQueueBatch} and handed to an
|
|
139
|
+
* {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
|
|
140
|
+
* `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
|
|
141
|
+
* the two packages share only this contract, so keep them in sync by hand.
|
|
142
|
+
*/
|
|
143
|
+
interface CapturedQueueMessage {
|
|
144
|
+
/** Delivery attempt number for this message (`message.attempts`). */
|
|
145
|
+
attempts: number;
|
|
146
|
+
/** The message body (JSON-encoded + capped by the catcher). */
|
|
147
|
+
body: unknown;
|
|
148
|
+
/** `true` when this failed delivery was the message's last (its retries are exhausted — the broker dead-letters it). */
|
|
149
|
+
deadLettered: boolean;
|
|
150
|
+
/** Handler error message when `outcome` is `error`; absent otherwise. */
|
|
151
|
+
error?: string;
|
|
152
|
+
/** The `lunora/queues.ts` export name that consumed it. */
|
|
153
|
+
exportName: string;
|
|
154
|
+
/** The delivered message id (`message.id`). */
|
|
155
|
+
messageId: string;
|
|
156
|
+
/** How the handler disposed of the message this attempt. */
|
|
157
|
+
outcome: QueueMessageOutcome;
|
|
158
|
+
/** The stable wrangler queue name the batch was delivered from (`batch.queue`). */
|
|
159
|
+
queue: string;
|
|
160
|
+
/** Original message timestamp in epoch-ms (`message.timestamp`). */
|
|
161
|
+
timestamp: number;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Persists a batch of consumed messages. The codegen worker wires this to POST the
|
|
165
|
+
* batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
|
|
166
|
+
* Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
|
|
167
|
+
* capture failure never changes delivery semantics.
|
|
168
|
+
*/
|
|
169
|
+
type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
|
|
251
170
|
interface DispatchOptions {
|
|
171
|
+
/**
|
|
172
|
+
* Optional capture sink. When set, the batch is instrumented and every
|
|
173
|
+
* message's final disposition is recorded and handed to this sink after the
|
|
174
|
+
* handler runs. Omitted in production unless queue capture is enabled, so a
|
|
175
|
+
* consumer pays no instrumentation cost by default.
|
|
176
|
+
*/
|
|
177
|
+
capture?: QueueCaptureSink;
|
|
252
178
|
/** Worker `env`, forwarded to the queue run context. */
|
|
253
179
|
env: Record<string, unknown>;
|
|
254
180
|
/** Injectable fetch for the `ctx.run` dispatcher (tests). */
|
|
255
181
|
fetchImpl?: typeof fetch;
|
|
256
182
|
}
|
|
257
183
|
/**
|
|
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
|
-
*/
|
|
184
|
+
* Look up the handler for `batch.queue` and invoke it with a fresh
|
|
185
|
+
* `QueueRunContext`. Throws a directed error when no push handler is registered
|
|
186
|
+
* for the delivered queue (a misconfiguration — the consumer was declared
|
|
187
|
+
* `pull`, or the queue name drifted from the `defineQueue` export).
|
|
188
|
+
*/
|
|
263
189
|
declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
|
|
190
|
+
/** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
|
|
191
|
+
type QueueEnv = Record<string, unknown>;
|
|
192
|
+
/** Options for {@link createQueueCaptureSink}. */
|
|
193
|
+
interface QueueCaptureOptions {
|
|
194
|
+
/** Pin the consumed-message log to a Cloudflare data-residency jurisdiction (match the worker's `jurisdiction`). */
|
|
195
|
+
jurisdiction?: string;
|
|
196
|
+
/** Shard the consumed-message log lives on; override if the worker sets a custom `defaultShardKey`. */
|
|
197
|
+
rootShard?: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether consumed queue messages should be captured into the studio's log.
|
|
201
|
+
* Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
|
|
202
|
+
* unset, capture is on only in a development environment. Mirrors
|
|
203
|
+
* `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
|
|
204
|
+
* same way.
|
|
205
|
+
*/
|
|
206
|
+
declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Build the {@link QueueCaptureSink} that records a processed batch into the
|
|
209
|
+
* studio's root-shard consumed-message log via the reserved `recordQueueMessage`
|
|
210
|
+
* admin RPC. Best-effort by contract: without the `SHARD` binding or
|
|
211
|
+
* `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
|
|
212
|
+
* rejection, so capture never changes delivery semantics.
|
|
213
|
+
*/
|
|
214
|
+
declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
|
|
215
|
+
/**
|
|
216
|
+
* Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
|
|
217
|
+
* into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
|
|
218
|
+
* A spec whose binding is absent from `env` is skipped here — the helpful "no
|
|
219
|
+
* queue named …" error is raised lazily by `ctx.queues.<name>.send(...)` when
|
|
220
|
+
* the missing queue is actually used.
|
|
221
|
+
*/
|
|
222
|
+
declare const createQueueContext: (env: Record<string, unknown>, specs: ReadonlyArray<QueueBindingSpec>) => Queues;
|
|
223
|
+
/**
|
|
224
|
+
* Build the `ctx.queues` map from `lunora/queues.ts` export name → Cloudflare
|
|
225
|
+
* `Queue` binding. Each property is a typed {@link QueueProducer}; accessing an
|
|
226
|
+
* export whose binding is absent throws a directed error naming the declared
|
|
227
|
+
* queues (raised lazily on first use).
|
|
228
|
+
*/
|
|
229
|
+
declare const createQueues: (options: LunoraQueuesOptions) => Queues;
|
|
230
|
+
/**
|
|
231
|
+
* The wrangler producer binding name for a queue export: `emailQueue` →
|
|
232
|
+
* `QUEUE_EMAIL_QUEUE`, `email` → `QUEUE_EMAIL`. The `QUEUE_` prefix namespaces
|
|
233
|
+
* these away from `SHARD`/`SESSION`/`SCHEDULER`/`WORKFLOW_*`/`CONTAINER_*` so a
|
|
234
|
+
* queue export can never collide with the built-in bindings.
|
|
235
|
+
*/
|
|
236
|
+
declare const queueBindingName: (exportName: string) => string;
|
|
237
|
+
/**
|
|
238
|
+
* The stable queue name wrangler registers (`queues.producers[].queue` and
|
|
239
|
+
* `queues.consumers[].queue`): `emailQueue` → `email-queue`. Used as the
|
|
240
|
+
* deployed queue's identifier when no explicit `name` override is given.
|
|
241
|
+
*/
|
|
242
|
+
declare const queueDefaultName: (exportName: string) => string;
|
|
243
|
+
/**
|
|
244
|
+
* Declare a Cloudflare Queue deployed alongside the app. Pure validation +
|
|
245
|
+
* branding: codegen discovers the export, emits the typed `ctx.queues.<name>`
|
|
246
|
+
* producer and (for push consumers) the worker `queue()` dispatch; the config
|
|
247
|
+
* layer reconciles the wrangler `queues.producers[]` / `queues.consumers[]`
|
|
248
|
+
* entries from the same definition.
|
|
249
|
+
*
|
|
250
|
+
* ```ts
|
|
251
|
+
* // lunora/queues.ts
|
|
252
|
+
* import { defineQueue } from "@lunora/queue";
|
|
253
|
+
* import { api } from "./_generated/api";
|
|
254
|
+
*
|
|
255
|
+
* export const emailQueue = defineQueue<{ to: string }>({
|
|
256
|
+
* handler: async (ctx, batch) => {
|
|
257
|
+
* for (const message of batch.messages) {
|
|
258
|
+
* await ctx.run(api.email.send, { to: message.body.to });
|
|
259
|
+
* message.ack();
|
|
260
|
+
* }
|
|
261
|
+
* },
|
|
262
|
+
* });
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
|
|
266
|
+
*
|
|
267
|
+
* ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
|
|
268
|
+
* Lunora functions over the admin-authenticated dispatch endpoint (the same
|
|
269
|
+
* trusted path the scheduler and workflows use), so those calls run with the
|
|
270
|
+
* system identity — **end-user RLS is not applied**. Treat a queue handler as
|
|
271
|
+
* trusted server code: validate `message.body` (it may be attacker-influenced if
|
|
272
|
+
* anything user-facing can enqueue) before acting on it, and don't forward an
|
|
273
|
+
* unchecked body straight into a privileged mutation.
|
|
274
|
+
*/
|
|
275
|
+
declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
|
|
276
|
+
/** True when a value is a `defineQueue` result (the runtime brand check). */
|
|
277
|
+
declare const isQueueDefinition: (value: unknown) => value is QueueDefinition;
|
|
264
278
|
interface RunContextOptions {
|
|
265
279
|
env: Record<string, unknown>;
|
|
266
280
|
exportName: string;
|
|
@@ -268,4 +282,4 @@ interface RunContextOptions {
|
|
|
268
282
|
}
|
|
269
283
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
270
284
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
271
|
-
export { type ArgsOf, type
|
|
285
|
+
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 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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1 @@
|
|
|
1
|
-
|
|
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';
|
|
1
|
+
import{createQueueCaptureSink as t,shouldCaptureQueue as r}from"./packem_shared/createQueueCaptureSink-saac8Ywl.mjs";import{createQueueContext as a}from"./packem_shared/createQueueContext-jtkVvBM-.mjs";import{default as i}from"./packem_shared/createQueues-CrQQIHtU.mjs";import{defineQueue as p,isQueueDefinition as m,queueBindingName as x,queueDefaultName as Q}from"./packem_shared/defineQueue-DJYVNfQY.mjs";import{dispatchQueueBatch as d}from"./packem_shared/dispatchQueueBatch-BYb3OFCX.mjs";import{createQueueRunContext as C}from"./packem_shared/createQueueRunContext-Dg_fCCaC.mjs";export{t as createQueueCaptureSink,a as createQueueContext,C as createQueueRunContext,i as createQueues,p as defineQueue,d as dispatchQueueBatch,m as isQueueDefinition,x as queueBindingName,Q as queueDefaultName,r as shouldCaptureQueue};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as p}from"@lunora/errors";const N="__lunora_admin__:recordQueueMessage",_="__root__",f=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,h=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],E=5e3,y=e=>{const t=e.LUNORA_QUEUE_CAPTURE;return typeof t=="string"?t==="1"||t.toLowerCase()==="true":h.some(n=>{const o=e[n];return typeof o=="string"&&f.test(o)})},g=(e,t={})=>{const n=t.rootShard??_;return async o=>{if(o.length===0)return;const i=e.SHARD,a=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(i===void 0||a===void 0)return;let s=i;if(t.jurisdiction!==void 0){if(typeof i.jurisdiction!="function")throw new TypeError(`@lunora/queue: Durable Object namespace does not support jurisdiction("${t.jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`);s=i.jurisdiction(t.jurisdiction)}const d=s.get(s.idFromName(n)),u=new AbortController,l=setTimeout(()=>{u.abort()},E);try{const r=await d.fetch("https://shard.internal/rpc",{body:JSON.stringify({args:{messages:o},functionPath:N}),headers:{authorization:`Bearer ${a}`,"content-type":"application/json"},method:"POST",signal:u.signal});if(!r.ok){const c=await r.text().catch(()=>"");throw new p("INTERNAL",`@lunora/queue: capture write to the root shard failed (${String(r.status)} ${r.statusText})${c===""?"":`: ${c}`}`)}await r.body?.cancel()}finally{clearTimeout(l)}}};export{g as createQueueCaptureSink,y as shouldCaptureQueue};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import i from"./createQueues-CrQQIHtU.mjs";const f=(e,c)=>{const t={};for(const o of c){const n=e[o.binding];n&&typeof n.send=="function"&&typeof n.sendBatch=="function"&&(t[o.exportName]=n)}return i({bindings:t})};export{f as createQueueContext};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";const g=e=>({debug:(t,...n)=>{console.debug(e,t,...n)},error:(t,...n)=>{console.error(e,t,...n)},info:(t,...n)=>{console.info(e,t,...n)},warn:(t,...n)=>{console.warn(e,t,...n)}}),w=e=>{let t="";for(let n=0;n<e.length;n+=32768)t+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(t)},p=e=>w(e).replaceAll("+","-").replaceAll("/","_").replace(/=+$/,"");new TextDecoder;const y=new TextEncoder,d="=",b=e=>{for(let t=0;t<e.length;t+=1)if(e.charCodeAt(t)>255)return!1;return!0},m=e=>p(y.encode(JSON.stringify(e))),I=e=>!e.startsWith(d)&&b(e)?e:`${d}${p(y.encode(e))}`,R="/_lunora/scheduler/dispatch",A=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)},O=(e,t,n)=>{try{const o=JSON.parse(n)?.error;if(typeof o=="object"&&o!==null&&typeof o.code=="string"){const{code:i,data:c,message:s}=o;return new a(i,typeof s=="string"?s:void 0,{data:c,status:t})}}catch{}return new a("INTERNAL",`${e}: function dispatch failed (${String(t)}): ${n}`,{status:t})},$=e=>{const{label:t}=e,n=globalThis.fetch,o=e.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(i,c,s={})=>{if(typeof o!="function")throw new TypeError(`${t}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const l=e.env.LUNORA_ORIGIN_URL;if(typeof l!="string"||l.length===0)throw new a("INTERNAL",`${t}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const u=e.env.LUNORA_ADMIN_TOKEN;if(typeof u!="string"||u.length===0)throw new a("INTERNAL",`${t}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const N=`${A(l)}${R}`,f={authorization:`Bearer ${u}`,"content-type":"application/json"};e.identity?.userId!==void 0&&(f["x-lunora-userid"]=I(e.identity.userId)),e.identity?.claims!==void 0&&(f["x-lunora-identity"]=m(e.identity.claims));const r=await o(N,{body:JSON.stringify({args:c??{},functionPath:i.__lunoraRef,shardKey:s.shardKey}),headers:f,method:"POST"});if(!r.ok)throw O(t,r.status,await r.text());const h=await r.text();if(h.length!==0)try{return JSON.parse(h)}catch{throw new a("INTERNAL",`${t}: function dispatch returned a non-JSON body (${String(r.status)}): ${h}`,{status:r.status})}}},L=e=>({env:e.env,log:g(`[queue:${e.exportName}]`),run:$({env:e.env,fetchImpl:e.fetchImpl,label:"@lunora/queue"})});export{L as createQueueRunContext};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const u=o=>({send:async(s,n)=>{await o.send(s,n)},sendBatch:async(s,n)=>{const t=[...s];if(t.length>100)throw new Error(`@lunora/queue: sendBatch exceeds ${String(100)} (got ${String(t.length)}) — split across calls`);await o.sendBatch(t,n)}}),d=o=>{const s=o.bindings??{},n=Object.create(null);for(const[r,e]of Object.entries(s))n[r]=u(e);const t=Object.keys(n),a=r=>{const e=t.length===0?"no queues are declared":`known queues: ${t.join(", ")}`,c=()=>Promise.reject(new Error(`@lunora/queue: no queue named "${r}" (${e})`));return{send:c,sendBatch:c}};return new Proxy(n,{get(r,e){if(typeof e=="string")return Object.hasOwn(r,e)?r[e]:a(e)}})};export{d as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=e=>`QUEUE_${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"_").toUpperCase()}`,o=e=>e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"-").toLowerCase(),r=e=>{const u=e.mode??"push";if(u!=="push"&&u!=="pull")throw new TypeError(`defineQueue: \`mode\` must be "push" or "pull" (got ${JSON.stringify(e.mode)})`);if(u==="push"&&typeof e.handler!="function")throw new TypeError('defineQueue: `handler` must be a function for a push consumer (omit it only when `mode: "pull"`)');if(e.name!==void 0&&(typeof e.name!="string"||e.name.length===0))throw new TypeError("defineQueue: `name` must be a non-empty string when provided");return{...e,isLunoraQueue:!0,mode:u}},t=e=>typeof e=="object"&&e!==null&&e.isLunoraQueue===!0;export{r as defineQueue,t as isQueueDefinition,n as queueBindingName,o as queueDefaultName};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as l}from"@lunora/errors";import{createQueueRunContext as f}from"./createQueueRunContext-Dg_fCCaC.mjs";const m=3,d=e=>{if(e instanceof Date)return e.getTime();const t=typeof e=="number"?e:Number(e);return Number.isFinite(t)?t:0},h=(e,t)=>new Proxy(e,{get:(r,a)=>a==="ack"?()=>{t.set(r,"ack"),r.ack()}:a==="retry"?o=>{t.set(r,"retry"),r.retry(o)}:Reflect.get(r,a,r)}),y=(e,t,r)=>new Proxy(e,{get:(a,o)=>o==="ackAll"?()=>{r("ack"),a.ackAll()}:o==="retryAll"?i=>{r("retry"),a.retryAll(i)}:o==="messages"?t:Reflect.get(a,o,a)}),g=e=>{const t=new Map,r=e.messages,a=r.map(i=>h(i,t)),o=y(e,a,i=>{for(const s of r)t.has(s)||t.set(s,i)});return{dispositions:t,originals:r,wrappedBatch:o}},w=e=>{if(e instanceof Error)return e.message;if(typeof e=="string")return e;if(e!==null&&typeof e=="object")try{return JSON.stringify(e)}catch{return"[unserializable thrown value]"}return String(e)},b=(e,t,r,a,o)=>{const i=a?w(o):void 0,s=typeof t.definition.maxRetries=="number"?t.definition.maxRetries:m;return e.originals.map(u=>{const c=e.dispositions.get(u)??(a?"error":"ack"),n=typeof u.attempts=="number"?u.attempts:1;return{attempts:n,body:u.body,deadLettered:c!=="ack"&&n>s,error:c==="error"?i:void 0,exportName:t.exportName,messageId:u.id,outcome:c,queue:r,timestamp:d(u.timestamp)}})},x=async(e,t,r)=>{const a=Object.hasOwn(t,e.queue)?t[e.queue]:void 0;if(a===void 0){const n=Object.keys(t),p=n.length===0?"no push queues are declared":`known push queues: ${n.join(", ")}`;throw new l("INTERNAL",`@lunora/queue: received a batch for queue "${e.queue}" but no push handler is registered (${p})`)}const{handler:o}=a.definition;if(typeof o!="function")throw new TypeError(`@lunora/queue: queue "${e.queue}" (${a.exportName}) has no push handler — it is declared as a pull consumer`);const i=f({env:r.env,exportName:a.exportName,fetchImpl:r.fetchImpl});if(r.capture===void 0){await o(i,e);return}const s=g(e);let u=!1,c;try{await o(i,s.wrappedBatch)}catch(n){u=!0,c=n}try{const n=b(s,a,e.queue,u,c);await r.capture(n)}catch(n){console.warn("@lunora/queue: capture sink failed (delivery unaffected):",n)}if(u)throw c};export{x as dispatchQueueBatch};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/queue",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.20",
|
|
4
4
|
"description": "Cloudflare Queues for Lunora: defineQueue producers + consumers, the ctx.queues surface, and the generated queue() worker handler",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"background-jobs",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@lunora/errors": "1.0.0-alpha.16",
|
|
48
|
+
"@lunora/platform": "1.0.0-alpha.6"
|
|
49
|
+
},
|
|
46
50
|
"engines": {
|
|
47
51
|
"node": "^22.15.0 || >=24.11.0"
|
|
48
52
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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 };
|
|
@@ -1,71 +0,0 @@
|
|
|
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 };
|
|
@@ -1,33 +0,0 @@
|
|
|
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 };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const queueBindingName = (exportName) => `QUEUE_${exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "_").toUpperCase()}`;
|
|
2
|
-
const queueDefaultName = (exportName) => exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "-").toLowerCase();
|
|
3
|
-
const defineQueue = (config) => {
|
|
4
|
-
const mode = config.mode ?? "push";
|
|
5
|
-
if (mode !== "push" && mode !== "pull") {
|
|
6
|
-
throw new TypeError(`defineQueue: \`mode\` must be "push" or "pull" (got ${JSON.stringify(config.mode)})`);
|
|
7
|
-
}
|
|
8
|
-
if (mode === "push" && typeof config.handler !== "function") {
|
|
9
|
-
throw new TypeError('defineQueue: `handler` must be a function for a push consumer (omit it only when `mode: "pull"`)');
|
|
10
|
-
}
|
|
11
|
-
if (config.name !== void 0 && (typeof config.name !== "string" || config.name.length === 0)) {
|
|
12
|
-
throw new TypeError("defineQueue: `name` must be a non-empty string when provided");
|
|
13
|
-
}
|
|
14
|
-
return { ...config, isLunoraQueue: true, mode };
|
|
15
|
-
};
|
|
16
|
-
const isQueueDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraQueue === true;
|
|
17
|
-
|
|
18
|
-
export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { createQueueRunContext } from './createQueueRunContext-DVqG7Oyk.mjs';
|
|
2
|
-
|
|
3
|
-
const dispatchQueueBatch = async (batch, registry, options) => {
|
|
4
|
-
const entry = registry[batch.queue];
|
|
5
|
-
if (entry === void 0) {
|
|
6
|
-
const known = Object.keys(registry);
|
|
7
|
-
const suffix = known.length === 0 ? "no push queues are declared" : `known push queues: ${known.join(", ")}`;
|
|
8
|
-
throw new Error(`@lunora/queue: received a batch for queue "${batch.queue}" but no push handler is registered (${suffix})`);
|
|
9
|
-
}
|
|
10
|
-
const { handler } = entry.definition;
|
|
11
|
-
if (typeof handler !== "function") {
|
|
12
|
-
throw new TypeError(`@lunora/queue: queue "${batch.queue}" (${entry.exportName}) has no push handler — it is declared as a pull consumer`);
|
|
13
|
-
}
|
|
14
|
-
const context = createQueueRunContext({ env: options.env, exportName: entry.exportName, fetchImpl: options.fetchImpl });
|
|
15
|
-
await handler(context, batch);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export { dispatchQueueBatch };
|