@amqp-contract/client 0.21.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 +227 -5
- package/dist/index.d.cts +120 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +120 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +226 -6
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +441 -17
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
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");
|
|
@@ -20,11 +22,69 @@ function compressBuffer(buffer, algorithm) {
|
|
|
20
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();
|
|
21
23
|
}
|
|
22
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
|
+
};
|
|
63
|
+
//#endregion
|
|
23
64
|
//#region src/client.ts
|
|
24
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
|
+
/**
|
|
25
75
|
* Type-safe AMQP client for publishing messages
|
|
26
76
|
*/
|
|
27
77
|
var TypedAmqpClient = class TypedAmqpClient {
|
|
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;
|
|
28
88
|
constructor(contract, amqpClient, defaultPublishOptions, logger, telemetry = _amqp_contract_core.defaultTelemetryProvider) {
|
|
29
89
|
this.contract = contract;
|
|
30
90
|
this.amqpClient = amqpClient;
|
|
@@ -42,15 +102,77 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
42
102
|
* Connections are automatically shared across clients with the same URLs and
|
|
43
103
|
* connection options, following RabbitMQ best practices.
|
|
44
104
|
*/
|
|
45
|
-
static create({ contract, urls, connectionOptions, defaultPublishOptions, logger, telemetry }) {
|
|
105
|
+
static create({ contract, urls, connectionOptions, defaultPublishOptions, logger, telemetry, connectTimeoutMs }) {
|
|
46
106
|
const client = new TypedAmqpClient(contract, new _amqp_contract_core.AmqpClient(contract, {
|
|
47
107
|
urls,
|
|
48
|
-
connectionOptions
|
|
108
|
+
connectionOptions,
|
|
109
|
+
connectTimeoutMs
|
|
49
110
|
}), {
|
|
50
111
|
persistent: true,
|
|
51
112
|
...defaultPublishOptions
|
|
52
113
|
}, logger, telemetry ?? _amqp_contract_core.defaultTelemetryProvider);
|
|
53
|
-
return client.waitForConnectionReady().
|
|
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
|
+
});
|
|
54
176
|
}
|
|
55
177
|
/**
|
|
56
178
|
* Publish a message using a defined publisher
|
|
@@ -118,10 +240,108 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
118
240
|
});
|
|
119
241
|
}
|
|
120
242
|
/**
|
|
121
|
-
*
|
|
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`.
|
|
122
335
|
*/
|
|
123
336
|
close() {
|
|
124
|
-
|
|
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);
|
|
125
345
|
}
|
|
126
346
|
waitForConnectionReady() {
|
|
127
347
|
return this.amqpClient.waitForConnect();
|
|
@@ -134,4 +354,6 @@ Object.defineProperty(exports, "MessageValidationError", {
|
|
|
134
354
|
return _amqp_contract_core.MessageValidationError;
|
|
135
355
|
}
|
|
136
356
|
});
|
|
357
|
+
exports.RpcCancelledError = RpcCancelledError;
|
|
358
|
+
exports.RpcTimeoutError = RpcTimeoutError;
|
|
137
359
|
exports.TypedAmqpClient = TypedAmqpClient;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompressionAlgorithm, ContractDefinition, InferPublisherNames, PublisherEntry } from "@amqp-contract/contract";
|
|
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
3
|
import { Future, Result } from "@swan-io/boxed";
|
|
4
4
|
import * as amqp from "amqplib";
|
|
@@ -47,11 +47,39 @@ interface AmqpConnectionManagerOptions {
|
|
|
47
47
|
connectionOptions?: AmqpConnectionOptions;
|
|
48
48
|
}
|
|
49
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
|
|
50
74
|
//#region src/types.d.ts
|
|
51
75
|
/**
|
|
52
|
-
* Infer the TypeScript type from a schema
|
|
76
|
+
* Infer the TypeScript type from a schema (input side, used for publish payloads).
|
|
53
77
|
*/
|
|
54
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;
|
|
55
83
|
/**
|
|
56
84
|
* Infer publisher message input type.
|
|
57
85
|
* Works with both PublisherDefinition and EventPublisherConfig since both have
|
|
@@ -62,18 +90,22 @@ type PublisherInferInput<TPublisher extends PublisherEntry> = TPublisher extends
|
|
|
62
90
|
payload: StandardSchemaV1;
|
|
63
91
|
};
|
|
64
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];
|
|
65
95
|
/**
|
|
66
|
-
*
|
|
96
|
+
* Input type accepted by `client.publish(name, ...)` for a specific publisher.
|
|
67
97
|
*/
|
|
68
|
-
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];
|
|
69
101
|
/**
|
|
70
|
-
*
|
|
102
|
+
* Input type accepted by `client.call(name, request, ...)`.
|
|
71
103
|
*/
|
|
72
|
-
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;
|
|
73
105
|
/**
|
|
74
|
-
*
|
|
106
|
+
* Output (validated) response type returned by `client.call(name, ...)`.
|
|
75
107
|
*/
|
|
76
|
-
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;
|
|
77
109
|
//#endregion
|
|
78
110
|
//#region src/client.d.ts
|
|
79
111
|
/**
|
|
@@ -107,6 +139,31 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
107
139
|
* By default, persistent is set to true for message durability.
|
|
108
140
|
*/
|
|
109
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">;
|
|
110
167
|
};
|
|
111
168
|
/**
|
|
112
169
|
* Type-safe AMQP client for publishing messages
|
|
@@ -117,6 +174,16 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
117
174
|
private readonly defaultPublishOptions;
|
|
118
175
|
private readonly logger?;
|
|
119
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?;
|
|
120
187
|
private constructor();
|
|
121
188
|
/**
|
|
122
189
|
* Create a type-safe AMQP client from a contract.
|
|
@@ -134,8 +201,22 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
134
201
|
connectionOptions,
|
|
135
202
|
defaultPublishOptions,
|
|
136
203
|
logger,
|
|
137
|
-
telemetry
|
|
204
|
+
telemetry,
|
|
205
|
+
connectTimeoutMs
|
|
138
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;
|
|
139
220
|
/**
|
|
140
221
|
* Publish a message using a defined publisher
|
|
141
222
|
*
|
|
@@ -156,11 +237,39 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
|
|
|
156
237
|
*/
|
|
157
238
|
publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): Future<Result<void, TechnicalError | MessageValidationError>>;
|
|
158
239
|
/**
|
|
159
|
-
*
|
|
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`.
|
|
160
269
|
*/
|
|
161
270
|
close(): Future<Result<void, TechnicalError>>;
|
|
162
271
|
private waitForConnectionReady;
|
|
163
272
|
}
|
|
164
273
|
//#endregion
|
|
165
|
-
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 };
|
|
166
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@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.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;;;;;;
|
|
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":"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"}
|