@lunora/queue 1.0.0-alpha.3 → 1.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -169,6 +169,102 @@ interface QueueBindingSpec {
169
169
  /** The stable wrangler queue name, e.g. `email-queue`. */
170
170
  name: string;
171
171
  }
172
+ /** One declared queue, keyed for batch routing by its stable wrangler name. */
173
+ interface QueueRegistryEntry {
174
+ /**
175
+ * The `defineQueue` result (carries the push handler). The body type is
176
+ * erased to `any` here because the registry is heterogeneous — different
177
+ * queues carry different message bodies, and the handler param is
178
+ * contravariant, so a precise `QueueDefinition<Body>` would not be assignable
179
+ * to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
180
+ * batch straight through, so the erasure is type-only.
181
+ */
182
+ definition: QueueDefinition<any>;
183
+ /** The `lunora/queues.ts` export name, for log correlation. */
184
+ exportName: string;
185
+ }
186
+ /** Map of stable wrangler queue name → registry entry, built by codegen. */
187
+ type QueueRegistry = Record<string, QueueRegistryEntry>;
188
+ /** The disposition a consumer left one message in for a single delivery attempt. */
189
+ type QueueMessageOutcome = "ack" | "error" | "retry";
190
+ /**
191
+ * One consumed message as captured by {@link dispatchQueueBatch} and handed to an
192
+ * {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
193
+ * `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
194
+ * the two packages share only this contract, so keep them in sync by hand.
195
+ */
196
+ interface CapturedQueueMessage {
197
+ /** Delivery attempt number for this message (`message.attempts`). */
198
+ attempts: number;
199
+ /** The message body (JSON-encoded + capped by the catcher). */
200
+ body: unknown;
201
+ /** `true` when this non-ack disposition exhausted the queue's `maxRetries` (dead-letters next). */
202
+ deadLettered: boolean;
203
+ /** Handler error message when `outcome` is `error`; absent otherwise. */
204
+ error?: string;
205
+ /** The `lunora/queues.ts` export name that consumed it. */
206
+ exportName: string;
207
+ /** The delivered message id (`message.id`). */
208
+ messageId: string;
209
+ /** How the handler disposed of the message this attempt. */
210
+ outcome: QueueMessageOutcome;
211
+ /** The stable wrangler queue name the batch was delivered from (`batch.queue`). */
212
+ queue: string;
213
+ /** Original message timestamp in epoch-ms (`message.timestamp`). */
214
+ timestamp: number;
215
+ }
216
+ /**
217
+ * Persists a batch of consumed messages. The codegen worker wires this to POST the
218
+ * batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
219
+ * Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
220
+ * capture failure never changes delivery semantics.
221
+ */
222
+ type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
223
+ interface DispatchOptions {
224
+ /**
225
+ * Optional capture sink. When set, the batch is instrumented and every
226
+ * message's final disposition is recorded and handed to this sink after the
227
+ * handler runs. Omitted in production unless queue capture is enabled, so a
228
+ * consumer pays no instrumentation cost by default.
229
+ */
230
+ capture?: QueueCaptureSink;
231
+ /** Worker `env`, forwarded to the queue run context. */
232
+ env: Record<string, unknown>;
233
+ /** Injectable fetch for the `ctx.run` dispatcher (tests). */
234
+ fetchImpl?: typeof fetch;
235
+ }
236
+ /**
237
+ * Look up the handler for `batch.queue` and invoke it with a fresh
238
+ * `QueueRunContext`. Throws a directed error when no push handler is registered
239
+ * for the delivered queue (a misconfiguration — the consumer was declared
240
+ * `pull`, or the queue name drifted from the `defineQueue` export).
241
+ */
242
+ declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
243
+ /** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
244
+ type QueueEnv = Record<string, unknown>;
245
+ /** Options for {@link createQueueCaptureSink}. */
246
+ interface QueueCaptureOptions {
247
+ /** Pin the consumed-message log to a Cloudflare data-residency jurisdiction (match the worker's `jurisdiction`). */
248
+ jurisdiction?: string;
249
+ /** Shard the consumed-message log lives on; override if the worker sets a custom `defaultShardKey`. */
250
+ rootShard?: string;
251
+ }
252
+ /**
253
+ * Whether consumed queue messages should be captured into the studio's log.
254
+ * Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
255
+ * unset, capture is on only in a development environment. Mirrors
256
+ * `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
257
+ * same way.
258
+ */
259
+ declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
260
+ /**
261
+ * Build the {@link QueueCaptureSink} that records a processed batch into the
262
+ * studio's root-shard consumed-message log via the reserved `recordQueueMessage`
263
+ * admin RPC. Best-effort by contract: without the `SHARD` binding or
264
+ * `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
265
+ * rejection, so capture never changes delivery semantics.
266
+ */
267
+ declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
172
268
  /**
173
269
  * Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
174
270
  * into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
@@ -232,35 +328,6 @@ declare const queueDefaultName: (exportName: string) => string;
232
328
  declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
233
329
  /** True when a value is a `defineQueue` result (the runtime brand check). */
