@btravstack/amqp-worker 0.15.0 → 0.17.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/dist/index.cjs CHANGED
@@ -61,46 +61,11 @@ const brokerLog = (observers) => {
61
61
  //#endregion
62
62
  //#region src/unit.ts
63
63
  /**
64
- * Where `messageUnits` leaves the forked context for the piece wrappers to read.
65
- * A symbol rather than a name: the dispatcher merges it into the context every
66
- * handler sees, and a string key would appear on the record a handler written
67
- * against the `{ inject, sync }` arm destructures.
68
- */
69
- const UNIT_SCOPE = Symbol("@btravstack/amqp-worker/unit-scope");
70
- /**
71
- * The declared record, as a getter per name resolved on read out of the fork.
72
- * Neither writable nor configurable — a handler reads what the fork holds, and
73
- * cannot reshape the record under the next delivery. With no `unit.message`
74
- * bound there is nothing to resolve from, so the record is empty whatever was
75
- * declared.
76
- */
77
- const unitRecordOf = (forked, record) => {
78
- const unit = {};
79
- if (forked === void 0) return unit;
80
- for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
81
- enumerable: true,
82
- get: () => forked.get(port)
83
- });
84
- return unit;
85
- };
86
- /**
87
64
  * One handler entry — a function, or `[handler, options]` — with its piece's
88
65
  * declared record put on `context.unit`. Applied once per piece as di
89
66
  * constructs it, so a delivery costs one record and one context object.
90
67
  */
91
- const withUnit = (record, entry) => {
92
- const wrap = (handler) => (helpers, message) => {
93
- const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
94
- return handler({
95
- ...helpers,
96
- context: {
97
- ...rest,
98
- unit: unitRecordOf(forked, record)
99
- }
100
- }, message);
101
- };
102
- return Array.isArray(entry) ? [wrap(entry[0]), entry[1]] : wrap(entry);
103
- };
68
+ const withUnit = (record, entry) => Array.isArray(entry) ? [(0, _btravstack_core.withUnitRecord)(record, entry[0]), entry[1]] : (0, _btravstack_core.withUnitRecord)(record, entry);
104
69
  //#endregion
105
70
  //#region src/handler.ts
106
71
  /** One id, declared once, so no contract instantiating it warns about a duplicate. */
@@ -144,13 +109,8 @@ const HANDLER_PREFIX = "AmqpHandler:";
144
109
  */
145
110
  const AmqpHandler = (contract, key) => {
146
111
  const port = class extends (0, _btravstack_di.Port)(`${HANDLER_PREFIX}${key}`) {};
147
- return (options) => {
148
- const record = options.unit ?? {};
149
- return Object.assign((0, _btravstack_di.Provider)(port)({
150
- inject: options.inject,
151
- sync: (services) => withUnit(record, options.sync(services))
152
- }), { unit: record });
153
- };
112
+ const mint = (0, _btravstack_core.mintPiece)(port, withUnit);
113
+ return (options) => mint(options);
154
114
  };
155
115
  //#endregion
156
116
  //#region src/message-units.ts
