@lunora/queue 1.0.0-alpha.31 → 1.0.0-alpha.33

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
@@ -14,13 +14,39 @@ 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
+ * The at-least-once dedup key for THIS ONE CALL, sent to the dispatch
19
+ * endpoint as the body's `id` and forwarded to the shard as the
20
+ * replay-dedup `mutationId` — so a redelivery that re-runs the same call
21
+ * applies it exactly once instead of twice.
22
+ *
23
+ * Must be unique per call and stable across redeliveries. It is NOT
24
+ * {@link RunFunctionOptions.messageId}: the shard's dedup table is keyed
25
+ * `(identity, mutationId)` with no function path in it, and every
26
+ * server-initiated dispatch shares the `"system:"` identity, so reusing
27
+ * one id across two calls makes the second return the FIRST call's cached
28
+ * result without ever executing. A per-message id is 1:N with the calls a
29
+ * handler makes; this is 1:1.
30
+ *
31
+ * `@lunora/queue`'s `message.run` derives one automatically as
32
+ * `<messageId>#<n>`, `n` counting that message's calls in order. That is
33
+ * stable across redeliveries because the handler replays from the start —
34
+ * it relies on the handler issuing its `run` calls in a DETERMINISTIC
35
+ * order, which at-least-once replay already assumes. A handler whose call
36
+ * order varies per attempt (branching on `Date.now()`, `Math.random()`,
37
+ * or unordered concurrent settles) must pass its own stable ids instead.
38
+ *
39
+ * Optional; when omitted the dispatch is at-least-once.
40
+ */
41
+ dedupId?: string;
17
42
  /**
18
43
  * 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.
44
+ * message's `id`), for failure attribution only — never sent to the
45
+ * dispatch endpoint. Carried onto the `LunoraError` a deterministic
46
+ * dispatch failure throws, so a batching consumer (`@lunora/queue`'s push
47
+ * handler) can read it back and attribute the failure to the one item that
48
+ * caused it instead of the whole batch. Deliberately NOT the dedup key —
49
+ * see {@link RunFunctionOptions.dedupId}. Optional and inert when omitted.
24
50
  */
25
51
  messageId?: string;
26
52
  /** Route the call to a specific shard (defaults to the worker's root shard). */
package/dist/index.d.ts CHANGED
@@ -14,13 +14,39 @@ 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
+ * The at-least-once dedup key for THIS ONE CALL, sent to the dispatch
19
+ * endpoint as the body's `id` and forwarded to the shard as the
20
+ * replay-dedup `mutationId` — so a redelivery that re-runs the same call
21
+ * applies it exactly once instead of twice.
22
+ *
23
+ * Must be unique per call and stable across redeliveries. It is NOT
24
+ * {@link RunFunctionOptions.messageId}: the shard's dedup table is keyed
25
+ * `(identity, mutationId)` with no function path in it, and every
26
+ * server-initiated dispatch shares the `"system:"` identity, so reusing
27
+ * one id across two calls makes the second return the FIRST call's cached
28
+ * result without ever executing. A per-message id is 1:N with the calls a
29
+ * handler makes; this is 1:1.
30
+ *
31
+ * `@lunora/queue`'s `message.run` derives one automatically as
32
+ * `<messageId>#<n>`, `n` counting that message's calls in order. That is
33
+ * stable across redeliveries because the handler replays from the start —
34
+ * it relies on the handler issuing its `run` calls in a DETERMINISTIC
35
+ * order, which at-least-once replay already assumes. A handler whose call
36
+ * order varies per attempt (branching on `Date.now()`, `Math.random()`,
37
+ * or unordered concurrent settles) must pass its own stable ids instead.
38
+ *
39
+ * Optional; when omitted the dispatch is at-least-once.
40
+ */
41
+ dedupId?: string;
17
42
  /**
18
43
  * 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.
44
+ * message's `id`), for failure attribution only — never sent to the
45
+ * dispatch endpoint. Carried onto the `LunoraError` a deterministic
46
+ * dispatch failure throws, so a batching consumer (`@lunora/queue`'s push
47
+ * handler) can read it back and attribute the failure to the one item that
48
+ * caused it instead of the whole batch. Deliberately NOT the dedup key —
49
+ * see {@link RunFunctionOptions.dedupId}. Optional and inert when omitted.
24
50
  */
25
51
  messageId?: string;
