@lunora/queue 1.0.0-alpha.10 → 1.0.0-alpha.12
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 +3 -55
- package/dist/index.d.ts +3 -55
- package/dist/index.mjs +1 -6
- package/dist/packem_shared/createQueueCaptureSink-saac8Ywl.mjs +1 -0
- package/dist/packem_shared/createQueueContext-BuXG9VQI.mjs +1 -0
- package/dist/packem_shared/createQueueRunContext-B-TpEaZa.mjs +1 -0
- package/dist/packem_shared/createQueues-BZBGfWI4.mjs +1 -0
- package/dist/packem_shared/defineQueue-DJYVNfQY.mjs +1 -0
- package/dist/packem_shared/dispatchQueueBatch-GZMtLZNW.mjs +1 -0
- package/package.json +3 -2
- package/dist/packem_shared/createQueueCaptureSink-B8WE0eHf.mjs +0 -64
- package/dist/packem_shared/createQueueContext-D0XCdCsd.mjs +0 -14
- package/dist/packem_shared/createQueueRunContext-C8jboCk6.mjs +0 -94
- package/dist/packem_shared/createQueues-14-vSICK.mjs +0 -33
- package/dist/packem_shared/defineQueue-D40gREfg.mjs +0 -18
- package/dist/packem_shared/dispatchQueueBatch-DSEWhEy8.mjs +0 -132
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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
4
|
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
5
|
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
@@ -24,60 +26,6 @@ interface DispatchLogger {
|
|
|
24
26
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
27
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
28
|
}
|
|
27
|
-
/** How a queue message body is serialized on the wire (Cloudflare default `"json"`). */
|
|
28
|
-
type QueueContentType = "bytes" | "json" | "text" | "v8";
|
|
29
|
-
/** Options for a single `producer.send(body, options?)`. */
|
|
30
|
-
interface QueueSendOptions {
|
|
31
|
-
/** Wire serialization for this message (defaults to the queue's content type). */
|
|
32
|
-
contentType?: QueueContentType;
|
|
33
|
-
/** Per-message delivery delay in seconds (0–43200, i.e. up to 12 hours). */
|
|
34
|
-
delaySeconds?: number;
|
|
35
|
-
}
|
|
36
|
-
/** Options for a `producer.sendBatch(messages, options?)`. */
|
|
37
|
-
interface QueueSendBatchOptions {
|
|
38
|
-
/** Delivery delay applied to the whole batch, in seconds. */
|
|
39
|
-
delaySeconds?: number;
|
|
40
|
-
}
|
|
41
|
-
/** One entry in a `sendBatch` call — a body plus optional per-message overrides. */
|
|
42
|
-
interface MessageSendRequestLike<Body = unknown> {
|
|
43
|
-
body: Body;
|
|
44
|
-
contentType?: QueueContentType;
|
|
45
|
-
delaySeconds?: number;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Minimal structural projection of workers-types' `Queue<Body>` (the producer
|
|
49
|
-
* binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
|
|
50
|
-
* we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
|
|
51
|
-
*/
|
|
52
|
-
interface QueueBindingLike<Body = unknown> {
|
|
53
|
-
send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
|
|
54
|
-
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
|
|
55
|
-
}
|
|
56
|
-
/** Options for retrying a message / batch (`message.retry({ delaySeconds })`). */
|
|
57
|
-
interface QueueRetryOptions {
|
|
58
|
-
delaySeconds?: number;
|
|
59
|
-
}
|
|
60
|
-
/** Structural mirror of workers-types' `Message<Body>` (one delivered message). */
|
|
61
|
-
interface MessageLike<Body = unknown> {
|
|
62
|
-
/** Acknowledge this message so it is not redelivered. */
|
|
63
|
-
ack: () => void;
|
|
64
|
-
readonly attempts: number;
|
|
65
|
-
readonly body: Body;
|
|
66
|
-
readonly id: string;
|
|
67
|
-
/** Explicitly retry this message (optionally after a delay). */
|
|
68
|
-
retry: (options?: QueueRetryOptions) => void;
|
|
69
|
-
readonly timestamp: Date;
|
|
70
|
-
}
|
|
71
|
-
/** Structural mirror of workers-types' `MessageBatch<Body>` handed to a consumer. */
|
|
72
|
-
interface MessageBatchLike<Body = unknown> {
|
|
73
|
-
/** Acknowledge every message in the batch. */
|
|
74
|
-
ackAll: () => void;
|
|
75
|
-
readonly messages: ReadonlyArray<MessageLike<Body>>;
|
|
76
|
-
/** The queue name this batch was delivered from (`batch.queue`), used to route. */
|
|
77
|
-
readonly queue: string;
|
|
78
|
-
/** Retry every message in the batch (optionally after a delay). */
|
|
79
|
-
retryAll: (options?: QueueRetryOptions) => void;
|
|
80
|
-
}
|
|
81
29
|
/**
|
|
82
30
|
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
83
31
|
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
@@ -334,4 +282,4 @@ interface RunContextOptions {
|
|
|
334
282
|
}
|
|
335
283
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
336
284
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
337
|
-
export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, 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,3 +1,5 @@
|
|
|
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
4
|
* Shared types for dispatching a Lunora function back into the worker from a
|
|
3
5
|
* server-initiated context (a workflow body, a queue handler, a scheduled job).
|
|
@@ -24,60 +26,6 @@ interface DispatchLogger {
|
|
|
24
26
|
info: (message: unknown, ...rest: unknown[]) => void;
|
|
25
27
|
warn: (message: unknown, ...rest: unknown[]) => void;
|
|
26
28
|
}
|
|
27
|
-
/** How a queue message body is serialized on the wire (Cloudflare default `"json"`). */
|
|
28
|
-
type QueueContentType = "bytes" | "json" | "text" | "v8";
|
|
29
|
-
/** Options for a single `producer.send(body, options?)`. */
|
|
30
|
-
interface QueueSendOptions {
|
|
31
|
-
/** Wire serialization for this message (defaults to the queue's content type). */
|
|
32
|
-
contentType?: QueueContentType;
|
|
33
|
-
/** Per-message delivery delay in seconds (0–43200, i.e. up to 12 hours). */
|
|
34
|
-
delaySeconds?: number;
|
|
35
|
-
}
|
|
36
|
-
/** Options for a `producer.sendBatch(messages, options?)`. */
|
|
37
|
-
interface QueueSendBatchOptions {
|
|
38
|
-
/** Delivery delay applied to the whole batch, in seconds. */
|
|
39
|
-
delaySeconds?: number;
|
|
40
|
-
}
|
|
41
|
-
/** One entry in a `sendBatch` call — a body plus optional per-message overrides. */
|
|
42
|
-
interface MessageSendRequestLike<Body = unknown> {
|
|
43
|
-
body: Body;
|
|
44
|
-
contentType?: QueueContentType;
|
|
45
|
-
delaySeconds?: number;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Minimal structural projection of workers-types' `Queue<Body>` (the producer
|
|
49
|
-
* binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
|
|
50
|
-
* we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
|
|
51
|
-
*/
|
|
52
|
-
interface QueueBindingLike<Body = unknown> {
|
|
53
|
-
send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
|
|
54
|
-
sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
|
|
55
|
-
}
|
|
56
|
-
/** Options for retrying a message / batch (`message.retry({ delaySeconds })`). */
|
|
57
|
-
interface QueueRetryOptions {
|
|
58
|
-
delaySeconds?: number;
|
|
59
|
-
}
|
|
60
|
-
/** Structural mirror of workers-types' `Message<Body>` (one delivered message). */
|
|
61
|
-
interface MessageLike<Body = unknown> {
|
|
62
|
-
/** Acknowledge this message so it is not redelivered. */
|
|
63
|
-
ack: () => void;
|
|
64
|
-
readonly attempts: number;
|
|
65
|
-
readonly body: Body;
|
|
66
|
-
readonly id: string;
|
|
67
|
-
/** Explicitly retry this message (optionally after a delay). */
|
|
68
|
-
retry: (options?: QueueRetryOptions) => void;
|
|
69
|
-
readonly timestamp: Date;
|
|
70
|
-
}
|
|
71
|
-
/** Structural mirror of workers-types' `MessageBatch<Body>` handed to a consumer. */
|
|
72
|
-
interface MessageBatchLike<Body = unknown> {
|
|
73
|
-
/** Acknowledge every message in the batch. */
|
|
74
|
-
ackAll: () => void;
|
|
75
|
-
readonly messages: ReadonlyArray<MessageLike<Body>>;
|
|
76
|
-
/** The queue name this batch was delivered from (`batch.queue`), used to route. */
|
|
77
|
-
readonly queue: string;
|
|
78
|
-
/** Retry every message in the batch (optionally after a delay). */
|
|
79
|
-
retryAll: (options?: QueueRetryOptions) => void;
|
|
80
|
-
}
|
|
81
29
|
/**
|
|
82
30
|
* The typed producer bound to `ctx.queues.<name>`. Sending is a side effect, so
|
|
83
31
|
* the generated context exposes this only on `MutationCtx` / `ActionCtx` (never
|
|
@@ -334,4 +282,4 @@ interface RunContextOptions {
|
|
|
334
282
|
}
|
|
335
283
|
/** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
|
|
336
284
|
declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
|
|
337
|
-
export { type ArgsOf, type CapturedQueueMessage, type FunctionReference, type LunoraQueuesOptions, 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,6 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createQueueContext } from './packem_shared/createQueueContext-D0XCdCsd.mjs';
|
|
3
|
-
export { default as createQueues } from './packem_shared/createQueues-14-vSICK.mjs';
|
|
4
|
-
export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName } from './packem_shared/defineQueue-D40gREfg.mjs';
|
|
5
|
-
export { dispatchQueueBatch } from './packem_shared/dispatchQueueBatch-DSEWhEy8.mjs';
|
|
6
|
-
export { createQueueRunContext } from './packem_shared/createQueueRunContext-C8jboCk6.mjs';
|
|
1
|
+
import{createQueueCaptureSink as t,shouldCaptureQueue as r}from"./packem_shared/createQueueCaptureSink-saac8Ywl.mjs";import{createQueueContext as a}from"./packem_shared/createQueueContext-BuXG9VQI.mjs";import{default as i}from"./packem_shared/createQueues-BZBGfWI4.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-GZMtLZNW.mjs";import{createQueueRunContext as C}from"./packem_shared/createQueueRunContext-B-TpEaZa.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-BZBGfWI4.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 p=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)}}),N="/_lunora/scheduler/dispatch",y=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},g=(t,e,n)=>{try{const o=JSON.parse(n)?.error;if(typeof o=="object"&&o!==null&&typeof o.code=="string"){const{code:s,data:c,message:i}=o;return new a(s,typeof i=="string"?i:void 0,{data:c,status:e})}}catch{}return new a("INTERNAL",`${t}: function dispatch failed (${String(e)}): ${n}`,{status:e})},w=t=>{const{label:e}=t,n=globalThis.fetch,o=t.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(s,c,i={})=>{if(typeof o!="function")throw new TypeError(`${e}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const u=t.env.LUNORA_ORIGIN_URL;if(typeof u!="string"||u.length===0)throw new a("INTERNAL",`${e}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const l=t.env.LUNORA_ADMIN_TOKEN;if(typeof l!="string"||l.length===0)throw new a("INTERNAL",`${e}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const d=`${y(u)}${N}`,f={authorization:`Bearer ${l}`,"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"]=JSON.stringify(t.identity.claims));const r=await o(d,{body:JSON.stringify({args:c??{},functionPath:s.__lunoraRef,shardKey:i.shardKey}),headers:f,method:"POST"});if(!r.ok)throw g(e,r.status,await r.text());const h=await r.text();if(h.length!==0)try{return JSON.parse(h)}catch{throw new a("INTERNAL",`${e}: function dispatch returned a non-JSON body (${String(r.status)}): ${h}`,{status:r.status})}}},I=t=>({env:t.env,log:p(`[queue:${t.exportName}]`),run:w({env:t.env,fetchImpl:t.fetchImpl,label:"@lunora/queue"})});export{I as createQueueRunContext};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const u=r=>({send:async(s,n)=>{await r.send(s,n)},sendBatch:async(s,n)=>{await r.sendBatch(s,n)}}),d=r=>{const s=r.bindings??{},n=Object.create(null);for(const[t,e]of Object.entries(s))n[t]=u(e);const c=Object.keys(n),a=t=>{const e=c.length===0?"no queues are declared":`known queues: ${c.join(", ")}`,o=()=>Promise.reject(new Error(`@lunora/queue: no queue named "${t}" (${e})`));return{send:o,sendBatch:o}};return new Proxy(n,{get(t,e){if(typeof e=="string")return Object.hasOwn(t,e)?t[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 m}from"@lunora/errors";import{createQueueRunContext as d}from"./createQueueRunContext-B-TpEaZa.mjs";const l=3,f=e=>{if(e instanceof Date)return e.getTime();const r=typeof e=="number"?e:Number(e);return Number.isFinite(r)?r:0},h=e=>{const r=new Map,o=e.messages,n=o.map(t=>({ack:()=>{r.set(t,"ack"),t.ack()},get attempts(){return t.attempts},get body(){return t.body},get id(){return t.id},retry:a=>{r.set(t,"retry"),t.retry(a)},get timestamp(){return t.timestamp}})),i=t=>{for(const a of o)r.has(a)||r.set(a,t)},c={ackAll:()=>{i("ack"),e.ackAll()},messages:n,queue:e.queue,retryAll:t=>{i("retry"),e.retryAll(t)}};return{dispositions:r,originals:o,wrappedBatch:c}},y=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)},g=(e,r,o,n,i)=>{const c=n?y(i):void 0,t=typeof r.definition.maxRetries=="number"?r.definition.maxRetries:l;return e.originals.map(a=>{const s=e.dispositions.get(a)??(n?"error":"ack"),u=typeof a.attempts=="number"?a.attempts:1;return{attempts:u,body:a.body,deadLettered:s!=="ack"&&u>t,error:s==="error"?c:void 0,exportName:r.exportName,messageId:a.id,outcome:s,queue:o,timestamp:f(a.timestamp)}})},w=async(e,r,o)=>{const n=Object.hasOwn(r,e.queue)?r[e.queue]:void 0;if(n===void 0){const u=Object.keys(r),p=u.length===0?"no push queues are declared":`known push queues: ${u.join(", ")}`;throw new m("INTERNAL",`@lunora/queue: received a batch for queue "${e.queue}" but no push handler is registered (${p})`)}const{handler:i}=n.definition;if(typeof i!="function")throw new TypeError(`@lunora/queue: queue "${e.queue}" (${n.exportName}) has no push handler — it is declared as a pull consumer`);const c=d({env:o.env,exportName:n.exportName,fetchImpl:o.fetchImpl});if(o.capture===void 0){await i(c,e);return}const t=h(e);let a=!1,s;try{await i(c,t.wrappedBatch)}catch(u){a=!0,s=u}try{const u=g(t,n,e.queue,a,s);await o.capture(u)}catch(u){console.warn("@lunora/queue: capture sink failed (delivery unaffected):",u)}if(a)throw s};export{w 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.12",
|
|
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",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
47
|
+
"@lunora/errors": "1.0.0-alpha.9",
|
|
48
|
+
"@lunora/platform": "1.0.0-alpha.1"
|
|
48
49
|
},
|
|
49
50
|
"engines": {
|
|
50
51
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const RECORD_QUEUE_MESSAGE_OP = "__lunora_admin__:recordQueueMessage";
|
|
4
|
-
const DEFAULT_ROOT_SHARD = "__root__";
|
|
5
|
-
const DEV_ENVIRONMENT_PATTERN = /^(?:dev(?:elopment)?|local(?:host)?|test)$/iu;
|
|
6
|
-
const ENVIRONMENT_VARS = ["CF_ENV", "ENVIRONMENT", "NODE_ENV", "WORKER_ENV"];
|
|
7
|
-
const CAPTURE_FETCH_TIMEOUT_MS = 5e3;
|
|
8
|
-
const shouldCaptureQueue = (env) => {
|
|
9
|
-
const flag = env["LUNORA_QUEUE_CAPTURE"];
|
|
10
|
-
if (typeof flag === "string") {
|
|
11
|
-
return flag === "1" || flag.toLowerCase() === "true";
|
|
12
|
-
}
|
|
13
|
-
return ENVIRONMENT_VARS.some((key) => {
|
|
14
|
-
const value = env[key];
|
|
15
|
-
return typeof value === "string" && DEV_ENVIRONMENT_PATTERN.test(value);
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
const createQueueCaptureSink = (env, options = {}) => {
|
|
19
|
-
const rootShard = options.rootShard ?? DEFAULT_ROOT_SHARD;
|
|
20
|
-
return async (messages) => {
|
|
21
|
-
if (messages.length === 0) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const binding = env["SHARD"];
|
|
25
|
-
const adminToken = typeof env["LUNORA_ADMIN_TOKEN"] === "string" ? env["LUNORA_ADMIN_TOKEN"] : void 0;
|
|
26
|
-
if (binding === void 0 || adminToken === void 0) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
let namespace = binding;
|
|
30
|
-
if (options.jurisdiction !== void 0) {
|
|
31
|
-
if (typeof binding.jurisdiction !== "function") {
|
|
32
|
-
throw new TypeError(
|
|
33
|
-
`@lunora/queue: Durable Object namespace does not support jurisdiction("${options.jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
namespace = binding.jurisdiction(options.jurisdiction);
|
|
37
|
-
}
|
|
38
|
-
const stub = namespace.get(namespace.idFromName(rootShard));
|
|
39
|
-
const controller = new AbortController();
|
|
40
|
-
const timeout = setTimeout(() => {
|
|
41
|
-
controller.abort();
|
|
42
|
-
}, CAPTURE_FETCH_TIMEOUT_MS);
|
|
43
|
-
try {
|
|
44
|
-
const response = await stub.fetch("https://shard.internal/rpc", {
|
|
45
|
-
body: JSON.stringify({ args: { messages }, functionPath: RECORD_QUEUE_MESSAGE_OP }),
|
|
46
|
-
headers: { authorization: `Bearer ${adminToken}`, "content-type": "application/json" },
|
|
47
|
-
method: "POST",
|
|
48
|
-
signal: controller.signal
|
|
49
|
-
});
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
const detail = await response.text().catch(() => "");
|
|
52
|
-
throw new LunoraError(
|
|
53
|
-
"INTERNAL",
|
|
54
|
-
`@lunora/queue: capture write to the root shard failed (${String(response.status)} ${response.statusText})${detail === "" ? "" : `: ${detail}`}`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
await response.body?.cancel();
|
|
58
|
-
} finally {
|
|
59
|
-
clearTimeout(timeout);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export { createQueueCaptureSink, shouldCaptureQueue };
|
|
@@ -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,94 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const createDispatchLogger = (prefix) => {
|
|
4
|
-
return {
|
|
5
|
-
debug: (message, ...rest) => {
|
|
6
|
-
console.debug(prefix, message, ...rest);
|
|
7
|
-
},
|
|
8
|
-
error: (message, ...rest) => {
|
|
9
|
-
console.error(prefix, message, ...rest);
|
|
10
|
-
},
|
|
11
|
-
info: (message, ...rest) => {
|
|
12
|
-
console.info(prefix, message, ...rest);
|
|
13
|
-
},
|
|
14
|
-
warn: (message, ...rest) => {
|
|
15
|
-
console.warn(prefix, message, ...rest);
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
|
|
21
|
-
const trimTrailingSlashes = (value) => {
|
|
22
|
-
let end = value.length;
|
|
23
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
24
|
-
end -= 1;
|
|
25
|
-
}
|
|
26
|
-
return value.slice(0, end);
|
|
27
|
-
};
|
|
28
|
-
const toDispatchError = (label, status, rawBody) => {
|
|
29
|
-
try {
|
|
30
|
-
const parsed = JSON.parse(rawBody);
|
|
31
|
-
const errorBody = parsed?.error;
|
|
32
|
-
if (typeof errorBody === "object" && errorBody !== null && typeof errorBody.code === "string") {
|
|
33
|
-
const { code, data, message } = errorBody;
|
|
34
|
-
return new LunoraError(code, typeof message === "string" ? message : void 0, { data, status });
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
38
|
-
return new LunoraError("INTERNAL", `${label}: function dispatch failed (${String(status)}): ${rawBody}`, { status });
|
|
39
|
-
};
|
|
40
|
-
const createDispatchRunner = (options) => {
|
|
41
|
-
const { label } = options;
|
|
42
|
-
const globalFetch = globalThis.fetch;
|
|
43
|
-
const fetchImpl = options.fetchImpl ?? (typeof globalFetch === "function" ? globalFetch.bind(globalThis) : void 0);
|
|
44
|
-
return async (function_, args, runOptions = {}) => {
|
|
45
|
-
if (typeof fetchImpl !== "function") {
|
|
46
|
-
throw new TypeError(`${label}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);
|
|
47
|
-
}
|
|
48
|
-
const origin = options.env.LUNORA_ORIGIN_URL;
|
|
49
|
-
if (typeof origin !== "string" || origin.length === 0) {
|
|
50
|
-
throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);
|
|
51
|
-
}
|
|
52
|
-
const token = options.env.LUNORA_ADMIN_TOKEN;
|
|
53
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
54
|
-
throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);
|
|
55
|
-
}
|
|
56
|
-
const url = `${trimTrailingSlashes(origin)}${SCHEDULER_DISPATCH_PATH}`;
|
|
57
|
-
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
58
|
-
if (options.identity?.userId !== void 0) {
|
|
59
|
-
headers["x-lunora-userid"] = options.identity.userId;
|
|
60
|
-
}
|
|
61
|
-
if (options.identity?.claims !== void 0) {
|
|
62
|
-
headers["x-lunora-identity"] = JSON.stringify(options.identity.claims);
|
|
63
|
-
}
|
|
64
|
-
const response = await fetchImpl(url, {
|
|
65
|
-
body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
|
|
66
|
-
headers,
|
|
67
|
-
method: "POST"
|
|
68
|
-
});
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
throw toDispatchError(label, response.status, await response.text());
|
|
71
|
-
}
|
|
72
|
-
const text = await response.text();
|
|
73
|
-
if (text.length === 0) {
|
|
74
|
-
return void 0;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(text);
|
|
78
|
-
} catch {
|
|
79
|
-
throw new LunoraError("INTERNAL", `${label}: function dispatch returned a non-JSON body (${String(response.status)}): ${text}`, {
|
|
80
|
-
status: response.status
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const createQueueRunContext = (options) => {
|
|
87
|
-
return {
|
|
88
|
-
env: options.env,
|
|
89
|
-
log: createDispatchLogger(`[queue:${options.exportName}]`),
|
|
90
|
-
run: createDispatchRunner({ env: options.env, fetchImpl: options.fetchImpl, label: "@lunora/queue" })
|
|
91
|
-
};
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
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,132 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { createQueueRunContext } from './createQueueRunContext-C8jboCk6.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 = Object.hasOwn(registry, batch.queue) ? registry[batch.queue] : void 0;
|
|
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 (captureError) {
|
|
125
|
-
console.warn("@lunora/queue: capture sink failed (delivery unaffected):", captureError);
|
|
126
|
-
}
|
|
127
|
-
if (threw) {
|
|
128
|
-
throw handlerError;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
export { dispatchQueueBatch };
|