@@ -163,9 +123,8 @@ const AmqpHandler = (contract, key) => {
163
123
  * runs unchanged, so the ambient `currentUnit()` record is what the unit leaves
164
124
  * for the adapters that read it.
165
125
  *
166
- * The forked context rides the dispatcher's context under {@link UNIT_SCOPE},
167
- * where each piece's own wrapper turns it into the typed `context.unit` record
168
- * that piece declared — the records live with the pieces, so a hand-composed
126
+ * The forked context rides the dispatcher's context, where each piece's own
127
+ * wrapper turns it into the typed `context.unit` record that piece declared — the records live with the pieces, so a hand-composed
169
128
  * `amqp()` gets them without threading a second option through the starter.
170
129
  *
171
130
  * **The kernel's per-unit `AbortSignal` rides that record too**, and it is the
@@ -173,17 +132,19 @@ const AmqpHandler = (contract, key) => {
173
132
  * handler that must stop when the kernel stops waiting reads
174
133
  * `currentUnit()?.signal`.
175
134
  */
176
- const messageUnits = (host, observers, unit) => (args, next) => {
177
- const settle = (0, _btravstack_core.observe)(observers, {
135
+ const messageUnits = (host, observers, unit) => (args, next) => (0, _btravstack_core.dispatchUnit)({
136
+ host,
137
+ observers,
138
+ operation: {
178
139
  component: "amqp",
179
140
  name: "delivery",
180
141
  attributes: { handler: args.handlerName }
181
- });
182
- return host.run(metaFor(args.rawMessage), (scope) => unit === void 0 ? next() : scope.fork(unit, [[AmqpMessagePort, args.message]]).flatMap((forked) => next({ context: { [UNIT_SCOPE]: forked } }))).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
183
- outcome: "error",
184
- cause: failure.tag === "Err" ? failure.error : failure.cause
185
- }));
186
- };
142
+ },
143
+ meta: metaFor(args.rawMessage),
144
+ unit,
145
+ seed: [AmqpMessagePort, args.message],
146
+ next
147
+ });
187
148
  /**
188
149
  * `UnitMeta.id` must be unique per unit, and a **delivery tag is not one**: tags
189
150
  * are per-channel and restart at `1` after a reconnect, which
@@ -255,10 +216,7 @@ const amqp = (options) => {
255
216
  needs: [_btravstack_config.Env, AmqpHandlersPort],
256
217
  provides: [
257
218
  config,
258
- _btravstack_di.Provider.member(_btravstack_core.Observers)({
259
- inject: {},
260
- value: _btravstack_core.noObserver
261
- }),
219
+ _btravstack_core.noObserverMember,
262
220
  (0, _btravstack_di.Provider)(AmqpRuntime)({
263
221
  inject: {
264
222
  config: AmqpConfig,
@@ -291,19 +249,7 @@ const amqp = (options) => {
291
249
  * slices claiming one key are di's duplicate-provider defect at build.
292
250
  */
293
251
  const AmqpHandlers = (contract) => {
294
- const build = (0, _btravstack_di.Provider)(AmqpHandlersPort);
295
- const compose = (pieces) => build({
296
- inject: Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(12), piece.port])),
297
- sync: (services) => services
298
- });
299
- const whole = (options) => {
300
- const record = options.unit ?? {};
301
- return build({
302
- inject: options.inject,
303
- sync: (services) => Object.fromEntries(Object.entries(options.sync(services)).map(([key, entry]) => [key, withUnit(record, entry)]))
304
- });
305
- };
306
- return ((first) => Array.isArray(first) ? compose(first) : whole(first));
252
+ return (0, _btravstack_core.composeByPrefix)(AmqpHandlersPort, HANDLER_PREFIX, withUnit);
307
253
  };
308
254
  const startFailed = (cause) => new _btravstack_core.RuntimeStartFailed({
309
255
  runtime: "amqp",
package/dist/index.d.cts CHANGED
@@ -1,21 +1,7 @@
1
1
  import { ConfigInvalid, Env } from "@btravstack/config";
2
- import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope, ServiceOf } from "@btravstack/di";
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";
3
4
  import { ConsumerOptions, TypedAmqpWorker, WorkerInferHandlers } from "@amqp-contract/worker";
4
- import { Runtime, RuntimePort } from "@btravstack/core";
5
- //#region src/unit.d.ts
6
- /** What a bound unit module exports — the port instances a handler of that kind may read. */
7
- type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
8
- /** The declared `unit:` record, as the services a handler reads off `context.unit`. */
9
- type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
10
- /**
11
- * A piece injecting a port the bound `unit.message` module does not export.
12
- * `unknown` when every declared port is covered — including the empty case, so
13
- * a worker whose pieces declare no `unit:` is gated on nothing.
14
- */
15
- type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
16
- readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
17
- };
18
- //#endregion
19
5
  //#region src/handler.d.ts
20
6
  /** The consumer and rpc names `C` declares — the keys of its handlers record. */
21
7
  type HandlerKeyOf<C extends AnyAmqpContract> = keyof WorkerInferHandlers<C> & string;
@@ -65,8 +51,6 @@ declare const HANDLER_PREFIX = "AmqpHandler:";
65
51
  /** What a minted piece returns. */
