@lunora/queue 1.0.0-alpha.29 → 1.0.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,13 +26,19 @@ import { api } from "./_generated/api";
26
26
  export const emailQueue = defineQueue<{ to: string }>({
27
27
  handler: async (ctx, batch) => {
28
28
  for (const message of batch.messages) {
29
- await ctx.run(api.email.send, { to: message.body.to });
29
+ await message.run(api.email.send, { to: message.body.to });
30
30
  message.ack();
31
31
  }
32
32
  },
33
33
  });
34
34
  ```
35
35
 
36
+ `message.run(...)` is `ctx.run(...)` pinned to that message. Call it inside the
37
+ batch loop: when the dispatched function fails deterministically (`400`, `403`,
38
+ `404`, `422`), the consumer acks just that message and retries the rest instead
39
+ of letting one poison message retry — and eventually dead-letter — the whole
40
+ batch.
41
+
36
42
  ```ts
37
43
  // inside a mutation/action
38
44
  await ctx.queues.emailQueue.send({ to: user.email });
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { QueueBindingLike, MessageBatchLike, QueueSendOptions, MessageSendRequestLike, QueueSendBatchOptions } from '@lunora/platform';
1
+ import { QueueBindingLike, MessageBatchLike, MessageLike, QueueSendOptions, MessageSendRequestLike, QueueSendBatchOptions } from '@lunora/platform';
2
2
  export type { MessageBatchLike, MessageLike, MessageSendRequestLike, QueueBindingLike, QueueContentType, QueueRetryOptions, QueueSendBatchOptions, QueueSendOptions } from '@lunora/platform';
3
3
  /**
4
4
  * Shared types for dispatching a Lunora function back into the worker from a
@@ -14,6 +14,15 @@ interface FunctionReference {
14
14
  type ArgsOf<F> = F extends FunctionReference ? Record<string, unknown> : never;
15
15
  /** Options for a function call made via a dispatch runner. */
16
16
  interface RunFunctionOptions {
17
+ /**
18
+ * Correlate this call with a caller-defined message/item id (e.g. a queue
19
+ * message's `id`). Purely local bookkeeping — never sent to the dispatch
20
+ * endpoint — carried onto the `LunoraError` a deterministic dispatch
21
+ * failure throws, so a batching consumer (`@lunora/queue`'s push handler)
22
+ * can read it back and attribute the failure to the one item that caused
23
+ * it instead of the whole batch. Optional and inert when omitted.
24
+ */
25
+ messageId?: string;
17
26
  /** Route the call to a specific shard (defaults to the worker's root shard). */
18
27
  shardKey?: string;
19
28
  /** Abort the dispatch after this many ms; the abort is retryable. Overrides the runner's default. */
@@ -63,13 +72,39 @@ interface QueueRunContext {
63
72
  readonly env: Record<string, unknown>;
64
73
  /** Queue-name-prefixed logger. */
65
74
  readonly log: DispatchLogger;
66
- /** Invoke a Lunora function (query/mutation/action) by reference. */
75
+ /**
76
+ * Invoke a Lunora function (query/mutation/action) by reference.
77
+ *
78
+ * Batch-unaware: a failure it throws is attributed to nothing, so a
79
+ * deterministic failure retries the whole batch. Inside the `batch.messages`
80
+ * loop, prefer {@link QueueMessage.run}, which pins the call to its message.
81
+ */
67
82
  readonly run: DispatchRunFunction;
68
83
  }
84
+ /**
85
+ * One delivered message as the push handler sees it: the Cloudflare `Message`
86
+ * plus `run` — a {@link QueueRunContext.run} pinned to THIS message.
87
+ *
88
+ * Prefer `message.run(api.x.y, args)` over `ctx.run(...)` inside the batch
89
+ * loop. The pin is what lets the dispatcher attribute a deterministic dispatch
90
+ * failure (400/403/404/422) to the one message that caused it: that message is
91
+ * acked and every other one is retried, instead of the whole batch being
92
+ * re-delivered because of a single poison message. A plain `ctx.run` call
93
+ * carries no message id, so its failure stays unattributed and the whole batch
94
+ * retries.
95
+ */
96
+ interface QueueMessage<Body = unknown> extends MessageLike<Body> {
97
+ /** {@link QueueRunContext.run}, pinned to this message for failure attribution. */
98
+ readonly run: DispatchRunFunction;
99
+ }
100
+ /** The delivered batch as the push handler sees it — {@link QueueMessage}s rather than bare `Message`s. */
101
+ interface QueueMessageBatch<Body = unknown> extends Omit<MessageBatchLike<Body>, "messages"> {
102
+ readonly messages: ReadonlyArray<QueueMessage<Body>>;
103
+ }
69
104
  /** Whether a declared queue is consumed by this worker (push) or polled externally (pull). */
70
105
  type QueueConsumerMode = "pull" | "push";
71
106
  /** The handler body run for each delivered batch (push consumers only). */
72
- type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: MessageBatchLike<Body>) => Promise<void> | void;
107
+ type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: QueueMessageBatch<Body>) => Promise<void> | void;
73
108
  /** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
74
109
  interface QueueConsumerTuning {
75
110
  /** Name of the dead-letter queue messages land in after `maxRetries`. */
