@lunora/queue 1.0.0-alpha.81 → 1.0.0-alpha.83

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
@@ -44,6 +44,18 @@ batch.
44
44
  await ctx.queues.emailQueue.send({ to: user.email });
45
45
  ```
46
46
 
47
+ For one event with several consumers, declare a topic and subscribe to it.
48
+ Each subscription is its own queue, with its own retries and dead-letter queue:
49
+
50
+ ```ts
51
+ // lunora/queues.ts
52
+ export const signups = defineTopic<{ userId: string }>();
53
+ export const welcomeEmail = defineSubscription(signups, { handler: async (ctx, batch) => {} });
54
+
55
+ // inside a mutation/action
56
+ await ctx.topics.signups.publish({ userId });
57
+ ```
58
+
47
59
  Codegen emits the typed `ctx.queues` producer and the worker `queue()` dispatch;
48
60
  `@lunora/config` reconciles the wrangler `queues.producers[]` /
49
61
  `queues.consumers[]` entries from the same definition. Scaffold a new queue with
package/dist/index.d.mts CHANGED
@@ -170,6 +170,64 @@ interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
170
170
  /** Runtime brand identifying a `defineQueue` result. */
171
171
  isLunoraQueue: true;
172
172
  }
173
+ /**
174
+ * The typed publisher bound to `ctx.topics.<name>`. A publish is one send to
175
+ * **each** subscription's queue, in parallel, so it is not atomic: when one send
176
+ * fails the call rejects, and a retry re-delivers to the subscriptions whose send
177
+ * already succeeded. Delivery is at-least-once per subscription — make handlers
178
+ * idempotent. A topic with no subscriptions publishes to nobody.
179
+ */
180
+ interface TopicPublisher<Payload = unknown> {
181
+ /** Publish one message to every subscription. */
182
+ publish: (payload: Payload, options?: QueueSendOptions) => Promise<void>;
183
+ /** Publish a batch (at most 100) to every subscription in one send per subscription. */
184
+ publishBatch: (messages: Iterable<MessageSendRequestLike<Payload>>, options?: QueueSendBatchOptions) => Promise<void>;
185
+ }
186
+ /** `ctx.topics` — declared topic export names → typed publishers. Codegen narrows it to the exact names. */
187
+ interface Topics {
188
+ [exportName: string]: TopicPublisher;
189
+ }
190
+ /** The branded result of `defineTopic`, discovered by codegen. Carries no deploy config of its own. */
191
+ interface TopicDefinition<Payload = unknown> {
192
+ /**
193
+ * Phantom carrier for the payload type, so codegen can type `ctx.topics.<name>`.
194
+ * Named like {@link QueueDefinition}'s so one emitted helper reads both. Never
195
+ * assigned at runtime.
196
+ */
197
+ readonly __lunoraBody?: Payload;
198
+ /** Runtime brand identifying a `defineTopic` result. */
199
+ isLunoraTopic: true;
200
+ }
201
+ /** The config passed to `defineSubscription`: a push consumer's handler plus its own retry/DLQ tuning. */
202
+ interface SubscriptionConfig<Payload = unknown> extends QueueConsumerTuning {
203
+ handler: QueueHandler<Payload>;
204
+ /**
205
+ * Stable wrangler queue name for this subscription. Defaults to the
206
+ * kebab-cased export name, exactly like a `defineQueue` export.
207
+ */
208
+ name?: string;
209
+ }
210
+ /**
211
+ * The branded result of `defineSubscription`: a push {@link QueueDefinition}
212
+ * (so dispatch, retries, the DLQ, wrangler reconcile and the studio treat it as
213
+ * the queue it is) tagged with the topic it consumes.
214
+ */
215
+ interface SubscriptionDefinition<Payload = unknown> extends QueueDefinition<Payload> {
216
+ handler: QueueHandler<Payload>;
217
+ mode: "push";
218
+ /** The topic this subscription consumes. */
219
+ topic: TopicDefinition<Payload>;
220
+ }
221
+ /** Wiring info for one declared topic, emitted by codegen into the generated shard. */
222
+ interface TopicBindingSpec {
223
+ /** The `lunora/queues.ts` topic export name, e.g. `signups`. */
224
+ exportName: string;
225
+ /** One entry per subscription: its export name and its `Queue` producer binding. */
226
+ subscriptions: ReadonlyArray<{
227
+ binding: string;
228
+ exportName: string;
229
+ }>;
230
+ }
173
231
  /** Wiring info for one declared queue, emitted by codegen into the generated shard/handler. */
174
232
  interface QueueBindingSpec {
175
233
  /** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
@@ -390,4 +448,24 @@ interface RunContextOptions {
390
448
  }
391
449
  /** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
392
450
  declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
393
- 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 };
451
+ /**
452
+ * Declare a topic in `lunora/queues.ts`. The payload type is the only thing a
453
+ * topic carries; delivery config lives on each subscription.
454
+ */
455
+ declare const defineTopic: <Payload = unknown>() => TopicDefinition<Payload>;
456
+ /**
457
+ * Subscribe to a topic. The result is a push queue definition — export it from
458
+ * `lunora/queues.ts` and it deploys, retries and dead-letters exactly like a
459
+ * `defineQueue` export of the same name.
460
+ *
461
+ * ⚠️ Same trust model as a queue handler: `message.run(...)` dispatches with the
462
+ * system identity, so validate `message.body` before acting on it.
463
+ */
464
+ declare const defineSubscription: <Payload>(topic: TopicDefinition<Payload>, config: SubscriptionConfig<Payload>) => SubscriptionDefinition<Payload>;
465
+ /**
466
+ * Build `ctx.topics` for a request from the Worker `env` and the codegen-emitted
467
+ * specs. A topic with no subscriptions publishes to nobody; a topic with an
468
+ * unbound subscription rejects every publish (see {@link unboundPublisher}).
469
+ */
470
+ declare const createTopicContext: (env: Record<string, unknown>, specs: ReadonlyArray<TopicBindingSpec>) => Topics;
471
+ 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, type SubscriptionConfig, type SubscriptionDefinition, type TopicBindingSpec, type TopicDefinition, type TopicPublisher, type Topics, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, createTopicContext, defineQueue, defineSubscription, defineTopic, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
package/dist/index.d.ts CHANGED
@@ -170,6 +170,64 @@ interface QueueDefinition<Body = unknown> extends QueueConfig<Body> {
170
170
  /** Runtime brand identifying a `defineQueue` result. */
171
171
  isLunoraQueue: true;
172
172
  }
173
+ /**
174
+ * The typed publisher bound to `ctx.topics.<name>`. A publish is one send to
175
+ * **each** subscription's queue, in parallel, so it is not atomic: when one send
176
+ * fails the call rejects, and a retry re-delivers to the subscriptions whose send
177
+ * already succeeded. Delivery is at-least-once per subscription — make handlers
178
+ * idempotent. A topic with no subscriptions publishes to nobody.
179
+ */
180
+ interface TopicPublisher<Payload = unknown> {
181
+ /** Publish one message to every subscription. */
182
+ publish: (payload: Payload, options?: QueueSendOptions) => Promise<void>;
183
+ /** Publish a batch (at most 100) to every subscription in one send per subscription. */
184
+ publishBatch: (messages: Iterable<MessageSendRequestLike<Payload>>, options?: QueueSendBatchOptions) => Promise<void>;
185
+ }
186
+ /** `ctx.topics` — declared topic export names → typed publishers. Codegen narrows it to the exact names. */
187
+ interface Topics {
188
+ [exportName: string]: TopicPublisher;
189
+ }
190
+ /** The branded result of `defineTopic`, discovered by codegen. Carries no deploy config of its own. */
191
+ interface TopicDefinition<Payload = unknown> {
192
+ /**
193
+ * Phantom carrier for the payload type, so codegen can type `ctx.topics.<name>`.
194
+ * Named like {@link QueueDefinition}'s so one emitted helper reads both. Never
195
+ * assigned at runtime.
196
+ */
197
+ readonly __lunoraBody?: Payload;
198
+ /** Runtime brand identifying a `defineTopic` result. */
199
+ isLunoraTopic: true;
200
+ }
201
+ /** The config passed to `defineSubscription`: a push consumer's handler plus its own retry/DLQ tuning. */
202
+ interface SubscriptionConfig<Payload = unknown> extends QueueConsumerTuning {
203
+ handler: QueueHandler<Payload>;
204
+ /**
205
+ * Stable wrangler queue name for this subscription. Defaults to the
206
+ * kebab-cased export name, exactly like a `defineQueue` export.
207
+ */
208
+ name?: string;
209
+ }
210
+ /**
211
+ * The branded result of `defineSubscription`: a push {@link QueueDefinition}
212
+ * (so dispatch, retries, the DLQ, wrangler reconcile and the studio treat it as
213
+ * the queue it is) tagged with the topic it consumes.
214
+ */
215
+ interface SubscriptionDefinition<Payload = unknown> extends QueueDefinition<Payload> {
216
+ handler: QueueHandler<Payload>;
217
+ mode: "push";
218
+ /** The topic this subscription consumes. */
219
+ topic: TopicDefinition<Payload>;
220
+ }
221
+ /** Wiring info for one declared topic, emitted by codegen into the generated shard. */
222
+ interface TopicBindingSpec {
223
+ /** The `lunora/queues.ts` topic export name, e.g. `signups`. */
224
+ exportName: string;
225
+ /** One entry per subscription: its export name and its `Queue` producer binding. */
226
+ subscriptions: ReadonlyArray<{
227
+ binding: string;
228
+ exportName: string;
229
+ }>;
230
+ }
173
231
  /** Wiring info for one declared queue, emitted by codegen into the generated shard/handler. */
174
232
  interface QueueBindingSpec {
175
233
  /** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
@@ -390,4 +448,24 @@ interface RunContextOptions {
390
448
  }
391
449
  /** Assemble the {@link QueueRunContext} passed to a `defineQueue` handler. */
392
450
  declare const createQueueRunContext: (options: RunContextOptions) => QueueRunContext;
393
- 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 };
451
+ /**
452
+ * Declare a topic in `lunora/queues.ts`. The payload type is the only thing a
453
+ * topic carries; delivery config lives on each subscription.
454
+ */
455
+ declare const defineTopic: <Payload = unknown>() => TopicDefinition<Payload>;
456
+ /**
457
+ * Subscribe to a topic. The result is a push queue definition — export it from
458
+ * `lunora/queues.ts` and it deploys, retries and dead-letters exactly like a
459
+ * `defineQueue` export of the same name.
460
+ *
461
+ * ⚠️ Same trust model as a queue handler: `message.run(...)` dispatches with the
462
+ * system identity, so validate `message.body` before acting on it.
463
+ */
464
+ declare const defineSubscription: <Payload>(topic: TopicDefinition<Payload>, config: SubscriptionConfig<Payload>) => SubscriptionDefinition<Payload>;
465
+ /**
466
+ * Build `ctx.topics` for a request from the Worker `env` and the codegen-emitted
467
+ * specs. A topic with no subscriptions publishes to nobody; a topic with an
468
+ * unbound subscription rejects every publish (see {@link unboundPublisher}).
469
+ */
470
+ declare const createTopicContext: (env: Record<string, unknown>, specs: ReadonlyArray<TopicBindingSpec>) => Topics;
471
+ 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, type SubscriptionConfig, type SubscriptionDefinition, type TopicBindingSpec, type TopicDefinition, type TopicPublisher, type Topics, createQueueCaptureSink, createQueueContext, createQueueRunContext, createQueues, createTopicContext, defineQueue, defineSubscription, defineTopic, dispatchQueueBatch, isQueueDefinition, queueBindingName, queueDefaultName, shouldCaptureQueue };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createQueueCaptureSink as t,shouldCaptureQueue as r}from"./packem_shared/createQueueCaptureSink-CSOPftvj.mjs";import{createQueueContext as a}from"./packem_shared/createQueueContext-CZ3ZpUFb.mjs";import{default as i}from"./packem_shared/createQueues-BVwkHkeb.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-B8GbPNBs.mjs";import{c as C}from"./packem_shared/run-context-wO-J6k6F.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 o}from"./packem_shared/createQueueCaptureSink-CSOPftvj.mjs";import{createQueueContext as i}from"./packem_shared/createQueueContext-aj6p-gxk.mjs";import{createQueues as n}from"./packem_shared/createQueues-BVaXTnAh.mjs";import{defineQueue as f,isQueueDefinition as c,queueBindingName as x,queueDefaultName as m}from"./packem_shared/defineQueue-TY4-i3nG.mjs";import{dispatchQueueBatch as d}from"./packem_shared/dispatchQueueBatch-B8GbPNBs.mjs";import{c as C}from"./packem_shared/run-context-wO-J6k6F.mjs";import{createTopicContext as l,defineSubscription as q,defineTopic as B}from"./packem_shared/createTopicContext-nG16dV5W.mjs";export{t as createQueueCaptureSink,i as createQueueContext,C as createQueueRunContext,n as createQueues,l as createTopicContext,f as defineQueue,q as defineSubscription,B as defineTopic,d as dispatchQueueBatch,c as isQueueDefinition,x as queueBindingName,m as queueDefaultName,o as shouldCaptureQueue};
@@ -0,0 +1 @@
1
+ import{resolveQueueBinding as r,createQueues as s}from"./createQueues-BVaXTnAh.mjs";const u=(t,i)=>{const e={};for(const n of i){const o=r(t,n.binding);o&&(e[n.exportName]=o)}return s({bindings:e})};export{u as createQueueContext};
@@ -0,0 +1 @@
1
+ import{LunoraError as a}from"@lunora/errors";import{h as g,R as h}from"./requeue-envelope-BjL4JftH.mjs";const i=100,d=43200,l=(t,n)=>{if(t!==void 0&&t>d)throw new a("VALIDATION_ERROR",`@lunora/queue: ${n} delaySeconds is ${String(t)}, over the Cloudflare Queues ceiling of ${String(d)} (12 hours) — use @lunora/scheduler for longer schedules`)},$=(t,n)=>{if(g(t))throw new a("VALIDATION_ERROR",`@lunora/queue: ${n} body may not contain the reserved key "${h}"`)},f=(t,n,e)=>{l(n?.delaySeconds,e),$(t,e)},E=(t,n,e)=>{l(n?.delaySeconds,e);const s=[...t];if(s.length>i)throw new a("VALIDATION_ERROR",`@lunora/queue: ${e} exceeds ${String(i)} (got ${String(s.length)}) — split across calls`);for(const[r,o]of s.entries())f(o.body,o,`${e} message ${String(r)}`);return s},O=t=>({send:async(n,e)=>{f(n,e,"send"),await t.send(n,e)},sendBatch:async(n,e)=>{await t.sendBatch(E(n,e,"sendBatch"),e)}}),y=(t,n)=>{const e=t[n];return e&&typeof e.send=="function"&&typeof e.sendBatch=="function"?e:void 0},R=(t,n,e)=>{const s=Object.assign(Object.create(null),t),r=Object.keys(s),o=r.length===0?`no ${n}s are declared`:`known ${n}s: ${r.join(", ")}`;return new Proxy(s,{get(u,c){if(typeof c=="string")return Object.hasOwn(u,c)?u[c]:e(()=>Promise.reject(new Error(`@lunora/queue: no ${n} named "${c}" (${o})`)))}})},w=t=>{const n=t.bindings??{},e={};for(const[s,r]of Object.entries(n))e[s]=O(r);return R(e,"queue",s=>({send:s,sendBatch:s}))};export{f as assertSendable,w as createQueues,R as namedLookup,E as prepareBatch,y as resolveQueueBinding};
@@ -0,0 +1 @@
1
+ import{resolveQueueBinding as c,namedLookup as p,prepareBatch as a,assertSendable as h}from"./createQueues-BVaXTnAh.mjs";const f=()=>({isLunoraTopic:!0}),b=n=>typeof n=="object"&&n!==null&&n.isLunoraTopic===!0,w=(n,e)=>{if(!b(n))throw new TypeError("defineSubscription: the first argument must be a `defineTopic()` result");if(typeof e.handler!="function")throw new TypeError("defineSubscription: `handler` must be a function");if(e.name!==void 0&&(typeof e.name!="string"||e.name.length===0))throw new TypeError("defineSubscription: `name` must be a non-empty string when provided");return{...e,isLunoraQueue:!0,mode:"push",topic:n}},l=n=>({publish:async(e,t)=>{h(e,t,"publish"),await Promise.all(n.map(async r=>r.send(e,t)))},publishBatch:async(e,t)=>{const r=a(e,t,"publishBatch");await Promise.all(n.map(async i=>i.sendBatch(r,t)))}}),d=n=>{const e=()=>Promise.reject(new Error(`@lunora/queue: missing queue binding(s) ${n.join(", ")} — add them to wrangler.jsonc (lunora dev reconciles them); nothing was published`));return{publish:e,publishBatch:e}},y=(n,e)=>{const t={};for(const r of e){const i=[],s=[];for(const o of r.subscriptions){const u=c(n,o.binding);u?i.push(u):s.push(o.binding)}t[r.exportName]=s.length>0?d(s):l(i)}return p(t,"topic",r=>({publish:r,publishBatch:r}))};export{y as createTopicContext,w as defineSubscription,f as defineTopic};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/queue",
3
- "version": "1.0.0-alpha.81",
3
+ "version": "1.0.0-alpha.83",
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.48",
48
- "@lunora/platform": "1.0.0-alpha.46"
48
+ "@lunora/platform": "1.0.0-alpha.47"
49
49
  },
50
50
  "engines": {
51
51
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
1
- import i from"./createQueues-BVwkHkeb.mjs";const s=(o,c)=>{const n={};for(const t of c){const e=o[t.binding];e&&typeof e.send=="function"&&typeof e.sendBatch=="function"&&(n[t.exportName]=e)}return i({bindings:n})};export{s as createQueueContext};
@@ -1 +0,0 @@
1
- import{LunoraError as u}from"@lunora/errors";import{h as g,R as f}from"./requeue-envelope-BjL4JftH.mjs";const i=100,l=43200,c=(e,n)=>{if(e!==void 0&&e>l)throw new u("VALIDATION_ERROR",`@lunora/queue: ${n} delaySeconds is ${String(e)}, over the Cloudflare Queues ceiling of ${String(l)} (12 hours) — use @lunora/scheduler for longer schedules`)},h=(e,n)=>{if(g(e))throw new u("VALIDATION_ERROR",`@lunora/queue: ${n} body may not contain the reserved key "${f}"`)},w=e=>({send:async(n,s)=>{c(s?.delaySeconds,"send"),h(n,"send"),await e.send(n,s)},sendBatch:async(n,s)=>{c(s?.delaySeconds,"sendBatch");const o=[...n];if(o.length>i)throw new u("VALIDATION_ERROR",`@lunora/queue: sendBatch exceeds ${String(i)} (got ${String(o.length)}) — split across calls`);for(const[a,r]of o.entries())c(r.delaySeconds,`sendBatch message ${String(a)}`),h(r.body,`sendBatch message ${String(a)}`);await e.sendBatch(o,s)}}),m=e=>{const n=e.bindings??{},s=Object.create(null);for(const[r,t]of Object.entries(n))s[r]=w(t);const o=Object.keys(s),a=r=>{const t=o.length===0?"no queues are declared":`known queues: ${o.join(", ")}`,d=()=>Promise.reject(new Error(`@lunora/queue: no queue named "${r}" (${t})`));return{send:d,sendBatch:d}};return new Proxy(s,{get(r,t){if(typeof t=="string")return Object.hasOwn(r,t)?r[t]:a(t)}})};export{m as default};