@btravstack/amqp-worker 0.45.0 → 0.46.0

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
@@ -13,7 +13,7 @@
13
13
 
14
14
  ```sh
15
15
  pnpm add @btravstack/amqp-worker @btravstack/core @btravstack/config @btravstack/di unthrown \
16
- @amqp-contract/worker@^3.0.0-beta.7 @opentelemetry/api
16
+ @amqp-contract/worker@^3.0.0-beta.11 @opentelemetry/api
17
17
  ```
18
18
 
19
19
  All six are peer dependencies — install them (`@opentelemetry/api` because
@@ -221,3 +221,13 @@ It keeps private infrastructure hidden and business exports explicit.
221
221
  ## License
222
222
 
223
223
  [MIT](./LICENSE) © Benoit TRAVERS
224
+
225
+ ## Broadcast subscriptions
226
+
227
+ `amqpSubscription(name, { contract, settings, inject?, sync })` consumes
228
+ best-effort facts beside an HTTP runtime, with one exclusive ephemeral queue
229
+ per replica. It reports failures and broker health and closes with the scope.
230
+ `InvalidAmqpSubscription` names a refused configuration;
231
+ `SubscriptionHandlers<C, E>` types the contract's handler record.
232
+ See [the subscription reference](https://btravstack.github.io/btravstack/reference/amqp-worker#broadcast-subscriptions-beside-another-runtime)
233
+ for the typed contract, settings port and delivery guarantees.
package/dist/index.cjs CHANGED
@@ -327,9 +327,77 @@ const AmqpModule = (name) => (options) => {
327
327
  });
328
328
  };
329
329
  //#endregion
330
+ //#region src/subscription.ts
331
+ /** A subscription requires explicitly ephemeral queues with no retry or dead-letter routing. */
332
+ var InvalidAmqpSubscription = class extends (0, unthrown.TaggedError)("InvalidAmqpSubscription") {
333
+ message = "subscriptions require consumers on exclusive auto-delete non-durable classic queues with no retry or dead letter, and no RPCs";
334
+ };
335
+ /**
336
+ * Consume broadcast facts for the application scope's lifetime, beside its one
337
+ * runtime. Every validated delivery is acknowledged, including handler failures;
338
+ * failures are reported to Observers. Each replica must name its own queues.
339
+ */
340
+ const amqpSubscription = (name, options) => {
341
+ const resource = (0, _btravstack_di.Port)(`AmqpSubscription:${name}`);
342
+ return (0, _btravstack_di.Module)(`AmqpSubscription:${name}`)({
343
+ provides: [
344
+ _btravstack_core.noObserverMember,
345
+ (0, _btravstack_di.Provider)(resource)({
346
+ inject: {
347
+ settings: options.settings,
348
+ observers: _btravstack_core.Observers,
349
+ ...Object.fromEntries(Object.entries(options.inject ?? {}).map(([key, port]) => [`dependency:${key}`, port]))
350
+ },
351
+ acquire: (services) => {
352
+ const consumers = Object.values(options.contract.consumers ?? {});
353
+ if (consumers.length === 0 || Object.keys(options.contract.rpcs ?? {}).length > 0 || consumers.some(({ queue }) => queue.type !== "classic" || queue.durable || queue.exclusive !== true || queue.autoDelete !== true || queue.retry.mode !== "none" || queue.deadLetter !== void 0 || queue.arguments?.["x-dead-letter-exchange"] !== void 0)) return (0, unthrown.ErrAsync)(new InvalidAmqpSubscription({ subscription: name }));
354
+ const dependencies = Object.fromEntries(Object.keys(options.inject ?? {}).map((key) => [key, services[`dependency:${key}`]]));
355
+ const handlers = options.sync(dependencies);
356
+ const observers = services.observers;
357
+ const wrapped = Object.fromEntries(Object.entries(handlers).map(([key, handler]) => [key, (...args) => {
358
+ const settle = (0, _btravstack_core.observe)(observers, {
359
+ component: "amqp",
360
+ name: "subscription",
361
+ attributes: {
362
+ subscription: name,
363
+ consumer: key
364
+ }
365
+ });
366
+ return (0, unthrown.fromSafePromise)((async () => {
367
+ const result = await (0, unthrown.OkAsync)().flatMap(() => handler(...args) ?? (0, unthrown.Ok)());
368
+ settle(result.isOk() ? { outcome: "ok" } : {
369
+ outcome: "error",
370
+ cause: result.isErr() ? result.error : result.cause
371
+ });
372
+ })());
373
+ }]));
374
+ return _amqp_contract_worker.TypedAmqpWorker.create({
375
+ contract: options.contract,
376
+ handlers: wrapped,
377
+ urls: [services.settings.url],
378
+ connectTimeoutMs: services.settings.connectTimeoutMs ?? 5e3,
379
+ logger: brokerLog(observers)
380
+ });
381
+ },
382
+ release: (worker) => worker.close().get()
383
+ }),
384
+ _btravstack_di.Provider.member(_btravstack_core.HealthChecks)({
385
+ inject: { worker: resource },
386
+ sync: ({ worker }) => ({
387
+ name: `amqp-subscription:${name}`,
388
+ check: () => worker.isConnected() ? (0, unthrown.OkAsync)() : (0, unthrown.ErrAsync)(new _btravstack_core.HealthCheckFailed({ reason: "broker disconnected" }))
389
+ })
390
+ })
391
+ ],
392
+ exports: [_btravstack_core.HealthChecks]
393
+ });
394
+ };
395
+ //#endregion
330
396
  exports.AmqpConfig = AmqpConfig;