@@ -171,10 +206,11 @@ interface CapturedQueueMessage {
171
206
  type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
172
207
  interface DispatchOptions {
173
208
  /**
174
- * Optional capture sink. When set, the batch is instrumented and every
175
- * message's final disposition is recorded and handed to this sink after the
176
- * handler runs. Omitted in production unless queue capture is enabled, so a
177
- * consumer pays no instrumentation cost by default.
209
+ * Optional capture sink. When set, every message's final disposition is
210
+ * turned into a record and handed to this sink after the handler runs.
211
+ * Omitted in production unless queue capture is enabled, so a consumer pays
212
+ * no record-building or sink cost by default. Delivery semantics — including
213
+ * poison-message isolation — do not depend on it.
178
214
  */
179
215
  capture?: QueueCaptureSink;
180
216
  /** Worker `env`, forwarded to the queue run context. */
@@ -257,7 +293,7 @@ declare const queueDefaultName: (exportName: string) => string;
257
293
  * export const emailQueue = defineQueue<{ to: string }>({
258
294
  * handler: async (ctx, batch) => {
259
295
  * for (const message of batch.messages) {
260
- * await ctx.run(api.email.send, { to: message.body.to });
296
+ * await message.run(api.email.send, { to: message.body.to });
261
297
  * message.ack();
262
298
  * }
263
299
  * },
@@ -266,6 +302,12 @@ declare const queueDefaultName: (exportName: string) => string;
266
302
  *
267
303
  * Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
268
304
  *
305
+ * `message.run(...)` is `ctx.run(...)` pinned to that message — prefer it inside
306
+ * the batch loop. A deterministic failure (400/403/404/422) from a pinned call
307
+ * is attributed to its message: that one is acked and the rest are retried,
308
+ * instead of one poison message re-delivering (and eventually dead-lettering)
309
+ * the whole batch.
310
+ *
269
311
  * ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
270
312
  * Lunora functions over the admin-authenticated dispatch endpoint (the same
271
313
  * trusted path the scheduler and workflows use), so those calls run with the
@@ -284,4 +326,4 @@ interface RunContextOptions {
284
326
  }
285
327
  /** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
286
328
  declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
287
- 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 };
329
+ export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueDefinition, type QueueEnv, type QueueHandler, type DispatchLogger as QueueLogger, type QueueMessage, type QueueMessageBatch, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type Queues, type RunFunctionOptions, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { QueueBindingLike, MessageBatchLike, QueueSendOptions, MessageSendRequestLike, QueueSendBatchOptions } from '@lunora/platform';
1
+ import { QueueBindingLike, MessageBatchLike, MessageLike, QueueSendOptions, MessageSendRequestLike, QueueSendBatchOptions } from '@lunora/platform';
2
2
  export type { MessageBatchLike, MessageLike, MessageSendRequestLike, QueueBindingLike, QueueContentType, QueueRetryOptions, QueueSendBatchOptions, QueueSendOptions } from '@lunora/platform';
3
3
  /**
4
4
  * Shared types for dispatching a Lunora function back into the worker from a
@@ -14,6 +14,15 @@ interface FunctionReference {
14
14
  type ArgsOf<F> = F extends FunctionReference ? Record<string, unknown> : never;
15
15
  /** Options for a function call made via a dispatch runner. */
16
16
  interface RunFunctionOptions {
17
+ /**
18
+ * Correlate this call with a caller-defined message/item id (e.g. a queue
19
+ * message's `id`). Purely local bookkeeping — never sent to the dispatch
20
+ * endpoint — carried onto the `LunoraError` a deterministic dispatch
21
+ * failure throws, so a batching consumer (`@lunora/queue`'s push handler)
22
+ * can read it back and attribute the failure to the one item that caused
23
+ * it instead of the whole batch. Optional and inert when omitted.
24
+ */
25
+ messageId?: string;
17
26
  /** Route the call to a specific shard (defaults to the worker's root shard). */
18
27
  shardKey?: string;
19
28
  /** Abort the dispatch after this many ms; the abort is retryable. Overrides the runner's default. */
@@ -63,13 +72,39 @@ interface QueueRunContext {
63
72
  readonly env: Record<string, unknown>;
64
73
  /** Queue-name-prefixed logger. */
65
74
  readonly log: DispatchLogger;
66
- /** Invoke a Lunora function (query/mutation/action) by reference. */
75
+ /**
76
+ * Invoke a Lunora function (query/mutation/action) by reference.
77
+ *
78
+ * Batch-unaware: a failure it throws is attributed to nothing, so a
79
+ * deterministic failure retries the whole batch. Inside the `batch.messages`
80
+ * loop, prefer {@link QueueMessage.run}, which pins the call to its message.
81
+ */
67
82
  readonly run: DispatchRunFunction;
68
83
  }
84
+ /**
85
+ * One delivered message as the push handler sees it: the Cloudflare `Message`
86
+ * plus `run` — a {@link QueueRunContext.run} pinned to THIS message.
87
+ *
88
+ * Prefer `message.run(api.x.y, args)` over `ctx.run(...)` inside the batch
89
+ * loop. The pin is what lets the dispatcher attribute a deterministic dispatch
90
+ * failure (400/403/404/422) to the one message that caused it: that message is
91
+ * acked and every other one is retried, instead of the whole batch being
92
+ * re-delivered because of a single poison message. A plain `ctx.run` call
93
+ * carries no message id, so its failure stays unattributed and the whole batch
94
+ * retries.
95
+ */
96
+ interface QueueMessage<Body = unknown> extends MessageLike<Body> {
97
+ /** {@link QueueRunContext.run}, pinned to this message for failure attribution. */
98
+ readonly run: DispatchRunFunction;
99
+ }
100
+ /** The delivered batch as the push handler sees it — {@link QueueMessage}s rather than bare `Message`s. */
101
+ interface QueueMessageBatch<Body = unknown> extends Omit<MessageBatchLike<Body>, "messages"> {
102
+ readonly messages: ReadonlyArray<QueueMessage<Body>>;
103
+ }
69
104
  /** Whether a declared queue is consumed by this worker (push) or polled externally (pull). */
70
105
  type QueueConsumerMode = "pull" | "push";
71
106
  /** The handler body run for each delivered batch (push consumers only). */
72
- type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: MessageBatchLike<Body>) => Promise<void> | void;
107
+ type QueueHandler<Body = unknown> = (context: QueueRunContext, batch: QueueMessageBatch<Body>) => Promise<void> | void;
73
108
  /** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
74
109
  interface QueueConsumerTuning {
75
110
  /** Name of the dead-letter queue messages land in after `maxRetries`. */
@@ -171,10 +206,11 @@ interface CapturedQueueMessage {
171
206
  type QueueCaptureSink = (messages: CapturedQueueMessage[]) => Promise<void> | void;
172
207
  interface DispatchOptions {
173
208
  /**
174
- * Optional capture sink. When set, the batch is instrumented and every
175
- * message's final disposition is recorded and handed to this sink after the
176
- * handler runs. Omitted in production unless queue capture is enabled, so a
177
- * consumer pays no instrumentation cost by default.
209
+ * Optional capture sink. When set, every message's final disposition is
210
+ * turned into a record and handed to this sink after the handler runs.
211
+ * Omitted in production unless queue capture is enabled, so a consumer pays
212
+ * no record-building or sink cost by default. Delivery semantics — including
213
+ * poison-message isolation — do not depend on it.
178
214
  */
179
215
  capture?: QueueCaptureSink;
180
216
  /** Worker `env`, forwarded to the queue run context. */
@@ -257,7 +293,7 @@ declare const queueDefaultName: (exportName: string) => string;
257
293
  * export const emailQueue = defineQueue<{ to: string }>({
258
294
  * handler: async (ctx, batch) => {
259
295
  * for (const message of batch.messages) {
260
- * await ctx.run(api.email.send, { to: message.body.to });
296
+ * await message.run(api.email.send, { to: message.body.to });
261
297
  * message.ack();
262
298
  * }
263
299
  * },
@@ -266,6 +302,12 @@ declare const queueDefaultName: (exportName: string) => string;
266
302
  *
267
303
  * Enqueue from a mutation or action: `await ctx.queues.emailQueue.send({ to })`.
268
304
  *
305
+ * `message.run(...)` is `ctx.run(...)` pinned to that message — prefer it inside
306
+ * the batch loop. A deterministic failure (400/403/404/422) from a pinned call
307
+ * is attributed to its message: that one is acked and the rest are retried,
308
+ * instead of one poison message re-delivering (and eventually dead-lettering)
309
+ * the whole batch.
310
+ *
269
311
  * ⚠️ **Privileged dispatch.** A push handler's `ctx.run(...)` calls back into
270
312
  * Lunora functions over the admin-authenticated dispatch endpoint (the same
271
313
  * trusted path the scheduler and workflows use), so those calls run with the
@@ -284,4 +326,4 @@ interface RunContextOptions {
284
326
  }
285
327
  /** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
286
328
  declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
287
- 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 };
329
+ export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, type QueueBindingSpec, type QueueCaptureOptions, type QueueCaptureSink, type QueueConfig, type QueueConsumerMode, type QueueConsumerTuning, type QueueDefinition, type QueueEnv, type QueueHandler, type DispatchLogger as QueueLogger, type QueueMessage, type QueueMessageBatch, type QueueProducer, type QueueRegistry, type QueueRegistryEntry, type QueueRunContext, type DispatchRunFunction as QueueRunFunction, type Queues, type RunFunctionOptions, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, defineQueue, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createQueueCaptureSink as t,shouldCaptureQueue as r}from"./packem_shared/createQueueCaptureSink-D8pUrrb_.mjs";import{createQueueContext as a}from"./packem_shared/createQueueContext-B9PgIzBM.mjs";import{default as i}from"./packem_shared/createQueues-FpKDoLFX.mjs";import{defineQueue as p,isQueueDefinition as m,queueBindingName as x,queueDefaultName as Q}from"./packem_shared/defineQueue-TY4-i3nG.mjs";import{dispatchQueueBatch as d}from"./packem_shared/dispatchQueueBatch-CGI3kDNW.mjs";import{createQueueRunContext as C}from"./packem_shared/createQueueRunContext-D3rlzWUO.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};
1
+ import{createQueueCaptureSink as t,shouldCaptureQueue as r}from"./packem_shared/createQueueCaptureSink-D8pUrrb_.mjs";import{createQueueContext as a}from"./packem_shared/createQueueContext-B9PgIzBM.mjs";import{default as i}from"./packem_shared/createQueues-FpKDoLFX.mjs";import{defineQueue as p,isQueueDefinition as m,queueBindingName as x,queueDefaultName as Q}from"./packem_shared/defineQueue-TY4-i3nG.mjs";import{dispatchQueueBatch as s}from"./packem_shared/dispatchQueueBatch-dejEVSC6.mjs";import{c as C}from"./packem_shared/run-context-C97v6VyP.mjs";export{t as createQueueCaptureSink,a as createQueueContext,C as createQueueRunContext,i as createQueues,p as defineQueue,s as dispatchQueueBatch,m as isQueueDefinition,x as queueBindingName,Q as queueDefaultName,r as shouldCaptureQueue};
@@ -0,0 +1 @@
1
+ import{c as o}from"./run-context-C97v6VyP.mjs";export{o as createQueueRunContext};
@@ -0,0 +1 @@
1
+ import{c as l,k as w,M as h}from"./run-context-C97v6VyP.mjs";import{LunoraError as y}from"@lunora/errors";const v=3,k=e=>{if(e instanceof Date)return e.getTime();const n=typeof e=="number"?e:Number(e);return Number.isFinite(n)?n:0},x=(e,n)=>(t,r,o)=>e(t,r,{...o,messageId:n}),b=(e,n,t)=>{const r=x(t,e.id);return new Proxy(e,{get:(o,i)=>i==="ack"?()=>{n.set(o,"ack"),o.ack()}:i==="retry"?u=>{n.set(o,"retry"),o.retry(u)}:i==="run"?r:Reflect.get(o,i,o)})},q=(e,n,t)=>new Proxy(e,{get:(r,o)=>o==="ackAll"?()=>{t("ack"),r.ackAll()}:o==="retryAll"?i=>{t("retry"),r.retryAll(i)}:o==="messages"?n:Reflect.get(r,o,r)}),R=(e,n)=>{const t=new Map,r=e.messages,o=r.map(c=>b(c,t,n)),u=q(e,o,c=>{for(const a of r)t.has(a)||t.set(a,c)});return{dispositions:t,originals:r,wrappedBatch:u}},N=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)},A=(e,n,t,r,o,i)=>{const u=r?N(o):void 0,c=typeof n.definition.maxRetries=="number"?n.definition.maxRetries:v,a=r?"error":"ack";return e.originals.map(s=>{const p=s===i,d=e.dispositions.get(s),f=p?"error":d??a,m=typeof s.attempts=="number"?s.attempts:1;return{attempts:m,body:s.body,deadLettered:!p&&f!=="ack"&&m>c,error:f==="error"?u:void 0,exportName:n.exportName,messageId:s.id,outcome:f,queue:t,timestamp:k(s.timestamp)}})},g=(e,n,t)=>{if(!n||!w(t))return;const r=h(t);if(r===void 0)return;const o=e.originals.find(i=>i.id===r);if(!(o===void 0||e.dispositions.has(o)))return o},M=(e,n)=>{n.ack();for(const t of e.originals)t!==n&&!e.dispositions.has(t)&&(e.dispositions.set(t,"retry"),t.retry())},I=async(e,n,t)=>{const r=Object.hasOwn(n,e.queue)?n[e.queue]:void 0;if(r===void 0){const d=Object.keys(n),f=d.length===0?"no push queues are declared":`known push queues: ${d.join(", ")}`;throw new y("INTERNAL",`@lunora/queue: received a batch for queue "${e.queue}" but no push handler is registered (${f})`)}const{handler:o}=r.definition;if(typeof o!="function")throw new TypeError(`@lunora/queue: queue "${e.queue}" (${r.exportName}) has no push handler — it is declared as a pull consumer`);const i=l({env:t.env,exportName:r.exportName,fetchImpl:t.fetchImpl}),u=R(e,i.run);let c=!1,a;try{await o(i,u.wrappedBatch)}catch(d){c=!0,a=d}const s=g(u,c,a);s!==void 0&&M(u,s);const p=c&&s===void 0;if(t.capture!==void 0)try{await t.capture(A(u,r,e.queue,c,a,s))}catch(d){console.warn("@lunora/queue: capture sink failed (delivery unaffected):",d)}if(p)throw a};export{I as dispatchQueueBatch};
@@ -0,0 +1 @@
1
+ import{LunoraError as c,isLunoraError as N}from"@lunora/errors";const A=t=>({debug:(e,...n)=>{console.debug(t,e,...n)},error:(e,...n)=>{console.error(t,e,...n)},info:(e,...n)=>{console.info(t,e,...n)},warn:(e,...n)=>{console.warn(t,e,...n)}}),L=t=>{let e="";for(let n=0;n<t.length;n+=32768)e+=String.fromCharCode(...t.subarray(n,n+32768));return btoa(e)},b=t=>L(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/,"");new TextDecoder;const w=new TextEncoder,g="=",S=t=>{for(let e=0;e<t.length;e+=1)if(t.charCodeAt(e)>255)return!1;return!0},T=t=>b(w.encode(JSON.stringify(t))),_=t=>!t.startsWith(g)&&S(t)?t:`${g}${b(w.encode(t))}`,E="/_lunora/scheduler/dispatch",x=3e4,U=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},I=Symbol("lunoraDispatchFailure"),$=Symbol("lunoraDispatchMessageId"),m=(t,e)=>(Object.defineProperty(t,I,{value:!0}),e!==void 0&&Object.defineProperty(t,$,{value:e}),t),j=t=>N(t)?t[$]:void 0,D=(t,e,n,i)=>{try{const o=JSON.parse(n)?.error;if(typeof o=="object"&&o!==null&&typeof o.code=="string"){const{code:h,data:u,message:s}=o;return m(new c(h,typeof s=="string"?s:void 0,{data:u,status:e}),i)}}catch{}return m(new c("INTERNAL",`${t}: function dispatch failed (${String(e)}): ${n}`,{status:e}),i)},k=new Set([400,403,404,422]),K=t=>N(t)&&t[I]===!0&&k.has(t.status),C=(t,e,n)=>new c("INTERNAL",`${t}: function dispatch to "${e}" timed out after ${String(n)}ms`,{status:503}),J=t=>{const{label:e}=t,n=globalThis.fetch,i=t.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(o,h,u={})=>{if(typeof i!="function")throw new TypeError(`${e}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const s=t.env.LUNORA_ORIGIN_URL;if(typeof s!="string"||s.length===0)throw new c("INTERNAL",`${e}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const f=t.env.LUNORA_ADMIN_TOKEN;if(typeof f!="string"||f.length===0)throw new c("INTERNAL",`${e}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const v=`${U(s)}${E}`,d={authorization:`Bearer ${f}`,"content-type":"application/json"};t.identity?.userId!==void 0&&(d["x-lunora-userid"]=_(t.identity.userId)),t.identity?.claims!==void 0&&(d["x-lunora-identity"]=T(t.identity.claims));const y=u.timeoutMs??x,O=AbortSignal.timeout(y),p=r=>{throw r instanceof Error&&r.name==="TimeoutError"?C(e,o.__lunoraRef,y):r};let a;try{a=await i(v,{body:JSON.stringify({args:h??{},functionPath:o.__lunoraRef,shardKey:u.shardKey}),headers:d,method:"POST",signal:O})}catch(r){return p(r)}if(!a.ok){let r;try{r=await a.text()}catch(R){return p(R)}throw D(e,a.status,r,u.messageId)}let l;try{l=await a.text()}catch(r){return p(r)}if(l.length!==0)try{return JSON.parse(l)}catch{throw new c("INTERNAL",`${e}: function dispatch returned a non-JSON body (${String(a.status)}): ${l}`,{status:a.status})}}},P=t=>({env:t.env,log:A(`[queue:${t.exportName}]`),run:J({env:t.env,fetchImpl:t.fetchImpl,label:"@lunora/queue"})});export{j as M,P as c,K as k};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/queue",
3
- "version": "1.0.0-alpha.29",
3
+ "version": "1.0.0-alpha.30",
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",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@lunora/errors": "1.0.0-alpha.22",
48
- "@lunora/platform": "1.0.0-alpha.13"
48
+ "@lunora/platform": "1.0.0-alpha.14"
49
49
  },
50
50
  "engines": {
51
51
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
1
- import{LunoraError as c}from"@lunora/errors";const R=t=>({debug:(e,...n)=>{console.debug(t,e,...n)},error:(e,...n)=>{console.error(t,e,...n)},info:(e,...n)=>{console.info(t,e,...n)},warn:(e,...n)=>{console.warn(t,e,...n)}}),A=t=>{let e="";for(let n=0;n<t.length;n+=32768)e+=String.fromCharCode(...t.subarray(n,n+32768));return btoa(e)},m=t=>A(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/,"");new TextDecoder;const w=new TextEncoder,g="=",O=t=>{for(let e=0;e<t.length;e+=1)if(t.charCodeAt(e)>255)return!1;return!0},v=t=>m(w.encode(JSON.stringify(t))),T=t=>!t.startsWith(g)&&O(t)?t:`${g}${m(w.encode(t))}`,L="/_lunora/scheduler/dispatch",_=3e4,E=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},S=Symbol("lunoraDispatchFailure"),N=t=>(Object.defineProperty(t,S,{value:!0}),t),x=(t,e,n)=>{try{const o=JSON.parse(n)?.error;if(typeof o=="object"&&o!==null&&typeof o.code=="string"){const{code:i,data:u,message:s}=o;return N(new c(i,typeof s=="string"?s:void 0,{data:u,status:e}))}}catch{}return N(new c("INTERNAL",`${t}: function dispatch failed (${String(e)}): ${n}`,{status:e}))},U=(t,e,n)=>new c("INTERNAL",`${t}: function dispatch to "${e}" timed out after ${String(n)}ms`,{status:503}),C=t=>{const{label:e}=t,n=globalThis.fetch,o=t.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(i,u,s={})=>{if(typeof o!="function")throw new TypeError(`${e}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const h=t.env.LUNORA_ORIGIN_URL;if(typeof h!="string"||h.length===0)throw new c("INTERNAL",`${e}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const f=t.env.LUNORA_ADMIN_TOKEN;if(typeof f!="string"||f.length===0)throw new c("INTERNAL",`${e}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const b=`${E(h)}${L}`,d={authorization:`Bearer ${f}`,"content-type":"application/json"};t.identity?.userId!==void 0&&(d["x-lunora-userid"]=T(t.identity.userId)),t.identity?.claims!==void 0&&(d["x-lunora-identity"]=v(t.identity.claims));const y=s.timeoutMs??_,$=AbortSignal.timeout(y),p=r=>{throw r instanceof Error&&r.name==="TimeoutError"?U(e,i.__lunoraRef,y):r};let a;try{a=await o(b,{body:JSON.stringify({args:u??{},functionPath:i.__lunoraRef,shardKey:s.shardKey}),headers:d,method:"POST",signal:$})}catch(r){return p(r)}if(!a.ok){let r;try{r=await a.text()}catch(I){return p(I)}throw x(e,a.status,r)}let l;try{l=await a.text()}catch(r){return p(r)}if(l.length!==0)try{return JSON.parse(l)}catch{throw new c("INTERNAL",`${e}: function dispatch returned a non-JSON body (${String(a.status)}): ${l}`,{status:a.status})}}},J=t=>({env:t.env,log:R(`[queue:${t.exportName}]`),run:C({env:t.env,fetchImpl:t.fetchImpl,label:"@lunora/queue"})});export{J as createQueueRunContext};
@@ -1 +0,0 @@
1
- import{LunoraError as d}from"@lunora/errors";import{createQueueRunContext as m}from"./createQueueRunContext-D3rlzWUO.mjs";const p=3,l=e=>{if(e instanceof Date)return e.getTime();const t=typeof e=="number"?e:Number(e);return Number.isFinite(t)?t:0},w=(e,t)=>new Proxy(e,{get:(r,n)=>n==="ack"?()=>{t.set(r,"ack"),r.ack()}:n==="retry"?o=>{t.set(r,"retry"),r.retry(o)}:Reflect.get(r,n,r)}),y=(e,t,r)=>new Proxy(e,{get:(n,o)=>o==="ackAll"?()=>{r("ack"),n.ackAll()}:o==="retryAll"?i=>{r("retry"),n.retryAll(i)}:o==="messages"?t:Reflect.get(n,o,n)}),h=e=>{const t=new Map,r=e.messages,n=r.map(c=>w(c,t)),i=y(e,n,c=>{for(const s of r)t.has(s)||t.set(s,c)});return{dispositions:t,originals:r,wrappedBatch:i}},x=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)},k=(e,t,r,n,o)=>{const i=n?x(o):void 0,c=typeof t.definition.maxRetries=="number"?t.definition.maxRetries:p;return e.originals.map(s=>{const u=e.dispositions.get(s)??(n?"error":"ack"),f=typeof s.attempts=="number"?s.attempts:1;return{attempts:f,body:s.body,deadLettered:u!=="ack"&&f>c,error:u==="error"?i:void 0,exportName:t.exportName,messageId:s.id,outcome:u,queue:r,timestamp:l(s.timestamp)}})},R=async(e,t,r)=>{const n=Object.hasOwn(t,e.queue)?t[e.queue]:void 0;if(n===void 0){const u=Object.keys(t),f=u.length===0?"no push queues are declared":`known push queues: ${u.join(", ")}`;throw new d("INTERNAL",`@lunora/queue: received a batch for queue "${e.queue}" but no push handler is registered (${f})`)}const{handler:o}=n.definition;if(typeof o!="function")throw new TypeError(`@lunora/queue: queue "${e.queue}" (${n.exportName}) has no push handler — it is declared as a pull consumer`);const i=m({env:r.env,exportName:n.exportName,fetchImpl:r.fetchImpl});if(r.capture===void 0){await o(i,e);return}const c=h(e);let s=!1,a;try{await o(i,c.wrappedBatch)}catch(u){s=!0,a=u}try{const u=k(c,n,e.queue,s,a);await r.capture(u)}catch(u){console.warn("@lunora/queue: capture sink failed (delivery unaffected):",u)}if(s)throw a};export{R as dispatchQueueBatch};