66
52
  type MintedHandler<C extends AnyAmqpContract, K extends HandlerKeyOf<C>, N, U extends Readonly<Record<string, AnyPort>>> = Provider<InstanceType<HandlerPortOf<C, K>>, never, N> & {
67
53
  readonly port: HandlerPortOf<C, K>;
68
- /** The declared `unit:` record, which `AmqpHandlers`'s array arm reads back off the piece. */
69
- readonly unit: U;
70
54
  /** Phantom: the ports this piece injects, which the root's `unit.message` must export. */
71
55
  readonly _declared?: InstanceType<U[keyof U]>;
72
56
  };
@@ -114,22 +98,8 @@ export declare class AmqpConfig extends AmqpConfig_base<{
114
98
  }> {}
115
99
  /** The runtime's port: what `amqp()` provides, and what the module `start` boots must export. */
116
100
  export declare class AmqpRuntime extends RuntimePort<Runtime<never, AmqpInfo>> {}
117
- /**
118
- * A module `unit.message` may bind, as the upper bound `amqp()` constrains its
119
- * own `Unit` type parameter to. `Module`'s `_exports` channel is contravariant,
120
- * so `Exports = never` — never `unknown` — is what makes a REAL module's own
121
- * (necessarily narrower) export type assignable to this bound: `(x: Concrete)
122
- * => void` is assignable to `(x: never) => void`, not to `(x: unknown) =>
123
- * void`.
124
- */
125
- type AnyUnitModule = Module<never, never, unknown>;
126
- /**
127
- * The needs a bound `unit.message` module still owes, or `never` when none is
128
- * bound. `Scope` is excluded, since nothing can ever provide it — the same
129
- * exemption `NeedsGate` itself carries — and so is the delivery, which the
130
- * fork's own seed discharges.
131
- */
132
- type UnitNeedsOf<Unit> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | AmqpMessageInstance> : never;
101
+ /** The needs a bound `unit.message` module still owes, less the delivery the fork's seed discharges. */
102
+ type UnitNeedsOf$1<Unit> = UnitNeedsOf<Unit, AmqpMessageInstance>;
133
103
  /**
134
104
  * The contract type `TypedAmqpWorker.create` accepts, extracted rather than
135
105
  * imported by name so `@amqp-contract/contract` stays out of the peer range.
@@ -192,7 +162,7 @@ type AmqpTuning<Unit extends AnyUnitModule | undefined = undefined> = {
192
162
  *
193
163
  * With `url` pinned the module reads nothing from the environment.
194
164
  */
195
- export declare const amqp: <TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined = undefined>(options: AmqpOptions<TContract, Unit>) => Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf<Unit>>;
165
+ export declare const amqp: <TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined = undefined>(options: AmqpOptions<TContract, Unit>) => Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf$1<Unit>>;
196
166
  /** One piece of the handlers record — what `AmqpHandler(contract, key)(…)` returns, as the composing form consumes it. */
197
167
  type PieceOf<C extends AnyAmqpContract> = { readonly [K in HandlerKeyOf<C>]: {
198
168
  readonly port: HandlerPortOf<C, K>;
@@ -211,19 +181,6 @@ type KeyOfPiece<P> = P extends {
211
181
  } ? K : never;
212
182
  /** The consumers and rpcs no piece in `T` covers. */
213
183
  type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exclude<HandlerKeyOf<C>, KeyOfPiece<T[number]>>;
214
- /**
215
- * A refused array: as long as the array the caller wrote, its head the caller's
216
- * own elements — which match — and its LAST element the marker paired with what
217
- * is wrong.
218
- *
219
- * TypeScript compares two equal-length tuples element by element, so the extra
220
- * diagnostic it reports lands on the trailing element and carries both the
221
- * sentence and the missing key. A fixed two-element tuple named the key only
222
- * when the array happened to be two elements long; every other arity was a
223
- * length mismatch, and the developer diffed the contract against the array by
224
- * hand.
225
- */
226
- type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
227
184
  /**
228
185
  * The record arm: the whole handlers record from one `sync`, with one `unit:`
229
186
  * record shared by every entry in it.
@@ -280,7 +237,7 @@ export declare const AmqpHandlers: <C extends AnyAmqpContract>(contract: C) => W
280
237
  //#endregion
281
238
  //#region src/amqp-module.d.ts
282
239
  /** The starter's own module, as the sugar adds it to the application's imports. */
283
- type AmqpStarter<TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined> = Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf<Unit>>;
240
+ type AmqpStarter<TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined> = Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf$1<Unit>>;
284
241
  /** The application's imports plus the starter — the tuple `Module(name)` is handed. */
285
242
  type Imports<I extends readonly AnyModule[], TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined> = readonly [...I, AmqpStarter<TContract, Unit>];
286
243
  /** The handlers provider plus the application's own — the tuple `Module(name)` is handed. */
package/dist/index.d.mts CHANGED
@@ -1,21 +1,7 @@
1
- import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope, ServiceOf } from "@btravstack/di";
1
+ import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, ServiceOf } from "@btravstack/di";
2
2
  import { ConsumerOptions, TypedAmqpWorker, WorkerInferHandlers } from "@amqp-contract/worker";