331
397
  exports.AmqpHandler = AmqpHandler;
332
398
  exports.AmqpHandlers = AmqpHandlers;
333
399
  exports.AmqpMessage = AmqpMessage;
334
400
  exports.AmqpModule = AmqpModule;
335
401
  exports.AmqpRuntime = AmqpRuntime;
402
+ exports.InvalidAmqpSubscription = InvalidAmqpSubscription;
403
+ exports.amqpSubscription = amqpSubscription;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ConfigInvalid, EnvReading } from "@btravstack/config";
2
- import { AnyUnitModule, Refuse, Runtime, RuntimePort, UnitGate, UnitNeedsOf, UnitRecordOf } from "@btravstack/core";
3
- import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, ServiceOf } from "@btravstack/di";
4
- import { ConsumerOptions, TypedAmqpWorker, WorkerInferHandlers } from "@amqp-contract/worker";
2
+ import { AnyUnitModule, HealthChecks, Refuse, Runtime, RuntimePort, UnitGate, UnitNeedsOf, UnitRecordOf } from "@btravstack/core";
3
+ import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope, ServiceOf } from "@btravstack/di";
4
+ import { ConnectionError, ConsumerOptions, TypedAmqpWorker, WorkerInferConsumerHandler, WorkerInferHandlers } from "@amqp-contract/worker";
5
+ import { AsyncResult, Result } from "unthrown";
5
6
  //#region src/handler.d.ts
6
7
  /** The consumer and rpc names `C` declares — the keys of its handlers record. */
7
8
  type HandlerKeyOf<C extends AnyAmqpContract> = keyof WorkerInferHandlers<C> & string;
@@ -276,4 +277,31 @@ type AmqpModuleOptions<TContract extends AnyAmqpContract, HandlersError, Handler
276
277
  */
277
278
  export declare const AmqpModule: <const Name extends string>(name: Name) => <TContract extends AnyAmqpContract, HandlersError, HandlersNeeds, Declared = never, Unit extends AnyUnitModule | undefined = undefined, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, TContract, Unit>, Provides<P, TContract, HandlersError, HandlersNeeds>>[] = [], const N extends readonly AnyPort[] | undefined = undefined>(options: AmqpModuleOptions<TContract, HandlersError, HandlersNeeds, Declared, Unit, I, P, X, N>) => Module<(Extract<(readonly [typeof AmqpRuntime, ...((import("@btravstack/core").HealthChecks extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").HealthChecks : never) | (import("@btravstack/core").Logger extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").Logger : never))[], ...X])[number], AnyModule> extends (infer T_2) ? T_2 extends Extract<(readonly [typeof AmqpRuntime, ...((import("@btravstack/core").HealthChecks extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").HealthChecks : never) | (import("@btravstack/core").Logger extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").Logger : never))[], ...X])[number], AnyModule> ? T_2 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[0] | ((readonly [typeof AmqpRuntime, ...((import("@btravstack/core").HealthChecks extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").HealthChecks : never) | (import("@btravstack/core").Logger extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").Logger : never))[], ...X])[number] extends (infer E_1) ? E_1 extends AnyPort ? InstanceType<E_1> : E_1 extends AnyProvider ? InstanceType<E_1["port"]> : never : never), ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[1] | ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_4 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[1], Exclude<((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_5) ? T_5 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_5 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[2], ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_6) ? T_6 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_6 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[0] | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_5) ? T_5 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_5 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]> | Exclude<((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_6) ? T_6 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_6 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[2], ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_6) ? T_6 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_6 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[0] | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_5) ? T_5 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_5 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]>>;