234
330
  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&lt;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
331
  interface RunContextOptions {
265
332
  env: Record<string, unknown>;
266
333
  exportName: string;
@@ -268,4 +335,4 @@ interface RunContextOptions {
268
335
  }
269
336
  /** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
270
337
  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 };
338
+ export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueEnv, 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, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
package/dist/index.d.ts CHANGED
@@ -169,6 +169,102 @@ interface QueueBindingSpec {
169
169
  /** The stable wrangler queue name, e.g. `email-queue`. */
170
170
  name: string;
171
171
  }
172
+ /** One declared queue, keyed for batch routing by its stable wrangler name. */
173
+ interface QueueRegistryEntry {
174
+ /**
175
+ * The `defineQueue` result (carries the push handler). The body type is
176
+ * erased to `any` here because the registry is heterogeneous — different
177
+ * queues carry different message bodies, and the handler param is
178
+ * contravariant, so a precise `QueueDefinition&lt;Body>` would not be assignable
179
+ * to a shared `unknown`-bodied slot. Runtime dispatch passes the delivered
180
+ * batch straight through, so the erasure is type-only.
181
+ */
182
+ definition: QueueDefinition<any>;
183
+ /** The `lunora/queues.ts` export name, for log correlation. */
184
+ exportName: string;
185
+ }
186
+ /** Map of stable wrangler queue name → registry entry, built by codegen. */
187
+ type QueueRegistry = Record<string, QueueRegistryEntry>;
188
+ /** The disposition a consumer left one message in for a single delivery attempt. */
189
+ type QueueMessageOutcome = "ack" | "error" | "retry";
190
+ /**
191
+ * One consumed message as captured by {@link dispatchQueueBatch} and handed to an
192
+ * {@link QueueCaptureSink}. Structurally matches `@lunora/do`'s
193
+ * `RecordQueueMessageInput` (the reserved `recordQueueMessage` admin RPC payload);
194
+ * the two packages share only this contract, so keep them in sync by hand.
195
+ */
196
+ interface CapturedQueueMessage {
197
+ /** Delivery attempt number for this message (`message.attempts`). */
198
+ attempts: number;
199
+ /** The message body (JSON-encoded + capped by the catcher). */
200
+ body: unknown;
201
+ /** `true` when this non-ack disposition exhausted the queue's `maxRetries` (dead-letters next). */
202
+ deadLettered: boolean;
203
+ /** Handler error message when `outcome` is `error`; absent otherwise. */
204
+ error?: string;
205
+ /** The `lunora/queues.ts` export name that consumed it. */
206
+ exportName: string;
207
+ /** The delivered message id (`message.id`). */
208
+ messageId: string;
209
+ /** How the handler disposed of the message this attempt. */
210
+ outcome: QueueMessageOutcome;
211
+ /** The stable wrangler queue name the batch was delivered from (`batch.queue`). */
212
+ queue: string;
213
+ /** Original message timestamp in epoch-ms (`message.timestamp`). */
214
+ timestamp: number;
215
+ }
216
+ /**
217
+ * Persists a batch of consumed messages. The codegen worker wires this to POST the
218
+ * batch to the root shard's `recordQueueMessage` admin RPC (the dev queue catcher).
219
+ * Best-effort by contract: {@link dispatchQueueBatch} swallows a rejection so a
220
+ * capture failure never changes delivery semantics.
221
+ */
222
+ type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
223
+ interface DispatchOptions {
224
+ /**
225
+ * Optional capture sink. When set, the batch is instrumented and every
226
+ * message's final disposition is recorded and handed to this sink after the
227
+ * handler runs. Omitted in production unless queue capture is enabled, so a
228
+ * consumer pays no instrumentation cost by default.
229
+ */
230
+ capture?: QueueCaptureSink;
231
+ /** Worker `env`, forwarded to the queue run context. */
232
+ env: Record<string, unknown>;
233
+ /** Injectable fetch for the `ctx.run` dispatcher (tests). */
234
+ fetchImpl?: typeof fetch;
235
+ }
236
+ /**
237
+ * Look up the handler for `batch.queue` and invoke it with a fresh
238
+ * `QueueRunContext`. Throws a directed error when no push handler is registered
239
+ * for the delivered queue (a misconfiguration — the consumer was declared
240
+ * `pull`, or the queue name drifted from the `defineQueue` export).
241
+ */
242
+ declare const dispatchQueueBatch: (batch: MessageBatchLike, registry: QueueRegistry, options: DispatchOptions) => Promise<void>;
243
+ /** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
244
+ type QueueEnv = Record<string, unknown>;
245
+ /** Options for {@link createQueueCaptureSink}. */
246
+ interface QueueCaptureOptions {
247
+ /** Pin the consumed-message log to a Cloudflare data-residency jurisdiction (match the worker's `jurisdiction`). */
248
+ jurisdiction?: string;
249
+ /** Shard the consumed-message log lives on; override if the worker sets a custom `defaultShardKey`. */
250
+ rootShard?: string;
251
+ }
252
+ /**
253
+ * Whether consumed queue messages should be captured into the studio's log.
254
+ * Explicit `LUNORA_QUEUE_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`) always wins;
255
+ * unset, capture is on only in a development environment. Mirrors
256
+ * `@lunora/mail`'s `shouldCaptureMail` so mail and queue dev capture toggle the
257
+ * same way.
258
+ */
259
+ declare const shouldCaptureQueue: (env: QueueEnv) => boolean;
260
+ /**
261
+ * Build the {@link QueueCaptureSink} that records a processed batch into the
262
+ * studio's root-shard consumed-message log via the reserved `recordQueueMessage`
263
+ * admin RPC. Best-effort by contract: without the `SHARD` binding or
264
+ * `LUNORA_ADMIN_TOKEN` it no-ops, and `dispatchQueueBatch` swallows a
265
+ * rejection, so capture never changes delivery semantics.
266
+ */
267
+ declare const createQueueCaptureSink: (env: QueueEnv, options?: QueueCaptureOptions) => QueueCaptureSink;
172
268
  /**
173
269
  * Build the `ctx.queues` map for a request: resolve every spec's `env[binding]`
174
270
  * into the `exportName → Queue binding` map and wrap it in {@link createQueues}.
@@ -232,35 +328,6 @@ declare const queueDefaultName: (exportName: string) => string;
232
328
  declare const defineQueue: <Body = unknown>(config: QueueConfig<Body>) => QueueDefinition<Body>;
233
329
  /** True when a value is a `defineQueue` result (the runtime brand check). */