3
3
  import { ConfigInvalid, Env } from "@btravstack/config";
4
- import { Runtime, RuntimePort } from "@btravstack/core";
5
- //#region src/unit.d.ts
6
- /** What a bound unit module exports — the port instances a handler of that kind may read. */
7
- type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
8
- /** The declared `unit:` record, as the services a handler reads off `context.unit`. */
9
- type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
10
- /**
11
- * A piece injecting a port the bound `unit.message` module does not export.
12
- * `unknown` when every declared port is covered — including the empty case, so
13
- * a worker whose pieces declare no `unit:` is gated on nothing.
14
- */
15
- type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
16
- readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
17
- };
18
- //#endregion
4
+ import { AnyUnitModule, Refuse, Runtime, RuntimePort, UnitGate, UnitNeedsOf, UnitRecordOf } from "@btravstack/core";
19
5
  //#region src/handler.d.ts
20
6
  /** The consumer and rpc names `C` declares — the keys of its handlers record. */
21
7
  type HandlerKeyOf<C extends AnyAmqpContract> = keyof WorkerInferHandlers<C> & string;
@@ -65,8 +51,6 @@ declare const HANDLER_PREFIX = "AmqpHandler:";
65
51
  /** What a minted piece returns. */
66
52
  type MintedHandler<C extends AnyAmqpContract, K extends HandlerKeyOf<C>, N, U extends Readonly<Record<string, AnyPort>>> = Provider<InstanceType<HandlerPortOf<C, K>>, never, N> & {
67
53
  readonly port: HandlerPortOf<C, K>;
68
- /** The declared `unit:` record, which `AmqpHandlers`'s array arm reads back off the piece. */
69
- readonly unit: U;
70
54
  /** Phantom: the ports this piece injects, which the root's `unit.message` must export. */
71
55
  readonly _declared?: InstanceType<U[keyof U]>;
72
56
  };
@@ -114,22 +98,8 @@ export declare class AmqpConfig extends AmqpConfig_base<{
114
98
  }> {}
115
99
  /** The runtime's port: what `amqp()` provides, and what the module `start` boots must export. */
116
100
  export declare class AmqpRuntime extends RuntimePort<Runtime<never, AmqpInfo>> {}
117
- /**
118
- * A module `unit.message` may bind, as the upper bound `amqp()` constrains its
119
- * own `Unit` type parameter to. `Module`'s `_exports` channel is contravariant,
120
- * so `Exports = never` — never `unknown` — is what makes a REAL module's own
121
- * (necessarily narrower) export type assignable to this bound: `(x: Concrete)
122
- * => void` is assignable to `(x: never) => void`, not to `(x: unknown) =>
123
- * void`.
124
- */
125
- type AnyUnitModule = Module<never, never, unknown>;
126
- /**
127
- * The needs a bound `unit.message` module still owes, or `never` when none is
128
- * bound. `Scope` is excluded, since nothing can ever provide it — the same
129
- * exemption `NeedsGate` itself carries — and so is the delivery, which the
130
- * fork's own seed discharges.
131
- */
132
- type UnitNeedsOf<Unit> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | AmqpMessageInstance> : never;
101
+ /** The needs a bound `unit.message` module still owes, less the delivery the fork's seed discharges. */
102
+ type UnitNeedsOf$1<Unit> = UnitNeedsOf<Unit, AmqpMessageInstance>;
133
103
  /**
134
104
  * The contract type `TypedAmqpWorker.create` accepts, extracted rather than
135
105
  * imported by name so `@amqp-contract/contract` stays out of the peer range.
@@ -192,7 +162,7 @@ type AmqpTuning<Unit extends AnyUnitModule | undefined = undefined> = {
192
162
  *
193
163
  * With `url` pinned the module reads nothing from the environment.
194
164
  */