278
279
  //#endregion
279
- export type { AmqpConnectionOptions, AmqpInfo, AmqpMessageOf, AmqpMessagePortOf, AmqpModuleOptions, AmqpOptions, HandlerPortOf, HandlersInstanceOf, HandlersPortOf };
280
+ //#region src/subscription.d.ts
281
+ declare const InvalidAmqpSubscription_base: import("unthrown").TaggedErrorConstructor<"InvalidAmqpSubscription">;
282
+ /** A subscription requires explicitly ephemeral queues with no retry or dead-letter routing. */
283
+ export declare class InvalidAmqpSubscription extends InvalidAmqpSubscription_base<{
284
+ readonly subscription: string;
285
+ }> {
286
+ message: string;
287
+ }
288
+ /** Best-effort handlers retain their contract's typed input and may run synchronously. */
289
+ type SubscriptionHandlers<C extends AnyAmqpContract, E> = { readonly [K in C["consumers"] extends Record<string, unknown> ? keyof C["consumers"] : never]: (...args: Parameters<WorkerInferConsumerHandler<C, K>>) => void | Result<void, E> | AsyncResult<void, E>; };
290
+ /**
291
+ * Consume broadcast facts for the application scope's lifetime, beside its one
292
+ * runtime. Every validated delivery is acknowledged, including handler failures;
293
+ * failures are reported to Observers. Each replica must name its own queues.
294
+ */
295
+ export declare const amqpSubscription: <C extends AnyAmqpContract, Settings extends {
296
+ readonly url: string;
297
+ readonly connectTimeoutMs?: number;
298
+ }, const SettingsId extends string, E, const D extends Readonly<Record<string, AnyPort>> = Record<never, never>>(name: string, options: {
299
+ readonly contract: C;
300
+ readonly settings: PortClassOf<SettingsId, Settings>;
301
+ readonly inject?: D;
302
+ readonly sync: (services: { readonly [K in keyof D]: ServiceOf<InstanceType<D[K]>>; }) => SubscriptionHandlers<C, E>;
303
+ } & ([keyof D] extends [never] ? unknown : {
304
+ readonly inject: D;
305
+ })) => Module<HealthChecks, ConnectionError | InvalidAmqpSubscription, InstanceType<PortClassOf<SettingsId, Settings>> | InstanceType<D[keyof D]> | Scope>;
306
+ //#endregion
307
+ export type { AmqpConnectionOptions, AmqpInfo, AmqpMessageOf, AmqpMessagePortOf, AmqpModuleOptions, AmqpOptions, HandlerPortOf, HandlersInstanceOf, HandlersPortOf, SubscriptionHandlers };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,8 @@
1
- import { AnyUnitModule, Refuse, Runtime, RuntimePort, UnitGate, UnitNeedsOf, UnitRecordOf } from "@btravstack/core";
2
- import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, ServiceOf } from "@btravstack/di";
3
- import { ConsumerOptions, TypedAmqpWorker, WorkerInferHandlers } from "@amqp-contract/worker";
1
+ import { AnyUnitModule, HealthChecks, Refuse, Runtime, RuntimePort, UnitGate, UnitNeedsOf, UnitRecordOf } from "@btravstack/core";
2
+ import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope, ServiceOf } from "@btravstack/di";
3
+ import { ConnectionError, ConsumerOptions, TypedAmqpWorker, WorkerInferConsumerHandler, WorkerInferHandlers } from "@amqp-contract/worker";
4
4
  import { ConfigInvalid, EnvReading } from "@btravstack/config";
