@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/dist/index.d.mts
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 {
|
|
4
|
-
import * as amqp from "amqplib";
|
|
5
|
-
import { TcpSocketConnectOpts } from "net";
|
|
3
|
+
import { AsyncResult } from "unthrown";
|
|
6
4
|
import { ConnectionOptions } from "tls";
|
|
5
|
+
import { TcpSocketConnectOpts } from "net";
|
|
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.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":["
|
|
1
|
+
{"version":3,"file":"index.d.mts","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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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractQueue } from "@amqp-contract/contract";
|
|
2
|
-
import { AmqpClient, MessageValidationError, MessagingSemanticConventions, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordLateRpcReply, recordPublishMetric, startPublishSpan } from "@amqp-contract/core";
|
|
3
|
-
import {
|
|
2
|
+
import { AmqpClient, MessageValidationError, MessagingSemanticConventions, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordLateRpcReply, recordPublishMetric, safeJsonParse, startPublishSpan } from "@amqp-contract/core";
|
|
3
|
+
import { TaggedError, err, fromPromise, fromSafePromise, ok } from "unthrown";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { deflate, gzip } from "node:zlib";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -13,50 +13,47 @@ const deflateAsync = promisify(deflate);
|
|
|
13
13
|
*
|
|
14
14
|
* @param buffer - The buffer to compress
|
|
15
15
|
* @param algorithm - The compression algorithm to use
|
|
16
|
-
* @returns
|
|
16
|
+
* @returns An AsyncResult resolving to the compressed buffer or a TechnicalError
|
|
17
17
|
*
|
|
18
18
|
* @internal
|
|
19
19
|
*/
|
|
20
20
|
function compressBuffer(buffer, algorithm) {
|
|
21
|
-
return match(algorithm).with("gzip", () =>
|
|
21
|
+
return match(algorithm).with("gzip", () => fromPromise(gzipAsync(buffer), (error) => new TechnicalError("Failed to compress with gzip", error))).with("deflate", () => fromPromise(deflateAsync(buffer), (error) => new TechnicalError("Failed to compress with deflate", error))).exhaustive();
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/errors.ts
|
|
25
25
|
/**
|
|
26
|
-
* Captured `Error.captureStackTrace` shim — only present on Node.js.
|
|
27
|
-
*/
|
|
28
|
-
function captureStack(target, ctor) {
|
|
29
|
-
const ErrorConstructor = Error;
|
|
30
|
-
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(target, ctor);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
26
|
* Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
|
|
34
27
|
* before the RPC server publishes a reply with the matching `correlationId`.
|
|
35
28
|
*
|
|
36
29
|
* The pending call is removed from the in-memory correlation map; if a reply
|
|
37
30
|
* arrives after the timeout it is dropped (and a debug log is emitted by the
|
|
38
|
-
* client if a logger is configured).
|
|
31
|
+
* client if a logger is configured). Carries a namespaced `_tag` of
|
|
32
|
+
* `"@amqp-contract/RpcTimeoutError"`; the `Error.name` is kept bare
|
|
33
|
+
* (`"RpcTimeoutError"`).
|
|
39
34
|
*/
|
|
40
|
-
var RpcTimeoutError = class extends
|
|
35
|
+
var RpcTimeoutError = class extends TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError" }) {
|
|
41
36
|
constructor(rpcName, timeoutMs) {
|
|
42
|
-
super(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
super({
|
|
38
|
+
message: `RPC call to "${rpcName}" timed out after ${timeoutMs}ms with no reply received`,
|
|
39
|
+
rpcName,
|
|
40
|
+
timeoutMs
|
|
41
|
+
});
|
|
47
42
|
}
|
|
48
43
|
};
|
|
49
44
|
/**
|
|
50
45
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
51
46
|
* reply is received. The correlation map is cleared on close and every pending
|
|
52
|
-
* caller's promise resolves with `err(RpcCancelledError)`.
|
|
47
|
+
* caller's promise resolves with `err(RpcCancelledError)`. Carries a namespaced
|
|
48
|
+
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
49
|
+
* (`"RpcCancelledError"`).
|
|
53
50
|
*/
|
|
54
|
-
var RpcCancelledError = class extends
|
|
51
|
+
var RpcCancelledError = class extends TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError" }) {
|
|
55
52
|
constructor(rpcName) {
|
|
56
|
-
super(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
super({
|
|
54
|
+
message: `RPC call to "${rpcName}" was cancelled because the client was closed`,
|
|
55
|
+
rpcName
|
|
56
|
+
});
|
|
60
57
|
}
|
|
61
58
|
};
|
|
62
59
|
//#endregion
|
|
@@ -74,6 +71,11 @@ const DIRECT_REPLY_TO = "amq.rabbitmq.reply-to";
|
|
|
74
71
|
* Type-safe AMQP client for publishing messages
|
|
75
72
|
*/
|
|
76
73
|
var TypedAmqpClient = class TypedAmqpClient {
|
|
74
|
+
contract;
|
|
75
|
+
amqpClient;
|
|
76
|
+
defaultPublishOptions;
|
|
77
|
+
logger;
|
|
78
|
+
telemetry;
|
|
77
79
|
/**
|
|
78
80
|
* In-flight RPC calls keyed by `correlationId`. Cleared when a reply is
|
|
79
81
|
* received, when the call times out, or when the client is closed.
|
|
@@ -110,14 +112,15 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
110
112
|
persistent: true,
|
|
111
113
|
...defaultPublishOptions
|
|
112
114
|
}, logger, telemetry ?? defaultTelemetryProvider);
|
|
113
|
-
const setup = client.waitForConnectionReady().
|
|
114
|
-
return
|
|
115
|
+
const setup = client.waitForConnectionReady().flatMap(() => client.setupReplyConsumerIfNeeded());
|
|
116
|
+
return fromSafePromise((async () => {
|
|
115
117
|
const setupResult = await setup;
|
|
116
|
-
if (setupResult.isOk())
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
if (!setupResult.isOk()) {
|
|
119
|
+
const closeResult = await client.close();
|
|
120
|
+
if (closeResult.isErr()) logger?.warn("Failed to close client after connection failure", { error: closeResult.error });
|
|
121
|
+
}
|
|
122
|
+
return setupResult.map(() => client);
|
|
123
|
+
})()).flatMap((result) => result);
|
|
121
124
|
}
|
|
122
125
|
/**
|
|
123
126
|
* If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`
|
|
@@ -126,8 +129,8 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
126
129
|
*/
|
|
127
130
|
setupReplyConsumerIfNeeded() {
|
|
128
131
|
const rpcs = this.contract.rpcs ?? {};
|
|
129
|
-
if (Object.keys(rpcs).length === 0) return
|
|
130
|
-
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).
|
|
132
|
+
if (Object.keys(rpcs).length === 0) return ok(void 0).toAsync();
|
|
133
|
+
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).tap((tag) => {
|
|
131
134
|
this.replyConsumerTag = tag;
|
|
132
135
|
}).map(() => void 0);
|
|
133
136
|
}
|
|
@@ -156,13 +159,12 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
156
159
|
}
|
|
157
160
|
this.pendingCalls.delete(correlationId);
|
|
158
161
|
clearTimeout(pending.timer);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} catch (error) {
|
|
163
|
-
pending.resolve(err(new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error)));
|
|
162
|
+
const parseResult = safeJsonParse(msg.content, (error) => new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error));
|
|
163
|
+
if (!parseResult.isOk()) {
|
|
164
|
+
pending.resolve(err(parseResult.isErr() ? parseResult.error : new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, parseResult.cause)));
|
|
164
165
|
return;
|
|
165
166
|
}
|
|
167
|
+
const parsed = parseResult.value;
|
|
166
168
|
let rawValidation;
|
|
167
169
|
try {
|
|
168
170
|
rawValidation = pending.responseSchema["~standard"].validate(parsed);
|
|
@@ -199,8 +201,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
199
201
|
const span = startPublishSpan(this.telemetry, exchange.name, routingKey, { [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName) });
|
|
200
202
|
const validateMessage = () => {
|
|
201
203
|
const validationResult = publisher.message.payload["~standard"].validate(message);
|
|
202
|
-
|
|
203
|
-
return ResultAsync.fromPromise(promise, (error) => new TechnicalError("Validation failed", error)).andThen((validation) => {
|
|
204
|
+
return fromPromise(validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult), (error) => new TechnicalError("Validation failed", error)).flatMap((validation) => {
|
|
204
205
|
if (validation.issues) return err(new MessageValidationError(String(publisherName), validation.issues));
|
|
205
206
|
return ok(validation.value);
|
|
206
207
|
});
|
|
@@ -217,9 +218,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
217
218
|
publishOptions.contentEncoding = compression;
|
|
218
219
|
return compressBuffer(messageBuffer, compression);
|
|
219
220
|
}
|
|
220
|
-
return
|
|
221
|
+
return ok(validatedMessage).toAsync();
|
|
221
222
|
};
|
|
222
|
-
return preparePayload().
|
|
223
|
+
return preparePayload().flatMap((payload) => this.amqpClient.publish(publisher.exchange.name, publisher.routingKey ?? "", payload, publishOptions).flatMap((published) => {
|
|
223
224
|
if (!published) return err(new TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
|
|
224
225
|
this.logger?.info("Message published successfully", {
|
|
225
226
|
publisherName: String(publisherName),
|
|
@@ -230,11 +231,11 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
230
231
|
return ok(void 0);
|
|
231
232
|
}));
|
|
232
233
|
};
|
|
233
|
-
return validateMessage().
|
|
234
|
+
return validateMessage().flatMap((validatedMessage) => publishMessage(validatedMessage)).tap(() => {
|
|
234
235
|
const durationMs = Date.now() - startTime;
|
|
235
236
|
endSpanSuccess(span);
|
|
236
237
|
recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);
|
|
237
|
-
}).
|
|
238
|
+
}).tapErr((error) => {
|
|
238
239
|
const durationMs = Date.now() - startTime;
|
|
239
240
|
endSpanError(span, error);
|
|
240
241
|
recordPublishMetric(this.telemetry, exchange.name, routingKey, false, durationMs);
|
|
@@ -246,19 +247,23 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
246
247
|
* The request payload is validated against the RPC's request schema, then
|
|
247
248
|
* published to the AMQP default exchange with the server's queue name as
|
|
248
249
|
* routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID
|
|
249
|
-
* `correlationId`. The returned
|
|
250
|
+
* `correlationId`. The returned AsyncResult resolves once a matching reply
|
|
250
251
|
* arrives and validates against the response schema, or once `timeoutMs`
|
|
251
252
|
* elapses (whichever comes first).
|
|
252
253
|
*
|
|
253
254
|
* @example
|
|
254
255
|
* ```typescript
|
|
255
256
|
* const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });
|
|
256
|
-
*
|
|
257
|
+
* result.match({
|
|
258
|
+
* ok: (value) => console.log(value.sum), // 3
|
|
259
|
+
* err: (error) => console.error(error),
|
|
260
|
+
* defect: (cause) => console.error(cause),
|
|
261
|
+
* });
|
|
257
262
|
* ```
|
|
258
263
|
*/
|
|
259
264
|
call(rpcName, request, options) {
|
|
260
265
|
const TIMEOUT_MAX_MS = 2147483647;
|
|
261
|
-
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return
|
|
266
|
+
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return err(new TechnicalError(`Invalid timeoutMs for RPC call to "${String(rpcName)}": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`)).toAsync();
|
|
262
267
|
const startTime = Date.now();
|
|
263
268
|
const rpc = this.contract.rpcs[rpcName];
|
|
264
269
|
const requestSchema = rpc.request.payload;
|
|
@@ -267,9 +272,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
267
272
|
const span = startPublishSpan(this.telemetry, "", queueName, { [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName) });
|
|
268
273
|
const correlationId = randomUUID();
|
|
269
274
|
let resolveCall;
|
|
270
|
-
const callResultAsync =
|
|
275
|
+
const callResultAsync = fromSafePromise(new Promise((res) => {
|
|
271
276
|
resolveCall = res;
|
|
272
|
-
}));
|
|
277
|
+
})).flatMap((result) => result);
|
|
273
278
|
const timer = setTimeout(() => {
|
|
274
279
|
if (!this.pendingCalls.has(correlationId)) return;
|
|
275
280
|
this.pendingCalls.delete(correlationId);
|
|
@@ -286,10 +291,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
286
291
|
try {
|
|
287
292
|
rawValidation = requestSchema["~standard"].validate(request);
|
|
288
293
|
} catch (error) {
|
|
289
|
-
return
|
|
294
|
+
return err(new TechnicalError("RPC request validation threw", error)).toAsync();
|
|
290
295
|
}
|
|
291
|
-
|
|
292
|
-
return ResultAsync.fromPromise(validationPromise, (error) => new TechnicalError("RPC request validation threw", error)).andThen((validation) => validation.issues ? err(new MessageValidationError(String(rpcName), validation.issues)) : ok(validation.value));
|
|
296
|
+
return fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new TechnicalError("RPC request validation threw", error)).flatMap((validation) => validation.issues ? err(new MessageValidationError(String(rpcName), validation.issues)) : ok(validation.value));
|
|
293
297
|
};
|
|
294
298
|
const publishRequest = (validatedRequest) => {
|
|
295
299
|
const { compression: _ignoredCompression, ...defaultsWithoutCompression } = this.defaultPublishOptions;
|
|
@@ -300,19 +304,19 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
300
304
|
correlationId,
|
|
301
305
|
contentType: "application/json"
|
|
302
306
|
};
|
|
303
|
-
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).
|
|
307
|
+
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).flatMap((published) => published ? ok(void 0) : err(new TechnicalError(`Failed to publish RPC request for "${String(rpcName)}": channel buffer full`)));
|
|
304
308
|
};
|
|
305
|
-
return validateRequest().
|
|
309
|
+
return validateRequest().flatMap((validated) => publishRequest(validated)).flatMap(() => callResultAsync).orElse((error) => {
|
|
306
310
|
if (this.pendingCalls.has(correlationId)) {
|
|
307
311
|
clearTimeout(timer);
|
|
308
312
|
this.pendingCalls.delete(correlationId);
|
|
309
313
|
}
|
|
310
|
-
return
|
|
311
|
-
}).
|
|
314
|
+
return err(error).toAsync();
|
|
315
|
+
}).tap(() => {
|
|
312
316
|
const durationMs = Date.now() - startTime;
|
|
313
317
|
endSpanSuccess(span);
|
|
314
318
|
recordPublishMetric(this.telemetry, "", queueName, true, durationMs);
|
|
315
|
-
}).
|
|
319
|
+
}).tapErr((error) => {
|
|
316
320
|
const durationMs = Date.now() - startTime;
|
|
317
321
|
endSpanError(span, error);
|
|
318
322
|
recordPublishMetric(this.telemetry, "", queueName, false, durationMs);
|
|
@@ -331,7 +335,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
331
335
|
return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {
|
|
332
336
|
this.logger?.warn("Failed to cancel RPC reply consumer during close", { error });
|
|
333
337
|
return ok(void 0);
|
|
334
|
-
}) :
|
|
338
|
+
}) : ok(void 0).toAsync()).flatMap(() => this.amqpClient.close());
|
|
335
339
|
}
|
|
336
340
|
waitForConnectionReady() {
|
|
337
341
|
return this.amqpClient.waitForConnect();
|