@btravstack/amqp-worker 0.3.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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/index.cjs +215 -0
- package/dist/index.d.cts +178 -0
- package/dist/index.d.mts +178 -0
- package/dist/index.mjs +209 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benoit TRAVERS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @btravstack/amqp-worker
|
|
2
|
+
|
|
3
|
+
> The **consuming half** of AMQP for [`@btravstack/core`](../core) โ publishing
|
|
4
|
+
> belongs to `@amqp-contract/client`, which the outbox relay in
|
|
5
|
+
> `examples/order-amqp-worker` uses directly. An
|
|
6
|
+
> [`amqp-contract`](https://github.com/btravstack/amqp-contract) worker under
|
|
7
|
+
> the kernel's lifecycle โ one unit per delivery, and a drain with exactly one
|
|
8
|
+
> deadline, the kernel's own.
|
|
9
|
+
|
|
10
|
+
๐ **[Documentation](https://btravstack.github.io/btravstack/how-to/consume-amqp-messages)** ยท
|
|
11
|
+
[Reference](https://btravstack.github.io/btravstack/reference/amqp-worker) ยท
|
|
12
|
+
[API Reference](https://btravstack.github.io/btravstack/api/amqp-worker/)
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @btravstack/amqp-worker @btravstack/core @btravstack/config @btravstack/di unthrown \
|
|
16
|
+
@amqp-contract/worker @opentelemetry/api
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
All six are peer dependencies โ install them (`@opentelemetry/api` because
|
|
20
|
+
`@amqp-contract/worker` itself peers on it). Node `>=20`.
|
|
21
|
+
|
|
22
|
+
## A worked example
|
|
23
|
+
|
|
24
|
+
<!-- doctest: prelude
|
|
25
|
+
import { NonRetryableError, RetryableError } from "@amqp-contract/worker";
|
|
26
|
+
import { orderContract } from "@btravstack/example-order-amqp-contract";
|
|
27
|
+
import { PlaceOrder } from "@btravstack/example-order-application";
|
|
28
|
+
import { TenantId } from "@btravstack/example-order-domain";
|
|
29
|
+
import { Module } from "@btravstack/di";
|
|
30
|
+
import { OrderApplicationModule, OrderRepository, Outbox } from "@btravstack/example-order-application";
|
|
31
|
+
import { OrderPersistenceModule } from "@btravstack/example-order-infrastructure";
|
|
32
|
+
import { observability } from "@btravstack/observability";
|
|
33
|
+
|
|
34
|
+
// The application module the README's composition imports โ the orders
|
|
35
|
+
// vertical plus its persistence, as an application would compose it.
|
|
36
|
+
const AppModule = Module("App")({
|
|
37
|
+
imports: [OrderApplicationModule, OrderPersistenceModule, observability()],
|
|
38
|
+
exports: [PlaceOrder, OrderRepository, Outbox, Logger],
|
|
39
|
+
});
|
|
40
|
+
-->
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Env } from "@btravstack/config";
|
|
44
|
+
import { AmqpHandlers, AmqpModule } from "@btravstack/amqp-worker";
|
|
45
|
+
import { runMain, Logger } from "@btravstack/core";
|
|
46
|
+
import { ErrAsync, OkAsync, P } from "unthrown";
|
|
47
|
+
|
|
48
|
+
// The handlers, as a service: one entry per consumer the contract declares,
|
|
49
|
+
// built by di from the use cases it lists โ no injected context โ on the
|
|
50
|
+
// starter's own handlers port, typed by the contract (a consumer serves one
|
|
51
|
+
// handlers record, so there is nothing to name).
|
|
52
|
+
const orderHandlers = AmqpHandlers(orderContract)(
|
|
53
|
+
{ placeOrder: PlaceOrder },
|
|
54
|
+
{
|
|
55
|
+
sync: ({ placeOrder }) => ({
|
|
56
|
+
orderNotifications: (message) =>
|
|
57
|
+
placeOrder
|
|
58
|
+
.execute(
|
|
59
|
+
TenantId(message.payload.tenantId),
|
|
60
|
+
message.payload.id,
|
|
61
|
+
message.payload.payload?.quantity ?? 0,
|
|
62
|
+
)
|
|
63
|
+
.map(() => undefined)
|
|
64
|
+
// A modeled domain error is permanent: dead-letter it, no retry.
|
|
65
|
+
.mapErrCases((matcher) =>
|
|
66
|
+
matcher.with(
|
|
67
|
+
P.tag("InvalidQuantity"),
|
|
68
|
+
P.tag("InvalidOrderId"),
|
|
69
|
+
P.tag("DuplicateOrder"),
|
|
70
|
+
(error) => new NonRetryableError(error._tag, error),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
// Infrastructure failing is not: recover the defect into a retry.
|
|
74
|
+
.recoverDefect((cause) =>
|
|
75
|
+
ErrAsync(new RetryableError("placing the order failed", cause)),
|
|
76
|
+
),
|
|
77
|
+
// Every consumer the contract declares must be covered โ an uncovered
|
|
78
|
+
// key is refused at this call, not on the first delivery.
|
|
79
|
+
orderAudit: () => OkAsync(undefined),
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const Worker = AmqpModule("Worker")({
|
|
85
|
+
needs: [Env],
|
|
86
|
+
contract: orderContract,
|
|
87
|
+
handlers: orderHandlers,
|
|
88
|
+
imports: [AppModule],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await runMain(Worker);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`AmqpModule` imports the starter โ `AmqpRuntime`, `AmqpConfig` bound from
|
|
95
|
+
`AMQP_URL` (default `amqp://127.0.0.1:5672`) โ provides the handlers and
|
|
96
|
+
exports the runtime port; the handlers are checked against the contract at the
|
|
97
|
+
call, not on the first delivery. `runtimeInfo()` reads `{ queues }` back once
|
|
98
|
+
consuming.
|
|
99
|
+
|
|
100
|
+
A worker with several consumers can be several slices instead of one record:
|
|
101
|
+
`AmqpHandler(contract, key)({ name: Dep }, arm)` mints a provider for ONE consumer or
|
|
102
|
+
rpc, typed by the key alone, and `AmqpHandlers(contract)([...])` composes an
|
|
103
|
+
array of them into the same handlers provider `AmqpModule` takes โ the array
|
|
104
|
+
must cover every key the contract declares, and each piece's own port must
|
|
105
|
+
still be discharged (`provides`), since the composed provider's deps are the
|
|
106
|
+
pieces' ports, not what they close over. Two slices claiming one key are di's
|
|
107
|
+
duplicate-provider defect at build, which is the point: a consumer belongs to
|
|
108
|
+
exactly one slice.
|
|
109
|
+
|
|
110
|
+
## Options
|
|
111
|
+
|
|
112
|
+
`AmqpModule(name)({...})` takes `amqp()`'s options plus `handlers` and the
|
|
113
|
+
module lists (`imports`, `provides`, `exports`, `needs`). The `amqp()`
|
|
114
|
+
primitive does not take `handlers` as an option โ it **needs** the handlers
|
|
115
|
+
provider on its own port, which is how the composition root supplies it:
|
|
116
|
+
|
|
117
|
+
| Option | What it is |
|
|
118
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| `contract` | the `amqp-contract` contract; the queues consumed are read off it |
|
|
120
|
+
| `handlers` | the handlers provider โ what `AmqpHandlers(contract)(...)` returns for this contract |
|
|
121
|
+
| `url` | pins the broker instead of reading `AMQP_URL` (default `amqp://127.0.0.1:5672`) โ a test's container |
|
|
122
|
+
| `connectionOptions` | `AmqpConnectionOptions`, the library's own connection tuning: heartbeat, reconnect interval, `findServers`, TLS |
|
|
123
|
+
| `defaultConsumerOptions` | the library's `ConsumerOptions`, applied to every handler: `prefetch` (the throughput knob), `priority`, โฆ |
|
|
124
|
+
| `connectTimeoutMs` | how long startup waits before an unreachable broker is a `RuntimeStartFailed` (the library defaults to 30 s) |
|
|
125
|
+
|
|
126
|
+
The full table โ required/optional, defaults, and the reasoning โ lives on
|
|
127
|
+
[the reference page](https://btravstack.github.io/btravstack/reference/amqp-worker),
|
|
128
|
+
which is this list's one detailed home.
|
|
129
|
+
|
|
130
|
+
## What it decides, and what it does not
|
|
131
|
+
|
|
132
|
+
`Result` โ ack / retry / dead-letter is a **three-way** split and none of it is
|
|
133
|
+
this package's: a `RetryableError` / `NonRetryableError` is routed by
|
|
134
|
+
`amqp-contract`'s own dispatch against the queue's `retry` config, and a
|
|
135
|
+
`Defect` is nacked once, straight to the dead-letter queue, never touching that
|
|
136
|
+
budget โ which is why the handler above recovers its own defects into a
|
|
137
|
+
`RetryableError`. The drain calls `worker.close({ drainTimeoutMs: null })`
|
|
138
|
+
raced against the kernel's deadline: the library is told to wait forever, so
|
|
139
|
+
there is no second timeout in the process to keep in step. Each delivery is one
|
|
140
|
+
unit, `traceId` the publisher's `messageId`. The rest is on the
|
|
141
|
+
[documentation site](https://btravstack.github.io/btravstack/reference/amqp-worker).
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
[MIT](./LICENSE) ยฉ Benoit TRAVERS
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _btravstack_di = require("@btravstack/di");
|
|
3
|
+
let _amqp_contract_worker = require("@amqp-contract/worker");
|
|
4
|
+
let _btravstack_config = require("@btravstack/config");
|
|
5
|
+
let _btravstack_core = require("@btravstack/core");
|
|
6
|
+
let unthrown = require("unthrown");
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
8
|
+
//#region src/handler.ts
|
|
9
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
10
|
+
const HANDLER_PREFIX = "AmqpHandler:";
|
|
11
|
+
/**
|
|
12
|
+
* One consumer or rpc of a contract, as a provider on a port of its own.
|
|
13
|
+
*
|
|
14
|
+
* A worker with several consumers is several pieces, each declaring the services
|
|
15
|
+
* its own handler calls; `AmqpHandlers(contract)([...])` composes them.
|
|
16
|
+
* `contract` is read for its TYPE only, and types both `key` and the handler โ
|
|
17
|
+
* so a consumer the contract does not declare is a compile error here.
|
|
18
|
+
*
|
|
19
|
+
* There is no name to give: the contract key IS the port's name.
|
|
20
|
+
*/
|
|
21
|
+
const AmqpHandler = (contract, key) => {
|
|
22
|
+
const port = class extends (0, _btravstack_di.Port)(`${HANDLER_PREFIX}${key}`) {};
|
|
23
|
+
return (0, _btravstack_di.Provider)(port);
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/message-units.ts
|
|
27
|
+
/**
|
|
28
|
+
* Open one kernel unit per delivery. It injects nothing โ `next()` unchanged is
|
|
29
|
+
* the whole of the chain โ so the ambient `currentUnit()` record is what the
|
|
30
|
+
* unit leaves for the adapters that read it.
|
|
31
|
+
*
|
|
32
|
+
* **The kernel's per-unit `AbortSignal` rides that record too**, and it is the
|
|
33
|
+
* only route to it here: a handler has no parameter to receive one through. A
|
|
34
|
+
* handler that must stop when the kernel stops waiting reads
|
|
35
|
+
* `currentUnit()?.signal`.
|
|
36
|
+
*/
|
|
37
|
+
const messageUnits = (host) => (args, next) => host.run(metaFor(args.rawMessage), () => next());
|
|
38
|
+
/**
|
|
39
|
+
* `UnitMeta.id` must be unique per unit, and a **delivery tag is not one**: tags
|
|
40
|
+
* are per-channel and restart at `1` after a reconnect, which
|
|
41
|
+
* amqp-connection-manager performs silently underneath this worker.
|
|
42
|
+
* `consumerTag + deliveryTag` almost fixes it, until `ConsumerOptions` lets a
|
|
43
|
+
* caller pin `consumerTag`. Minting is the only form the rule survives.
|
|
44
|
+
*
|
|
45
|
+
* A W3C `traceparent` header wins when the publisher sends one; otherwise
|
|
46
|
+
* `messageId`, with `correlationId` as the RPC-shaped fallback.
|
|
47
|
+
*
|
|
48
|
+
* Only a NON-BLANK id is adopted, and the trim is load-bearing: `??` guards
|
|
49
|
+
* nullish alone, so a publisher setting `messageId: ""` would give every
|
|
50
|
+
* delivery the same blank trace id.
|
|
51
|
+
*/
|
|
52
|
+
const metaFor = (raw) => {
|
|
53
|
+
const id = (0, node_crypto.randomUUID)();
|
|
54
|
+
const parent = raw.properties.headers?.["traceparent"];
|
|
55
|
+
const fromParent = typeof parent === "string" ? traceIdOfTraceparent(parent) : void 0;
|
|
56
|
+
const inbound = [raw.properties.messageId, raw.properties.correlationId].map((value) => value?.trim() ?? "").find((value) => value !== "");
|
|
57
|
+
return {
|
|
58
|
+
kind: "delivery",
|
|
59
|
+
id,
|
|
60
|
+
traceId: fromParent ?? inbound ?? id
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The trace id inside a W3C `traceparent` header, which outranks `messageId`
|
|
65
|
+
* because it is the one value minted to span processes. Only the trace-id field
|
|
66
|
+
* is taken: `traceId` is a correlation id, not a span context, so the parent's
|
|
67
|
+
* span id is dropped rather than half-carried. The all-zero id is the spec's own
|
|
68
|
+
* "invalid" and is refused like a malformed header.
|
|
69
|
+
*/
|
|
70
|
+
const traceIdOfTraceparent = (header) => {
|
|
71
|
+
const traceId = /^[\da-f]{2}-([\da-f]{32})-[\da-f]{16}-[\da-f]{2}$/.exec(header.trim())?.[1];
|
|
72
|
+
return traceId === void 0 || /^0{32}$/.test(traceId) ? void 0 : traceId;
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/amqp-runtime.ts
|
|
76
|
+
/**
|
|
77
|
+
* The broker, as a service: `amqp()` binds it from `AMQP_URL` (default
|
|
78
|
+
* `amqp://127.0.0.1:5672`) unless pinned, and anything else in the graph may
|
|
79
|
+
* read it โ a publisher sharing the consumer's broker, say.
|
|
80
|
+
*/
|
|
81
|
+
var AmqpConfig = class extends (0, _btravstack_di.Port)("AmqpConfig") {};
|
|
82
|
+
/** The runtime's port: what `amqp()` provides, and what the module `start` boots must export. */
|
|
83
|
+
var AmqpRuntime = class extends _btravstack_core.RuntimePort {};
|
|
84
|
+
/**
|
|
85
|
+
* The handlers' port โ one id, the starter's own, which an application never
|
|
86
|
+
* names. Generic at the value level (one `Port(...)` call, so no duplicate-id
|
|
87
|
+
* warning however many contracts instantiate it) and fixed per contract at the
|
|
88
|
+
* type level through `HandlersPortOf<C>`, so a provider built for one contract
|
|
89
|
+
* cannot be handed to a module declaring another. Exported for this package's
|
|
90
|
+
* tests, not from `index.ts`.
|
|
91
|
+
*/
|
|
92
|
+
const AmqpHandlersPort = (0, _btravstack_di.Port)("AmqpHandlers");
|
|
93
|
+
/**
|
|
94
|
+
* The AMQP starter: a module providing the runtime and its configuration (bound
|
|
95
|
+
* from `AMQP_URL` unless pinned), built over the handlers the application
|
|
96
|
+
* provides on the starter's own handlers port โ the module's one need. Import
|
|
97
|
+
* it next to the application, provide the handlers, export `AmqpRuntime`.
|
|
98
|
+
*
|
|
99
|
+
* With `url` pinned the module reads nothing from the environment.
|
|
100
|
+
*/
|
|
101
|
+
const amqp = (options) => {
|
|
102
|
+
const config = options.url === void 0 ? _btravstack_config.Config.provider(AmqpConfig)(_btravstack_config.Config.object({ url: _btravstack_config.Config.string("AMQP_URL", { default: "amqp://127.0.0.1:5672" }) })) : (0, _btravstack_di.Provider)(AmqpConfig)({ value: { url: options.url } });
|
|
103
|
+
return (0, _btravstack_di.Module)("Amqp")({
|
|
104
|
+
needs: [_btravstack_config.Env, AmqpHandlersPort],
|
|
105
|
+
provides: [config, (0, _btravstack_di.Provider)(AmqpRuntime)({
|
|
106
|
+
config: AmqpConfig,
|
|
107
|
+
handlers: AmqpHandlersPort
|
|
108
|
+
}, { sync: ({ config: bound, handlers }) => ({
|
|
109
|
+
name: "amqp",
|
|
110
|
+
resolves: [],
|
|
111
|
+
start: (host) => createWorker(host, bound, options, handlers)
|
|
112
|
+
}) })],
|
|
113
|
+
exports: [AmqpRuntime, AmqpConfig]
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* The handlers as a provider, from the contract. Three call forms, one port.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* AmqpHandlers(orderContract)({ logger: Logger }, { sync: ({ logger }) => ({ orderNotifications: (m) => โฆ }) })
|
|
121
|
+
* AmqpHandlers(orderContract)([orderNotifications, orderAudit])
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* The first two are di's own `Provider(port)` on the starter's handlers port
|
|
125
|
+
* typed for the contract. The third takes the pieces `AmqpHandler(contract,
|
|
126
|
+
* key)` builds: they are the provider's deps, keyed by the contract key each
|
|
127
|
+
* piece's port id carries, so the services record IS the handlers record. Every
|
|
128
|
+
* declared key must be covered, and two slices claiming one key are di's
|
|
129
|
+
* duplicate-provider defect at build.
|
|
130
|
+
*/
|
|
131
|
+
const AmqpHandlers = (contract) => {
|
|
132
|
+
const build = (0, _btravstack_di.Provider)(AmqpHandlersPort);
|
|
133
|
+
const compose = (pieces) => build(Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(12), piece.port])), { sync: (services) => services });
|
|
134
|
+
return ((first, second) => second === void 0 && Array.isArray(first) ? compose(first) : build(first, second));
|
|
135
|
+
};
|
|
136
|
+
const startFailed = (cause) => new _btravstack_core.RuntimeStartFailed({
|
|
137
|
+
runtime: "amqp",
|
|
138
|
+
cause
|
|
139
|
+
});
|
|
140
|
+
/**
|
|
141
|
+
* Every queue the contract's consumers and RPCs drain, sorted and
|
|
142
|
+
* de-duplicated. Derived rather than configured, so `Serving.info` cannot
|
|
143
|
+
* disagree with what the worker actually consumes.
|
|
144
|
+
*/
|
|
145
|
+
const queuesOf = (contract) => [...new Set(Object.values({
|
|
146
|
+
...contract.consumers,
|
|
147
|
+
...contract.rpcs
|
|
148
|
+
}).map((entry) => entry.queue.name))].sort();
|
|
149
|
+
const createWorker = (host, config, options, handlers) => _amqp_contract_worker.TypedAmqpWorker.create({
|
|
150
|
+
contract: options.contract,
|
|
151
|
+
handlers,
|
|
152
|
+
middleware: messageUnits(host),
|
|
153
|
+
urls: [config.url],
|
|
154
|
+
...options.connectionOptions === void 0 ? {} : { connectionOptions: options.connectionOptions },
|
|
155
|
+
...options.defaultConsumerOptions === void 0 ? {} : { defaultConsumerOptions: options.defaultConsumerOptions },
|
|
156
|
+
...options.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs }
|
|
157
|
+
}).map((worker) => consume(worker, queuesOf(options.contract))).recoverDefect((cause) => (0, unthrown.ErrAsync)(startFailed(cause)));
|
|
158
|
+
const consume = (worker, queues) => {
|
|
159
|
+
let closing;
|
|
160
|
+
const beginClose = () => closing ??= worker.close({ drainTimeoutMs: null });
|
|
161
|
+
let deadline;
|
|
162
|
+
const stopped = () => deadline === void 0 ? beginClose() : (0, _btravstack_core.releasedBy)(deadline, beginClose());
|
|
163
|
+
return {
|
|
164
|
+
info: { queues },
|
|
165
|
+
drain: (signal) => {
|
|
166
|
+
deadline = signal;
|
|
167
|
+
return stopped();
|
|
168
|
+
},
|
|
169
|
+
stop: () => stopped()
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/amqp-module.ts
|
|
174
|
+
/**
|
|
175
|
+
* `Module(name)({...})` for an AMQP deployment: everything a di module takes,
|
|
176
|
+
* plus the contract and the handlers provider. The sugar imports the starter,
|
|
177
|
+
* provides the handlers and exports `AmqpRuntime`, handing back exactly the
|
|
178
|
+
* module `Module(...)` would have declared over the augmented tuples.
|
|
179
|
+
*
|
|
180
|
+
* ```ts
|
|
181
|
+
* export const OrderAmqpWorker = AmqpModule("OrderAmqpWorker")({
|
|
182
|
+
* contract: orderContract,
|
|
183
|
+
* handlers: orderHandlers,
|
|
184
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule],
|
|
185
|
+
* exports: [Logger],
|
|
186
|
+
* });
|
|
187
|
+
* await runMain(OrderAmqpWorker);
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
const AmqpModule = (name) => (options) => {
|
|
191
|
+
const { contract, handlers, url, connectionOptions, defaultConsumerOptions, connectTimeoutMs } = options;
|
|
192
|
+
const imports = options.imports ?? [];
|
|
193
|
+
const provides = options.provides ?? [];
|
|
194
|
+
const exports = options.exports ?? [];
|
|
195
|
+
const starter = amqp({
|
|
196
|
+
contract,
|
|
197
|
+
...url === void 0 ? {} : { url },
|
|
198
|
+
...connectionOptions === void 0 ? {} : { connectionOptions },
|
|
199
|
+
...defaultConsumerOptions === void 0 ? {} : { defaultConsumerOptions },
|
|
200
|
+
...connectTimeoutMs === void 0 ? {} : { connectTimeoutMs }
|
|
201
|
+
});
|
|
202
|
+
return (0, _btravstack_di.Module)(name)({
|
|
203
|
+
imports: [...imports, starter],
|
|
204
|
+
provides: [handlers, ...provides],
|
|
205
|
+
exports: [AmqpRuntime, ...exports],
|
|
206
|
+
needs: options.needs ?? []
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
exports.AmqpConfig = AmqpConfig;
|
|
211
|
+
exports.AmqpHandler = AmqpHandler;
|
|
212
|
+
exports.AmqpHandlers = AmqpHandlers;
|
|
213
|
+
exports.AmqpModule = AmqpModule;
|
|
214
|
+
exports.AmqpRuntime = AmqpRuntime;
|
|
215
|
+
exports.amqp = amqp;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ConsumerOptions, TypedAmqpWorker, WorkerInferHandlers } from "@amqp-contract/worker";
|
|
2
|
+
import { ConfigInvalid, Env } from "@btravstack/config";
|
|
3
|
+
import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider } from "@btravstack/di";
|
|
4
|
+
import { Runtime, RuntimePort } from "@btravstack/core";
|
|
5
|
+
//#region src/handler.d.ts
|
|
6
|
+
/** The consumer and rpc names `C` declares โ the keys of its handlers record. */
|
|
7
|
+
type HandlerKeyOf<C extends AnyAmqpContract> = keyof WorkerInferHandlers<C> & string;
|
|
8
|
+
/**
|
|
9
|
+
* The port one piece targets. Its id carries the contract key, which is what
|
|
10
|
+
* makes two slices claiming one consumer di's duplicate-provider defect rather
|
|
11
|
+
* than a silent merge.
|
|
12
|
+
*/
|
|
13
|
+
type HandlerPortOf<C extends AnyAmqpContract, K extends HandlerKeyOf<C>> = PortClassOf<`${typeof HANDLER_PREFIX}${K}`, WorkerInferHandlers<C>[K]>;
|
|
14
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
15
|
+
declare const HANDLER_PREFIX = "AmqpHandler:";
|
|
16
|
+
/**
|
|
17
|
+
* One consumer or rpc of a contract, as a provider on a port of its own.
|
|
18
|
+
*
|
|
19
|
+
* A worker with several consumers is several pieces, each declaring the services
|
|
20
|
+
* its own handler calls; `AmqpHandlers(contract)([...])` composes them.
|
|
21
|
+
* `contract` is read for its TYPE only, and types both `key` and the handler โ
|
|
22
|
+
* so a consumer the contract does not declare is a compile error here.
|
|
23
|
+
*
|
|
24
|
+
* There is no name to give: the contract key IS the port's name.
|
|
25
|
+
*/
|
|
26
|
+
declare const AmqpHandler: <C extends AnyAmqpContract, const K extends HandlerKeyOf<C>>(contract: C, key: K) => ReturnType<typeof Provider<HandlerPortOf<C, K>>>;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/amqp-runtime.d.ts
|
|
29
|
+
/** What the worker publishes once it is consuming, read back through `RunningApp.runtimeInfo()`. */
|
|
30
|
+
type AmqpInfo = {
|
|
31
|
+
readonly queues: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
declare const AmqpConfig_base: import("@btravstack/di").PortClass<"AmqpConfig">;
|
|
34
|
+
/**
|
|
35
|
+
* The broker, as a service: `amqp()` binds it from `AMQP_URL` (default
|
|
36
|
+
* `amqp://127.0.0.1:5672`) unless pinned, and anything else in the graph may
|
|
37
|
+
* read it โ a publisher sharing the consumer's broker, say.
|
|
38
|
+
*/
|
|
39
|
+
declare class AmqpConfig extends AmqpConfig_base<{
|
|
40
|
+
readonly url: string;
|
|
41
|
+
}> {}
|
|
42
|
+
/** The runtime's port: what `amqp()` provides, and what the module `start` boots must export. */
|
|
43
|
+
declare class AmqpRuntime extends RuntimePort<Runtime<never, AmqpInfo>> {}
|
|
44
|
+
/**
|
|
45
|
+
* The contract type `TypedAmqpWorker.create` accepts, extracted rather than
|
|
46
|
+
* imported by name so `@amqp-contract/contract` stays out of the peer range.
|
|
47
|
+
*/
|
|
48
|
+
type AnyAmqpContract = Parameters<typeof TypedAmqpWorker.create>[0]["contract"];
|
|
49
|
+
/**
|
|
50
|
+
* The connection tuning `TypedAmqpWorker.create` accepts โ heartbeat,
|
|
51
|
+
* reconnect interval, `findServers`, TLS/socket options. The library declares
|
|
52
|
+
* it (`AmqpConnectionManagerOptions`) without exporting it by name, so it is
|
|
53
|
+
* reached by index for the same reason {@link AnyAmqpContract} is.
|
|
54
|
+
*/
|
|
55
|
+
type AmqpConnectionOptions = NonNullable<Parameters<typeof TypedAmqpWorker.create>[0]["connectionOptions"]>;
|
|
56
|
+
/** The handlers port class, typed for `C`: what `AmqpHandlers(contract)(โฆ).port` is. */
|
|
57
|
+
type HandlersPortOf<C extends AnyAmqpContract> = PortClassOf<"AmqpHandlers", WorkerInferHandlers<C>>;
|
|
58
|
+
/** The handlers port's instance for `C` โ the module's one need. */
|
|
59
|
+
type HandlersInstanceOf<C extends AnyAmqpContract> = PortInstance<"AmqpHandlers", WorkerInferHandlers<C>>;
|
|
60
|
+
type AmqpOptions<TContract extends AnyAmqpContract> = {
|
|
61
|
+
/**
|
|
62
|
+
* The contract; the handlers port is typed by it โ one handler per
|
|
63
|
+
* `consumers` / `rpcs` key, with no injected context, since a handler is
|
|
64
|
+
* built by di from the services it declares.
|
|
65
|
+
*/
|
|
66
|
+
readonly contract: TContract;
|
|
67
|
+
/** Pins the broker instead of reading `AMQP_URL` โ a test's container. */
|
|
68
|
+
readonly url?: string;
|
|
69
|
+
/** Connection tuning, `@amqp-contract/worker`'s own type: heartbeat, reconnect interval, `findServers`, TLS/socket options. */
|
|
70
|
+
readonly connectionOptions?: AmqpConnectionOptions;
|
|
71
|
+
/** Consumer defaults applied to every handler, the library's own type: `prefetch`, `priority`, `arguments`, `consumerTag`, `exclusive`. */
|
|
72
|
+
readonly defaultConsumerOptions?: ConsumerOptions;
|
|
73
|
+
/**
|
|
74
|
+
* How long `create` waits for the connection before failing. Passed straight
|
|
75
|
+
* through โ it is a top-level `CreateWorkerOptions` field, NOT nested under
|
|
76
|
+
* `connectionOptions`, where setting it is silently inert. Without it an
|
|
77
|
+
* unreachable broker takes the library's 30s default to report.
|
|
78
|
+
*/
|
|
79
|
+
readonly connectTimeoutMs?: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The AMQP starter: a module providing the runtime and its configuration (bound
|
|
83
|
+
* from `AMQP_URL` unless pinned), built over the handlers the application
|
|
84
|
+
* provides on the starter's own handlers port โ the module's one need. Import
|
|
85
|
+
* it next to the application, provide the handlers, export `AmqpRuntime`.
|
|
86
|
+
*
|
|
87
|
+
* With `url` pinned the module reads nothing from the environment.
|
|
88
|
+
*/
|
|
89
|
+
declare const amqp: <TContract extends AnyAmqpContract>(options: AmqpOptions<TContract>) => Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract>>;
|
|
90
|
+
/** One piece of the handlers record โ what `AmqpHandler(contract, key)(โฆ)` returns, as the composing form consumes it. */
|
|
91
|
+
type PieceOf<C extends AnyAmqpContract> = { readonly [K in HandlerKeyOf<C>]: {
|
|
92
|
+
readonly port: HandlerPortOf<C, K>;
|
|
93
|
+
}; }[HandlerKeyOf<C>];
|
|
94
|
+
/** The key a piece carries, read back off its port id. */
|
|
95
|
+
type KeyOfPiece<P> = P extends {
|
|
96
|
+
readonly port: {
|
|
97
|
+
readonly portId: `${typeof HANDLER_PREFIX}${infer K}`;
|
|
98
|
+
};
|
|
99
|
+
} ? K : never;
|
|
100
|
+
/** The consumers and rpcs no piece in `T` covers. */
|
|
101
|
+
type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exclude<HandlerKeyOf<C>, KeyOfPiece<T[number]>>;
|
|
102
|
+
/**
|
|
103
|
+
* The composing arm. Declared LAST in the intersection below on purpose:
|
|
104
|
+
* TypeScript reports the last overload's failure, so a non-covering array is
|
|
105
|
+
* refused against the `"UNCOVERED HANDLERS โ โฆ"` marker rather than degrading
|
|
106
|
+
* to di's `Qualification`, which names nothing.
|
|
107
|
+
*
|
|
108
|
+
* The marker is a sentence rather than a bare label because it prints last,
|
|
109
|
+
* after the caller's own several-hundred-character piece type.
|
|
110
|
+
*/
|
|
111
|
+
type Compose<C extends AnyAmqpContract> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : readonly ["UNCOVERED HANDLERS โ the contract declares a consumer this array does not cover", Uncovered<C, T>]) => Provider<HandlersInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
|
|
112
|
+
readonly port: HandlersPortOf<C>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The handlers as a provider, from the contract. Three call forms, one port.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* AmqpHandlers(orderContract)({ logger: Logger }, { sync: ({ logger }) => ({ orderNotifications: (m) => โฆ }) })
|
|
119
|
+
* AmqpHandlers(orderContract)([orderNotifications, orderAudit])
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* The first two are di's own `Provider(port)` on the starter's handlers port
|
|
123
|
+
* typed for the contract. The third takes the pieces `AmqpHandler(contract,
|
|
124
|
+
* key)` builds: they are the provider's deps, keyed by the contract key each
|
|
125
|
+
* piece's port id carries, so the services record IS the handlers record. Every
|
|
126
|
+
* declared key must be covered, and two slices claiming one key are di's
|
|
127
|
+
* duplicate-provider defect at build.
|
|
128
|
+
*/
|
|
129
|
+
declare const AmqpHandlers: <C extends AnyAmqpContract>(contract: C) => ReturnType<typeof Provider<HandlersPortOf<C>>> & Compose<C>;
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/amqp-module.d.ts
|
|
132
|
+
/** The starter's own module, as the sugar adds it to the application's imports. */
|
|
133
|
+
type AmqpStarter<TContract extends AnyAmqpContract> = Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract>>;
|
|
134
|
+
/** The application's imports plus the starter โ the tuple `Module(name)` is handed. */
|
|
135
|
+
type Imports<I extends readonly AnyModule[], TContract extends AnyAmqpContract> = readonly [...I, AmqpStarter<TContract>];
|
|
136
|
+
/** The handlers provider plus the application's own โ the tuple `Module(name)` is handed. */
|
|
137
|
+
type Provides<P extends readonly AnyProvider[], TContract extends AnyAmqpContract, HandlersError, HandlersNeeds> = readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P];
|
|
138
|
+
type AmqpModuleOptions<TContract extends AnyAmqpContract, HandlersError, HandlersNeeds, I extends readonly AnyModule[], P extends readonly AnyProvider[], X extends readonly Exportable<Imports<I, TContract>, Provides<P, TContract, HandlersError, HandlersNeeds>>[], N extends readonly AnyPort[]> = {
|
|
139
|
+
readonly contract: TContract;
|
|
140
|
+
/** The application's handlers โ what `AmqpHandlers(contract)(โฆ)` returns for THIS contract. */
|
|
141
|
+
readonly handlers: Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>;
|
|
142
|
+
/** Pins the broker instead of reading `AMQP_URL` โ a test's container. */
|
|
143
|
+
readonly url?: string;
|
|
144
|
+
/** See `AmqpOptions.connectionOptions`. */
|
|
145
|
+
readonly connectionOptions?: AmqpConnectionOptions;
|
|
146
|
+
/** See `AmqpOptions.defaultConsumerOptions`. */
|
|
147
|
+
readonly defaultConsumerOptions?: ConsumerOptions;
|
|
148
|
+
/** See `AmqpOptions.connectTimeoutMs`. */
|
|
149
|
+
readonly connectTimeoutMs?: number;
|
|
150
|
+
readonly imports?: I;
|
|
151
|
+
readonly provides?: P;
|
|
152
|
+
/** The application's own exports; `AmqpRuntime` is added, since `start` resolves it. */
|
|
153
|
+
readonly exports?: X;
|
|
154
|
+
/**
|
|
155
|
+
* What this root's OWN providers expect from outside. di's gate is re-stated
|
|
156
|
+
* over the augmented tuples below, so forgetting one is an error at THIS call.
|
|
157
|
+
*/
|
|
158
|
+
readonly needs?: N;
|
|
159
|
+
} & NeedsGate<Imports<I, TContract>, Provides<P, TContract, HandlersError, HandlersNeeds>, N>;
|
|
160
|
+
/**
|
|
161
|
+
* `Module(name)({...})` for an AMQP deployment: everything a di module takes,
|
|
162
|
+
* plus the contract and the handlers provider. The sugar imports the starter,
|
|
163
|
+
* provides the handlers and exports `AmqpRuntime`, handing back exactly the
|
|
164
|
+
* module `Module(...)` would have declared over the augmented tuples.
|
|
165
|
+
*
|
|
166
|
+
* ```ts
|
|
167
|
+
* export const OrderAmqpWorker = AmqpModule("OrderAmqpWorker")({
|
|
168
|
+
* contract: orderContract,
|
|
169
|
+
* handlers: orderHandlers,
|
|
170
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule],
|
|
171
|
+
* exports: [Logger],
|
|
172
|
+
* });
|
|
173
|
+
* await runMain(OrderAmqpWorker);
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare const AmqpModule: <const Name extends string>(name: Name) => <TContract extends AnyAmqpContract, HandlersError, HandlersNeeds, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, TContract>, Provides<P, TContract, HandlersError, HandlersNeeds>>[] = [], const N extends readonly AnyPort[] = []>(options: AmqpModuleOptions<TContract, HandlersError, HandlersNeeds, I, P, X, N>) => Module<(Extract<(readonly [typeof AmqpRuntime, ...X])[number], AnyModule> extends (infer T) ? T extends Extract<(readonly [typeof AmqpRuntime, ...X])[number], AnyModule> ? T extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [typeof AmqpRuntime, ...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_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 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>])[number] extends (infer T_2) ? T_2 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_2 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[1], Exclude<((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)[2], ((readonly [...I, AmqpStarter<TContract>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((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)[0]> | Exclude<((readonly [...I, AmqpStarter<TContract>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[2], ((readonly [...I, AmqpStarter<TContract>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((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)[0]>>;
|
|
177
|
+
//#endregion
|
|
178
|
+
export { AmqpConfig, type AmqpConnectionOptions, AmqpHandler, AmqpHandlers, type AmqpInfo, AmqpModule, type AmqpModuleOptions, type AmqpOptions, AmqpRuntime, type HandlerPortOf, type HandlersInstanceOf, type HandlersPortOf, amqp };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider } from "@btravstack/di";
|
|
2
|
+
import { ConsumerOptions, TypedAmqpWorker, WorkerInferHandlers } from "@amqp-contract/worker";
|
|
3
|
+
import { ConfigInvalid, Env } from "@btravstack/config";
|
|
4
|
+
import { Runtime, RuntimePort } from "@btravstack/core";
|
|
5
|
+
//#region src/handler.d.ts
|
|
6
|
+
/** The consumer and rpc names `C` declares โ the keys of its handlers record. */
|
|
7
|
+
type HandlerKeyOf<C extends AnyAmqpContract> = keyof WorkerInferHandlers<C> & string;
|
|
8
|
+
/**
|
|
9
|
+
* The port one piece targets. Its id carries the contract key, which is what
|
|
10
|
+
* makes two slices claiming one consumer di's duplicate-provider defect rather
|
|
11
|
+
* than a silent merge.
|
|
12
|
+
*/
|
|
13
|
+
type HandlerPortOf<C extends AnyAmqpContract, K extends HandlerKeyOf<C>> = PortClassOf<`${typeof HANDLER_PREFIX}${K}`, WorkerInferHandlers<C>[K]>;
|
|
14
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
15
|
+
declare const HANDLER_PREFIX = "AmqpHandler:";
|
|
16
|
+
/**
|
|
17
|
+
* One consumer or rpc of a contract, as a provider on a port of its own.
|
|
18
|
+
*
|
|
19
|
+
* A worker with several consumers is several pieces, each declaring the services
|
|
20
|
+
* its own handler calls; `AmqpHandlers(contract)([...])` composes them.
|
|
21
|
+
* `contract` is read for its TYPE only, and types both `key` and the handler โ
|
|
22
|
+
* so a consumer the contract does not declare is a compile error here.
|
|
23
|
+
*
|
|
24
|
+
* There is no name to give: the contract key IS the port's name.
|
|
25
|
+
*/
|
|
26
|
+
declare const AmqpHandler: <C extends AnyAmqpContract, const K extends HandlerKeyOf<C>>(contract: C, key: K) => ReturnType<typeof Provider<HandlerPortOf<C, K>>>;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/amqp-runtime.d.ts
|
|
29
|
+
/** What the worker publishes once it is consuming, read back through `RunningApp.runtimeInfo()`. */
|
|
30
|
+
type AmqpInfo = {
|
|
31
|
+
readonly queues: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
declare const AmqpConfig_base: import("@btravstack/di").PortClass<"AmqpConfig">;
|
|
34
|
+
/**
|
|
35
|
+
* The broker, as a service: `amqp()` binds it from `AMQP_URL` (default
|
|
36
|
+
* `amqp://127.0.0.1:5672`) unless pinned, and anything else in the graph may
|
|
37
|
+
* read it โ a publisher sharing the consumer's broker, say.
|
|
38
|
+
*/
|
|
39
|
+
declare class AmqpConfig extends AmqpConfig_base<{
|
|
40
|
+
readonly url: string;
|
|
41
|
+
}> {}
|
|
42
|
+
/** The runtime's port: what `amqp()` provides, and what the module `start` boots must export. */
|
|
43
|
+
declare class AmqpRuntime extends RuntimePort<Runtime<never, AmqpInfo>> {}
|
|
44
|
+
/**
|
|
45
|
+
* The contract type `TypedAmqpWorker.create` accepts, extracted rather than
|
|
46
|
+
* imported by name so `@amqp-contract/contract` stays out of the peer range.
|
|
47
|
+
*/
|
|
48
|
+
type AnyAmqpContract = Parameters<typeof TypedAmqpWorker.create>[0]["contract"];
|
|
49
|
+
/**
|
|
50
|
+
* The connection tuning `TypedAmqpWorker.create` accepts โ heartbeat,
|
|
51
|
+
* reconnect interval, `findServers`, TLS/socket options. The library declares
|
|
52
|
+
* it (`AmqpConnectionManagerOptions`) without exporting it by name, so it is
|
|
53
|
+
* reached by index for the same reason {@link AnyAmqpContract} is.
|
|
54
|
+
*/
|
|
55
|
+
type AmqpConnectionOptions = NonNullable<Parameters<typeof TypedAmqpWorker.create>[0]["connectionOptions"]>;
|
|
56
|
+
/** The handlers port class, typed for `C`: what `AmqpHandlers(contract)(โฆ).port` is. */
|
|
57
|
+
type HandlersPortOf<C extends AnyAmqpContract> = PortClassOf<"AmqpHandlers", WorkerInferHandlers<C>>;
|
|
58
|
+
/** The handlers port's instance for `C` โ the module's one need. */
|
|
59
|
+
type HandlersInstanceOf<C extends AnyAmqpContract> = PortInstance<"AmqpHandlers", WorkerInferHandlers<C>>;
|
|
60
|
+
type AmqpOptions<TContract extends AnyAmqpContract> = {
|
|
61
|
+
/**
|
|
62
|
+
* The contract; the handlers port is typed by it โ one handler per
|
|
63
|
+
* `consumers` / `rpcs` key, with no injected context, since a handler is
|
|
64
|
+
* built by di from the services it declares.
|
|
65
|
+
*/
|
|
66
|
+
readonly contract: TContract;
|
|
67
|
+
/** Pins the broker instead of reading `AMQP_URL` โ a test's container. */
|
|
68
|
+
readonly url?: string;
|
|
69
|
+
/** Connection tuning, `@amqp-contract/worker`'s own type: heartbeat, reconnect interval, `findServers`, TLS/socket options. */
|
|
70
|
+
readonly connectionOptions?: AmqpConnectionOptions;
|
|
71
|
+
/** Consumer defaults applied to every handler, the library's own type: `prefetch`, `priority`, `arguments`, `consumerTag`, `exclusive`. */
|
|
72
|
+
readonly defaultConsumerOptions?: ConsumerOptions;
|
|
73
|
+
/**
|
|
74
|
+
* How long `create` waits for the connection before failing. Passed straight
|
|
75
|
+
* through โ it is a top-level `CreateWorkerOptions` field, NOT nested under
|
|
76
|
+
* `connectionOptions`, where setting it is silently inert. Without it an
|
|
77
|
+
* unreachable broker takes the library's 30s default to report.
|
|
78
|
+
*/
|
|
79
|
+
readonly connectTimeoutMs?: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The AMQP starter: a module providing the runtime and its configuration (bound
|
|
83
|
+
* from `AMQP_URL` unless pinned), built over the handlers the application
|
|
84
|
+
* provides on the starter's own handlers port โ the module's one need. Import
|
|
85
|
+
* it next to the application, provide the handlers, export `AmqpRuntime`.
|
|
86
|
+
*
|
|
87
|
+
* With `url` pinned the module reads nothing from the environment.
|
|
88
|
+
*/
|
|
89
|
+
declare const amqp: <TContract extends AnyAmqpContract>(options: AmqpOptions<TContract>) => Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract>>;
|
|
90
|
+
/** One piece of the handlers record โ what `AmqpHandler(contract, key)(โฆ)` returns, as the composing form consumes it. */
|
|
91
|
+
type PieceOf<C extends AnyAmqpContract> = { readonly [K in HandlerKeyOf<C>]: {
|
|
92
|
+
readonly port: HandlerPortOf<C, K>;
|
|
93
|
+
}; }[HandlerKeyOf<C>];
|
|
94
|
+
/** The key a piece carries, read back off its port id. */
|
|
95
|
+
type KeyOfPiece<P> = P extends {
|
|
96
|
+
readonly port: {
|
|
97
|
+
readonly portId: `${typeof HANDLER_PREFIX}${infer K}`;
|
|
98
|
+
};
|
|
99
|
+
} ? K : never;
|
|
100
|
+
/** The consumers and rpcs no piece in `T` covers. */
|
|
101
|
+
type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exclude<HandlerKeyOf<C>, KeyOfPiece<T[number]>>;
|
|
102
|
+
/**
|
|
103
|
+
* The composing arm. Declared LAST in the intersection below on purpose:
|
|
104
|
+
* TypeScript reports the last overload's failure, so a non-covering array is
|
|
105
|
+
* refused against the `"UNCOVERED HANDLERS โ โฆ"` marker rather than degrading
|
|
106
|
+
* to di's `Qualification`, which names nothing.
|
|
107
|
+
*
|
|
108
|
+
* The marker is a sentence rather than a bare label because it prints last,
|
|
109
|
+
* after the caller's own several-hundred-character piece type.
|
|
110
|
+
*/
|
|
111
|
+
type Compose<C extends AnyAmqpContract> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : readonly ["UNCOVERED HANDLERS โ the contract declares a consumer this array does not cover", Uncovered<C, T>]) => Provider<HandlersInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
|
|
112
|
+
readonly port: HandlersPortOf<C>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The handlers as a provider, from the contract. Three call forms, one port.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* AmqpHandlers(orderContract)({ logger: Logger }, { sync: ({ logger }) => ({ orderNotifications: (m) => โฆ }) })
|
|
119
|
+
* AmqpHandlers(orderContract)([orderNotifications, orderAudit])
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* The first two are di's own `Provider(port)` on the starter's handlers port
|
|
123
|
+
* typed for the contract. The third takes the pieces `AmqpHandler(contract,
|
|
124
|
+
* key)` builds: they are the provider's deps, keyed by the contract key each
|
|
125
|
+
* piece's port id carries, so the services record IS the handlers record. Every
|
|
126
|
+
* declared key must be covered, and two slices claiming one key are di's
|
|
127
|
+
* duplicate-provider defect at build.
|
|
128
|
+
*/
|
|
129
|
+
declare const AmqpHandlers: <C extends AnyAmqpContract>(contract: C) => ReturnType<typeof Provider<HandlersPortOf<C>>> & Compose<C>;
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/amqp-module.d.ts
|
|
132
|
+
/** The starter's own module, as the sugar adds it to the application's imports. */
|
|
133
|
+
type AmqpStarter<TContract extends AnyAmqpContract> = Module<AmqpRuntime | AmqpConfig, ConfigInvalid, Env | HandlersInstanceOf<TContract>>;
|
|
134
|
+
/** The application's imports plus the starter โ the tuple `Module(name)` is handed. */
|
|
135
|
+
type Imports<I extends readonly AnyModule[], TContract extends AnyAmqpContract> = readonly [...I, AmqpStarter<TContract>];
|
|
136
|
+
/** The handlers provider plus the application's own โ the tuple `Module(name)` is handed. */
|
|
137
|
+
type Provides<P extends readonly AnyProvider[], TContract extends AnyAmqpContract, HandlersError, HandlersNeeds> = readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P];
|
|
138
|
+
type AmqpModuleOptions<TContract extends AnyAmqpContract, HandlersError, HandlersNeeds, I extends readonly AnyModule[], P extends readonly AnyProvider[], X extends readonly Exportable<Imports<I, TContract>, Provides<P, TContract, HandlersError, HandlersNeeds>>[], N extends readonly AnyPort[]> = {
|
|
139
|
+
readonly contract: TContract;
|
|
140
|
+
/** The application's handlers โ what `AmqpHandlers(contract)(โฆ)` returns for THIS contract. */
|
|
141
|
+
readonly handlers: Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>;
|
|
142
|
+
/** Pins the broker instead of reading `AMQP_URL` โ a test's container. */
|
|
143
|
+
readonly url?: string;
|
|
144
|
+
/** See `AmqpOptions.connectionOptions`. */
|
|
145
|
+
readonly connectionOptions?: AmqpConnectionOptions;
|
|
146
|
+
/** See `AmqpOptions.defaultConsumerOptions`. */
|
|
147
|
+
readonly defaultConsumerOptions?: ConsumerOptions;
|
|
148
|
+
/** See `AmqpOptions.connectTimeoutMs`. */
|
|
149
|
+
readonly connectTimeoutMs?: number;
|
|
150
|
+
readonly imports?: I;
|
|
151
|
+
readonly provides?: P;
|
|
152
|
+
/** The application's own exports; `AmqpRuntime` is added, since `start` resolves it. */
|
|
153
|
+
readonly exports?: X;
|
|
154
|
+
/**
|
|
155
|
+
* What this root's OWN providers expect from outside. di's gate is re-stated
|
|
156
|
+
* over the augmented tuples below, so forgetting one is an error at THIS call.
|
|
157
|
+
*/
|
|
158
|
+
readonly needs?: N;
|
|
159
|
+
} & NeedsGate<Imports<I, TContract>, Provides<P, TContract, HandlersError, HandlersNeeds>, N>;
|
|
160
|
+
/**
|
|
161
|
+
* `Module(name)({...})` for an AMQP deployment: everything a di module takes,
|
|
162
|
+
* plus the contract and the handlers provider. The sugar imports the starter,
|
|
163
|
+
* provides the handlers and exports `AmqpRuntime`, handing back exactly the
|
|
164
|
+
* module `Module(...)` would have declared over the augmented tuples.
|
|
165
|
+
*
|
|
166
|
+
* ```ts
|
|
167
|
+
* export const OrderAmqpWorker = AmqpModule("OrderAmqpWorker")({
|
|
168
|
+
* contract: orderContract,
|
|
169
|
+
* handlers: orderHandlers,
|
|
170
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule],
|
|
171
|
+
* exports: [Logger],
|
|
172
|
+
* });
|
|
173
|
+
* await runMain(OrderAmqpWorker);
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare const AmqpModule: <const Name extends string>(name: Name) => <TContract extends AnyAmqpContract, HandlersError, HandlersNeeds, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, TContract>, Provides<P, TContract, HandlersError, HandlersNeeds>>[] = [], const N extends readonly AnyPort[] = []>(options: AmqpModuleOptions<TContract, HandlersError, HandlersNeeds, I, P, X, N>) => Module<(Extract<(readonly [typeof AmqpRuntime, ...X])[number], AnyModule> extends (infer T) ? T extends Extract<(readonly [typeof AmqpRuntime, ...X])[number], AnyModule> ? T extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [typeof AmqpRuntime, ...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_1) ? T_1 extends (readonly [Provider<HandlersInstanceOf<TContract>, HandlersError, HandlersNeeds>, ...P])[number] ? T_1 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>])[number] extends (infer T_2) ? T_2 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_2 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[1], Exclude<((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)[2], ((readonly [...I, AmqpStarter<TContract>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((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)[0]> | Exclude<((readonly [...I, AmqpStarter<TContract>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[2], ((readonly [...I, AmqpStarter<TContract>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, AmqpStarter<TContract>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((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)[0]>>;
|
|
177
|
+
//#endregion
|
|
178
|
+
export { AmqpConfig, type AmqpConnectionOptions, AmqpHandler, AmqpHandlers, type AmqpInfo, AmqpModule, type AmqpModuleOptions, type AmqpOptions, AmqpRuntime, type HandlerPortOf, type HandlersInstanceOf, type HandlersPortOf, amqp };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
2
|
+
import { TypedAmqpWorker } from "@amqp-contract/worker";
|
|
3
|
+
import { Config, Env } from "@btravstack/config";
|
|
4
|
+
import { RuntimePort, RuntimeStartFailed, releasedBy } from "@btravstack/core";
|
|
5
|
+
import { ErrAsync } from "unthrown";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
//#region src/handler.ts
|
|
8
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
9
|
+
const HANDLER_PREFIX = "AmqpHandler:";
|
|
10
|
+
/**
|
|
11
|
+
* One consumer or rpc of a contract, as a provider on a port of its own.
|
|
12
|
+
*
|
|
13
|
+
* A worker with several consumers is several pieces, each declaring the services
|
|
14
|
+
* its own handler calls; `AmqpHandlers(contract)([...])` composes them.
|
|
15
|
+
* `contract` is read for its TYPE only, and types both `key` and the handler โ
|
|
16
|
+
* so a consumer the contract does not declare is a compile error here.
|
|
17
|
+
*
|
|
18
|
+
* There is no name to give: the contract key IS the port's name.
|
|
19
|
+
*/
|
|
20
|
+
const AmqpHandler = (contract, key) => {
|
|
21
|
+
const port = class extends Port(`${HANDLER_PREFIX}${key}`) {};
|
|
22
|
+
return Provider(port);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/message-units.ts
|
|
26
|
+
/**
|
|
27
|
+
* Open one kernel unit per delivery. It injects nothing โ `next()` unchanged is
|
|
28
|
+
* the whole of the chain โ so the ambient `currentUnit()` record is what the
|
|
29
|
+
* unit leaves for the adapters that read it.
|
|
30
|
+
*
|
|
31
|
+
* **The kernel's per-unit `AbortSignal` rides that record too**, and it is the
|
|
32
|
+
* only route to it here: a handler has no parameter to receive one through. A
|
|
33
|
+
* handler that must stop when the kernel stops waiting reads
|
|
34
|
+
* `currentUnit()?.signal`.
|
|
35
|
+
*/
|
|
36
|
+
const messageUnits = (host) => (args, next) => host.run(metaFor(args.rawMessage), () => next());
|
|
37
|
+
/**
|
|
38
|
+
* `UnitMeta.id` must be unique per unit, and a **delivery tag is not one**: tags
|
|
39
|
+
* are per-channel and restart at `1` after a reconnect, which
|
|
40
|
+
* amqp-connection-manager performs silently underneath this worker.
|
|
41
|
+
* `consumerTag + deliveryTag` almost fixes it, until `ConsumerOptions` lets a
|
|
42
|
+
* caller pin `consumerTag`. Minting is the only form the rule survives.
|
|
43
|
+
*
|
|
44
|
+
* A W3C `traceparent` header wins when the publisher sends one; otherwise
|
|
45
|
+
* `messageId`, with `correlationId` as the RPC-shaped fallback.
|
|
46
|
+
*
|
|
47
|
+
* Only a NON-BLANK id is adopted, and the trim is load-bearing: `??` guards
|
|
48
|
+
* nullish alone, so a publisher setting `messageId: ""` would give every
|
|
49
|
+
* delivery the same blank trace id.
|
|
50
|
+
*/
|
|
51
|
+
const metaFor = (raw) => {
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
const parent = raw.properties.headers?.["traceparent"];
|
|
54
|
+
const fromParent = typeof parent === "string" ? traceIdOfTraceparent(parent) : void 0;
|
|
55
|
+
const inbound = [raw.properties.messageId, raw.properties.correlationId].map((value) => value?.trim() ?? "").find((value) => value !== "");
|
|
56
|
+
return {
|
|
57
|
+
kind: "delivery",
|
|
58
|
+
id,
|
|
59
|
+
traceId: fromParent ?? inbound ?? id
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The trace id inside a W3C `traceparent` header, which outranks `messageId`
|
|
64
|
+
* because it is the one value minted to span processes. Only the trace-id field
|
|
65
|
+
* is taken: `traceId` is a correlation id, not a span context, so the parent's
|
|
66
|
+
* span id is dropped rather than half-carried. The all-zero id is the spec's own
|
|
67
|
+
* "invalid" and is refused like a malformed header.
|
|
68
|
+
*/
|
|
69
|
+
const traceIdOfTraceparent = (header) => {
|
|
70
|
+
const traceId = /^[\da-f]{2}-([\da-f]{32})-[\da-f]{16}-[\da-f]{2}$/.exec(header.trim())?.[1];
|
|
71
|
+
return traceId === void 0 || /^0{32}$/.test(traceId) ? void 0 : traceId;
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/amqp-runtime.ts
|
|
75
|
+
/**
|
|
76
|
+
* The broker, as a service: `amqp()` binds it from `AMQP_URL` (default
|
|
77
|
+
* `amqp://127.0.0.1:5672`) unless pinned, and anything else in the graph may
|
|
78
|
+
* read it โ a publisher sharing the consumer's broker, say.
|
|
79
|
+
*/
|
|
80
|
+
var AmqpConfig = class extends Port("AmqpConfig") {};
|
|
81
|
+
/** The runtime's port: what `amqp()` provides, and what the module `start` boots must export. */
|
|
82
|
+
var AmqpRuntime = class extends RuntimePort {};
|
|
83
|
+
/**
|
|
84
|
+
* The handlers' port โ one id, the starter's own, which an application never
|
|
85
|
+
* names. Generic at the value level (one `Port(...)` call, so no duplicate-id
|
|
86
|
+
* warning however many contracts instantiate it) and fixed per contract at the
|
|
87
|
+
* type level through `HandlersPortOf<C>`, so a provider built for one contract
|
|
88
|
+
* cannot be handed to a module declaring another. Exported for this package's
|
|
89
|
+
* tests, not from `index.ts`.
|
|
90
|
+
*/
|
|
91
|
+
const AmqpHandlersPort = Port("AmqpHandlers");
|
|
92
|
+
/**
|
|
93
|
+
* The AMQP starter: a module providing the runtime and its configuration (bound
|
|
94
|
+
* from `AMQP_URL` unless pinned), built over the handlers the application
|
|
95
|
+
* provides on the starter's own handlers port โ the module's one need. Import
|
|
96
|
+
* it next to the application, provide the handlers, export `AmqpRuntime`.
|
|
97
|
+
*
|
|
98
|
+
* With `url` pinned the module reads nothing from the environment.
|
|
99
|
+
*/
|
|
100
|
+
const amqp = (options) => {
|
|
101
|
+
const config = options.url === void 0 ? Config.provider(AmqpConfig)(Config.object({ url: Config.string("AMQP_URL", { default: "amqp://127.0.0.1:5672" }) })) : Provider(AmqpConfig)({ value: { url: options.url } });
|
|
102
|
+
return Module("Amqp")({
|
|
103
|
+
needs: [Env, AmqpHandlersPort],
|
|
104
|
+
provides: [config, Provider(AmqpRuntime)({
|
|
105
|
+
config: AmqpConfig,
|
|
106
|
+
handlers: AmqpHandlersPort
|
|
107
|
+
}, { sync: ({ config: bound, handlers }) => ({
|
|
108
|
+
name: "amqp",
|
|
109
|
+
resolves: [],
|
|
110
|
+
start: (host) => createWorker(host, bound, options, handlers)
|
|
111
|
+
}) })],
|
|
112
|
+
exports: [AmqpRuntime, AmqpConfig]
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The handlers as a provider, from the contract. Three call forms, one port.
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* AmqpHandlers(orderContract)({ logger: Logger }, { sync: ({ logger }) => ({ orderNotifications: (m) => โฆ }) })
|
|
120
|
+
* AmqpHandlers(orderContract)([orderNotifications, orderAudit])
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* The first two are di's own `Provider(port)` on the starter's handlers port
|
|
124
|
+
* typed for the contract. The third takes the pieces `AmqpHandler(contract,
|
|
125
|
+
* key)` builds: they are the provider's deps, keyed by the contract key each
|
|
126
|
+
* piece's port id carries, so the services record IS the handlers record. Every
|
|
127
|
+
* declared key must be covered, and two slices claiming one key are di's
|
|
128
|
+
* duplicate-provider defect at build.
|
|
129
|
+
*/
|
|
130
|
+
const AmqpHandlers = (contract) => {
|
|
131
|
+
const build = Provider(AmqpHandlersPort);
|
|
132
|
+
const compose = (pieces) => build(Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(12), piece.port])), { sync: (services) => services });
|
|
133
|
+
return ((first, second) => second === void 0 && Array.isArray(first) ? compose(first) : build(first, second));
|
|
134
|
+
};
|
|
135
|
+
const startFailed = (cause) => new RuntimeStartFailed({
|
|
136
|
+
runtime: "amqp",
|
|
137
|
+
cause
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Every queue the contract's consumers and RPCs drain, sorted and
|
|
141
|
+
* de-duplicated. Derived rather than configured, so `Serving.info` cannot
|
|
142
|
+
* disagree with what the worker actually consumes.
|
|
143
|
+
*/
|
|
144
|
+
const queuesOf = (contract) => [...new Set(Object.values({
|
|
145
|
+
...contract.consumers,
|
|
146
|
+
...contract.rpcs
|
|
147
|
+
}).map((entry) => entry.queue.name))].sort();
|
|
148
|
+
const createWorker = (host, config, options, handlers) => TypedAmqpWorker.create({
|
|
149
|
+
contract: options.contract,
|
|
150
|
+
handlers,
|
|
151
|
+
middleware: messageUnits(host),
|
|
152
|
+
urls: [config.url],
|
|
153
|
+
...options.connectionOptions === void 0 ? {} : { connectionOptions: options.connectionOptions },
|
|
154
|
+
...options.defaultConsumerOptions === void 0 ? {} : { defaultConsumerOptions: options.defaultConsumerOptions },
|
|
155
|
+
...options.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs }
|
|
156
|
+
}).map((worker) => consume(worker, queuesOf(options.contract))).recoverDefect((cause) => ErrAsync(startFailed(cause)));
|
|
157
|
+
const consume = (worker, queues) => {
|
|
158
|
+
let closing;
|
|
159
|
+
const beginClose = () => closing ??= worker.close({ drainTimeoutMs: null });
|
|
160
|
+
let deadline;
|
|
161
|
+
const stopped = () => deadline === void 0 ? beginClose() : releasedBy(deadline, beginClose());
|
|
162
|
+
return {
|
|
163
|
+
info: { queues },
|
|
164
|
+
drain: (signal) => {
|
|
165
|
+
deadline = signal;
|
|
166
|
+
return stopped();
|
|
167
|
+
},
|
|
168
|
+
stop: () => stopped()
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/amqp-module.ts
|
|
173
|
+
/**
|
|
174
|
+
* `Module(name)({...})` for an AMQP deployment: everything a di module takes,
|
|
175
|
+
* plus the contract and the handlers provider. The sugar imports the starter,
|
|
176
|
+
* provides the handlers and exports `AmqpRuntime`, handing back exactly the
|
|
177
|
+
* module `Module(...)` would have declared over the augmented tuples.
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* export const OrderAmqpWorker = AmqpModule("OrderAmqpWorker")({
|
|
181
|
+
* contract: orderContract,
|
|
182
|
+
* handlers: orderHandlers,
|
|
183
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule],
|
|
184
|
+
* exports: [Logger],
|
|
185
|
+
* });
|
|
186
|
+
* await runMain(OrderAmqpWorker);
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
const AmqpModule = (name) => (options) => {
|
|
190
|
+
const { contract, handlers, url, connectionOptions, defaultConsumerOptions, connectTimeoutMs } = options;
|
|
191
|
+
const imports = options.imports ?? [];
|
|
192
|
+
const provides = options.provides ?? [];
|
|
193
|
+
const exports = options.exports ?? [];
|
|
194
|
+
const starter = amqp({
|
|
195
|
+
contract,
|
|
196
|
+
...url === void 0 ? {} : { url },
|
|
197
|
+
...connectionOptions === void 0 ? {} : { connectionOptions },
|
|
198
|
+
...defaultConsumerOptions === void 0 ? {} : { defaultConsumerOptions },
|
|
199
|
+
...connectTimeoutMs === void 0 ? {} : { connectTimeoutMs }
|
|
200
|
+
});
|
|
201
|
+
return Module(name)({
|
|
202
|
+
imports: [...imports, starter],
|
|
203
|
+
provides: [handlers, ...provides],
|
|
204
|
+
exports: [AmqpRuntime, ...exports],
|
|
205
|
+
needs: options.needs ?? []
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
export { AmqpConfig, AmqpHandler, AmqpHandlers, AmqpModule, AmqpRuntime, amqp };
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@btravstack/amqp-worker",
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"amqp",
|
|
7
|
+
"graceful-shutdown",
|
|
8
|
+
"lifecycle",
|
|
9
|
+
"rabbitmq",
|
|
10
|
+
"runtime",
|
|
11
|
+
"typescript",
|
|
12
|
+
"unthrown"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/btravstack/btravstack#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/btravstack/btravstack/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/btravstack/btravstack.git",
|
|
23
|
+
"directory": "packages/amqp-worker"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.mjs",
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"default": "./dist/index.mjs"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./dist/index.d.cts",
|
|
41
|
+
"default": "./dist/index.cjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@amqp-contract/contract": "3.0.0-beta.6",
|
|
48
|
+
"@amqp-contract/testing": "3.0.0-beta.6",
|
|
49
|
+
"@amqp-contract/worker": "3.0.0-beta.6",
|
|
50
|
+
"@btravstack/tsconfig": "0.2.0",
|
|
51
|
+
"@opentelemetry/api": "1.9.1",
|
|
52
|
+
"@types/node": "26.2.0",
|
|
53
|
+
"@unthrown/vitest": "5.7.0",
|
|
54
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
55
|
+
"tsdown": "0.22.14",
|
|
56
|
+
"typescript": "7.0.2",
|
|
57
|
+
"unthrown": "5.7.0",
|
|
58
|
+
"vitest": "4.1.11",
|
|
59
|
+
"zod": "4.4.3",
|
|
60
|
+
"@btravstack/config": "0.3.0",
|
|
61
|
+
"@btravstack/core": "0.3.0",
|
|
62
|
+
"@btravstack/internal-test-infra": "0.0.0",
|
|
63
|
+
"@btravstack/testing": "0.3.0",
|
|
64
|
+
"@btravstack/di": "0.3.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@amqp-contract/worker": "^3.0.0-beta.6",
|
|
68
|
+
"@opentelemetry/api": "^1",
|
|
69
|
+
"unthrown": "^5.0.0",
|
|
70
|
+
"@btravstack/config": "^0.3.0",
|
|
71
|
+
"@btravstack/core": "^0.3.0",
|
|
72
|
+
"@btravstack/di": "^0.3.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=20"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
|
|
79
|
+
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
|
|
80
|
+
"test": "vitest run --coverage",
|
|
81
|
+
"test:types": "tsc --noEmit -p tsconfig.test-d.json",
|
|
82
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|
|
83
|
+
}
|
|
84
|
+
}
|