5
+ import { AsyncResult, Result } from "unthrown";
5
6
  //#region src/handler.d.ts
6
7
  /** The consumer and rpc names `C` declares — the keys of its handlers record. */
7
8
  type HandlerKeyOf<C extends AnyAmqpContract> = keyof WorkerInferHandlers<C> & string;
@@ -276,4 +277,31 @@ type AmqpModuleOptions<TContract extends AnyAmqpContract, HandlersError, Handler
276
277
  */
277
278
  export declare const AmqpModule: <const Name extends string>(name: Name) => <TContract extends AnyAmqpContract, HandlersError, HandlersNeeds, Declared = never, Unit extends AnyUnitModule | undefined = undefined, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, TContract, Unit>, Provides<P, TContract, HandlersError, HandlersNeeds>>[] = [], const N extends readonly AnyPort[] | undefined = undefined>(options: AmqpModuleOptions<TContract, HandlersError, HandlersNeeds, Declared, Unit, I, P, X, N>) => Module<(Extract<(readonly [typeof AmqpRuntime, ...((import("@btravstack/core").HealthChecks extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").HealthChecks : never) | (import("@btravstack/core").Logger extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").Logger : never))[], ...X])[number], AnyModule> extends (infer T_2) ? T_2 extends Extract<(readonly [typeof AmqpRuntime, ...((import("@btravstack/core").HealthChecks extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").HealthChecks : never) | (import("@btravstack/core").Logger extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").Logger : never))[], ...X])[number], AnyModule> ? T_2 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[0] | ((readonly [typeof AmqpRuntime, ...((import("@btravstack/core").HealthChecks extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").HealthChecks : never) | (import("@btravstack/core").Logger extends ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T) ? T extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T extends Module<infer X_1, infer _E, infer _N> ? X_1 : never : never : never) | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 extends Provider<infer X_2, infer _E_1, infer _N_1> ? X_2 : never : never : never) ? typeof import("@btravstack/core").Logger : never))[], ...X])[number] extends (infer E_1) ? E_1 extends AnyPort ? InstanceType<E_1> : E_1 extends AnyProvider ? InstanceType<E_1["port"]> : never : never), ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[1] | ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_4 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[1], Exclude<((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_5) ? T_5 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_5 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[2], ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_6) ? T_6 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_6 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[0] | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_5) ? T_5 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_5 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]> | Exclude<((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_6) ? T_6 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_6 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[2], ((readonly [...I, AmqpStarter<TContract, Unit>])[number] extends (infer T_6) ? T_6 extends (readonly [...I, AmqpStarter<TContract, Unit>])[number] ? T_6 extends Module<infer X_3, infer E, infer N_1> ? readonly [X_3, E, N_1] : never : never : never)[0] | ((readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] extends (infer T_5) ? T_5 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_5 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]>>;
278
279
  //#endregion
279
- export type { AmqpConnectionOptions, AmqpInfo, AmqpMessageOf, AmqpMessagePortOf, AmqpModuleOptions, AmqpOptions, HandlerPortOf, HandlersInstanceOf, HandlersPortOf };
280
+ //#region src/subscription.d.ts
281
+ declare const InvalidAmqpSubscription_base: import("unthrown").TaggedErrorConstructor<"InvalidAmqpSubscription">;
282
+ /** A subscription requires explicitly ephemeral queues with no retry or dead-letter routing. */
283
+ export declare class InvalidAmqpSubscription extends InvalidAmqpSubscription_base<{
284
+ readonly subscription: string;
285
+ }> {
286
+ message: string;
287
+ }
288
+ /** Best-effort handlers retain their contract's typed input and may run synchronously. */
289
+ type SubscriptionHandlers<C extends AnyAmqpContract, E> = { readonly [K in C["consumers"] extends Record<string, unknown> ? keyof C["consumers"] : never]: (...args: Parameters<WorkerInferConsumerHandler<C, K>>) => void | Result<void, E> | AsyncResult<void, E>; };
290
+ /**
291
+ * Consume broadcast facts for the application scope's lifetime, beside its one
292
+ * runtime. Every validated delivery is acknowledged, including handler failures;
293
+ * failures are reported to Observers. Each replica must name its own queues.
294
+ */
295
+ export declare const amqpSubscription: <C extends AnyAmqpContract, Settings extends {
296
+ readonly url: string;
297
+ readonly connectTimeoutMs?: number;
298
+ }, const SettingsId extends string, E, const D extends Readonly<Record<string, AnyPort>> = Record<never, never>>(name: string, options: {
299
+ readonly contract: C;
300
+ readonly settings: PortClassOf<SettingsId, Settings>;
301
+ readonly inject?: D;
302
+ readonly sync: (services: { readonly [K in keyof D]: ServiceOf<InstanceType<D[K]>>; }) => SubscriptionHandlers<C, E>;
303
+ } & ([keyof D] extends [never] ? unknown : {
304
+ readonly inject: D;
305
+ })) => Module<HealthChecks, ConnectionError | InvalidAmqpSubscription, InstanceType<PortClassOf<SettingsId, Settings>> | InstanceType<D[keyof D]> | Scope>;
306
+ //#endregion
307
+ export type { AmqpConnectionOptions, AmqpInfo, AmqpMessageOf, AmqpMessagePortOf, AmqpModuleOptions, AmqpOptions, HandlerPortOf, HandlersInstanceOf, HandlersPortOf, SubscriptionHandlers };
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { Observers, RuntimePort, RuntimeStartFailed, composeByPrefix, dispatchUnit, infrastructureExports, mintPiece, noObserverMember, observe, releasedBy, traceIdOfTraceparent, withUnitRecord } from "@btravstack/core";
1
+ import { HealthCheckFailed, HealthChecks, Observers, RuntimePort, RuntimeStartFailed, composeByPrefix, dispatchUnit, infrastructureExports, mintPiece, noObserverMember, observe, releasedBy, traceIdOfTraceparent, withUnitRecord } from "@btravstack/core";
2
2
  import { Module, Port, Provider } from "@btravstack/di";
3
3
  import { TypedAmqpWorker } from "@amqp-contract/worker";
4
4
  import { Config, Env } from "@btravstack/config";
5
- import { P } from "unthrown";
5
+ import { ErrAsync, Ok, OkAsync, P, TaggedError, fromSafePromise } from "unthrown";
6
6
  import { randomUUID } from "node:crypto";
7
7
  //#region src/broker-log.ts
8
8
  /**
@@ -326,4 +326,70 @@ const AmqpModule = (name) => (options) => {
326
326
  });
327
327
  };
328
328
  //#endregion
329
- export { AmqpConfig, AmqpHandler, AmqpHandlers, AmqpMessage, AmqpModule, AmqpRuntime };
329
+ //#region src/subscription.ts
330
+ /** A subscription requires explicitly ephemeral queues with no retry or dead-letter routing. */
331
+ var InvalidAmqpSubscription = class extends TaggedError("InvalidAmqpSubscription") {
332
+ message = "subscriptions require consumers on exclusive auto-delete non-durable classic queues with no retry or dead letter, and no RPCs";
333
+ };
334
+ /**
335
+ * Consume broadcast facts for the application scope's lifetime, beside its one
336
+ * runtime. Every validated delivery is acknowledged, including handler failures;
337
+ * failures are reported to Observers. Each replica must name its own queues.
338
+ */
339
+ const amqpSubscription = (name, options) => {
340
+ const resource = Port(`AmqpSubscription:${name}`);
341
+ return Module(`AmqpSubscription:${name}`)({
342
+ provides: [
343
+ noObserverMember,
344
+ Provider(resource)({
345
+ inject: {
346
+ settings: options.settings,
347
+ observers: Observers,
348
+ ...Object.fromEntries(Object.entries(options.inject ?? {}).map(([key, port]) => [`dependency:${key}`, port]))
349
+ },
350
+ acquire: (services) => {
351
+ const consumers = Object.values(options.contract.consumers ?? {});
352
+ if (consumers.length === 0 || Object.keys(options.contract.rpcs ?? {}).length > 0 || consumers.some(({ queue }) => queue.type !== "classic" || queue.durable || queue.exclusive !== true || queue.autoDelete !== true || queue.retry.mode !== "none" || queue.deadLetter !== void 0 || queue.arguments?.["x-dead-letter-exchange"] !== void 0)) return ErrAsync(new InvalidAmqpSubscription({ subscription: name }));
353
+ const dependencies = Object.fromEntries(Object.keys(options.inject ?? {}).map((key) => [key, services[`dependency:${key}`]]));
354
+ const handlers = options.sync(dependencies);
355
+ const observers = services.observers;
356
+ const wrapped = Object.fromEntries(Object.entries(handlers).map(([key, handler]) => [key, (...args) => {
357
+ const settle = observe(observers, {
358
+ component: "amqp",
359
+ name: "subscription",
360
+ attributes: {
361
+ subscription: name,
362
+ consumer: key
363
+ }
364
+ });
365
+ return fromSafePromise((async () => {
366
+ const result = await OkAsync().flatMap(() => handler(...args) ?? Ok());
367
+ settle(result.isOk() ? { outcome: "ok" } : {
368
+ outcome: "error",
369
+ cause: result.isErr() ? result.error : result.cause
370
+ });
371
+ })());
372
+ }]));
373
+ return TypedAmqpWorker.create({
374
+ contract: options.contract,
375
+ handlers: wrapped,
376
+ urls: [services.settings.url],
377
+ connectTimeoutMs: services.settings.connectTimeoutMs ?? 5e3,
378
+ logger: brokerLog(observers)
379
+ });
380
+ },
381
+ release: (worker) => worker.close().get()
382
+ }),
383
+ Provider.member(HealthChecks)({
384
+ inject: { worker: resource },
385
+ sync: ({ worker }) => ({
386
+ name: `amqp-subscription:${name}`,
387
+ check: () => worker.isConnected() ? OkAsync() : ErrAsync(new HealthCheckFailed({ reason: "broker disconnected" }))
388
+ })
389
+ })
390
+ ],
391
+ exports: [HealthChecks]
392
+ });
393
+ };
394
+ //#endregion
395
+ export { AmqpConfig, AmqpHandler, AmqpHandlers, AmqpMessage, AmqpModule, AmqpRuntime, InvalidAmqpSubscription, amqpSubscription };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/amqp-worker",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "The AMQP starter for @btravstack/core: the handlers as a port, one unit per delivery, and a drain the kernel's deadline releases",
5
5
  "keywords": [
6
6
  "amqp",
@@ -47,11 +47,11 @@
47
47
  "@amqp-contract/contract": "3.0.0-beta.11",
48
48
  "@amqp-contract/testing": "3.0.0-beta.11",
49
49
  "@amqp-contract/worker": "3.0.0-beta.11",
50
- "@btravstack/config": "0.45.0",
51
- "@btravstack/core": "0.45.0",
52
- "@btravstack/di": "0.45.0",
50
+ "@btravstack/config": "0.46.0",
51
+ "@btravstack/core": "0.46.0",
52
+ "@btravstack/di": "0.46.0",
53
53
  "@btravstack/internal-test-infra": "0.0.0",
54
- "@btravstack/testing": "0.45.0",
54
+ "@btravstack/testing": "0.46.0",
55
55
  "@btravstack/tsconfig": "0.4.0",
56
56
  "@opentelemetry/api": "1.9.1",
57
57
  "@types/node": "26.6.5",
@@ -65,12 +65,12 @@
65
65
  "zod": "4.6.7"
66
66
  },
67
67
  "peerDependencies": {
68
- "@amqp-contract/worker": "^3.0.0-beta.7",
69
- "@btravstack/config": "^0.45.0",
70
- "@btravstack/core": "^0.45.0",
71
- "@btravstack/di": "^0.45.0",
68
+ "@amqp-contract/worker": "^3.0.0-beta.11",
69
+ "@btravstack/config": "^0.46.0",
70
+ "@btravstack/core": "^0.46.0",
71
+ "@btravstack/di": "^0.46.0",
72
72
  "@opentelemetry/api": "^1",
73
- "unthrown": "^5.7.0"
73
+ "unthrown": "^5.11.0"
74
74
  },
75
75
  "engines": {
76
76
  "node": ">=22.22"