@amqp-contract/client 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +244 -18
- package/dist/index.d.cts +133 -17
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +132 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +237 -12
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +454 -29
- package/package.json +29 -29
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value:
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _amqp_contract_contract = require("@amqp-contract/contract");
|
|
2
3
|
let _amqp_contract_core = require("@amqp-contract/core");
|
|
3
4
|
let _swan_io_boxed = require("@swan-io/boxed");
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
4
6
|
let node_zlib = require("node:zlib");
|
|
5
7
|
let ts_pattern = require("ts-pattern");
|
|
6
8
|
let node_util = require("node:util");
|
|
7
|
-
|
|
8
9
|
//#region src/compression.ts
|
|
9
10
|
const gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
10
11
|
const deflateAsync = (0, node_util.promisify)(node_zlib.deflate);
|
|
@@ -20,16 +21,74 @@ const deflateAsync = (0, node_util.promisify)(node_zlib.deflate);
|
|
|
20
21
|
function compressBuffer(buffer, algorithm) {
|
|
21
22
|
return (0, ts_pattern.match)(algorithm).with("gzip", () => _swan_io_boxed.Future.fromPromise(gzipAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to compress with gzip", error))).with("deflate", () => _swan_io_boxed.Future.fromPromise(deflateAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to compress with deflate", error))).exhaustive();
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/errors.ts
|
|
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
|
+
* Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
|
|
35
|
+
* before the RPC server publishes a reply with the matching `correlationId`.
|
|
36
|
+
*
|
|
37
|
+
* The pending call is removed from the in-memory correlation map; if a reply
|
|
38
|
+
* arrives after the timeout it is dropped (and a debug log is emitted by the
|
|
39
|
+
* client if a logger is configured).
|
|
40
|
+
*/
|
|
41
|
+
var RpcTimeoutError = class extends Error {
|
|
42
|
+
constructor(rpcName, timeoutMs) {
|
|
43
|
+
super(`RPC call to "${rpcName}" timed out after ${timeoutMs}ms with no reply received`);
|
|
44
|
+
this.rpcName = rpcName;
|
|
45
|
+
this.timeoutMs = timeoutMs;
|
|
46
|
+
this.name = "RpcTimeoutError";
|
|
47
|
+
captureStack(this, this.constructor);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Returned from any in-flight RPC call when the client is closed before the
|
|
52
|
+
* reply is received. The correlation map is cleared on close and every pending
|
|
53
|
+
* caller's promise resolves with `Result.Error(RpcCancelledError)`.
|
|
54
|
+
*/
|
|
55
|
+
var RpcCancelledError = class extends Error {
|
|
56
|
+
constructor(rpcName) {
|
|
57
|
+
super(`RPC call to "${rpcName}" was cancelled because the client was closed`);
|
|
58
|
+
this.rpcName = rpcName;
|
|
59
|
+
this.name = "RpcCancelledError";
|
|
60
|
+
captureStack(this, this.constructor);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
24
63
|
//#endregion
|
|
25
64
|
//#region src/client.ts
|
|
26
65
|
/**
|
|
66
|
+
* The RabbitMQ direct-reply-to pseudo-queue. Publishing with `replyTo` set to
|
|
67
|
+
* this value tells the server to deliver the response back to the consumer
|
|
68
|
+
* subscribed on this queue on the same channel — no real queue is created and
|
|
69
|
+
* no setup is required beyond consuming from it once with `noAck: true`.
|
|
70
|
+
*
|
|
71
|
+
* @see https://www.rabbitmq.com/docs/direct-reply-to
|
|
72
|
+
*/
|
|
73
|
+
const DIRECT_REPLY_TO = "amq.rabbitmq.reply-to";
|
|
74
|
+
/**
|
|
27
75
|
* Type-safe AMQP client for publishing messages
|
|
28
76
|
*/
|
|
29
77
|
var TypedAmqpClient = class TypedAmqpClient {
|
|
30
|
-
|
|
78
|
+
/**
|
|
79
|
+
* In-flight RPC calls keyed by `correlationId`. Cleared when a reply is
|
|
80
|
+
* received, when the call times out, or when the client is closed.
|
|
81
|
+
*/
|
|
82
|
+
pendingCalls = /* @__PURE__ */ new Map();
|
|
83
|
+
/**
|
|
84
|
+
* Consumer tag of the reply consumer subscribed on `amq.rabbitmq.reply-to`.
|
|
85
|
+
* Set when the contract has at least one entry in `rpcs`; undefined otherwise.
|
|
86
|
+
*/
|
|
87
|
+
replyConsumerTag;
|
|
88
|
+
constructor(contract, amqpClient, defaultPublishOptions, logger, telemetry = _amqp_contract_core.defaultTelemetryProvider) {
|
|
31
89
|
this.contract = contract;
|
|
32
90
|
this.amqpClient = amqpClient;
|
|
91
|
+
this.defaultPublishOptions = defaultPublishOptions;
|
|
33
92
|
this.logger = logger;
|
|
34
93
|
this.telemetry = telemetry;
|
|
35
94
|
}
|
|
@@ -43,12 +102,77 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
43
102
|
* Connections are automatically shared across clients with the same URLs and
|
|
44
103
|
* connection options, following RabbitMQ best practices.
|
|
45
104
|
*/
|
|
46
|
-
static create({ contract, urls, connectionOptions, logger, telemetry }) {
|
|
105
|
+
static create({ contract, urls, connectionOptions, defaultPublishOptions, logger, telemetry, connectTimeoutMs }) {
|
|
47
106
|
const client = new TypedAmqpClient(contract, new _amqp_contract_core.AmqpClient(contract, {
|
|
48
107
|
urls,
|
|
49
|
-
connectionOptions
|
|
50
|
-
|
|
51
|
-
|
|
108
|
+
connectionOptions,
|
|
109
|
+
connectTimeoutMs
|
|
110
|
+
}), {
|
|
111
|
+
persistent: true,
|
|
112
|
+
...defaultPublishOptions
|
|
113
|
+
}, logger, telemetry ?? _amqp_contract_core.defaultTelemetryProvider);
|
|
114
|
+
return client.waitForConnectionReady().flatMapOk(() => client.setupReplyConsumerIfNeeded()).flatMap((result) => result.match({
|
|
115
|
+
Ok: () => _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(client)),
|
|
116
|
+
Error: (error) => client.close().tapError((closeError) => {
|
|
117
|
+
logger?.warn("Failed to close client after connection failure", { error: closeError });
|
|
118
|
+
}).map(() => _swan_io_boxed.Result.Error(error))
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`
|
|
123
|
+
* once. Replies for every in-flight call arrive on this single consumer and
|
|
124
|
+
* are demultiplexed by `correlationId`.
|
|
125
|
+
*/
|
|
126
|
+
setupReplyConsumerIfNeeded() {
|
|
127
|
+
const rpcs = this.contract.rpcs ?? {};
|
|
128
|
+
if (Object.keys(rpcs).length === 0) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
129
|
+
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).tapOk((tag) => {
|
|
130
|
+
this.replyConsumerTag = tag;
|
|
131
|
+
}).mapOk(() => void 0);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Demultiplex an RPC reply by `correlationId`, validate the body against the
|
|
135
|
+
* call's response schema, and resolve the awaiting caller. Replies with no
|
|
136
|
+
* matching pending call (e.g. arriving after the call timed out) are dropped
|
|
137
|
+
* with a debug log.
|
|
138
|
+
*/
|
|
139
|
+
handleRpcReply(msg) {
|
|
140
|
+
if (!msg) return;
|
|
141
|
+
const correlationId = msg.properties.correlationId;
|
|
142
|
+
if (typeof correlationId !== "string") {
|
|
143
|
+
this.logger?.warn("Received RPC reply without correlationId; dropping", { deliveryTag: msg.fields.deliveryTag });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const pending = this.pendingCalls.get(correlationId);
|
|
147
|
+
if (!pending) {
|
|
148
|
+
this.logger?.debug("Received RPC reply for unknown correlationId", { correlationId });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.pendingCalls.delete(correlationId);
|
|
152
|
+
clearTimeout(pending.timer);
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(msg.content.toString());
|
|
156
|
+
} catch (error) {
|
|
157
|
+
pending.resolve(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error)));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let rawValidation;
|
|
161
|
+
try {
|
|
162
|
+
rawValidation = pending.responseSchema["~standard"].validate(parsed);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
pending.resolve(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).then((validation) => {
|
|
168
|
+
if (validation.issues) {
|
|
169
|
+
pending.resolve(_swan_io_boxed.Result.Error(new _amqp_contract_core.MessageValidationError(pending.rpcName, validation.issues)));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
pending.resolve(_swan_io_boxed.Result.Ok(validation.value));
|
|
173
|
+
}, (error) => {
|
|
174
|
+
pending.resolve(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
175
|
+
});
|
|
52
176
|
}
|
|
53
177
|
/**
|
|
54
178
|
* Publish a message using a defined publisher
|
|
@@ -81,7 +205,10 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
81
205
|
});
|
|
82
206
|
};
|
|
83
207
|
const publishMessage = (validatedMessage) => {
|
|
84
|
-
const { compression, ...restOptions } =
|
|
208
|
+
const { compression, ...restOptions } = {
|
|
209
|
+
...this.defaultPublishOptions,
|
|
210
|
+
...options
|
|
211
|
+
};
|
|
85
212
|
const publishOptions = { ...restOptions };
|
|
86
213
|
const preparePayload = () => {
|
|
87
214
|
if (compression) {
|
|
@@ -113,21 +240,120 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
113
240
|
});
|
|
114
241
|
}
|
|
115
242
|
/**
|
|
116
|
-
*
|
|
243
|
+
* Invoke an RPC defined via `defineRpc` and await the typed response.
|
|
244
|
+
*
|
|
245
|
+
* The request payload is validated against the RPC's request schema, then
|
|
246
|
+
* published to the AMQP default exchange with the server's queue name as
|
|
247
|
+
* routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID
|
|
248
|
+
* `correlationId`. The returned Future resolves once a matching reply
|
|
249
|
+
* arrives and validates against the response schema, or once `timeoutMs`
|
|
250
|
+
* elapses (whichever comes first).
|
|
251
|
+
*
|
|
252
|
+
* @typeParam TName - An RPC name from `contract.rpcs`.
|
|
253
|
+
* @param rpcName - The RPC name from the contract.
|
|
254
|
+
* @param request - The request payload, validated against the request schema.
|
|
255
|
+
* @param options - Per-call options. `timeoutMs` is required.
|
|
256
|
+
*
|
|
257
|
+
* @returns `Result.Ok(response)` on a successful round-trip; `Result.Error`
|
|
258
|
+
* on validation, transport, timeout, or cancel.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const result = await client
|
|
263
|
+
* .call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 })
|
|
264
|
+
* .toPromise();
|
|
265
|
+
* if (result.isOk()) console.log(result.value.sum); // 3
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
call(rpcName, request, options) {
|
|
269
|
+
const TIMEOUT_MAX_MS = 2147483647;
|
|
270
|
+
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(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)}`)));
|
|
271
|
+
const startTime = Date.now();
|
|
272
|
+
const rpc = this.contract.rpcs[rpcName];
|
|
273
|
+
const requestSchema = rpc.request.payload;
|
|
274
|
+
const responseSchema = rpc.response.payload;
|
|
275
|
+
const queueName = (0, _amqp_contract_contract.extractQueue)(rpc.queue).name;
|
|
276
|
+
const span = (0, _amqp_contract_core.startPublishSpan)(this.telemetry, "", queueName, { [_amqp_contract_core.MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName) });
|
|
277
|
+
const correlationId = (0, node_crypto.randomUUID)();
|
|
278
|
+
const callFuture = _swan_io_boxed.Future.make((resolve) => {
|
|
279
|
+
const timer = setTimeout(() => {
|
|
280
|
+
if (!this.pendingCalls.get(correlationId)) return;
|
|
281
|
+
this.pendingCalls.delete(correlationId);
|
|
282
|
+
resolve(_swan_io_boxed.Result.Error(new RpcTimeoutError(String(rpcName), options.timeoutMs)));
|
|
283
|
+
}, options.timeoutMs);
|
|
284
|
+
this.pendingCalls.set(correlationId, {
|
|
285
|
+
rpcName: String(rpcName),
|
|
286
|
+
responseSchema,
|
|
287
|
+
resolve,
|
|
288
|
+
timer
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
const validateRequest = () => {
|
|
292
|
+
let rawValidation;
|
|
293
|
+
try {
|
|
294
|
+
rawValidation = requestSchema["~standard"].validate(request);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("RPC request validation threw", error)));
|
|
297
|
+
}
|
|
298
|
+
const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
|
|
299
|
+
return _swan_io_boxed.Future.fromPromise(validationPromise).mapError((error) => new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).mapOkToResult((validation) => validation.issues ? _swan_io_boxed.Result.Error(new _amqp_contract_core.MessageValidationError(String(rpcName), validation.issues)) : _swan_io_boxed.Result.Ok(validation.value));
|
|
300
|
+
};
|
|
301
|
+
const publishRequest = (validatedRequest) => {
|
|
302
|
+
const { compression: _ignoredCompression, ...defaultsWithoutCompression } = this.defaultPublishOptions;
|
|
303
|
+
const publishOptions = {
|
|
304
|
+
...defaultsWithoutCompression,
|
|
305
|
+
...options.publishOptions,
|
|
306
|
+
replyTo: DIRECT_REPLY_TO,
|
|
307
|
+
correlationId,
|
|
308
|
+
contentType: "application/json"
|
|
309
|
+
};
|
|
310
|
+
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).mapOkToResult((published) => published ? _swan_io_boxed.Result.Ok(void 0) : _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`Failed to publish RPC request for "${String(rpcName)}": channel buffer full`)));
|
|
311
|
+
};
|
|
312
|
+
return validateRequest().flatMapOk((validated) => publishRequest(validated)).flatMap((preflight) => {
|
|
313
|
+
if (preflight.isError()) {
|
|
314
|
+
const pending = this.pendingCalls.get(correlationId);
|
|
315
|
+
if (pending) {
|
|
316
|
+
clearTimeout(pending.timer);
|
|
317
|
+
this.pendingCalls.delete(correlationId);
|
|
318
|
+
}
|
|
319
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(preflight.error));
|
|
320
|
+
}
|
|
321
|
+
return callFuture;
|
|
322
|
+
}).tapOk(() => {
|
|
323
|
+
const durationMs = Date.now() - startTime;
|
|
324
|
+
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
325
|
+
(0, _amqp_contract_core.recordPublishMetric)(this.telemetry, "", queueName, true, durationMs);
|
|
326
|
+
}).tapError((error) => {
|
|
327
|
+
const durationMs = Date.now() - startTime;
|
|
328
|
+
(0, _amqp_contract_core.endSpanError)(span, error);
|
|
329
|
+
(0, _amqp_contract_core.recordPublishMetric)(this.telemetry, "", queueName, false, durationMs);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Close the channel and connection. Cancels the reply consumer (if any) and
|
|
334
|
+
* rejects every in-flight RPC call with `RpcCancelledError`.
|
|
117
335
|
*/
|
|
118
336
|
close() {
|
|
119
|
-
|
|
337
|
+
for (const [, pending] of this.pendingCalls) {
|
|
338
|
+
clearTimeout(pending.timer);
|
|
339
|
+
pending.resolve(_swan_io_boxed.Result.Error(new RpcCancelledError(pending.rpcName)));
|
|
340
|
+
}
|
|
341
|
+
this.pendingCalls.clear();
|
|
342
|
+
return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).tapError((error) => {
|
|
343
|
+
this.logger?.warn("Failed to cancel RPC reply consumer during close", { error });
|
|
344
|
+
}) : _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0))).flatMap(() => this.amqpClient.close()).mapOk(() => void 0);
|
|
120
345
|
}
|
|
121
346
|
waitForConnectionReady() {
|
|
122
347
|
return this.amqpClient.waitForConnect();
|
|
123
348
|
}
|
|
124
349
|
};
|
|
125
|
-
|
|
126
350
|
//#endregion
|
|
127
|
-
Object.defineProperty(exports,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
351
|
+
Object.defineProperty(exports, "MessageValidationError", {
|
|
352
|
+
enumerable: true,
|
|
353
|
+
get: function() {
|
|
354
|
+
return _amqp_contract_core.MessageValidationError;
|
|
355
|
+
}
|
|
132
356
|
});
|
|
133
|
-
exports.
|
|
357
|
+
exports.RpcCancelledError = RpcCancelledError;
|
|
358
|
+
exports.RpcTimeoutError = RpcTimeoutError;
|
|
359
|
+
exports.TypedAmqpClient = TypedAmqpClient;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CompressionAlgorithm, ContractDefinition, InferPublisherNames, InferRpcNames, MessageDefinition, PublisherEntry, RpcDefinition } from "@amqp-contract/contract";
|
|
2
|
+
import { Logger, MessageValidationError, PublishOptions as PublishOptions$1, TechnicalError, TelemetryProvider } from "@amqp-contract/core";
|
|
3
|
+
import { Future, Result } from "@swan-io/boxed";
|
|
2
4
|
import * as amqp from "amqplib";
|
|
3
|
-
import { Options } from "amqplib";
|
|
4
5
|
import { TcpSocketConnectOpts } from "net";
|
|
5
6
|
import { ConnectionOptions } from "tls";
|
|
6
|
-
import { CompressionAlgorithm, ContractDefinition, InferPublisherNames, PublisherEntry } from "@amqp-contract/contract";
|
|
7
|
-
import { Future, Result } from "@swan-io/boxed";
|
|
8
7
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
9
8
|
|
|
10
|
-
//#region ../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@0.
|
|
9
|
+
//#region ../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts
|
|
11
10
|
type ConnectionUrl = string | amqp.Options.Connect | {
|
|
12
11
|
url: string;
|
|
13
12
|
connectionOptions?: AmqpConnectionOptions;
|
|
@@ -48,11 +47,39 @@ interface AmqpConnectionManagerOptions {
|
|
|
48
47
|
connectionOptions?: AmqpConnectionOptions;
|
|
49
48
|
}
|
|
50
49
|
//#endregion
|
|
50
|
+
//#region src/errors.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
|
|
53
|
+
* before the RPC server publishes a reply with the matching `correlationId`.
|
|
54
|
+
*
|
|
55
|
+
* The pending call is removed from the in-memory correlation map; if a reply
|
|
56
|
+
* arrives after the timeout it is dropped (and a debug log is emitted by the
|
|
57
|
+
* client if a logger is configured).
|
|
58
|
+
*/
|
|
59
|
+
declare class RpcTimeoutError extends Error {
|
|
60
|
+
readonly rpcName: string;
|
|
61
|
+
readonly timeoutMs: number;
|
|
62
|
+
constructor(rpcName: string, timeoutMs: number);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Returned from any in-flight RPC call when the client is closed before the
|
|
66
|
+
* reply is received. The correlation map is cleared on close and every pending
|
|
67
|
+
* caller's promise resolves with `Result.Error(RpcCancelledError)`.
|
|
68
|
+
*/
|
|
69
|
+
declare class RpcCancelledError extends Error {
|
|
70
|
+
readonly rpcName: string;
|
|
71
|
+
constructor(rpcName: string);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
51
74
|
//#region src/types.d.ts
|
|
52
75
|
/**
|
|
53
|
-
* Infer the TypeScript type from a schema
|
|
76
|
+
* Infer the TypeScript type from a schema (input side, used for publish payloads).
|
|
54
77
|
*/
|
|
55
78
|
type InferSchemaInput<TSchema extends StandardSchemaV1> = TSchema extends StandardSchemaV1<infer TInput> ? TInput : never;
|
|
79
|
+
/**
|
|
80
|
+
* Infer the TypeScript type from a schema (output side, used for validated responses).
|
|
81
|
+
*/
|
|
82
|
+
type InferSchemaOutput<TSchema extends StandardSchemaV1> = TSchema extends StandardSchemaV1<infer _TInput, infer TOutput> ? TOutput : never;
|
|
56
83
|
/**
|
|
57
84
|
* Infer publisher message input type.
|
|
58
85
|
* Works with both PublisherDefinition and EventPublisherConfig since both have
|
|
@@ -63,24 +90,28 @@ type PublisherInferInput<TPublisher extends PublisherEntry> = TPublisher extends
|
|
|
63
90
|
payload: StandardSchemaV1;
|
|
64
91
|
};
|
|
65
92
|
} ? InferSchemaInput<TPublisher["message"]["payload"]> : never;
|
|
93
|
+
type InferPublishers<TContract extends ContractDefinition> = NonNullable<TContract["publishers"]>;
|
|
94
|
+
type InferPublisher<TContract extends ContractDefinition, TName extends InferPublisherNames<TContract>> = InferPublishers<TContract>[TName];
|
|
66
95
|
/**
|
|
67
|
-
*
|
|
96
|
+
* Input type accepted by `client.publish(name, ...)` for a specific publisher.
|
|
68
97
|
*/
|
|
69
|
-
type
|
|
98
|
+
type ClientInferPublisherInput<TContract extends ContractDefinition, TName extends InferPublisherNames<TContract>> = PublisherInferInput<InferPublisher<TContract, TName>>;
|
|
99
|
+
type InferRpcs<TContract extends ContractDefinition> = NonNullable<TContract["rpcs"]>;
|
|
100
|
+
type InferRpc<TContract extends ContractDefinition, TName extends InferRpcNames<TContract>> = InferRpcs<TContract>[TName];
|
|
70
101
|
/**
|
|
71
|
-
*
|
|
102
|
+
* Input type accepted by `client.call(name, request, ...)`.
|
|
72
103
|
*/
|
|
73
|
-
type
|
|
104
|
+
type ClientInferRpcRequestInput<TContract extends ContractDefinition, TName extends InferRpcNames<TContract>> = InferRpc<TContract, TName> extends RpcDefinition<infer TRequest, MessageDefinition> ? TRequest extends MessageDefinition ? InferSchemaInput<TRequest["payload"]> : never : never;
|
|
74
105
|
/**
|
|
75
|
-
*
|
|
106
|
+
* Output (validated) response type returned by `client.call(name, ...)`.
|
|
76
107
|
*/
|
|
77
|
-
type
|
|
108
|
+
type ClientInferRpcResponseOutput<TContract extends ContractDefinition, TName extends InferRpcNames<TContract>> = InferRpc<TContract, TName> extends RpcDefinition<MessageDefinition, infer TResponse> ? TResponse extends MessageDefinition ? InferSchemaOutput<TResponse["payload"]> : never : never;
|
|
78
109
|
//#endregion
|
|
79
110
|
//#region src/client.d.ts
|
|
80
111
|
/**
|
|
81
|
-
* Publish options that extend
|
|
112
|
+
* Publish options that extend amqp-client's PublishOptions with optional compression support.
|
|
82
113
|
*/
|
|
83
|
-
type PublishOptions =
|
|
114
|
+
type PublishOptions = PublishOptions$1 & {
|
|
84
115
|
/**
|
|
85
116
|
* Optional compression algorithm to use for the message payload.
|
|
86
117
|
* When specified, the message will be compressed using the chosen algorithm
|
|
@@ -102,6 +133,37 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
102
133
|
* OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.
|
|
103
134
|
*/
|
|
104
135
|
telemetry?: TelemetryProvider | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Default publish options that will be applied to all publish operations.
|
|
138
|
+
* These can be overridden by options passed to the publish method.
|
|
139
|
+
* By default, persistent is set to true for message durability.
|
|
140
|
+
*/
|
|
141
|
+
defaultPublishOptions?: PublishOptions | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Maximum time in ms to wait for the AMQP connection to become ready before
|
|
144
|
+
* `create()` resolves to `Result.Error<TechnicalError>`. Without this option,
|
|
145
|
+
* `create()` waits forever — the underlying amqp-connection-manager retries
|
|
146
|
+
* indefinitely.
|
|
147
|
+
*/
|
|
148
|
+
connectTimeoutMs?: number | undefined;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Per-call options for `client.call()`.
|
|
152
|
+
*/
|
|
153
|
+
type CallOptions = {
|
|
154
|
+
/**
|
|
155
|
+
* Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves
|
|
156
|
+
* to `Result.Error<RpcTimeoutError>` and the in-memory correlation entry is
|
|
157
|
+
* cleared. A late reply arriving after the timeout is silently dropped.
|
|
158
|
+
*
|
|
159
|
+
* Required: RPC without a timeout is a footgun.
|
|
160
|
+
*/
|
|
161
|
+
timeoutMs: number;
|
|
162
|
+
/**
|
|
163
|
+
* Optional AMQP message properties to merge into the request. `replyTo` and
|
|
164
|
+
* `correlationId` are managed by the client and cannot be overridden.
|
|
165
|
+
*/
|
|
166
|
+
publishOptions?: Omit<PublishOptions$1, "replyTo" | "correlationId">;
|
|
105
167
|
};
|
|
106
168
|
/**
|
|
107
169
|
* Type-safe AMQP client for publishing messages
|
|
@@ -109,8 +171,19 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
109
171
|
declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
110
172
|
private readonly contract;
|
|
111
173
|
private readonly amqpClient;
|
|
174
|
+
private readonly defaultPublishOptions;
|
|
112
175
|
private readonly logger?;
|
|
113
176
|
private readonly telemetry;
|
|
177
|
+
/**
|
|
178
|
+
* In-flight RPC calls keyed by `correlationId`. Cleared when a reply is
|
|
179
|
+
* received, when the call times out, or when the client is closed.
|
|
180
|
+
*/
|
|
181
|
+
private readonly pendingCalls;
|
|
182
|
+
/**
|
|
183
|
+
* Consumer tag of the reply consumer subscribed on `amq.rabbitmq.reply-to`.
|
|
184
|
+
* Set when the contract has at least one entry in `rpcs`; undefined otherwise.
|
|
185
|
+
*/
|
|
186
|
+
private replyConsumerTag?;
|
|
114
187
|
private constructor();
|
|
115
188
|
/**
|
|
116
189
|
* Create a type-safe AMQP client from a contract.
|
|
@@ -126,9 +199,24 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
126
199
|
contract,
|
|
127
200
|
urls,
|
|
128
201
|
connectionOptions,
|
|
202
|
+
defaultPublishOptions,
|
|
129
203
|
logger,
|
|
130
|
-
telemetry
|
|
204
|
+
telemetry,
|
|
205
|
+
connectTimeoutMs
|
|
131
206
|
}: CreateClientOptions<TContract>): Future<Result<TypedAmqpClient<TContract>, TechnicalError>>;
|
|
207
|
+
/**
|
|
208
|
+
* If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`
|
|
209
|
+
* once. Replies for every in-flight call arrive on this single consumer and
|
|
210
|
+
* are demultiplexed by `correlationId`.
|
|
211
|
+
*/
|
|
212
|
+
private setupReplyConsumerIfNeeded;
|
|
213
|
+
/**
|
|
214
|
+
* Demultiplex an RPC reply by `correlationId`, validate the body against the
|
|
215
|
+
* call's response schema, and resolve the awaiting caller. Replies with no
|
|
216
|
+
* matching pending call (e.g. arriving after the call timed out) are dropped
|
|
217
|
+
* with a debug log.
|
|
218
|
+
*/
|
|
219
|
+
private handleRpcReply;
|
|
132
220
|
/**
|
|
133
221
|
* Publish a message using a defined publisher
|
|
134
222
|
*
|
|
@@ -149,11 +237,39 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
149
237
|
*/
|
|
150
238
|
publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): Future<Result<void, TechnicalError | MessageValidationError>>;
|
|
151
239
|
/**
|
|
152
|
-
*
|
|
240
|
+
* Invoke an RPC defined via `defineRpc` and await the typed response.
|
|
241
|
+
*
|
|
242
|
+
* The request payload is validated against the RPC's request schema, then
|
|
243
|
+
* published to the AMQP default exchange with the server's queue name as
|
|
244
|
+
* routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID
|
|
245
|
+
* `correlationId`. The returned Future resolves once a matching reply
|
|
246
|
+
* arrives and validates against the response schema, or once `timeoutMs`
|
|
247
|
+
* elapses (whichever comes first).
|
|
248
|
+
*
|
|
249
|
+
* @typeParam TName - An RPC name from `contract.rpcs`.
|
|
250
|
+
* @param rpcName - The RPC name from the contract.
|
|
251
|
+
* @param request - The request payload, validated against the request schema.
|
|
252
|
+
* @param options - Per-call options. `timeoutMs` is required.
|
|
253
|
+
*
|
|
254
|
+
* @returns `Result.Ok(response)` on a successful round-trip; `Result.Error`
|
|
255
|
+
* on validation, transport, timeout, or cancel.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* const result = await client
|
|
260
|
+
* .call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 })
|
|
261
|
+
* .toPromise();
|
|
262
|
+
* if (result.isOk()) console.log(result.value.sum); // 3
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
call<TName extends InferRpcNames<TContract>>(rpcName: TName, request: ClientInferRpcRequestInput<TContract, TName>, options: CallOptions): Future<Result<ClientInferRpcResponseOutput<TContract, TName>, TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError>>;
|
|
266
|
+
/**
|
|
267
|
+
* Close the channel and connection. Cancels the reply consumer (if any) and
|
|
268
|
+
* rejects every in-flight RPC call with `RpcCancelledError`.
|
|
153
269
|
*/
|
|
154
270
|
close(): Future<Result<void, TechnicalError>>;
|
|
155
271
|
private waitForConnectionReady;
|
|
156
272
|
}
|
|
157
273
|
//#endregion
|
|
158
|
-
export { type ClientInferPublisherInput, type CreateClientOptions, MessageValidationError, type PublishOptions, TypedAmqpClient };
|
|
274
|
+
export { type CallOptions, type ClientInferPublisherInput, type ClientInferRpcRequestInput, type ClientInferRpcResponseOutput, type CreateClientOptions, MessageValidationError, type PublishOptions, RpcCancelledError, RpcTimeoutError, TypedAmqpClient };
|
|
159
275
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":["amqp","EventEmitter","TcpSocketConnectOpts","ConnectionOptions","ChannelWrapper","CreateChannelOpts","ConnectionUrl","Options","Connect","AmqpConnectionOptions","url","connectionOptions","ConnectListener","Connection","connection","arg","ConnectFailedListener","Error","err","Buffer","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","AmqpConnectionManagerOptions","Promise","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","urls","callback","IAmqpConnectionManager","Function","ChannelModel","addListener","event","args","listener","reason","listeners","eventName","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","close","isConnected","channelCount","AmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","_connect","default"],"sources":["../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@0.
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":["amqp","EventEmitter","TcpSocketConnectOpts","ConnectionOptions","ChannelWrapper","CreateChannelOpts","ConnectionUrl","Options","Connect","AmqpConnectionOptions","url","connectionOptions","ConnectListener","Connection","connection","arg","ConnectFailedListener","Error","err","Buffer","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","AmqpConnectionManagerOptions","Promise","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","urls","callback","IAmqpConnectionManager","Function","ChannelModel","addListener","event","args","listener","reason","listeners","eventName","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","close","isConnected","channelCount","AmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","_connect","default"],"sources":["../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts","../src/errors.ts","../src/types.ts","../src/client.ts"],"x_google_ignoreList":[0],"mappings":";;;;;;;;;KAKYM,aAAAA,YAAyBN,IAAAA,CAAKO,OAAAA,CAAQC,OAAAA;EAC9CE,GAAAA;EACAC,iBAAAA,GAAoBF,qBAAAA;AAAAA;AAAAA,KAcZA,qBAAAA,IAAyBN,iBAAAA,GAAoBD,oBAAAA;EACrDkB,OAAAA;EACAC,OAAAA;EACAC,SAAAA;EACAC,cAAAA;EACAC,gBAAAA;EACAC,WAAAA;IACIC,SAAAA;IACAC,QAAAA;IACAC,QAAAA;IACAC,QAAAA,QAAgBV,MAAAA;EAAAA;IAEhBO,SAAAA;IACAG,QAAAA,QAAgBV,MAAAA;EAAAA;AAAAA;AAAAA,UAGPW,4BAAAA;EATTJ;EAWJM,0BAAAA;EATIJ;;;;EAcJK,sBAAAA;EAVoBd;;;AAGxB;;;;EAeIe,WAAAA,KAAgBE,QAAAA,GAAWD,IAAAA,EAAM7B,aAAAA,GAAgBA,aAAAA,+BAA4CyB,OAAAA,CAAQzB,aAAAA,GAAgBA,aAAAA;EAAhBA;EAErGK,iBAAAA,GAAoBF,qBAAAA;AAAAA;;;;;;;;;;;cChCX,eAAA,SAAwB,KAAA;EAAA,SAEjB,OAAA;EAAA,SACA,SAAA;cADA,OAAA,UACA,SAAA;AAAA;;;;;;cAaP,iBAAA,SAA0B,KAAA;EAAA,SACT,OAAA;cAAA,OAAA;AAAA;;;;;;KC1BzB,gBAAA,iBAAiC,gBAAA,IACpC,OAAA,SAAgB,gBAAA,iBAAiC,MAAA;;;;KAK9C,iBAAA,iBAAkC,gBAAA,IACrC,OAAA,SAAgB,gBAAA,iCAAiD,OAAA;;;;;;KAO9D,mBAAA,oBAAuC,cAAA,IAAkB,UAAA;EAC5D,OAAA;IAAW,OAAA,EAAS,gBAAA;EAAA;AAAA,IAElB,gBAAA,CAAiB,UAAA;AAAA,KAGhB,eAAA,mBAAkC,kBAAA,IAAsB,WAAA,CAAY,SAAA;AAAA,KACpE,cAAA,mBACe,kBAAA,gBACJ,mBAAA,CAAoB,SAAA,KAChC,eAAA,CAAgB,SAAA,EAAW,KAAA;;;;KAKnB,yBAAA,mBACQ,kBAAA,gBACJ,mBAAA,CAAoB,SAAA,KAChC,mBAAA,CAAoB,cAAA,CAAe,SAAA,EAAW,KAAA;AAAA,KAM7C,SAAA,mBAA4B,kBAAA,IAAsB,WAAA,CAAY,SAAA;AAAA,KAC9D,QAAA,mBACe,kBAAA,gBACJ,aAAA,CAAc,SAAA,KAC1B,SAAA,CAAU,SAAA,EAAW,KAAA;;;;KAKb,0BAAA,mBACQ,kBAAA,gBACJ,aAAA,CAAc,SAAA,KAE5B,QAAA,CAAS,SAAA,EAAW,KAAA,UAAe,aAAA,iBAA8B,iBAAA,IAC7D,QAAA,SAAiB,iBAAA,GACf,gBAAA,CAAiB,QAAA;;;;KAOb,4BAAA,mBACQ,kBAAA,gBACJ,aAAA,CAAc,SAAA,KAE5B,QAAA,CAAS,SAAA,EAAW,KAAA,UAAe,aAAA,CAAc,iBAAA,qBAC7C,SAAA,SAAkB,iBAAA,GAChB,iBAAA,CAAkB,SAAA;;;;;;KClBd,cAAA,GAAiB,gBAAA;EHxDJ;;;;;EG8DvB,WAAA,GAAc,oBAAA;AAAA;;;;KAMJ,mBAAA,mBAAsC,kBAAA;EAChD,QAAA,EAAU,SAAA;EACV,IAAA,EAAM,aAAA;EACN,iBAAA,GAAoB,4BAAA;EACpB,MAAA,GAAS,MAAA;EHxD8CP;;;;;EG8DvD,SAAA,GAAY,iBAAA;EH9D2CA;;;;;EGoEvD,qBAAA,GAAwB,cAAA;EH9DtBuB;;;;;;EGqEF,gBAAA;AAAA;;;;KAMU,WAAA;EHjEiC;;;;;;;EGyE3C,SAAA;EHxD2C;;;;EG8D3C,cAAA,GAAiB,IAAA,CAAK,gBAAA;AAAA;;;;cAMX,eAAA,mBAAkC,kBAAA;EAAA,iBAc1B,QAAA;EAAA,iBACA,UAAA;EAAA,iBACA,qBAAA;EAAA,iBACA,MAAA;EAAA,iBACA,SAAA;EHtFwB;;;;EAAA,iBGyE1B,YAAA;EFzGU;;;;EAAA,QE+GnB,gBAAA;EAAA,QAED,WAAA,CAAA;;;;;;AFjGT;;;;;SEmHS,MAAA,mBAAyB,kBAAA,CAAA,CAAA;IAC9B,QAAA;IACA,IAAA;IACA,iBAAA;IACA,qBAAA;IACA,MAAA;IACA,SAAA;IACA;EAAA,GACC,mBAAA,CAAoB,SAAA,IAAa,MAAA,CAAO,MAAA,CAAO,eAAA,CAAgB,SAAA,GAAY,cAAA;;;;;;UAkCtE,0BAAA;;AD3LoD;;;;;UC+MpD,cAAA;EDzMwB;;;;;;;;;;AAAuB;;;;EAMvD;;;;ECuRA,OAAA,eAAsB,mBAAA,CAAoB,SAAA,EAAA,CACxC,aAAA,EAAe,KAAA,EACf,OAAA,EAAS,yBAAA,CAA0B,SAAA,EAAW,KAAA,GAC9C,OAAA,GAAU,cAAA,GACT,MAAA,CAAO,MAAA,OAAa,cAAA,GAAiB,sBAAA;ED5RH;;;;;;;;AACmC;;;;;;;;;;;;;;;;;;EC+YxE,IAAA,eAAmB,aAAA,CAAc,SAAA,EAAA,CAC/B,OAAA,EAAS,KAAA,EACT,OAAA,EAAS,0BAAA,CAA2B,SAAA,EAAW,KAAA,GAC/C,OAAA,EAAS,WAAA,GACR,MAAA,CACD,MAAA,CACE,4BAAA,CAA6B,SAAA,EAAW,KAAA,GACxC,cAAA,GAAiB,sBAAA,GAAyB,eAAA,GAAkB,iBAAA;ED5YnC;AAAA;;;EC6hB7B,KAAA,CAAA,GAAS,MAAA,CAAO,MAAA,OAAa,cAAA;EAAA,QAiBrB,sBAAA;AAAA"}
|