26
52
  /** Route the call to a specific shard (defaults to the worker's root shard). */
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 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};
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-BBouObEO.mjs";import{c as C}from"./packem_shared/run-context-CTBT7TNr.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-CTBT7TNr.mjs";export{o as createQueueRunContext};
@@ -0,0 +1 @@
1
+ import{c as m,H as w,k as h}from"./run-context-CTBT7TNr.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)=>{let t=0;return(r,o,i)=>(t+=1,e(r,o,{...i,dedupId:`${n}#${String(t)}`,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,l=typeof s.attempts=="number"?s.attempts:1;return{attempts:l,body:s.body,deadLettered:!p&&f!=="ack"&&l>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())},B=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=m({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{B as dispatchQueueBatch};
@@ -0,0 +1 @@
1
+ import{LunoraError as u,isLunoraError as w}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,e,n)=>{if(e===void 0)return{dispose:()=>{},signal:t};const o=new AbortController,r=setTimeout(()=>{o.abort(n())},e);return{dispose:()=>{clearTimeout(r)},signal:o.signal}},E=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=>E(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/,"");new TextDecoder;const I=new TextEncoder,m="=",L=t=>{for(let e=0;e<t.length;e+=1)if(t.charCodeAt(e)>255)return!1;return!0},S=t=>b(I.encode(JSON.stringify(t))),_=t=>!t.startsWith(m)&&L(t)?t:`${m}${b(I.encode(t))}`,x="/_lunora/scheduler/dispatch",D=3e4,U=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},$=Symbol("lunoraDispatchFailure"),v=Symbol("lunoraDispatchMessageId"),N=(t,e)=>(Object.defineProperty(t,$,{value:!0}),e!==void 0&&Object.defineProperty(t,v,{value:e}),t),j=t=>w(t)?t[v]:void 0,C=(t,e,n,o)=>{try{const r=JSON.parse(n)?.error;if(typeof r=="object"&&r!==null&&typeof r.code=="string"){const{code:d,data:i,message:c}=r;return N(new u(d,typeof c=="string"?c:void 0,{data:i,status:e}),o)}}catch{}return N(new u("INTERNAL",`${t}: function dispatch failed (${String(e)}): ${n}`,{status:e}),o)},k=new Set([400,403,404,422]),K=t=>w(t)&&t[$]===!0&&k.has(t.status),J=(t,e,n)=>new u("INTERNAL",`${t}: function dispatch to "${e}" timed out after ${String(n)}ms`,{status:503}),M=t=>{const{label:e}=t,n=globalThis.fetch,o=t.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(r,d,i={})=>{if(typeof o!="function")throw new TypeError(`${e}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const c=t.env.LUNORA_ORIGIN_URL;if(typeof c!="string"||c.length===0)throw new u("INTERNAL",`${e}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const h=t.env.LUNORA_ADMIN_TOKEN;if(typeof h!="string"||h.length===0)throw new u("INTERNAL",`${e}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const O=`${U(c)}${x}`,f={authorization:`Bearer ${h}`,"content-type":"application/json"};t.identity?.userId!==void 0&&(f["x-lunora-userid"]=_(t.identity.userId)),t.identity?.claims!==void 0&&(f["x-lunora-identity"]=S(t.identity.claims));const p=i.timeoutMs??D,g=A(void 0,p,()=>new DOMException(`dispatch timed out after ${String(p)}ms`,"TimeoutError")),y=a=>{throw a instanceof Error&&a.name==="TimeoutError"?J(e,r.__lunoraRef,p):a};let s;try{try{s=await o(O,{body:JSON.stringify({args:d??{},functionPath:r.__lunoraRef,id:i.dedupId,shardKey:i.shardKey}),headers:f,method:"POST",signal:g.signal})}catch(l){return y(l)}if(!s.ok){let l;try{l=await s.text()}catch(T){return y(T)}throw C(e,s.status,l,i.messageId)}let a;try{a=await s.text()}catch(l){return y(l)}if(a.length===0)return;try{return JSON.parse(a)}catch{throw new u("INTERNAL",`${e}: function dispatch returned a non-JSON body (${String(s.status)}): ${a}`,{status:s.status})}}finally{g.dispose()}}},W=t=>({env:t.env,log:R(`[queue:${t.exportName}]`),run:M({env:t.env,fetchImpl:t.fetchImpl,label:"@lunora/queue"})});export{K as H,W as c,j as k};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/queue",
3
- "version": "1.0.0-alpha.31",
3
+ "version": "1.0.0-alpha.33",
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.15"
48
+ "@lunora/platform": "1.0.0-alpha.17"
49
49
  },
50
50
  "engines": {
51
51
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
1
- import{c as o}from"./run-context-C97v6VyP.mjs";export{o as createQueueRunContext};
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
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};