195
- export declare const amqp: <TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined = undefined>(options: AmqpOptions<TContract, Unit>) => Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf<Unit>>;
165
+ export declare const amqp: <TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined = undefined>(options: AmqpOptions<TContract, Unit>) => Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf$1<Unit>>;
196
166
  /** One piece of the handlers record — what `AmqpHandler(contract, key)(…)` returns, as the composing form consumes it. */
197
167
  type PieceOf<C extends AnyAmqpContract> = { readonly [K in HandlerKeyOf<C>]: {
198
168
  readonly port: HandlerPortOf<C, K>;
@@ -211,19 +181,6 @@ type KeyOfPiece<P> = P extends {
211
181
  } ? K : never;
212
182
  /** The consumers and rpcs no piece in `T` covers. */
213
183
  type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exclude<HandlerKeyOf<C>, KeyOfPiece<T[number]>>;
214
- /**
215
- * A refused array: as long as the array the caller wrote, its head the caller's
216
- * own elements — which match — and its LAST element the marker paired with what
217
- * is wrong.
218
- *
219
- * TypeScript compares two equal-length tuples element by element, so the extra
220
- * diagnostic it reports lands on the trailing element and carries both the
221
- * sentence and the missing key. A fixed two-element tuple named the key only
222
- * when the array happened to be two elements long; every other arity was a
223
- * length mismatch, and the developer diffed the contract against the array by
224
- * hand.
225
- */
226
- type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
227
184
  /**
228
185
  * The record arm: the whole handlers record from one `sync`, with one `unit:`
229
186
  * record shared by every entry in it.
@@ -280,7 +237,7 @@ export declare const AmqpHandlers: <C extends AnyAmqpContract>(contract: C) => W
280
237
  //#endregion
281
238
  //#region src/amqp-module.d.ts
282
239
  /** The starter's own module, as the sugar adds it to the application's imports. */
283
- type AmqpStarter<TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined> = Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf<Unit>>;
240
+ type AmqpStarter<TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined> = Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract> | UnitNeedsOf$1<Unit>>;
284
241
  /** The application's imports plus the starter — the tuple `Module(name)` is handed. */
285
242
  type Imports<I extends readonly AnyModule[], TContract extends AnyAmqpContract, Unit extends AnyUnitModule | undefined> = readonly [...I, AmqpStarter<TContract, Unit>];
286
243
  /** The handlers provider plus the application's own — the tuple `Module(name)` is handed. */
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Module, Port, Provider } from "@btravstack/di";
2
2
  import { TypedAmqpWorker } from "@amqp-contract/worker";
3
3
  import { Config, Env } from "@btravstack/config";
4
- import { Observers, RuntimePort, RuntimeStartFailed, noObserver, observe, releasedBy, traceIdOfTraceparent } from "@btravstack/core";
4
+ import { Observers, RuntimePort, RuntimeStartFailed, composeByPrefix, dispatchUnit, mintPiece, noObserverMember, observe, releasedBy, traceIdOfTraceparent, withUnitRecord } from "@btravstack/core";
5
5
  import { P } from "unthrown";
6
6
  import { randomUUID } from "node:crypto";
7
7
  //#region src/broker-log.ts
@@ -60,46 +60,11 @@ const brokerLog = (observers) => {
60
60
  //#endregion
61
61
  //#region src/unit.ts
62
62
  /**
63
- * Where `messageUnits` leaves the forked context for the piece wrappers to read.
64
- * A symbol rather than a name: the dispatcher merges it into the context every
65
- * handler sees, and a string key would appear on the record a handler written
66
- * against the `{ inject, sync }` arm destructures.
67
- */
68
- const UNIT_SCOPE = Symbol("@btravstack/amqp-worker/unit-scope");
69
- /**
70
- * The declared record, as a getter per name resolved on read out of the fork.
71
- * Neither writable nor configurable — a handler reads what the fork holds, and
72
- * cannot reshape the record under the next delivery. With no `unit.message`
73
- * bound there is nothing to resolve from, so the record is empty whatever was
74
- * declared.
75
- */
76
- const unitRecordOf = (forked, record) => {
77
- const unit = {};
78
- if (forked === void 0) return unit;
79
- for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
80
- enumerable: true,
81
- get: () => forked.get(port)
82
- });
83
- return unit;
84
- };
85
- /**
86
63
  * One handler entry — a function, or `[handler, options]` — with its piece's
87
64
  * declared record put on `context.unit`. Applied once per piece as di
88
65
  * constructs it, so a delivery costs one record and one context object.
89
66
  */