234
330
  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&lt;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
331
  interface RunContextOptions {
265
332
  env: Record<string, unknown>;
266
333
  exportName: string;
@@ -268,4 +335,4 @@ interface RunContextOptions {
268
335
  }
269
336
  /** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
270
337
  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 };
338
+ export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, type QueueBindingLike, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueContentType, type QueueDefinition, type QueueEnv, 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, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
+ export { createQueueCaptureSink, shouldCaptureQueue } from './packem_shared/createQueueCaptureSink-CRacRMqV.mjs';
1
2
  export { createQueueContext } from './packem_shared/createQueueContext-D0XCdCsd.mjs';
2
3
  export { default as createQueues } from './packem_shared/createQueues-14-vSICK.mjs';
3
4
  export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName } from './packem_shared/defineQueue-D40gREfg.mjs';
4
- export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-DR15pYS7.mjs';
5
+ export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-D4zU7C-C.mjs';
5
6
  export { createQueueRunContext } from './packem_shared/createQueueRunContext-_2hD-TK7.mjs';
@@ -0,0 +1,54 @@
1
+ const RECORD_QUEUE_MESSAGE_OP = "__lunora_admin__:recordQueueMessage";
2
+ const DEFAULT_ROOT_SHARD = "__root__";
3
+ const DEV_ENVIRONMENT_PATTERN = /^(?:dev(?:elopment)?|local(?:host)?|test)$/iu;
4
+ const ENVIRONMENT_VARS = ["CF_ENV", "ENVIRONMENT", "NODE_ENV", "WORKER_ENV"];
5
+ const CAPTURE_FETCH_TIMEOUT_MS = 5e3;
6
+ const shouldCaptureQueue = (env) => {
7
+ const flag = env["LUNORA_QUEUE_CAPTURE"];
8
+ if (typeof flag === "string") {
9
+ return flag === "1" || flag.toLowerCase() === "true";
10
+ }
11
+ return ENVIRONMENT_VARS.some((key) => {
12
+ const value = env[key];
13
+ return typeof value === "string" && DEV_ENVIRONMENT_PATTERN.test(value);
14
+ });
15
+ };
16
+ const createQueueCaptureSink = (env, options = {}) => {
17
+ const rootShard = options.rootShard ?? DEFAULT_ROOT_SHARD;
18
+ return async (messages) => {
19
+ if (messages.length === 0) {
20
+ return;
21
+ }
22
+ const binding = env["SHARD"];
23
+ const adminToken = typeof env["LUNORA_ADMIN_TOKEN"] === "string" ? env["LUNORA_ADMIN_TOKEN"] : void 0;
24
+ if (binding === void 0 || adminToken === void 0) {
25
+ return;
26
+ }
27
+ let namespace = binding;
28
+ if (options.jurisdiction !== void 0) {
29
+ if (typeof binding.jurisdiction !== "function") {
30
+ throw new TypeError(
31
+ `@lunora/queue: Durable Object namespace does not support jurisdiction("${options.jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
32
+ );
33
+ }
34
+ namespace = binding.jurisdiction(options.jurisdiction);
35
+ }
36
+ const stub = namespace.get(namespace.idFromName(rootShard));
37
+ const controller = new AbortController();
38
+ const timeout = setTimeout(() => {
39
+ controller.abort();
40
+ }, CAPTURE_FETCH_TIMEOUT_MS);
41
+ try {
42
+ await stub.fetch("https://shard.internal/rpc", {
43
+ body: JSON.stringify({ args: { messages }, functionPath: RECORD_QUEUE_MESSAGE_OP }),
44
+ headers: { authorization: `Bearer ${adminToken}`, "content-type": "application/json" },
45
+ method: "POST",
46
+ signal: controller.signal
47
+ });
48
+ } finally {
49
+ clearTimeout(timeout);
50
+ }
51
+ };
52
+ };
53
+
54
+ export { createQueueCaptureSink, shouldCaptureQueue };
@@ -0,0 +1,131 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { createQueueRunContext } from './createQueueRunContext-_2hD-TK7.mjs';
3
+
4
+ const DEFAULT_MAX_RETRIES = 3;
5
+ const timestampToMs = (value) => {
6
+ if (value instanceof Date) {
7
+ return value.getTime();
8
+ }
9
+ const asNumber = typeof value === "number" ? value : Number(value);
10
+ return Number.isFinite(asNumber) ? asNumber : 0;
11
+ };
12
+ const instrumentBatch = (batch) => {
13
+ const dispositions = /* @__PURE__ */ new Map();
14
+ const originals = batch.messages;
15
+ const wrappedMessages = originals.map((message) => {
16
+ return {
17
+ ack: () => {
18
+ dispositions.set(message, "ack");
19
+ message.ack();
20
+ },
21
+ get attempts() {
22
+ return message.attempts;
23
+ },
24
+ get body() {
25
+ return message.body;
26
+ },
27
+ get id() {
28
+ return message.id;
29
+ },
30
+ retry: (options) => {
31
+ dispositions.set(message, "retry");
32
+ message.retry(options);
33
+ },
34
+ get timestamp() {
35
+ return message.timestamp;
36
+ }
37
+ };
38
+ });
39
+ const fillUndecided = (outcome) => {
40
+ for (const message of originals) {
41
+ if (!dispositions.has(message)) {
42
+ dispositions.set(message, outcome);
43
+ }
44
+ }
45
+ };
46
+ const wrappedBatch = {
47
+ ackAll: () => {
48
+ fillUndecided("ack");
49
+ batch.ackAll();
50
+ },
51
+ messages: wrappedMessages,
52
+ queue: batch.queue,
53
+ retryAll: (options) => {
54
+ fillUndecided("retry");
55
+ batch.retryAll(options);
56
+ }
57
+ };
58
+ return { dispositions, originals, wrappedBatch };
59
+ };
60
+ const describeThrownError = (handlerError) => {
61
+ if (handlerError instanceof Error) {
62
+ return handlerError.message;
63
+ }
64
+ if (typeof handlerError === "string") {
65
+ return handlerError;
66
+ }
67
+ if (handlerError !== null && typeof handlerError === "object") {
68
+ try {
69
+ return JSON.stringify(handlerError);
70
+ } catch {
71
+ return "[unserializable thrown value]";
72
+ }
73
+ }
74
+ return String(handlerError);
75
+ };
76
+ const buildCaptureRecords = (harness, entry, queue, threw, handlerError) => {
77
+ const errorMessage = threw ? describeThrownError(handlerError) : void 0;
78
+ const maxRetries = typeof entry.definition.maxRetries === "number" ? entry.definition.maxRetries : DEFAULT_MAX_RETRIES;
79
+ return harness.originals.map((message) => {
80
+ const decided = harness.dispositions.get(message);
81
+ const outcome = decided ?? (threw ? "error" : "ack");
82
+ const attempts = typeof message.attempts === "number" ? message.attempts : 1;
83
+ return {
84
+ attempts,
85
+ body: message.body,
86
+ deadLettered: outcome !== "ack" && attempts >= maxRetries,
87
+ error: outcome === "error" ? errorMessage : void 0,
88
+ exportName: entry.exportName,
89
+ messageId: message.id,
90
+ outcome,
91
+ queue,
92
+ timestamp: timestampToMs(message.timestamp)
93
+ };
94
+ });
95
+ };
96
+ const dispatchQueueBatch = async (batch, registry, options) => {
97
+ const entry = registry[batch.queue];
98
+ if (entry === void 0) {
99
+ const known = Object.keys(registry);
100
+ const suffix = known.length === 0 ? "no push queues are declared" : `known push queues: ${known.join(", ")}`;
101
+ throw new LunoraError("INTERNAL", `@lunora/queue: received a batch for queue "${batch.queue}" but no push handler is registered (${suffix})`);
102
+ }
103
+ const { handler } = entry.definition;
104
+ if (typeof handler !== "function") {
105
+ throw new TypeError(`@lunora/queue: queue "${batch.queue}" (${entry.exportName}) has no push handler — it is declared as a pull consumer`);
106
+ }
107
+ const context = createQueueRunContext({ env: options.env, exportName: entry.exportName, fetchImpl: options.fetchImpl });
108
+ if (options.capture === void 0) {
109
+ await handler(context, batch);
110
+ return;
111
+ }
112
+ const harness = instrumentBatch(batch);
113
+ let threw = false;
114
+ let handlerError;
115
+ try {
116
+ await handler(context, harness.wrappedBatch);
117
+ } catch (error) {
118
+ threw = true;
119
+ handlerError = error;
120
+ }
121
+ try {
122
+ const records = buildCaptureRecords(harness, entry, batch.queue, threw, handlerError);
123
+ await options.capture(records);
124
+ } catch {
125
+ }
126
+ if (threw) {
127
+ throw handlerError;
128
+ }
129
+ };
130
+
131
+ export { dispatchQueueBatch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/queue",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.4",
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",
@@ -1,19 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { createQueueRunContext } from './createQueueRunContext-_2hD-TK7.mjs';
3
-
4
- const dispatchQueueBatch = async (batch, registry, options) => {
5
- const entry = registry[batch.queue];
6
- if (entry === void 0) {
7
- const known = Object.keys(registry);
8
- const suffix = known.length === 0 ? "no push queues are declared" : `known push queues: ${known.join(", ")}`;
9
- throw new LunoraError("INTERNAL", `@lunora/queue: received a batch for queue "${batch.queue}" but no push handler is registered (${suffix})`);
10
- }
11
- const { handler } = entry.definition;
12
- if (typeof handler !== "function") {
13
- throw new TypeError(`@lunora/queue: queue "${batch.queue}" (${entry.exportName}) has no push handler — it is declared as a pull consumer`);
14
- }
15
- const context = createQueueRunContext({ env: options.env, exportName: entry.exportName, fetchImpl: options.fetchImpl });
16
- await handler(context, batch);
17
- };
18
-
19
- export { dispatchQueueBatch };