@amqp-contract/client 0.24.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -10
- package/dist/index.cjs +72 -68
- package/dist/index.d.cts +111 -18
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +111 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +62 -58
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +113 -373
- package/package.json +20 -17
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
**Type-safe AMQP client for publishing messages using amqp-contract with explicit error handling via `Result` types.**
|
|
4
4
|
|
|
5
|
-
[](https://github.com/btravstack/amqp-contract/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@amqp-contract/client)
|
|
7
7
|
[](https://www.npmjs.com/package/@amqp-contract/client)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
|
|
11
|
-
📖 **[Full documentation →](https://
|
|
11
|
+
📖 **[Full documentation →](https://btravstack.github.io/amqp-contract/api/client)**
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -28,17 +28,20 @@ const client = (
|
|
|
28
28
|
contract,
|
|
29
29
|
urls: ["amqp://localhost"],
|
|
30
30
|
})
|
|
31
|
-
).
|
|
31
|
+
).unwrap();
|
|
32
32
|
|
|
33
33
|
// Publish message with explicit error handling
|
|
34
34
|
const result = await client.publish("orderCreated", {
|
|
35
35
|
orderId: "ORD-123",
|
|
36
36
|
amount: 99.99,
|
|
37
37
|
});
|
|
38
|
-
result.match(
|
|
39
|
-
() => console.log("Published successfully"),
|
|
40
|
-
(error) => console.error("Publish failed:", error),
|
|
41
|
-
)
|
|
38
|
+
result.match({
|
|
39
|
+
ok: () => console.log("Published successfully"),
|
|
40
|
+
err: (error) => console.error("Publish failed:", error),
|
|
41
|
+
defect: (cause) => {
|
|
42
|
+
throw cause;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
42
45
|
|
|
43
46
|
// Clean up
|
|
44
47
|
await client.close();
|
|
@@ -46,7 +49,7 @@ await client.close();
|
|
|
46
49
|
|
|
47
50
|
## Error Handling
|
|
48
51
|
|
|
49
|
-
The client uses `Result` types from [
|
|
52
|
+
The client uses `Result` types from [unthrown](https://github.com/btravstack/unthrown) for explicit error handling. Runtime errors are part of the type signature:
|
|
50
53
|
|
|
51
54
|
```typescript
|
|
52
55
|
publish(): Result<boolean, TechnicalError | MessageValidationError>
|
|
@@ -61,11 +64,11 @@ publish(): Result<boolean, TechnicalError | MessageValidationError>
|
|
|
61
64
|
|
|
62
65
|
## API
|
|
63
66
|
|
|
64
|
-
For complete API documentation, see the [Client API Reference](https://
|
|
67
|
+
For complete API documentation, see the [Client API Reference](https://btravstack.github.io/amqp-contract/api/client).
|
|
65
68
|
|
|
66
69
|
## Documentation
|
|
67
70
|
|
|
68
|
-
📖 **[Read the full documentation →](https://
|
|
71
|
+
📖 **[Read the full documentation →](https://btravstack.github.io/amqp-contract)**
|
|
69
72
|
|
|
70
73
|
## License
|
|
71
74
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _amqp_contract_contract = require("@amqp-contract/contract");
|
|
3
3
|
let _amqp_contract_core = require("@amqp-contract/core");
|
|
4
|
-
let
|
|
4
|
+
let unthrown = require("unthrown");
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
6
|
let node_zlib = require("node:zlib");
|
|
7
7
|
let node_util = require("node:util");
|
|
@@ -14,50 +14,47 @@ const deflateAsync = (0, node_util.promisify)(node_zlib.deflate);
|
|
|
14
14
|
*
|
|
15
15
|
* @param buffer - The buffer to compress
|
|
16
16
|
* @param algorithm - The compression algorithm to use
|
|
17
|
-
* @returns
|
|
17
|
+
* @returns An AsyncResult resolving to the compressed buffer or a TechnicalError
|
|
18
18
|
*
|
|
19
19
|
* @internal
|
|
20
20
|
*/
|
|
21
21
|
function compressBuffer(buffer, algorithm) {
|
|
22
|
-
return (0, ts_pattern.match)(algorithm).with("gzip", () =>
|
|
22
|
+
return (0, ts_pattern.match)(algorithm).with("gzip", () => (0, unthrown.fromPromise)(gzipAsync(buffer), (error) => new _amqp_contract_core.TechnicalError("Failed to compress with gzip", error))).with("deflate", () => (0, unthrown.fromPromise)(deflateAsync(buffer), (error) => new _amqp_contract_core.TechnicalError("Failed to compress with deflate", error))).exhaustive();
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/errors.ts
|
|
26
26
|
/**
|
|
27
|
-
* Captured `Error.captureStackTrace` shim — only present on Node.js.
|
|
28
|
-
*/
|
|
29
|
-
function captureStack(target, ctor) {
|
|
30
|
-
const ErrorConstructor = Error;
|
|
31
|
-
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(target, ctor);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
27
|
* Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
|
|
35
28
|
* before the RPC server publishes a reply with the matching `correlationId`.
|
|
36
29
|
*
|
|
37
30
|
* The pending call is removed from the in-memory correlation map; if a reply
|
|
38
31
|
* arrives after the timeout it is dropped (and a debug log is emitted by the
|
|
39
|
-
* client if a logger is configured).
|
|
32
|
+
* client if a logger is configured). Carries a namespaced `_tag` of
|
|
33
|
+
* `"@amqp-contract/RpcTimeoutError"`; the `Error.name` is kept bare
|
|
34
|
+
* (`"RpcTimeoutError"`).
|
|
40
35
|
*/
|
|
41
|
-
var RpcTimeoutError = class extends
|
|
36
|
+
var RpcTimeoutError = class extends (0, unthrown.TaggedError)("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError" }) {
|
|
42
37
|
constructor(rpcName, timeoutMs) {
|
|
43
|
-
super(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
super({
|
|
39
|
+
message: `RPC call to "${rpcName}" timed out after ${timeoutMs}ms with no reply received`,
|
|
40
|
+
rpcName,
|
|
41
|
+
timeoutMs
|
|
42
|
+
});
|
|
48
43
|
}
|
|
49
44
|
};
|
|
50
45
|
/**
|
|
51
46
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
52
47
|
* reply is received. The correlation map is cleared on close and every pending
|
|
53
|
-
* caller's promise resolves with `err(RpcCancelledError)`.
|
|
48
|
+
* caller's promise resolves with `err(RpcCancelledError)`. Carries a namespaced
|
|
49
|
+
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
50
|
+
* (`"RpcCancelledError"`).
|
|
54
51
|
*/
|
|
55
|
-
var RpcCancelledError = class extends
|
|
52
|
+
var RpcCancelledError = class extends (0, unthrown.TaggedError)("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError" }) {
|
|
56
53
|
constructor(rpcName) {
|
|
57
|
-
super(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
super({
|
|
55
|
+
message: `RPC call to "${rpcName}" was cancelled because the client was closed`,
|
|
56
|
+
rpcName
|
|
57
|
+
});
|
|
61
58
|
}
|
|
62
59
|
};
|
|
63
60
|
//#endregion
|
|
@@ -75,6 +72,11 @@ const DIRECT_REPLY_TO = "amq.rabbitmq.reply-to";
|
|
|
75
72
|
* Type-safe AMQP client for publishing messages
|
|
76
73
|
*/
|
|
77
74
|
var TypedAmqpClient = class TypedAmqpClient {
|
|
75
|
+
contract;
|
|
76
|
+
amqpClient;
|
|
77
|
+
defaultPublishOptions;
|
|
78
|
+
logger;
|
|
79
|
+
telemetry;
|
|
78
80
|
/**
|
|
79
81
|
* In-flight RPC calls keyed by `correlationId`. Cleared when a reply is
|
|
80
82
|
* received, when the call times out, or when the client is closed.
|
|
@@ -111,14 +113,15 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
111
113
|
persistent: true,
|
|
112
114
|
...defaultPublishOptions
|
|
113
115
|
}, logger, telemetry ?? _amqp_contract_core.defaultTelemetryProvider);
|
|
114
|
-
const setup = client.waitForConnectionReady().
|
|
115
|
-
return
|
|
116
|
+
const setup = client.waitForConnectionReady().flatMap(() => client.setupReplyConsumerIfNeeded());
|
|
117
|
+
return (0, unthrown.fromSafePromise)((async () => {
|
|
116
118
|
const setupResult = await setup;
|
|
117
|
-
if (setupResult.isOk())
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
if (!setupResult.isOk()) {
|
|
120
|
+
const closeResult = await client.close();
|
|
121
|
+
if (closeResult.isErr()) logger?.warn("Failed to close client after connection failure", { error: closeResult.error });
|
|
122
|
+
}
|
|
123
|
+
return setupResult.map(() => client);
|
|
124
|
+
})()).flatMap((result) => result);
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
127
|
* If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`
|
|
@@ -127,8 +130,8 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
127
130
|
*/
|
|
128
131
|
setupReplyConsumerIfNeeded() {
|
|
129
132
|
const rpcs = this.contract.rpcs ?? {};
|
|
130
|
-
if (Object.keys(rpcs).length === 0) return (0,
|
|
131
|
-
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).
|
|
133
|
+
if (Object.keys(rpcs).length === 0) return (0, unthrown.ok)(void 0).toAsync();
|
|
134
|
+
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).tap((tag) => {
|
|
132
135
|
this.replyConsumerTag = tag;
|
|
133
136
|
}).map(() => void 0);
|
|
134
137
|
}
|
|
@@ -157,28 +160,27 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
157
160
|
}
|
|
158
161
|
this.pendingCalls.delete(correlationId);
|
|
159
162
|
clearTimeout(pending.timer);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
} catch (error) {
|
|
164
|
-
pending.resolve((0, neverthrow.err)(new _amqp_contract_core.TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error)));
|
|
163
|
+
const parseResult = (0, _amqp_contract_core.safeJsonParse)(msg.content, (error) => new _amqp_contract_core.TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error));
|
|
164
|
+
if (!parseResult.isOk()) {
|
|
165
|
+
pending.resolve((0, unthrown.err)(parseResult.isErr() ? parseResult.error : new _amqp_contract_core.TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, parseResult.cause)));
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
168
|
+
const parsed = parseResult.value;
|
|
167
169
|
let rawValidation;
|
|
168
170
|
try {
|
|
169
171
|
rawValidation = pending.responseSchema["~standard"].validate(parsed);
|
|
170
172
|
} catch (error) {
|
|
171
|
-
pending.resolve((0,
|
|
173
|
+
pending.resolve((0, unthrown.err)(new _amqp_contract_core.TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
172
174
|
return;
|
|
173
175
|
}
|
|
174
176
|
(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).then((validation) => {
|
|
175
177
|
if (validation.issues) {
|
|
176
|
-
pending.resolve((0,
|
|
178
|
+
pending.resolve((0, unthrown.err)(new _amqp_contract_core.MessageValidationError(pending.rpcName, validation.issues)));
|
|
177
179
|
return;
|
|
178
180
|
}
|
|
179
|
-
pending.resolve((0,
|
|
181
|
+
pending.resolve((0, unthrown.ok)(validation.value));
|
|
180
182
|
}, (error) => {
|
|
181
|
-
pending.resolve((0,
|
|
183
|
+
pending.resolve((0, unthrown.err)(new _amqp_contract_core.TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
182
184
|
});
|
|
183
185
|
}
|
|
184
186
|
/**
|
|
@@ -200,10 +202,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
200
202
|
const span = (0, _amqp_contract_core.startPublishSpan)(this.telemetry, exchange.name, routingKey, { [_amqp_contract_core.MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName) });
|
|
201
203
|
const validateMessage = () => {
|
|
202
204
|
const validationResult = publisher.message.payload["~standard"].validate(message);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return (0, neverthrow.ok)(validation.value);
|
|
205
|
+
return (0, unthrown.fromPromise)(validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult), (error) => new _amqp_contract_core.TechnicalError("Validation failed", error)).flatMap((validation) => {
|
|
206
|
+
if (validation.issues) return (0, unthrown.err)(new _amqp_contract_core.MessageValidationError(String(publisherName), validation.issues));
|
|
207
|
+
return (0, unthrown.ok)(validation.value);
|
|
207
208
|
});
|
|
208
209
|
};
|
|
209
210
|
const publishMessage = (validatedMessage) => {
|
|
@@ -218,24 +219,24 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
218
219
|
publishOptions.contentEncoding = compression;
|
|
219
220
|
return compressBuffer(messageBuffer, compression);
|
|
220
221
|
}
|
|
221
|
-
return (0,
|
|
222
|
+
return (0, unthrown.ok)(validatedMessage).toAsync();
|
|
222
223
|
};
|
|
223
|
-
return preparePayload().
|
|
224
|
-
if (!published) return (0,
|
|
224
|
+
return preparePayload().flatMap((payload) => this.amqpClient.publish(publisher.exchange.name, publisher.routingKey ?? "", payload, publishOptions).flatMap((published) => {
|
|
225
|
+
if (!published) return (0, unthrown.err)(new _amqp_contract_core.TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
|
|
225
226
|
this.logger?.info("Message published successfully", {
|
|
226
227
|
publisherName: String(publisherName),
|
|
227
228
|
exchange: publisher.exchange.name,
|
|
228
229
|
routingKey: publisher.routingKey,
|
|
229
230
|
compressed: !!compression
|
|
230
231
|
});
|
|
231
|
-
return (0,
|
|
232
|
+
return (0, unthrown.ok)(void 0);
|
|
232
233
|
}));
|
|
233
234
|
};
|
|
234
|
-
return validateMessage().
|
|
235
|
+
return validateMessage().flatMap((validatedMessage) => publishMessage(validatedMessage)).tap(() => {
|
|
235
236
|
const durationMs = Date.now() - startTime;
|
|
236
237
|
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
237
238
|
(0, _amqp_contract_core.recordPublishMetric)(this.telemetry, exchange.name, routingKey, true, durationMs);
|
|
238
|
-
}).
|
|
239
|
+
}).tapErr((error) => {
|
|
239
240
|
const durationMs = Date.now() - startTime;
|
|
240
241
|
(0, _amqp_contract_core.endSpanError)(span, error);
|
|
241
242
|
(0, _amqp_contract_core.recordPublishMetric)(this.telemetry, exchange.name, routingKey, false, durationMs);
|
|
@@ -247,19 +248,23 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
247
248
|
* The request payload is validated against the RPC's request schema, then
|
|
248
249
|
* published to the AMQP default exchange with the server's queue name as
|
|
249
250
|
* routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID
|
|
250
|
-
* `correlationId`. The returned
|
|
251
|
+
* `correlationId`. The returned AsyncResult resolves once a matching reply
|
|
251
252
|
* arrives and validates against the response schema, or once `timeoutMs`
|
|
252
253
|
* elapses (whichever comes first).
|
|
253
254
|
*
|
|
254
255
|
* @example
|
|
255
256
|
* ```typescript
|
|
256
257
|
* const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });
|
|
257
|
-
*
|
|
258
|
+
* result.match({
|
|
259
|
+
* ok: (value) => console.log(value.sum), // 3
|
|
260
|
+
* err: (error) => console.error(error),
|
|
261
|
+
* defect: (cause) => console.error(cause),
|
|
262
|
+
* });
|
|
258
263
|
* ```
|
|
259
264
|
*/
|
|
260
265
|
call(rpcName, request, options) {
|
|
261
266
|
const TIMEOUT_MAX_MS = 2147483647;
|
|
262
|
-
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return (0,
|
|
267
|
+
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return (0, unthrown.err)(new _amqp_contract_core.TechnicalError(`Invalid timeoutMs for RPC call to "${String(rpcName)}": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`)).toAsync();
|
|
263
268
|
const startTime = Date.now();
|
|
264
269
|
const rpc = this.contract.rpcs[rpcName];
|
|
265
270
|
const requestSchema = rpc.request.payload;
|
|
@@ -268,13 +273,13 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
268
273
|
const span = (0, _amqp_contract_core.startPublishSpan)(this.telemetry, "", queueName, { [_amqp_contract_core.MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName) });
|
|
269
274
|
const correlationId = (0, node_crypto.randomUUID)();
|
|
270
275
|
let resolveCall;
|
|
271
|
-
const callResultAsync =
|
|
276
|
+
const callResultAsync = (0, unthrown.fromSafePromise)(new Promise((res) => {
|
|
272
277
|
resolveCall = res;
|
|
273
|
-
}));
|
|
278
|
+
})).flatMap((result) => result);
|
|
274
279
|
const timer = setTimeout(() => {
|
|
275
280
|
if (!this.pendingCalls.has(correlationId)) return;
|
|
276
281
|
this.pendingCalls.delete(correlationId);
|
|
277
|
-
resolveCall((0,
|
|
282
|
+
resolveCall((0, unthrown.err)(new RpcTimeoutError(String(rpcName), options.timeoutMs)));
|
|
278
283
|
}, options.timeoutMs);
|
|
279
284
|
this.pendingCalls.set(correlationId, {
|
|
280
285
|
rpcName: String(rpcName),
|
|
@@ -287,10 +292,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
287
292
|
try {
|
|
288
293
|
rawValidation = requestSchema["~standard"].validate(request);
|
|
289
294
|
} catch (error) {
|
|
290
|
-
return (0,
|
|
295
|
+
return (0, unthrown.err)(new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).toAsync();
|
|
291
296
|
}
|
|
292
|
-
|
|
293
|
-
return neverthrow.ResultAsync.fromPromise(validationPromise, (error) => new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).andThen((validation) => validation.issues ? (0, neverthrow.err)(new _amqp_contract_core.MessageValidationError(String(rpcName), validation.issues)) : (0, neverthrow.ok)(validation.value));
|
|
297
|
+
return (0, unthrown.fromPromise)(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).flatMap((validation) => validation.issues ? (0, unthrown.err)(new _amqp_contract_core.MessageValidationError(String(rpcName), validation.issues)) : (0, unthrown.ok)(validation.value));
|
|
294
298
|
};
|
|
295
299
|
const publishRequest = (validatedRequest) => {
|
|
296
300
|
const { compression: _ignoredCompression, ...defaultsWithoutCompression } = this.defaultPublishOptions;
|
|
@@ -301,19 +305,19 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
301
305
|
correlationId,
|
|
302
306
|
contentType: "application/json"
|
|
303
307
|
};
|
|
304
|
-
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).
|
|
308
|
+
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).flatMap((published) => published ? (0, unthrown.ok)(void 0) : (0, unthrown.err)(new _amqp_contract_core.TechnicalError(`Failed to publish RPC request for "${String(rpcName)}": channel buffer full`)));
|
|
305
309
|
};
|
|
306
|
-
return validateRequest().
|
|
310
|
+
return validateRequest().flatMap((validated) => publishRequest(validated)).flatMap(() => callResultAsync).orElse((error) => {
|
|
307
311
|
if (this.pendingCalls.has(correlationId)) {
|
|
308
312
|
clearTimeout(timer);
|
|
309
313
|
this.pendingCalls.delete(correlationId);
|
|
310
314
|
}
|
|
311
|
-
return (0,
|
|
312
|
-
}).
|
|
315
|
+
return (0, unthrown.err)(error).toAsync();
|
|
316
|
+
}).tap(() => {
|
|
313
317
|
const durationMs = Date.now() - startTime;
|
|
314
318
|
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
315
319
|
(0, _amqp_contract_core.recordPublishMetric)(this.telemetry, "", queueName, true, durationMs);
|
|
316
|
-
}).
|
|
320
|
+
}).tapErr((error) => {
|
|
317
321
|
const durationMs = Date.now() - startTime;
|
|
318
322
|
(0, _amqp_contract_core.endSpanError)(span, error);
|
|
319
323
|
(0, _amqp_contract_core.recordPublishMetric)(this.telemetry, "", queueName, false, durationMs);
|
|
@@ -326,13 +330,13 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
326
330
|
close() {
|
|
327
331
|
for (const [, pending] of this.pendingCalls) {
|
|
328
332
|
clearTimeout(pending.timer);
|
|
329
|
-
pending.resolve((0,
|
|
333
|
+
pending.resolve((0, unthrown.err)(new RpcCancelledError(pending.rpcName)));
|
|
330
334
|
}
|
|
331
335
|
this.pendingCalls.clear();
|
|
332
336
|
return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {
|
|
333
337
|
this.logger?.warn("Failed to cancel RPC reply consumer during close", { error });
|
|
334
|
-
return (0,
|
|
335
|
-
}) : (0,
|
|
338
|
+
return (0, unthrown.ok)(void 0);
|
|
339
|
+
}) : (0, unthrown.ok)(void 0).toAsync()).flatMap(() => this.amqpClient.close());
|
|
336
340
|
}
|
|
337
341
|
waitForConnectionReady() {
|
|
338
342
|
return this.amqpClient.waitForConnect();
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,92 @@
|
|
|
1
1
|
import { CompressionAlgorithm, ContractDefinition, InferPublisherNames, InferRpcNames, MessageDefinition, PublisherEntry, RpcDefinition } from "@amqp-contract/contract";
|
|
2
2
|
import { Logger, MessageValidationError, PublishOptions as PublishOptions$1, TechnicalError, TelemetryProvider } from "@amqp-contract/core";
|
|
3
|
-
import * as amqp from "amqplib";
|
|
4
|
-
import { TcpSocketConnectOpts } from "net";
|
|
5
3
|
import { ConnectionOptions } from "tls";
|
|
6
|
-
import {
|
|
4
|
+
import { TcpSocketConnectOpts } from "net";
|
|
5
|
+
import { AsyncResult } from "unthrown";
|
|
7
6
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
8
7
|
|
|
9
|
-
//#region ../../node_modules/.pnpm/
|
|
10
|
-
|
|
8
|
+
//#region ../../node_modules/.pnpm/amqplib@2.0.1/node_modules/amqplib/lib/properties.d.ts
|
|
9
|
+
declare namespace Options {
|
|
10
|
+
interface Connect {
|
|
11
|
+
protocol?: string;
|
|
12
|
+
hostname?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
username?: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
locale?: string;
|
|
17
|
+
frameMax?: number;
|
|
18
|
+
heartbeat?: number;
|
|
19
|
+
vhost?: string;
|
|
20
|
+
channelMax?: number;
|
|
21
|
+
credentials?: {
|
|
22
|
+
mechanism: string;
|
|
23
|
+
response(): Buffer;
|
|
24
|
+
username?: string;
|
|
25
|
+
password?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
interface AssertQueue {
|
|
29
|
+
exclusive?: boolean;
|
|
30
|
+
durable?: boolean;
|
|
31
|
+
autoDelete?: boolean;
|
|
32
|
+
arguments?: any;
|
|
33
|
+
messageTtl?: number;
|
|
34
|
+
expires?: number;
|
|
35
|
+
deadLetterExchange?: string;
|
|
36
|
+
deadLetterRoutingKey?: string;
|
|
37
|
+
maxLength?: number;
|
|
38
|
+
maxPriority?: number;
|
|
39
|
+
overflow?: string;
|
|
40
|
+
queueMode?: string;
|
|
41
|
+
}
|
|
42
|
+
interface DeleteQueue {
|
|
43
|
+
ifUnused?: boolean;
|
|
44
|
+
ifEmpty?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface AssertExchange {
|
|
47
|
+
durable?: boolean;
|
|
48
|
+
internal?: boolean;
|
|
49
|
+
autoDelete?: boolean;
|
|
50
|
+
alternateExchange?: string;
|
|
51
|
+
arguments?: any;
|
|
52
|
+
}
|
|
53
|
+
interface DeleteExchange {
|
|
54
|
+
ifUnused?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface Publish {
|
|
57
|
+
expiration?: string | number;
|
|
58
|
+
userId?: string;
|
|
59
|
+
CC?: string | string[];
|
|
60
|
+
mandatory?: boolean;
|
|
61
|
+
persistent?: boolean;
|
|
62
|
+
deliveryMode?: boolean | number;
|
|
63
|
+
BCC?: string | string[];
|
|
64
|
+
contentType?: string;
|
|
65
|
+
contentEncoding?: string;
|
|
66
|
+
headers?: any;
|
|
67
|
+
priority?: number;
|
|
68
|
+
correlationId?: string;
|
|
69
|
+
replyTo?: string;
|
|
70
|
+
messageId?: string;
|
|
71
|
+
timestamp?: number;
|
|
72
|
+
type?: string;
|
|
73
|
+
appId?: string;
|
|
74
|
+
}
|
|
75
|
+
interface Consume {
|
|
76
|
+
consumerTag?: string;
|
|
77
|
+
noLocal?: boolean;
|
|
78
|
+
noAck?: boolean;
|
|
79
|
+
exclusive?: boolean;
|
|
80
|
+
priority?: number;
|
|
81
|
+
arguments?: any;
|
|
82
|
+
}
|
|
83
|
+
interface Get {
|
|
84
|
+
noAck?: boolean;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region ../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@2.0.1/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts
|
|
89
|
+
type ConnectionUrl = string | Options.Connect | {
|
|
11
90
|
url: string;
|
|
12
91
|
connectionOptions?: AmqpConnectionOptions;
|
|
13
92
|
};
|
|
@@ -48,26 +127,36 @@ interface AmqpConnectionManagerOptions {
|
|
|
48
127
|
}
|
|
49
128
|
//#endregion
|
|
50
129
|
//#region src/errors.d.ts
|
|
130
|
+
declare const RpcTimeoutError_base: import("unthrown").TaggedErrorConstructor<"@amqp-contract/RpcTimeoutError">;
|
|
51
131
|
/**
|
|
52
132
|
* Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
|
|
53
133
|
* before the RPC server publishes a reply with the matching `correlationId`.
|
|
54
134
|
*
|
|
55
135
|
* The pending call is removed from the in-memory correlation map; if a reply
|
|
56
136
|
* arrives after the timeout it is dropped (and a debug log is emitted by the
|
|
57
|
-
* client if a logger is configured).
|
|
137
|
+
* client if a logger is configured). Carries a namespaced `_tag` of
|
|
138
|
+
* `"@amqp-contract/RpcTimeoutError"`; the `Error.name` is kept bare
|
|
139
|
+
* (`"RpcTimeoutError"`).
|
|
58
140
|
*/
|
|
59
|
-
declare class RpcTimeoutError extends
|
|
60
|
-
|
|
61
|
-
|
|
141
|
+
declare class RpcTimeoutError extends RpcTimeoutError_base<{
|
|
142
|
+
message: string;
|
|
143
|
+
rpcName: string;
|
|
144
|
+
timeoutMs: number;
|
|
145
|
+
}> {
|
|
62
146
|
constructor(rpcName: string, timeoutMs: number);
|
|
63
147
|
}
|
|
148
|
+
declare const RpcCancelledError_base: import("unthrown").TaggedErrorConstructor<"@amqp-contract/RpcCancelledError">;
|
|
64
149
|
/**
|
|
65
150
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
66
151
|
* reply is received. The correlation map is cleared on close and every pending
|
|
67
|
-
* caller's promise resolves with `err(RpcCancelledError)`.
|
|
152
|
+
* caller's promise resolves with `err(RpcCancelledError)`. Carries a namespaced
|
|
153
|
+
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
154
|
+
* (`"RpcCancelledError"`).
|
|
68
155
|
*/
|
|
69
|
-
declare class RpcCancelledError extends
|
|
70
|
-
|
|
156
|
+
declare class RpcCancelledError extends RpcCancelledError_base<{
|
|
157
|
+
message: string;
|
|
158
|
+
rpcName: string;
|
|
159
|
+
}> {
|
|
71
160
|
constructor(rpcName: string);
|
|
72
161
|
}
|
|
73
162
|
//#endregion
|
|
@@ -203,7 +292,7 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
203
292
|
logger,
|
|
204
293
|
telemetry,
|
|
205
294
|
connectTimeoutMs
|
|
206
|
-
}: CreateClientOptions<TContract>):
|
|
295
|
+
}: CreateClientOptions<TContract>): AsyncResult<TypedAmqpClient<TContract>, TechnicalError>;
|
|
207
296
|
/**
|
|
208
297
|
* If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`
|
|
209
298
|
* once. Replies for every in-flight call arrive on this single consumer and
|
|
@@ -232,29 +321,33 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
232
321
|
* and the `contentEncoding` property will be set automatically. Any `contentEncoding`
|
|
233
322
|
* value already in options will be overwritten by the compression algorithm.
|
|
234
323
|
*/
|
|
235
|
-
publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions):
|
|
324
|
+
publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): AsyncResult<void, TechnicalError | MessageValidationError>;
|
|
236
325
|
/**
|
|
237
326
|
* Invoke an RPC defined via `defineRpc` and await the typed response.
|
|
238
327
|
*
|
|
239
328
|
* The request payload is validated against the RPC's request schema, then
|
|
240
329
|
* published to the AMQP default exchange with the server's queue name as
|
|
241
330
|
* routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID
|
|
242
|
-
* `correlationId`. The returned
|
|
331
|
+
* `correlationId`. The returned AsyncResult resolves once a matching reply
|
|
243
332
|
* arrives and validates against the response schema, or once `timeoutMs`
|
|
244
333
|
* elapses (whichever comes first).
|
|
245
334
|
*
|
|
246
335
|
* @example
|
|
247
336
|
* ```typescript
|
|
248
337
|
* const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });
|
|
249
|
-
*
|
|
338
|
+
* result.match({
|
|
339
|
+
* ok: (value) => console.log(value.sum), // 3
|
|
340
|
+
* err: (error) => console.error(error),
|
|
341
|
+
* defect: (cause) => console.error(cause),
|
|
342
|
+
* });
|
|
250
343
|
* ```
|
|
251
344
|
*/
|
|
252
|
-
call<TName extends InferRpcNames<TContract>>(rpcName: TName, request: ClientInferRpcRequestInput<TContract, TName>, options: CallOptions):
|
|
345
|
+
call<TName extends InferRpcNames<TContract>>(rpcName: TName, request: ClientInferRpcRequestInput<TContract, TName>, options: CallOptions): AsyncResult<ClientInferRpcResponseOutput<TContract, TName>, TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError>;
|
|
253
346
|
/**
|
|
254
347
|
* Close the channel and connection. Cancels the reply consumer (if any) and
|
|
255
348
|
* rejects every in-flight RPC call with `RpcCancelledError`.
|
|
256
349
|
*/
|
|
257
|
-
close():
|
|
350
|
+
close(): AsyncResult<void, TechnicalError>;
|
|
258
351
|
private waitForConnectionReady;
|
|
259
352
|
}
|
|
260
353
|
//#endregion
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":["
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":["Empty","AssertQueue","queue","messageCount","consumerCount","PurgeQueue","DeleteQueue","AssertExchange","exchange","Consume","consumerTag","Connect","protocol","hostname","port","username","password","locale","frameMax","heartbeat","vhost","channelMax","credentials","mechanism","response","Buffer","exclusive","durable","autoDelete","arguments","messageTtl","expires","deadLetterExchange","deadLetterRoutingKey","maxLength","maxPriority","overflow","queueMode","ifUnused","ifEmpty","internal","alternateExchange","DeleteExchange","Publish","expiration","userId","CC","mandatory","persistent","deliveryMode","BCC","contentType","contentEncoding","headers","priority","correlationId","replyTo","messageId","timestamp","type","appId","noLocal","noAck","Get","tls","ConnectionOptions","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","Record","highWaterMark","content","fields","MessageFields","properties","MessageProperties","Message","GetMessageFields","ConsumeMessageFields","deliveryTag","redelivered","routingKey","CommonMessageFields","MessagePropertyHeaders","clusterId","XDeath","key","count","reason","time","value","host","product","version","platform","copyright","information","amqp","Options","Connect","url","connectionOptions","AmqpConnectionOptions","connection","Connection","arg","err","Error","ConnectionOptions","TcpSocketConnectOpts","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","Buffer","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","ConnectionUrl","urls","callback","Promise","EventEmitter","addListener","event","args","listener","ConnectListener","ConnectFailedListener","reason","listeners","eventName","Function","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","CreateChannelOpts","ChannelWrapper","close","isConnected","ChannelModel","channelCount","IAmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","AmqpConnectionManagerOptions","_connect"],"sources":["../../../node_modules/.pnpm/amqplib@2.0.1/node_modules/amqplib/lib/properties.d.ts","../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@2.0.1/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts","../src/errors.ts","../src/types.ts","../src/client.ts"],"x_google_ignoreList":[0,1],"mappings":"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"}
|