90
- const withUnit = (record, entry) => {
91
- const wrap = (handler) => (helpers, message) => {
92
- const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
93
- return handler({
94
- ...helpers,
95
- context: {
96
- ...rest,
97
- unit: unitRecordOf(forked, record)
98
- }
99
- }, message);
100
- };
101
- return Array.isArray(entry) ? [wrap(entry[0]), entry[1]] : wrap(entry);
102
- };
67
+ const withUnit = (record, entry) => Array.isArray(entry) ? [withUnitRecord(record, entry[0]), entry[1]] : withUnitRecord(record, entry);
103
68
  //#endregion
104
69
  //#region src/handler.ts
105
70
  /** One id, declared once, so no contract instantiating it warns about a duplicate. */
@@ -143,13 +108,8 @@ const HANDLER_PREFIX = "AmqpHandler:";
143
108
  */
144
109
  const AmqpHandler = (contract, key) => {
145
110
  const port = class extends Port(`${HANDLER_PREFIX}${key}`) {};
146
- return (options) => {
147
- const record = options.unit ?? {};
148
- return Object.assign(Provider(port)({
149
- inject: options.inject,
150
- sync: (services) => withUnit(record, options.sync(services))
151
- }), { unit: record });
152
- };
111
+ const mint = mintPiece(port, withUnit);
112
+ return (options) => mint(options);
153
113
  };
154
114
  //#endregion
155
115
  //#region src/message-units.ts
@@ -162,9 +122,8 @@ const AmqpHandler = (contract, key) => {
162
122
  * runs unchanged, so the ambient `currentUnit()` record is what the unit leaves
163
123
  * for the adapters that read it.
164
124
  *
165
- * The forked context rides the dispatcher's context under {@link UNIT_SCOPE},
166
- * where each piece's own wrapper turns it into the typed `context.unit` record
167
- * that piece declared — the records live with the pieces, so a hand-composed
125
+ * The forked context rides the dispatcher's context, where each piece's own
126
+ * wrapper turns it into the typed `context.unit` record that piece declared — the records live with the pieces, so a hand-composed
168
127
  * `amqp()` gets them without threading a second option through the starter.
169
128
  *
170
129
  * **The kernel's per-unit `AbortSignal` rides that record too**, and it is the
@@ -172,17 +131,19 @@ const AmqpHandler = (contract, key) => {
172
131
  * handler that must stop when the kernel stops waiting reads
173
132
  * `currentUnit()?.signal`.
174
133
  */
175
- const messageUnits = (host, observers, unit) => (args, next) => {
176
- const settle = observe(observers, {
134
+ const messageUnits = (host, observers, unit) => (args, next) => dispatchUnit({
135
+ host,
136
+ observers,
137
+ operation: {
177
138
  component: "amqp",
178
139
  name: "delivery",
179
140
  attributes: { handler: args.handlerName }
180
- });
181
- return host.run(metaFor(args.rawMessage), (scope) => unit === void 0 ? next() : scope.fork(unit, [[AmqpMessagePort, args.message]]).flatMap((forked) => next({ context: { [UNIT_SCOPE]: forked } }))).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
182
- outcome: "error",
183
- cause: failure.tag === "Err" ? failure.error : failure.cause
184
- }));
185
- };
141
+ },
142
+ meta: metaFor(args.rawMessage),
143
+ unit,
144
+ seed: [AmqpMessagePort, args.message],
145
+ next
146
+ });
186
147
  /**
187
148
  * `UnitMeta.id` must be unique per unit, and a **delivery tag is not one**: tags
188
149
  * are per-channel and restart at `1` after a reconnect, which
@@ -254,10 +215,7 @@ const amqp = (options) => {
254
215
  needs: [Env, AmqpHandlersPort],
255
216
  provides: [
256
217
  config,
257
- Provider.member(Observers)({
258
- inject: {},
259
- value: noObserver
260
- }),
218
+ noObserverMember,
261
219
  Provider(AmqpRuntime)({
262
220
  inject: {
263
221
  config: AmqpConfig,
@@ -290,19 +248,7 @@ const amqp = (options) => {
290
248
  * slices claiming one key are di's duplicate-provider defect at build.
291
249
  */
292
250
  const AmqpHandlers = (contract) => {
293
- const build = Provider(AmqpHandlersPort);
294
- const compose = (pieces) => build({
295
- inject: Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(12), piece.port])),
296
- sync: (services) => services
297
- });
298
- const whole = (options) => {
299
- const record = options.unit ?? {};
300
- return build({
301
- inject: options.inject,
302
- sync: (services) => Object.fromEntries(Object.entries(options.sync(services)).map(([key, entry]) => [key, withUnit(record, entry)]))
303
- });
304
- };
305
- return ((first) => Array.isArray(first) ? compose(first) : whole(first));
251
+ return composeByPrefix(AmqpHandlersPort, HANDLER_PREFIX, withUnit);
306
252
  };
307
253
  const startFailed = (cause) => new RuntimeStartFailed({
308
254
  runtime: "amqp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/amqp-worker",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",
@@ -44,30 +44,30 @@
44
44
  "./package.json": "./package.json"
45
45
  },
46
46
  "devDependencies": {
47
- "@amqp-contract/contract": "3.0.0-beta.7",
48
- "@amqp-contract/testing": "3.0.0-beta.7",
49
- "@amqp-contract/worker": "3.0.0-beta.7",
50
- "@btravstack/config": "0.15.0",
51
- "@btravstack/core": "0.15.0",
52
- "@btravstack/di": "0.15.0",
47
+ "@amqp-contract/contract": "3.0.0-beta.11",
48
+ "@amqp-contract/testing": "3.0.0-beta.11",
49
+ "@amqp-contract/worker": "3.0.0-beta.11",
50
+ "@btravstack/config": "0.17.0",
51
+ "@btravstack/core": "0.17.0",
52
+ "@btravstack/di": "0.17.0",
53
53
  "@btravstack/internal-test-infra": "0.0.0",
54
- "@btravstack/testing": "0.15.0",
54
+ "@btravstack/testing": "0.17.0",
55
55
  "@btravstack/tsconfig": "0.4.0",
56
56
  "@opentelemetry/api": "1.9.1",
57
- "@types/node": "26.4.1",
58
- "@unthrown/vitest": "5.8.0",
59
- "@vitest/coverage-v8": "4.1.11",
57
+ "@types/node": "26.6.4",
58
+ "@unthrown/vitest": "5.12.0",
59
+ "@vitest/coverage-v8": "5.0.3",
60
60
  "tsdown": "0.23.0",
61
61
  "typescript": "7.0.2",
62
- "unthrown": "5.8.0",
63
- "vitest": "4.1.11",
64
- "zod": "4.5.4"
62
+ "unthrown": "5.12.0",
63
+ "vitest": "5.0.3",
64
+ "zod": "4.6.5"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@amqp-contract/worker": "^3.0.0-beta.7",
68
- "@btravstack/config": "^0.15.0",
69
- "@btravstack/core": "^0.15.0",
70
- "@btravstack/di": "^0.15.0",
68
+ "@btravstack/config": "^0.17.0",
69
+ "@btravstack/core": "^0.17.0",
70
+ "@btravstack/di": "^0.17.0",
71
71
  "@opentelemetry/api": "^1",
72
72
  "unthrown": "^5.0.0"
73
73
  },