@amqp-contract/client 1.0.0 → 2.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/dist/index.cjs +21 -21
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +22 -22
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +32 -32
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -45,7 +45,7 @@ var RpcTimeoutError = class extends (0, unthrown.TaggedError)("@amqp-contract/Rp
|
|
|
45
45
|
/**
|
|
46
46
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
47
47
|
* reply is received. The correlation map is cleared on close and every pending
|
|
48
|
-
* caller's promise resolves with `
|
|
48
|
+
* caller's promise resolves with `Err(RpcCancelledError)`. Carries a namespaced
|
|
49
49
|
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
50
50
|
* (`"RpcCancelledError"`).
|
|
51
51
|
*/
|
|
@@ -130,7 +130,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
130
130
|
*/
|
|
131
131
|
setupReplyConsumerIfNeeded() {
|
|
132
132
|
const rpcs = this.contract.rpcs ?? {};
|
|
133
|
-
if (Object.keys(rpcs).length === 0) return (0, unthrown.
|
|
133
|
+
if (Object.keys(rpcs).length === 0) return (0, unthrown.Ok)(void 0).toAsync();
|
|
134
134
|
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).tap((tag) => {
|
|
135
135
|
this.replyConsumerTag = tag;
|
|
136
136
|
}).map(() => void 0);
|
|
@@ -162,7 +162,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
162
162
|
clearTimeout(pending.timer);
|
|
163
163
|
const parseResult = (0, _amqp_contract_core.safeJsonParse)(msg.content, (error) => new _amqp_contract_core.TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error));
|
|
164
164
|
if (!parseResult.isOk()) {
|
|
165
|
-
pending.resolve((0, unthrown.
|
|
165
|
+
pending.resolve((0, unthrown.Err)(parseResult.isErr() ? parseResult.error : new _amqp_contract_core.TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, parseResult.cause)));
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
const parsed = parseResult.value;
|
|
@@ -170,17 +170,17 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
170
170
|
try {
|
|
171
171
|
rawValidation = pending.responseSchema["~standard"].validate(parsed);
|
|
172
172
|
} catch (error) {
|
|
173
|
-
pending.resolve((0, unthrown.
|
|
173
|
+
pending.resolve((0, unthrown.Err)(new _amqp_contract_core.TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).then((validation) => {
|
|
177
177
|
if (validation.issues) {
|
|
178
|
-
pending.resolve((0, unthrown.
|
|
178
|
+
pending.resolve((0, unthrown.Err)(new _amqp_contract_core.MessageValidationError(pending.rpcName, validation.issues)));
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
|
-
pending.resolve((0, unthrown.
|
|
181
|
+
pending.resolve((0, unthrown.Ok)(validation.value));
|
|
182
182
|
}, (error) => {
|
|
183
|
-
pending.resolve((0, unthrown.
|
|
183
|
+
pending.resolve((0, unthrown.Err)(new _amqp_contract_core.TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
@@ -203,8 +203,8 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
203
203
|
const validateMessage = () => {
|
|
204
204
|
const validationResult = publisher.message.payload["~standard"].validate(message);
|
|
205
205
|
return (0, unthrown.fromPromise)(validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult), (error) => new _amqp_contract_core.TechnicalError("Validation failed", error)).flatMap((validation) => {
|
|
206
|
-
if (validation.issues) return (0, unthrown.
|
|
207
|
-
return (0, unthrown.
|
|
206
|
+
if (validation.issues) return (0, unthrown.Err)(new _amqp_contract_core.MessageValidationError(String(publisherName), validation.issues));
|
|
207
|
+
return (0, unthrown.Ok)(validation.value);
|
|
208
208
|
});
|
|
209
209
|
};
|
|
210
210
|
const publishMessage = (validatedMessage) => {
|
|
@@ -219,17 +219,17 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
219
219
|
publishOptions.contentEncoding = compression;
|
|
220
220
|
return compressBuffer(messageBuffer, compression);
|
|
221
221
|
}
|
|
222
|
-
return (0, unthrown.
|
|
222
|
+
return (0, unthrown.Ok)(validatedMessage).toAsync();
|
|
223
223
|
};
|
|
224
224
|
return preparePayload().flatMap((payload) => this.amqpClient.publish(publisher.exchange.name, publisher.routingKey ?? "", payload, publishOptions).flatMap((published) => {
|
|
225
|
-
if (!published) return (0, unthrown.
|
|
225
|
+
if (!published) return (0, unthrown.Err)(new _amqp_contract_core.TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
|
|
226
226
|
this.logger?.info("Message published successfully", {
|
|
227
227
|
publisherName: String(publisherName),
|
|
228
228
|
exchange: publisher.exchange.name,
|
|
229
229
|
routingKey: publisher.routingKey,
|
|
230
230
|
compressed: !!compression
|
|
231
231
|
});
|
|
232
|
-
return (0, unthrown.
|
|
232
|
+
return (0, unthrown.Ok)(void 0);
|
|
233
233
|
}));
|
|
234
234
|
};
|
|
235
235
|
return validateMessage().flatMap((validatedMessage) => publishMessage(validatedMessage)).tap(() => {
|
|
@@ -264,7 +264,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
264
264
|
*/
|
|
265
265
|
call(rpcName, request, options) {
|
|
266
266
|
const TIMEOUT_MAX_MS = 2147483647;
|
|
267
|
-
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return (0, unthrown.
|
|
267
|
+
if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return (0, unthrown.Err)(new _amqp_contract_core.TechnicalError(`Invalid timeoutMs for RPC call to "${String(rpcName)}": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`)).toAsync();
|
|
268
268
|
const startTime = Date.now();
|
|
269
269
|
const rpc = this.contract.rpcs[rpcName];
|
|
270
270
|
const requestSchema = rpc.request.payload;
|
|
@@ -279,7 +279,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
279
279
|
const timer = setTimeout(() => {
|
|
280
280
|
if (!this.pendingCalls.has(correlationId)) return;
|
|
281
281
|
this.pendingCalls.delete(correlationId);
|
|
282
|
-
resolveCall((0, unthrown.
|
|
282
|
+
resolveCall((0, unthrown.Err)(new RpcTimeoutError(String(rpcName), options.timeoutMs)));
|
|
283
283
|
}, options.timeoutMs);
|
|
284
284
|
this.pendingCalls.set(correlationId, {
|
|
285
285
|
rpcName: String(rpcName),
|
|
@@ -292,9 +292,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
292
292
|
try {
|
|
293
293
|
rawValidation = requestSchema["~standard"].validate(request);
|
|
294
294
|
} catch (error) {
|
|
295
|
-
return (0, unthrown.
|
|
295
|
+
return (0, unthrown.Err)(new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).toAsync();
|
|
296
296
|
}
|
|
297
|
-
return (0, unthrown.fromPromise)(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).flatMap((validation) => validation.issues ? (0, unthrown.
|
|
297
|
+
return (0, unthrown.fromPromise)(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new _amqp_contract_core.TechnicalError("RPC request validation threw", error)).flatMap((validation) => validation.issues ? (0, unthrown.Err)(new _amqp_contract_core.MessageValidationError(String(rpcName), validation.issues)) : (0, unthrown.Ok)(validation.value));
|
|
298
298
|
};
|
|
299
299
|
const publishRequest = (validatedRequest) => {
|
|
300
300
|
const { compression: _ignoredCompression, ...defaultsWithoutCompression } = this.defaultPublishOptions;
|
|
@@ -305,14 +305,14 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
305
305
|
correlationId,
|
|
306
306
|
contentType: "application/json"
|
|
307
307
|
};
|
|
308
|
-
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).flatMap((published) => published ? (0, unthrown.
|
|
308
|
+
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).flatMap((published) => published ? (0, unthrown.Ok)(void 0) : (0, unthrown.Err)(new _amqp_contract_core.TechnicalError(`Failed to publish RPC request for "${String(rpcName)}": channel buffer full`)));
|
|
309
309
|
};
|
|
310
310
|
return validateRequest().flatMap((validated) => publishRequest(validated)).flatMap(() => callResultAsync).orElse((error) => {
|
|
311
311
|
if (this.pendingCalls.has(correlationId)) {
|
|
312
312
|
clearTimeout(timer);
|
|
313
313
|
this.pendingCalls.delete(correlationId);
|
|
314
314
|
}
|
|
315
|
-
return (0, unthrown.
|
|
315
|
+
return (0, unthrown.Err)(error).toAsync();
|
|
316
316
|
}).tap(() => {
|
|
317
317
|
const durationMs = Date.now() - startTime;
|
|
318
318
|
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
@@ -330,13 +330,13 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
330
330
|
close() {
|
|
331
331
|
for (const [, pending] of this.pendingCalls) {
|
|
332
332
|
clearTimeout(pending.timer);
|
|
333
|
-
pending.resolve((0, unthrown.
|
|
333
|
+
pending.resolve((0, unthrown.Err)(new RpcCancelledError(pending.rpcName)));
|
|
334
334
|
}
|
|
335
335
|
this.pendingCalls.clear();
|
|
336
336
|
return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {
|
|
337
337
|
this.logger?.warn("Failed to cancel RPC reply consumer during close", { error });
|
|
338
|
-
return (0, unthrown.
|
|
339
|
-
}) : (0, unthrown.
|
|
338
|
+
return (0, unthrown.Ok)(void 0);
|
|
339
|
+
}) : (0, unthrown.Ok)(void 0).toAsync()).flatMap(() => this.amqpClient.close());
|
|
340
340
|
}
|
|
341
341
|
waitForConnectionReady() {
|
|
342
342
|
return this.amqpClient.waitForConnect();
|
package/dist/index.d.cts
CHANGED
|
@@ -149,7 +149,7 @@ declare const RpcCancelledError_base: import("unthrown").TaggedErrorConstructor<
|
|
|
149
149
|
/**
|
|
150
150
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
151
151
|
* reply is received. The correlation map is cleared on close and every pending
|
|
152
|
-
* caller's promise resolves with `
|
|
152
|
+
* caller's promise resolves with `Err(RpcCancelledError)`. Carries a namespaced
|
|
153
153
|
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
154
154
|
* (`"RpcCancelledError"`).
|
|
155
155
|
*/
|
|
@@ -230,7 +230,7 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
230
230
|
defaultPublishOptions?: PublishOptions | undefined;
|
|
231
231
|
/**
|
|
232
232
|
* Maximum time in ms to wait for the AMQP connection to become ready before
|
|
233
|
-
* `create()` resolves to an `
|
|
233
|
+
* `create()` resolves to an `Err(TechnicalError)`. Defaults to 30s
|
|
234
234
|
* (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to
|
|
235
235
|
* disable the timeout and let amqp-connection-manager retry indefinitely.
|
|
236
236
|
*/
|
|
@@ -242,7 +242,7 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
242
242
|
type CallOptions = {
|
|
243
243
|
/**
|
|
244
244
|
* Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves
|
|
245
|
-
* to `
|
|
245
|
+
* to `Err(RpcTimeoutError)` and the in-memory correlation entry is cleared.
|
|
246
246
|
* A late reply arriving after the timeout is silently dropped.
|
|
247
247
|
*
|
|
248
248
|
* Required: RPC without a timeout is a footgun.
|
package/dist/index.d.mts
CHANGED
|
@@ -149,7 +149,7 @@ declare const RpcCancelledError_base: import("unthrown").TaggedErrorConstructor<
|
|
|
149
149
|
/**
|
|
150
150
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
151
151
|
* reply is received. The correlation map is cleared on close and every pending
|
|
152
|
-
* caller's promise resolves with `
|
|
152
|
+
* caller's promise resolves with `Err(RpcCancelledError)`. Carries a namespaced
|
|
153
153
|
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
154
154
|
* (`"RpcCancelledError"`).
|
|
155
155
|
*/
|
|
@@ -230,7 +230,7 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
230
230
|
defaultPublishOptions?: PublishOptions | undefined;
|
|
231
231
|
/**
|
|
232
232
|
* Maximum time in ms to wait for the AMQP connection to become ready before
|
|
233
|
-
* `create()` resolves to an `
|
|
233
|
+
* `create()` resolves to an `Err(TechnicalError)`. Defaults to 30s
|
|
234
234
|
* (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to
|
|
235
235
|
* disable the timeout and let amqp-connection-manager retry indefinitely.
|
|
236
236
|
*/
|
|
@@ -242,7 +242,7 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
|
|
|
242
242
|
type CallOptions = {
|
|
243
243
|
/**
|
|
244
244
|
* Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves
|
|
245
|
-
* to `
|
|
245
|
+
* to `Err(RpcTimeoutError)` and the in-memory correlation entry is cleared.
|
|
246
246
|
* A late reply arriving after the timeout is silently dropped.
|
|
247
247
|
*
|
|
248
248
|
* Required: RPC without a timeout is a footgun.
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractQueue } from "@amqp-contract/contract";
|
|
2
2
|
import { AmqpClient, MessageValidationError, MessagingSemanticConventions, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordLateRpcReply, recordPublishMetric, safeJsonParse, startPublishSpan } from "@amqp-contract/core";
|
|
3
|
-
import {
|
|
3
|
+
import { Err, Ok, TaggedError, fromPromise, fromSafePromise } from "unthrown";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { deflate, gzip } from "node:zlib";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -44,7 +44,7 @@ var RpcTimeoutError = class extends TaggedError("@amqp-contract/RpcTimeoutError"
|
|
|
44
44
|
/**
|
|
45
45
|
* Returned from any in-flight RPC call when the client is closed before the
|
|
46
46
|
* reply is received. The correlation map is cleared on close and every pending
|
|
47
|
-
* caller's promise resolves with `
|
|
47
|
+
* caller's promise resolves with `Err(RpcCancelledError)`. Carries a namespaced
|
|
48
48
|
* `_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
49
49
|
* (`"RpcCancelledError"`).
|
|
50
50
|
*/
|
|
@@ -129,7 +129,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
129
129
|
*/
|
|
130
130
|
setupReplyConsumerIfNeeded() {
|
|
131
131
|
const rpcs = this.contract.rpcs ?? {};
|
|
132
|
-
if (Object.keys(rpcs).length === 0) return
|
|
132
|
+
if (Object.keys(rpcs).length === 0) return Ok(void 0).toAsync();
|
|
133
133
|
return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).tap((tag) => {
|
|
134
134
|
this.replyConsumerTag = tag;
|
|
135
135
|
}).map(() => void 0);
|
|
@@ -161,7 +161,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
161
161
|
clearTimeout(pending.timer);
|
|
162
162
|
const parseResult = safeJsonParse(msg.content, (error) => new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error));
|
|
163
163
|
if (!parseResult.isOk()) {
|
|
164
|
-
pending.resolve(
|
|
164
|
+
pending.resolve(Err(parseResult.isErr() ? parseResult.error : new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, parseResult.cause)));
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
const parsed = parseResult.value;
|
|
@@ -169,17 +169,17 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
169
169
|
try {
|
|
170
170
|
rawValidation = pending.responseSchema["~standard"].validate(parsed);
|
|
171
171
|
} catch (error) {
|
|
172
|
-
pending.resolve(
|
|
172
|
+
pending.resolve(Err(new TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
175
|
(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).then((validation) => {
|
|
176
176
|
if (validation.issues) {
|
|
177
|
-
pending.resolve(
|
|
177
|
+
pending.resolve(Err(new MessageValidationError(pending.rpcName, validation.issues)));
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
|
-
pending.resolve(
|
|
180
|
+
pending.resolve(Ok(validation.value));
|
|
181
181
|
}, (error) => {
|
|
182
|
-
pending.resolve(
|
|
182
|
+
pending.resolve(Err(new TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
@@ -202,8 +202,8 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
202
202
|
const validateMessage = () => {
|
|
203
203
|
const validationResult = publisher.message.payload["~standard"].validate(message);
|
|
204
204
|
return fromPromise(validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult), (error) => new TechnicalError("Validation failed", error)).flatMap((validation) => {
|
|
205
|
-
if (validation.issues) return
|
|
206
|
-
return
|
|
205
|
+
if (validation.issues) return Err(new MessageValidationError(String(publisherName), validation.issues));
|
|
206
|
+
return Ok(validation.value);
|
|
207
207
|
});
|
|
208
208
|
};
|
|
209
209
|
const publishMessage = (validatedMessage) => {
|
|
@@ -218,17 +218,17 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
218
218
|
publishOptions.contentEncoding = compression;
|
|
219
219
|
return compressBuffer(messageBuffer, compression);
|
|
220
220
|
}
|
|
221
|
-
return
|
|
221
|
+
return Ok(validatedMessage).toAsync();
|
|
222
222
|
};
|
|
223
223
|
return preparePayload().flatMap((payload) => this.amqpClient.publish(publisher.exchange.name, publisher.routingKey ?? "", payload, publishOptions).flatMap((published) => {
|
|
224
|
-
if (!published) return
|
|
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)`));
|
|
225
225
|
this.logger?.info("Message published successfully", {
|
|
226
226
|
publisherName: String(publisherName),
|
|
227
227
|
exchange: publisher.exchange.name,
|
|
228
228
|
routingKey: publisher.routingKey,
|
|
229
229
|
compressed: !!compression
|
|
230
230
|
});
|
|
231
|
-
return
|
|
231
|
+
return Ok(void 0);
|
|
232
232
|
}));
|
|
233
233
|
};
|
|
234
234
|
return validateMessage().flatMap((validatedMessage) => publishMessage(validatedMessage)).tap(() => {
|
|
@@ -263,7 +263,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
263
263
|
*/
|
|
264
264
|
call(rpcName, request, options) {
|
|
265
265
|
const TIMEOUT_MAX_MS = 2147483647;
|
|
266
|
-
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();
|
|
267
267
|
const startTime = Date.now();
|
|
268
268
|
const rpc = this.contract.rpcs[rpcName];
|
|
269
269
|
const requestSchema = rpc.request.payload;
|
|
@@ -278,7 +278,7 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
278
278
|
const timer = setTimeout(() => {
|
|
279
279
|
if (!this.pendingCalls.has(correlationId)) return;
|
|
280
280
|
this.pendingCalls.delete(correlationId);
|
|
281
|
-
resolveCall(
|
|
281
|
+
resolveCall(Err(new RpcTimeoutError(String(rpcName), options.timeoutMs)));
|
|
282
282
|
}, options.timeoutMs);
|
|
283
283
|
this.pendingCalls.set(correlationId, {
|
|
284
284
|
rpcName: String(rpcName),
|
|
@@ -291,9 +291,9 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
291
291
|
try {
|
|
292
292
|
rawValidation = requestSchema["~standard"].validate(request);
|
|
293
293
|
} catch (error) {
|
|
294
|
-
return
|
|
294
|
+
return Err(new TechnicalError("RPC request validation threw", error)).toAsync();
|
|
295
295
|
}
|
|
296
|
-
return fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new TechnicalError("RPC request validation threw", error)).flatMap((validation) => validation.issues ?
|
|
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));
|
|
297
297
|
};
|
|
298
298
|
const publishRequest = (validatedRequest) => {
|
|
299
299
|
const { compression: _ignoredCompression, ...defaultsWithoutCompression } = this.defaultPublishOptions;
|
|
@@ -304,14 +304,14 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
304
304
|
correlationId,
|
|
305
305
|
contentType: "application/json"
|
|
306
306
|
};
|
|
307
|
-
return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).flatMap((published) => published ?
|
|
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`)));
|
|
308
308
|
};
|
|
309
309
|
return validateRequest().flatMap((validated) => publishRequest(validated)).flatMap(() => callResultAsync).orElse((error) => {
|
|
310
310
|
if (this.pendingCalls.has(correlationId)) {
|
|
311
311
|
clearTimeout(timer);
|
|
312
312
|
this.pendingCalls.delete(correlationId);
|
|
313
313
|
}
|
|
314
|
-
return
|
|
314
|
+
return Err(error).toAsync();
|
|
315
315
|
}).tap(() => {
|
|
316
316
|
const durationMs = Date.now() - startTime;
|
|
317
317
|
endSpanSuccess(span);
|
|
@@ -329,13 +329,13 @@ var TypedAmqpClient = class TypedAmqpClient {
|
|
|
329
329
|
close() {
|
|
330
330
|
for (const [, pending] of this.pendingCalls) {
|
|
331
331
|
clearTimeout(pending.timer);
|
|
332
|
-
pending.resolve(
|
|
332
|
+
pending.resolve(Err(new RpcCancelledError(pending.rpcName)));
|
|
333
333
|
}
|
|
334
334
|
this.pendingCalls.clear();
|
|
335
335
|
return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {
|
|
336
336
|
this.logger?.warn("Failed to cancel RPC reply consumer during close", { error });
|
|
337
|
-
return
|
|
338
|
-
}) :
|
|
337
|
+
return Ok(void 0);
|
|
338
|
+
}) : Ok(void 0).toAsync()).flatMap(() => this.amqpClient.close());
|
|
339
339
|
}
|
|
340
340
|
waitForConnectionReady() {
|
|
341
341
|
return this.amqpClient.waitForConnect();
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/compression.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["import type { CompressionAlgorithm } from \"@amqp-contract/contract\";\nimport { TechnicalError } from \"@amqp-contract/core\";\nimport { fromPromise, type AsyncResult } from \"unthrown\";\nimport { deflate, gzip } from \"node:zlib\";\nimport { promisify } from \"node:util\";\nimport { match } from \"ts-pattern\";\n\nconst gzipAsync = promisify(gzip);\nconst deflateAsync = promisify(deflate);\n\n/**\n * Compress a buffer using the specified compression algorithm.\n *\n * @param buffer - The buffer to compress\n * @param algorithm - The compression algorithm to use\n * @returns An AsyncResult resolving to the compressed buffer or a TechnicalError\n *\n * @internal\n */\nexport function compressBuffer(\n buffer: Buffer,\n algorithm: CompressionAlgorithm,\n): AsyncResult<Buffer, TechnicalError> {\n return match(algorithm)\n .with(\"gzip\", () =>\n fromPromise(\n gzipAsync(buffer),\n (error) => new TechnicalError(\"Failed to compress with gzip\", error),\n ),\n )\n .with(\"deflate\", () =>\n fromPromise(\n deflateAsync(buffer),\n (error) => new TechnicalError(\"Failed to compress with deflate\", error),\n ),\n )\n .exhaustive();\n}\n","import { TaggedError } from \"unthrown\";\n\nexport { MessageValidationError } from \"@amqp-contract/core\";\n\n/**\n * Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses\n * before the RPC server publishes a reply with the matching `correlationId`.\n *\n * The pending call is removed from the in-memory correlation map; if a reply\n * arrives after the timeout it is dropped (and a debug log is emitted by the\n * client if a logger is configured). Carries a namespaced `_tag` of\n * `\"@amqp-contract/RpcTimeoutError\"`; the `Error.name` is kept bare\n * (`\"RpcTimeoutError\"`).\n */\nexport class RpcTimeoutError extends TaggedError(\"@amqp-contract/RpcTimeoutError\", {\n name: \"RpcTimeoutError\",\n})<{\n message: string;\n rpcName: string;\n timeoutMs: number;\n}> {\n constructor(rpcName: string, timeoutMs: number) {\n super({\n message: `RPC call to \"${rpcName}\" timed out after ${timeoutMs}ms with no reply received`,\n rpcName,\n timeoutMs,\n });\n }\n}\n\n/**\n * Returned from any in-flight RPC call when the client is closed before the\n * reply is received. The correlation map is cleared on close and every pending\n * caller's promise resolves with `err(RpcCancelledError)`. Carries a namespaced\n * `_tag` of `\"@amqp-contract/RpcCancelledError\"`; the `Error.name` is kept bare\n * (`\"RpcCancelledError\"`).\n */\nexport class RpcCancelledError extends TaggedError(\"@amqp-contract/RpcCancelledError\", {\n name: \"RpcCancelledError\",\n})<{\n message: string;\n rpcName: string;\n}> {\n constructor(rpcName: string) {\n super({\n message: `RPC call to \"${rpcName}\" was cancelled because the client was closed`,\n rpcName,\n });\n }\n}\n","import {\n extractQueue,\n type CompressionAlgorithm,\n type ContractDefinition,\n type InferPublisherNames,\n type InferRpcNames,\n} from \"@amqp-contract/contract\";\nimport {\n AmqpClient,\n PublishOptions as AmqpClientPublishOptions,\n type Logger,\n MessagingSemanticConventions,\n TechnicalError,\n type TelemetryProvider,\n defaultTelemetryProvider,\n endSpanError,\n endSpanSuccess,\n recordLateRpcReply,\n recordPublishMetric,\n safeJsonParse,\n startPublishSpan,\n} from \"@amqp-contract/core\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport { err, fromPromise, fromSafePromise, ok, type AsyncResult, type Result } from \"unthrown\";\nimport { randomUUID } from \"node:crypto\";\nimport { compressBuffer } from \"./compression.js\";\nimport { MessageValidationError, RpcCancelledError, RpcTimeoutError } from \"./errors.js\";\nimport type {\n ClientInferPublisherInput,\n ClientInferRpcRequestInput,\n ClientInferRpcResponseOutput,\n} from \"./types.js\";\n\n/**\n * The RabbitMQ direct-reply-to pseudo-queue. Publishing with `replyTo` set to\n * this value tells the server to deliver the response back to the consumer\n * subscribed on this queue on the same channel — no real queue is created and\n * no setup is required beyond consuming from it once with `noAck: true`.\n *\n * @see https://www.rabbitmq.com/docs/direct-reply-to\n */\nconst DIRECT_REPLY_TO = \"amq.rabbitmq.reply-to\";\n\n/**\n * In-flight RPC call tracked by `TypedAmqpClient`. The reply consumer\n * looks up entries by `correlationId` when responses arrive.\n */\ntype PendingCall = {\n rpcName: string;\n responseSchema: StandardSchemaV1;\n resolve: (\n result: Result<\n unknown,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >,\n ) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\n/**\n * Publish options that extend amqp-client's PublishOptions with optional compression support.\n */\nexport type PublishOptions = AmqpClientPublishOptions & {\n /**\n * Optional compression algorithm to use for the message payload.\n * When specified, the message will be compressed using the chosen algorithm\n * and the contentEncoding header will be set automatically.\n */\n compression?: CompressionAlgorithm | undefined;\n};\n\n/**\n * Options for creating a client\n */\nexport type CreateClientOptions<TContract extends ContractDefinition> = {\n contract: TContract;\n urls: ConnectionUrl[];\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n logger?: Logger | undefined;\n /**\n * Optional telemetry provider for tracing and metrics.\n * If not provided, uses the default provider which attempts to load OpenTelemetry.\n * OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.\n */\n telemetry?: TelemetryProvider | undefined;\n /**\n * Default publish options that will be applied to all publish operations.\n * These can be overridden by options passed to the publish method.\n * By default, persistent is set to true for message durability.\n */\n defaultPublishOptions?: PublishOptions | undefined;\n /**\n * Maximum time in ms to wait for the AMQP connection to become ready before\n * `create()` resolves to an `err(TechnicalError)`. Defaults to 30s\n * (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to\n * disable the timeout and let amqp-connection-manager retry indefinitely.\n */\n connectTimeoutMs?: number | null | undefined;\n};\n\n/**\n * Per-call options for `client.call()`.\n */\nexport type CallOptions = {\n /**\n * Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves\n * to `err(RpcTimeoutError)` and the in-memory correlation entry is cleared.\n * A late reply arriving after the timeout is silently dropped.\n *\n * Required: RPC without a timeout is a footgun.\n */\n timeoutMs: number;\n\n /**\n * Optional AMQP message properties to merge into the request. `replyTo` and\n * `correlationId` are managed by the client and cannot be overridden.\n */\n publishOptions?: Omit<AmqpClientPublishOptions, \"replyTo\" | \"correlationId\">;\n};\n\n/**\n * Type-safe AMQP client for publishing messages\n */\nexport class TypedAmqpClient<TContract extends ContractDefinition> {\n /**\n * In-flight RPC calls keyed by `correlationId`. Cleared when a reply is\n * received, when the call times out, or when the client is closed.\n */\n private readonly pendingCalls = new Map<string, PendingCall>();\n\n /**\n * Consumer tag of the reply consumer subscribed on `amq.rabbitmq.reply-to`.\n * Set when the contract has at least one entry in `rpcs`; undefined otherwise.\n */\n private replyConsumerTag?: string;\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n private readonly defaultPublishOptions: PublishOptions,\n private readonly logger?: Logger,\n private readonly telemetry: TelemetryProvider = defaultTelemetryProvider,\n ) {}\n\n /**\n * Create a type-safe AMQP client from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The client establishes\n * infrastructure asynchronously in the background once the connection is ready.\n *\n * Connections are automatically shared across clients with the same URLs and\n * connection options, following RabbitMQ best practices.\n */\n static create<TContract extends ContractDefinition>({\n contract,\n urls,\n connectionOptions,\n defaultPublishOptions,\n logger,\n telemetry,\n connectTimeoutMs,\n }: CreateClientOptions<TContract>): AsyncResult<TypedAmqpClient<TContract>, TechnicalError> {\n const client = new TypedAmqpClient(\n contract,\n new AmqpClient(contract, { urls, connectionOptions, connectTimeoutMs }),\n { persistent: true, ...defaultPublishOptions },\n logger,\n telemetry ?? defaultTelemetryProvider,\n );\n\n const setup = client\n .waitForConnectionReady()\n .flatMap(() => client.setupReplyConsumerIfNeeded());\n\n const inner = (async (): Promise<Result<TypedAmqpClient<TContract>, TechnicalError>> => {\n const setupResult = await setup;\n if (!setupResult.isOk()) {\n const closeResult = await client.close();\n if (closeResult.isErr()) {\n logger?.warn(\"Failed to close client after connection failure\", {\n error: closeResult.error,\n });\n }\n }\n // `map` runs only on Ok; an Err/Defect passes through with its value type\n // re-shaped to the client, so the failure surfaces unchanged.\n return setupResult.map(() => client);\n })();\n\n return fromSafePromise(inner).flatMap((result) => result);\n }\n\n /**\n * If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`\n * once. Replies for every in-flight call arrive on this single consumer and\n * are demultiplexed by `correlationId`.\n */\n private setupReplyConsumerIfNeeded(): AsyncResult<void, TechnicalError> {\n const rpcs = this.contract.rpcs ?? {};\n if (Object.keys(rpcs).length === 0) {\n return ok(undefined).toAsync();\n }\n\n return this.amqpClient\n .consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true })\n .tap((tag) => {\n this.replyConsumerTag = tag;\n })\n .map(() => undefined);\n }\n\n /**\n * Demultiplex an RPC reply by `correlationId`, validate the body against the\n * call's response schema, and resolve the awaiting caller. Replies with no\n * matching pending call (the call already timed out, was cancelled, or the\n * correlationId is unknown) are logged at warn — a non-zero rate of these\n * usually indicates a tuning problem (handler latency exceeds caller\n * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on\n * sustained drift without parsing logs.\n */\n private handleRpcReply(msg: Parameters<Parameters<AmqpClient[\"consume\"]>[1]>[0]): void {\n if (!msg) return;\n const correlationId = msg.properties.correlationId;\n if (typeof correlationId !== \"string\") {\n this.logger?.warn(\"Received RPC reply without correlationId; dropping\", {\n deliveryTag: msg.fields.deliveryTag,\n });\n recordLateRpcReply(this.telemetry, \"missing-correlation-id\");\n return;\n }\n const pending = this.pendingCalls.get(correlationId);\n if (!pending) {\n this.logger?.warn(\n \"Received RPC reply for unknown correlationId (caller already timed out or cancelled)\",\n { correlationId },\n );\n recordLateRpcReply(this.telemetry, \"unknown-correlation-id\");\n return;\n }\n this.pendingCalls.delete(correlationId);\n clearTimeout(pending.timer);\n\n const parseResult = safeJsonParse(\n msg.content,\n (error) =>\n new TechnicalError(`Failed to parse RPC reply JSON for \"${pending.rpcName}\"`, error),\n );\n if (!parseResult.isOk()) {\n pending.resolve(\n err(\n parseResult.isErr()\n ? parseResult.error\n : new TechnicalError(\n `Failed to parse RPC reply JSON for \"${pending.rpcName}\"`,\n parseResult.cause,\n ),\n ),\n );\n return;\n }\n const parsed = parseResult.value;\n\n // Wrap the validate call itself — a Standard Schema implementation may\n // throw synchronously, and the throw would otherwise escape the consume\n // callback and could crash the reply consumer.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = pending.responseSchema[\"~standard\"].validate(parsed);\n } catch (error: unknown) {\n pending.resolve(\n err(new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error)),\n );\n return;\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n validationPromise.then(\n (validation) => {\n if (validation.issues) {\n pending.resolve(err(new MessageValidationError(pending.rpcName, validation.issues)));\n return;\n }\n pending.resolve(ok(validation.value));\n },\n (error: unknown) => {\n pending.resolve(\n err(new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error)),\n );\n },\n );\n }\n\n /**\n * Publish a message using a defined publisher.\n *\n * @param publisherName - The name of the publisher to use\n * @param message - The message to publish\n * @param options - Optional publish options including compression, headers, priority, etc.\n *\n * @remarks\n * If `options.compression` is specified, the message will be compressed before publishing\n * and the `contentEncoding` property will be set automatically. Any `contentEncoding`\n * value already in options will be overwritten by the compression algorithm.\n */\n publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): AsyncResult<void, TechnicalError | MessageValidationError> {\n const startTime = Date.now();\n // Non-null assertions safe: TypeScript guarantees these exist for valid TName\n const publisher = this.contract.publishers![publisherName as string]!;\n const { exchange, routingKey } = publisher;\n\n // Start telemetry span\n const span = startPublishSpan(this.telemetry, exchange.name, routingKey, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName),\n });\n\n const validateMessage = (): AsyncResult<unknown, TechnicalError | MessageValidationError> => {\n const validationResult = publisher.message.payload[\"~standard\"].validate(message);\n const promise =\n validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult);\n return fromPromise(\n promise,\n (error): TechnicalError | MessageValidationError =>\n new TechnicalError(\"Validation failed\", error),\n ).flatMap((validation) => {\n if (validation.issues) {\n return err<TechnicalError | MessageValidationError>(\n new MessageValidationError(String(publisherName), validation.issues),\n );\n }\n return ok(validation.value);\n });\n };\n\n const publishMessage = (validatedMessage: unknown): AsyncResult<void, TechnicalError> => {\n // Merge default options with provided options\n const mergedOptions = { ...this.defaultPublishOptions, ...options };\n\n // Extract compression from merged options and create publish options without it\n const { compression, ...restOptions } = mergedOptions;\n const publishOptions: AmqpClientPublishOptions = { ...restOptions };\n\n // Prepare payload and options based on compression configuration\n const preparePayload = (): AsyncResult<Buffer | unknown, TechnicalError> => {\n if (compression) {\n // Compress the message payload\n const messageBuffer = Buffer.from(JSON.stringify(validatedMessage));\n publishOptions.contentEncoding = compression;\n return compressBuffer(messageBuffer, compression);\n }\n\n // No compression: use the channel's built-in JSON serialization\n return ok(validatedMessage).toAsync();\n };\n\n return preparePayload().flatMap((payload) =>\n this.amqpClient\n .publish(publisher.exchange.name, publisher.routingKey ?? \"\", payload, publishOptions)\n .flatMap((published) => {\n if (!published) {\n return err<TechnicalError>(\n new TechnicalError(\n `Failed to publish message for publisher \"${String(publisherName)}\": Channel rejected the message (buffer full or other channel issue)`,\n ),\n );\n }\n\n this.logger?.info(\"Message published successfully\", {\n publisherName: String(publisherName),\n exchange: publisher.exchange.name,\n routingKey: publisher.routingKey,\n compressed: !!compression,\n });\n\n return ok(undefined);\n }),\n );\n };\n\n return validateMessage()\n .flatMap((validatedMessage) => publishMessage(validatedMessage))\n .tap(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);\n })\n .tapErr((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, false, durationMs);\n });\n }\n\n /**\n * Invoke an RPC defined via `defineRpc` and await the typed response.\n *\n * The request payload is validated against the RPC's request schema, then\n * published to the AMQP default exchange with the server's queue name as\n * routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID\n * `correlationId`. The returned AsyncResult resolves once a matching reply\n * arrives and validates against the response schema, or once `timeoutMs`\n * elapses (whichever comes first).\n *\n * @example\n * ```typescript\n * const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });\n * result.match({\n * ok: (value) => console.log(value.sum), // 3\n * err: (error) => console.error(error),\n * defect: (cause) => console.error(cause),\n * });\n * ```\n */\n call<TName extends InferRpcNames<TContract>>(\n rpcName: TName,\n request: ClientInferRpcRequestInput<TContract, TName>,\n options: CallOptions,\n ): AsyncResult<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n > {\n type ResponseType = ClientInferRpcResponseOutput<TContract, TName>;\n type CallError = TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError;\n type CallResult = Result<ResponseType, CallError>;\n\n // setTimeout truncates fractional ms and clamps anything outside the\n // 32-bit signed integer range (~24.8 days) to 1ms, so reject those up\n // front as user errors rather than producing surprising behavior.\n const TIMEOUT_MAX_MS = 2_147_483_647;\n if (\n typeof options.timeoutMs !== \"number\" ||\n !Number.isFinite(options.timeoutMs) ||\n options.timeoutMs <= 0 ||\n options.timeoutMs > TIMEOUT_MAX_MS\n ) {\n return err<CallError>(\n new TechnicalError(\n `Invalid timeoutMs for RPC call to \"${String(rpcName)}\": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`,\n ),\n ).toAsync();\n }\n\n const startTime = Date.now();\n // Non-null assertion safe: TName is constrained to RPC names in the contract.\n const rpc = this.contract.rpcs![rpcName as string]!;\n const requestSchema = rpc.request.payload;\n const responseSchema = rpc.response.payload;\n const queueName = extractQueue(rpc.queue).name;\n\n // RPC publishes to the default exchange with the queue name as routing key.\n const span = startPublishSpan(this.telemetry, \"\", queueName, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName),\n });\n\n const correlationId = randomUUID();\n\n // Set up the reply future + pending entry up front so a reply that arrives\n // racing the publish round-trip can find a slot. Cleanup on preflight\n // failure happens in the `.orElse` below.\n let resolveCall!: (result: CallResult) => void;\n const callPromise = new Promise<CallResult>((res) => {\n resolveCall = res;\n });\n // `callPromise` resolves to a `Result` (never rejects), so lift it with\n // `fromSafePromise` and collapse the nested `Result` back into the channel.\n const callResultAsync: AsyncResult<ResponseType, CallError> = fromSafePromise(\n callPromise,\n ).flatMap((result) => result);\n\n const timer = setTimeout(() => {\n if (!this.pendingCalls.has(correlationId)) return;\n this.pendingCalls.delete(correlationId);\n resolveCall(err(new RpcTimeoutError(String(rpcName), options.timeoutMs)));\n }, options.timeoutMs);\n\n this.pendingCalls.set(correlationId, {\n rpcName: String(rpcName),\n responseSchema,\n resolve: resolveCall as PendingCall[\"resolve\"],\n timer,\n });\n\n const validateRequest = (): AsyncResult<unknown, TechnicalError | MessageValidationError> => {\n // Wrap the validate call — a Standard Schema implementation may throw\n // synchronously, and that throw would otherwise escape the chain and\n // leave the pending-call entry/timer dangling until timeout.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = requestSchema[\"~standard\"].validate(request);\n } catch (error: unknown) {\n return err<TechnicalError | MessageValidationError>(\n new TechnicalError(\"RPC request validation threw\", error),\n ).toAsync();\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n return fromPromise(\n validationPromise,\n (error): TechnicalError | MessageValidationError =>\n new TechnicalError(\"RPC request validation threw\", error),\n ).flatMap((validation) =>\n validation.issues\n ? err<TechnicalError | MessageValidationError>(\n new MessageValidationError(String(rpcName), validation.issues),\n )\n : ok(validation.value),\n );\n };\n\n const publishRequest = (validatedRequest: unknown): AsyncResult<void, TechnicalError> => {\n // Merge `defaultPublishOptions` (persistent, priority, headers, …) with\n // the per-call options, then layer the RPC-managed fields on top so they\n // cannot be overridden. `compression` is intentionally dropped: RPC v1\n // does not implement reply-side decompression, so request-side\n // compression would break the round-trip.\n const { compression: _ignoredCompression, ...defaultsWithoutCompression } =\n this.defaultPublishOptions;\n const publishOptions: AmqpClientPublishOptions = {\n ...defaultsWithoutCompression,\n ...options.publishOptions,\n replyTo: DIRECT_REPLY_TO,\n correlationId,\n contentType: \"application/json\",\n };\n return this.amqpClient\n .publish(\"\", queueName, validatedRequest, publishOptions)\n .flatMap((published) =>\n published\n ? ok(undefined)\n : err<TechnicalError>(\n new TechnicalError(\n `Failed to publish RPC request for \"${String(rpcName)}\": channel buffer full`,\n ),\n ),\n );\n };\n\n return validateRequest()\n .flatMap((validated) => publishRequest(validated))\n .flatMap(() => callResultAsync)\n .orElse((error: CallError) => {\n // If preflight failed (validate or publish), the pending entry still\n // exists and the timer is alive. Clean both up so the call doesn't\n // leak. Timer-fired errors and reply-resolved errors have already\n // cleaned the entry, so the .has() check guards against double cleanup.\n if (this.pendingCalls.has(correlationId)) {\n clearTimeout(timer);\n this.pendingCalls.delete(correlationId);\n }\n return err(error).toAsync();\n })\n .tap(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, \"\", queueName, true, durationMs);\n })\n .tapErr((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, \"\", queueName, false, durationMs);\n });\n }\n\n /**\n * Close the channel and connection. Cancels the reply consumer (if any) and\n * rejects every in-flight RPC call with `RpcCancelledError`.\n */\n close(): AsyncResult<void, TechnicalError> {\n // Reject pending calls first — once close() runs, no reply will arrive.\n for (const [, pending] of this.pendingCalls) {\n clearTimeout(pending.timer);\n pending.resolve(err(new RpcCancelledError(pending.rpcName)));\n }\n this.pendingCalls.clear();\n\n const cancelReply: AsyncResult<void, TechnicalError> = this.replyConsumerTag\n ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {\n this.logger?.warn(\"Failed to cancel RPC reply consumer during close\", { error });\n return ok(undefined);\n })\n : ok(undefined).toAsync();\n\n return cancelReply.flatMap(() => this.amqpClient.close());\n }\n\n private waitForConnectionReady(): AsyncResult<void, TechnicalError> {\n return this.amqpClient.waitForConnect();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/compression.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["import type { CompressionAlgorithm } from \"@amqp-contract/contract\";\nimport { TechnicalError } from \"@amqp-contract/core\";\nimport { fromPromise, type AsyncResult } from \"unthrown\";\nimport { deflate, gzip } from \"node:zlib\";\nimport { promisify } from \"node:util\";\nimport { match } from \"ts-pattern\";\n\nconst gzipAsync = promisify(gzip);\nconst deflateAsync = promisify(deflate);\n\n/**\n * Compress a buffer using the specified compression algorithm.\n *\n * @param buffer - The buffer to compress\n * @param algorithm - The compression algorithm to use\n * @returns An AsyncResult resolving to the compressed buffer or a TechnicalError\n *\n * @internal\n */\nexport function compressBuffer(\n buffer: Buffer,\n algorithm: CompressionAlgorithm,\n): AsyncResult<Buffer, TechnicalError> {\n return match(algorithm)\n .with(\"gzip\", () =>\n fromPromise(\n gzipAsync(buffer),\n (error) => new TechnicalError(\"Failed to compress with gzip\", error),\n ),\n )\n .with(\"deflate\", () =>\n fromPromise(\n deflateAsync(buffer),\n (error) => new TechnicalError(\"Failed to compress with deflate\", error),\n ),\n )\n .exhaustive();\n}\n","import { TaggedError } from \"unthrown\";\n\nexport { MessageValidationError } from \"@amqp-contract/core\";\n\n/**\n * Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses\n * before the RPC server publishes a reply with the matching `correlationId`.\n *\n * The pending call is removed from the in-memory correlation map; if a reply\n * arrives after the timeout it is dropped (and a debug log is emitted by the\n * client if a logger is configured). Carries a namespaced `_tag` of\n * `\"@amqp-contract/RpcTimeoutError\"`; the `Error.name` is kept bare\n * (`\"RpcTimeoutError\"`).\n */\nexport class RpcTimeoutError extends TaggedError(\"@amqp-contract/RpcTimeoutError\", {\n name: \"RpcTimeoutError\",\n})<{\n message: string;\n rpcName: string;\n timeoutMs: number;\n}> {\n constructor(rpcName: string, timeoutMs: number) {\n super({\n message: `RPC call to \"${rpcName}\" timed out after ${timeoutMs}ms with no reply received`,\n rpcName,\n timeoutMs,\n });\n }\n}\n\n/**\n * Returned from any in-flight RPC call when the client is closed before the\n * reply is received. The correlation map is cleared on close and every pending\n * caller's promise resolves with `Err(RpcCancelledError)`. Carries a namespaced\n * `_tag` of `\"@amqp-contract/RpcCancelledError\"`; the `Error.name` is kept bare\n * (`\"RpcCancelledError\"`).\n */\nexport class RpcCancelledError extends TaggedError(\"@amqp-contract/RpcCancelledError\", {\n name: \"RpcCancelledError\",\n})<{\n message: string;\n rpcName: string;\n}> {\n constructor(rpcName: string) {\n super({\n message: `RPC call to \"${rpcName}\" was cancelled because the client was closed`,\n rpcName,\n });\n }\n}\n","import {\n extractQueue,\n type CompressionAlgorithm,\n type ContractDefinition,\n type InferPublisherNames,\n type InferRpcNames,\n} from \"@amqp-contract/contract\";\nimport {\n AmqpClient,\n PublishOptions as AmqpClientPublishOptions,\n type Logger,\n MessagingSemanticConventions,\n TechnicalError,\n type TelemetryProvider,\n defaultTelemetryProvider,\n endSpanError,\n endSpanSuccess,\n recordLateRpcReply,\n recordPublishMetric,\n safeJsonParse,\n startPublishSpan,\n} from \"@amqp-contract/core\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport { Err, fromPromise, fromSafePromise, Ok, type AsyncResult, type Result } from \"unthrown\";\nimport { randomUUID } from \"node:crypto\";\nimport { compressBuffer } from \"./compression.js\";\nimport { MessageValidationError, RpcCancelledError, RpcTimeoutError } from \"./errors.js\";\nimport type {\n ClientInferPublisherInput,\n ClientInferRpcRequestInput,\n ClientInferRpcResponseOutput,\n} from \"./types.js\";\n\n/**\n * The RabbitMQ direct-reply-to pseudo-queue. Publishing with `replyTo` set to\n * this value tells the server to deliver the response back to the consumer\n * subscribed on this queue on the same channel — no real queue is created and\n * no setup is required beyond consuming from it once with `noAck: true`.\n *\n * @see https://www.rabbitmq.com/docs/direct-reply-to\n */\nconst DIRECT_REPLY_TO = \"amq.rabbitmq.reply-to\";\n\n/**\n * In-flight RPC call tracked by `TypedAmqpClient`. The reply consumer\n * looks up entries by `correlationId` when responses arrive.\n */\ntype PendingCall = {\n rpcName: string;\n responseSchema: StandardSchemaV1;\n resolve: (\n result: Result<\n unknown,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >,\n ) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\n/**\n * Publish options that extend amqp-client's PublishOptions with optional compression support.\n */\nexport type PublishOptions = AmqpClientPublishOptions & {\n /**\n * Optional compression algorithm to use for the message payload.\n * When specified, the message will be compressed using the chosen algorithm\n * and the contentEncoding header will be set automatically.\n */\n compression?: CompressionAlgorithm | undefined;\n};\n\n/**\n * Options for creating a client\n */\nexport type CreateClientOptions<TContract extends ContractDefinition> = {\n contract: TContract;\n urls: ConnectionUrl[];\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n logger?: Logger | undefined;\n /**\n * Optional telemetry provider for tracing and metrics.\n * If not provided, uses the default provider which attempts to load OpenTelemetry.\n * OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.\n */\n telemetry?: TelemetryProvider | undefined;\n /**\n * Default publish options that will be applied to all publish operations.\n * These can be overridden by options passed to the publish method.\n * By default, persistent is set to true for message durability.\n */\n defaultPublishOptions?: PublishOptions | undefined;\n /**\n * Maximum time in ms to wait for the AMQP connection to become ready before\n * `create()` resolves to an `Err(TechnicalError)`. Defaults to 30s\n * (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to\n * disable the timeout and let amqp-connection-manager retry indefinitely.\n */\n connectTimeoutMs?: number | null | undefined;\n};\n\n/**\n * Per-call options for `client.call()`.\n */\nexport type CallOptions = {\n /**\n * Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves\n * to `Err(RpcTimeoutError)` and the in-memory correlation entry is cleared.\n * A late reply arriving after the timeout is silently dropped.\n *\n * Required: RPC without a timeout is a footgun.\n */\n timeoutMs: number;\n\n /**\n * Optional AMQP message properties to merge into the request. `replyTo` and\n * `correlationId` are managed by the client and cannot be overridden.\n */\n publishOptions?: Omit<AmqpClientPublishOptions, \"replyTo\" | \"correlationId\">;\n};\n\n/**\n * Type-safe AMQP client for publishing messages\n */\nexport class TypedAmqpClient<TContract extends ContractDefinition> {\n /**\n * In-flight RPC calls keyed by `correlationId`. Cleared when a reply is\n * received, when the call times out, or when the client is closed.\n */\n private readonly pendingCalls = new Map<string, PendingCall>();\n\n /**\n * Consumer tag of the reply consumer subscribed on `amq.rabbitmq.reply-to`.\n * Set when the contract has at least one entry in `rpcs`; undefined otherwise.\n */\n private replyConsumerTag?: string;\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n private readonly defaultPublishOptions: PublishOptions,\n private readonly logger?: Logger,\n private readonly telemetry: TelemetryProvider = defaultTelemetryProvider,\n ) {}\n\n /**\n * Create a type-safe AMQP client from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The client establishes\n * infrastructure asynchronously in the background once the connection is ready.\n *\n * Connections are automatically shared across clients with the same URLs and\n * connection options, following RabbitMQ best practices.\n */\n static create<TContract extends ContractDefinition>({\n contract,\n urls,\n connectionOptions,\n defaultPublishOptions,\n logger,\n telemetry,\n connectTimeoutMs,\n }: CreateClientOptions<TContract>): AsyncResult<TypedAmqpClient<TContract>, TechnicalError> {\n const client = new TypedAmqpClient(\n contract,\n new AmqpClient(contract, { urls, connectionOptions, connectTimeoutMs }),\n { persistent: true, ...defaultPublishOptions },\n logger,\n telemetry ?? defaultTelemetryProvider,\n );\n\n const setup = client\n .waitForConnectionReady()\n .flatMap(() => client.setupReplyConsumerIfNeeded());\n\n const inner = (async (): Promise<Result<TypedAmqpClient<TContract>, TechnicalError>> => {\n const setupResult = await setup;\n if (!setupResult.isOk()) {\n const closeResult = await client.close();\n if (closeResult.isErr()) {\n logger?.warn(\"Failed to close client after connection failure\", {\n error: closeResult.error,\n });\n }\n }\n // `map` runs only on Ok; an Err/Defect passes through with its value type\n // re-shaped to the client, so the failure surfaces unchanged.\n return setupResult.map(() => client);\n })();\n\n return fromSafePromise(inner).flatMap((result) => result);\n }\n\n /**\n * If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`\n * once. Replies for every in-flight call arrive on this single consumer and\n * are demultiplexed by `correlationId`.\n */\n private setupReplyConsumerIfNeeded(): AsyncResult<void, TechnicalError> {\n const rpcs = this.contract.rpcs ?? {};\n if (Object.keys(rpcs).length === 0) {\n return Ok(undefined).toAsync();\n }\n\n return this.amqpClient\n .consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true })\n .tap((tag) => {\n this.replyConsumerTag = tag;\n })\n .map(() => undefined);\n }\n\n /**\n * Demultiplex an RPC reply by `correlationId`, validate the body against the\n * call's response schema, and resolve the awaiting caller. Replies with no\n * matching pending call (the call already timed out, was cancelled, or the\n * correlationId is unknown) are logged at warn — a non-zero rate of these\n * usually indicates a tuning problem (handler latency exceeds caller\n * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on\n * sustained drift without parsing logs.\n */\n private handleRpcReply(msg: Parameters<Parameters<AmqpClient[\"consume\"]>[1]>[0]): void {\n if (!msg) return;\n const correlationId = msg.properties.correlationId;\n if (typeof correlationId !== \"string\") {\n this.logger?.warn(\"Received RPC reply without correlationId; dropping\", {\n deliveryTag: msg.fields.deliveryTag,\n });\n recordLateRpcReply(this.telemetry, \"missing-correlation-id\");\n return;\n }\n const pending = this.pendingCalls.get(correlationId);\n if (!pending) {\n this.logger?.warn(\n \"Received RPC reply for unknown correlationId (caller already timed out or cancelled)\",\n { correlationId },\n );\n recordLateRpcReply(this.telemetry, \"unknown-correlation-id\");\n return;\n }\n this.pendingCalls.delete(correlationId);\n clearTimeout(pending.timer);\n\n const parseResult = safeJsonParse(\n msg.content,\n (error) =>\n new TechnicalError(`Failed to parse RPC reply JSON for \"${pending.rpcName}\"`, error),\n );\n if (!parseResult.isOk()) {\n pending.resolve(\n Err(\n parseResult.isErr()\n ? parseResult.error\n : new TechnicalError(\n `Failed to parse RPC reply JSON for \"${pending.rpcName}\"`,\n parseResult.cause,\n ),\n ),\n );\n return;\n }\n const parsed = parseResult.value;\n\n // Wrap the validate call itself — a Standard Schema implementation may\n // throw synchronously, and the throw would otherwise escape the consume\n // callback and could crash the reply consumer.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = pending.responseSchema[\"~standard\"].validate(parsed);\n } catch (error: unknown) {\n pending.resolve(\n Err(new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error)),\n );\n return;\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n validationPromise.then(\n (validation) => {\n if (validation.issues) {\n pending.resolve(Err(new MessageValidationError(pending.rpcName, validation.issues)));\n return;\n }\n pending.resolve(Ok(validation.value));\n },\n (error: unknown) => {\n pending.resolve(\n Err(new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error)),\n );\n },\n );\n }\n\n /**\n * Publish a message using a defined publisher.\n *\n * @param publisherName - The name of the publisher to use\n * @param message - The message to publish\n * @param options - Optional publish options including compression, headers, priority, etc.\n *\n * @remarks\n * If `options.compression` is specified, the message will be compressed before publishing\n * and the `contentEncoding` property will be set automatically. Any `contentEncoding`\n * value already in options will be overwritten by the compression algorithm.\n */\n publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): AsyncResult<void, TechnicalError | MessageValidationError> {\n const startTime = Date.now();\n // Non-null assertions safe: TypeScript guarantees these exist for valid TName\n const publisher = this.contract.publishers![publisherName as string]!;\n const { exchange, routingKey } = publisher;\n\n // Start telemetry span\n const span = startPublishSpan(this.telemetry, exchange.name, routingKey, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName),\n });\n\n const validateMessage = (): AsyncResult<unknown, TechnicalError | MessageValidationError> => {\n const validationResult = publisher.message.payload[\"~standard\"].validate(message);\n const promise =\n validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult);\n return fromPromise(\n promise,\n (error): TechnicalError | MessageValidationError =>\n new TechnicalError(\"Validation failed\", error),\n ).flatMap((validation) => {\n if (validation.issues) {\n return Err<TechnicalError | MessageValidationError>(\n new MessageValidationError(String(publisherName), validation.issues),\n );\n }\n return Ok(validation.value);\n });\n };\n\n const publishMessage = (validatedMessage: unknown): AsyncResult<void, TechnicalError> => {\n // Merge default options with provided options\n const mergedOptions = { ...this.defaultPublishOptions, ...options };\n\n // Extract compression from merged options and create publish options without it\n const { compression, ...restOptions } = mergedOptions;\n const publishOptions: AmqpClientPublishOptions = { ...restOptions };\n\n // Prepare payload and options based on compression configuration\n const preparePayload = (): AsyncResult<Buffer | unknown, TechnicalError> => {\n if (compression) {\n // Compress the message payload\n const messageBuffer = Buffer.from(JSON.stringify(validatedMessage));\n publishOptions.contentEncoding = compression;\n return compressBuffer(messageBuffer, compression);\n }\n\n // No compression: use the channel's built-in JSON serialization\n return Ok(validatedMessage).toAsync();\n };\n\n return preparePayload().flatMap((payload) =>\n this.amqpClient\n .publish(publisher.exchange.name, publisher.routingKey ?? \"\", payload, publishOptions)\n .flatMap((published) => {\n if (!published) {\n return Err<TechnicalError>(\n new TechnicalError(\n `Failed to publish message for publisher \"${String(publisherName)}\": Channel rejected the message (buffer full or other channel issue)`,\n ),\n );\n }\n\n this.logger?.info(\"Message published successfully\", {\n publisherName: String(publisherName),\n exchange: publisher.exchange.name,\n routingKey: publisher.routingKey,\n compressed: !!compression,\n });\n\n return Ok(undefined);\n }),\n );\n };\n\n return validateMessage()\n .flatMap((validatedMessage) => publishMessage(validatedMessage))\n .tap(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);\n })\n .tapErr((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, false, durationMs);\n });\n }\n\n /**\n * Invoke an RPC defined via `defineRpc` and await the typed response.\n *\n * The request payload is validated against the RPC's request schema, then\n * published to the AMQP default exchange with the server's queue name as\n * routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID\n * `correlationId`. The returned AsyncResult resolves once a matching reply\n * arrives and validates against the response schema, or once `timeoutMs`\n * elapses (whichever comes first).\n *\n * @example\n * ```typescript\n * const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });\n * result.match({\n * ok: (value) => console.log(value.sum), // 3\n * err: (error) => console.error(error),\n * defect: (cause) => console.error(cause),\n * });\n * ```\n */\n call<TName extends InferRpcNames<TContract>>(\n rpcName: TName,\n request: ClientInferRpcRequestInput<TContract, TName>,\n options: CallOptions,\n ): AsyncResult<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n > {\n type ResponseType = ClientInferRpcResponseOutput<TContract, TName>;\n type CallError = TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError;\n type CallResult = Result<ResponseType, CallError>;\n\n // setTimeout truncates fractional ms and clamps anything outside the\n // 32-bit signed integer range (~24.8 days) to 1ms, so reject those up\n // front as user errors rather than producing surprising behavior.\n const TIMEOUT_MAX_MS = 2_147_483_647;\n if (\n typeof options.timeoutMs !== \"number\" ||\n !Number.isFinite(options.timeoutMs) ||\n options.timeoutMs <= 0 ||\n options.timeoutMs > TIMEOUT_MAX_MS\n ) {\n return Err<CallError>(\n new TechnicalError(\n `Invalid timeoutMs for RPC call to \"${String(rpcName)}\": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`,\n ),\n ).toAsync();\n }\n\n const startTime = Date.now();\n // Non-null assertion safe: TName is constrained to RPC names in the contract.\n const rpc = this.contract.rpcs![rpcName as string]!;\n const requestSchema = rpc.request.payload;\n const responseSchema = rpc.response.payload;\n const queueName = extractQueue(rpc.queue).name;\n\n // RPC publishes to the default exchange with the queue name as routing key.\n const span = startPublishSpan(this.telemetry, \"\", queueName, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName),\n });\n\n const correlationId = randomUUID();\n\n // Set up the reply future + pending entry up front so a reply that arrives\n // racing the publish round-trip can find a slot. Cleanup on preflight\n // failure happens in the `.orElse` below.\n let resolveCall!: (result: CallResult) => void;\n const callPromise = new Promise<CallResult>((res) => {\n resolveCall = res;\n });\n // `callPromise` resolves to a `Result` (never rejects), so lift it with\n // `fromSafePromise` and collapse the nested `Result` back into the channel.\n const callResultAsync: AsyncResult<ResponseType, CallError> = fromSafePromise(\n callPromise,\n ).flatMap((result) => result);\n\n const timer = setTimeout(() => {\n if (!this.pendingCalls.has(correlationId)) return;\n this.pendingCalls.delete(correlationId);\n resolveCall(Err(new RpcTimeoutError(String(rpcName), options.timeoutMs)));\n }, options.timeoutMs);\n\n this.pendingCalls.set(correlationId, {\n rpcName: String(rpcName),\n responseSchema,\n resolve: resolveCall as PendingCall[\"resolve\"],\n timer,\n });\n\n const validateRequest = (): AsyncResult<unknown, TechnicalError | MessageValidationError> => {\n // Wrap the validate call — a Standard Schema implementation may throw\n // synchronously, and that throw would otherwise escape the chain and\n // leave the pending-call entry/timer dangling until timeout.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = requestSchema[\"~standard\"].validate(request);\n } catch (error: unknown) {\n return Err<TechnicalError | MessageValidationError>(\n new TechnicalError(\"RPC request validation threw\", error),\n ).toAsync();\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n return fromPromise(\n validationPromise,\n (error): TechnicalError | MessageValidationError =>\n new TechnicalError(\"RPC request validation threw\", error),\n ).flatMap((validation) =>\n validation.issues\n ? Err<TechnicalError | MessageValidationError>(\n new MessageValidationError(String(rpcName), validation.issues),\n )\n : Ok(validation.value),\n );\n };\n\n const publishRequest = (validatedRequest: unknown): AsyncResult<void, TechnicalError> => {\n // Merge `defaultPublishOptions` (persistent, priority, headers, …) with\n // the per-call options, then layer the RPC-managed fields on top so they\n // cannot be overridden. `compression` is intentionally dropped: RPC v1\n // does not implement reply-side decompression, so request-side\n // compression would break the round-trip.\n const { compression: _ignoredCompression, ...defaultsWithoutCompression } =\n this.defaultPublishOptions;\n const publishOptions: AmqpClientPublishOptions = {\n ...defaultsWithoutCompression,\n ...options.publishOptions,\n replyTo: DIRECT_REPLY_TO,\n correlationId,\n contentType: \"application/json\",\n };\n return this.amqpClient\n .publish(\"\", queueName, validatedRequest, publishOptions)\n .flatMap((published) =>\n published\n ? Ok(undefined)\n : Err<TechnicalError>(\n new TechnicalError(\n `Failed to publish RPC request for \"${String(rpcName)}\": channel buffer full`,\n ),\n ),\n );\n };\n\n return validateRequest()\n .flatMap((validated) => publishRequest(validated))\n .flatMap(() => callResultAsync)\n .orElse((error: CallError) => {\n // If preflight failed (validate or publish), the pending entry still\n // exists and the timer is alive. Clean both up so the call doesn't\n // leak. Timer-fired errors and reply-resolved errors have already\n // cleaned the entry, so the .has() check guards against double cleanup.\n if (this.pendingCalls.has(correlationId)) {\n clearTimeout(timer);\n this.pendingCalls.delete(correlationId);\n }\n return Err(error).toAsync();\n })\n .tap(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, \"\", queueName, true, durationMs);\n })\n .tapErr((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, \"\", queueName, false, durationMs);\n });\n }\n\n /**\n * Close the channel and connection. Cancels the reply consumer (if any) and\n * rejects every in-flight RPC call with `RpcCancelledError`.\n */\n close(): AsyncResult<void, TechnicalError> {\n // Reject pending calls first — once close() runs, no reply will arrive.\n for (const [, pending] of this.pendingCalls) {\n clearTimeout(pending.timer);\n pending.resolve(Err(new RpcCancelledError(pending.rpcName)));\n }\n this.pendingCalls.clear();\n\n const cancelReply: AsyncResult<void, TechnicalError> = this.replyConsumerTag\n ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {\n this.logger?.warn(\"Failed to cancel RPC reply consumer during close\", { error });\n return Ok(undefined);\n })\n : Ok(undefined).toAsync();\n\n return cancelReply.flatMap(() => this.amqpClient.close());\n }\n\n private waitForConnectionReady(): AsyncResult<void, TechnicalError> {\n return this.amqpClient.waitForConnect();\n }\n}\n"],"mappings":"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"}
|
package/docs/index.md
CHANGED
|
@@ -72,7 +72,7 @@ MessageValidationError_base<{
|
|
|
72
72
|
|
|
73
73
|
| Property | Modifier | Type | Inherited from | Defined in |
|
|
74
74
|
| ------ | ------ | ------ | ------ | ------ |
|
|
75
|
-
| <a id="_tag"></a> `_tag` | `readonly` | `"@amqp-contract/MessageValidationError"` | `MessageValidationError_base._tag` | node\_modules/.pnpm/unthrown@0.
|
|
75
|
+
| <a id="_tag"></a> `_tag` | `readonly` | `"@amqp-contract/MessageValidationError"` | `MessageValidationError_base._tag` | node\_modules/.pnpm/unthrown@1.0.0/node\_modules/unthrown/dist/index.d.mts:778 |
|
|
76
76
|
| <a id="cause"></a> `cause?` | `public` | `unknown` | `MessageValidationError_base.cause` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:24 |
|
|
77
77
|
| <a id="issues"></a> `issues` | `readonly` | `unknown` | `MessageValidationError_base.issues` | packages/core/dist/index.d.mts:43 |
|
|
78
78
|
| <a id="message"></a> `message` | `public` | `string` | `MessageValidationError_base.message` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1075 |
|
|
@@ -84,11 +84,11 @@ MessageValidationError_base<{
|
|
|
84
84
|
|
|
85
85
|
### RpcCancelledError
|
|
86
86
|
|
|
87
|
-
Defined in: [packages/client/src/errors.ts:38](https://github.com/btravstack/amqp-contract/blob/
|
|
87
|
+
Defined in: [packages/client/src/errors.ts:38](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L38)
|
|
88
88
|
|
|
89
89
|
Returned from any in-flight RPC call when the client is closed before the
|
|
90
90
|
reply is received. The correlation map is cleared on close and every pending
|
|
91
|
-
caller's promise resolves with `
|
|
91
|
+
caller's promise resolves with `Err(RpcCancelledError)`. Carries a namespaced
|
|
92
92
|
`_tag` of `"@amqp-contract/RpcCancelledError"`; the `Error.name` is kept bare
|
|
93
93
|
(`"RpcCancelledError"`).
|
|
94
94
|
|
|
@@ -107,7 +107,7 @@ caller's promise resolves with `err(RpcCancelledError)`. Carries a namespaced
|
|
|
107
107
|
new RpcCancelledError(rpcName): RpcCancelledError;
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
Defined in: [packages/client/src/errors.ts:44](https://github.com/btravstack/amqp-contract/blob/
|
|
110
|
+
Defined in: [packages/client/src/errors.ts:44](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L44)
|
|
111
111
|
|
|
112
112
|
###### Parameters
|
|
113
113
|
|
|
@@ -134,18 +134,18 @@ TaggedError("@amqp-contract/RpcCancelledError", {
|
|
|
134
134
|
|
|
135
135
|
| Property | Modifier | Type | Inherited from | Defined in |
|
|
136
136
|
| ------ | ------ | ------ | ------ | ------ |
|
|
137
|
-
| <a id="_tag-1"></a> `_tag` | `readonly` | `"@amqp-contract/RpcCancelledError"` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", })._tag` | node\_modules/.pnpm/unthrown@0.
|
|
137
|
+
| <a id="_tag-1"></a> `_tag` | `readonly` | `"@amqp-contract/RpcCancelledError"` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", })._tag` | node\_modules/.pnpm/unthrown@1.0.0/node\_modules/unthrown/dist/index.d.mts:778 |
|
|
138
138
|
| <a id="cause-1"></a> `cause?` | `public` | `unknown` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", }).cause` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:24 |
|
|
139
139
|
| <a id="message-1"></a> `message` | `public` | `string` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", }).message` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1075 |
|
|
140
140
|
| <a id="name-1"></a> `name` | `public` | `string` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", }).name` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1074 |
|
|
141
|
-
| <a id="rpcname"></a> `rpcName` | `readonly` | `string` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", }).rpcName` | [packages/client/src/errors.ts:42](https://github.com/btravstack/amqp-contract/blob/
|
|
141
|
+
| <a id="rpcname"></a> `rpcName` | `readonly` | `string` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", }).rpcName` | [packages/client/src/errors.ts:42](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L42) |
|
|
142
142
|
| <a id="stack-1"></a> `stack?` | `public` | `string` | `TaggedError("@amqp-contract/RpcCancelledError", { name: "RpcCancelledError", }).stack` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 |
|
|
143
143
|
|
|
144
144
|
***
|
|
145
145
|
|
|
146
146
|
### RpcTimeoutError
|
|
147
147
|
|
|
148
|
-
Defined in: [packages/client/src/errors.ts:15](https://github.com/btravstack/amqp-contract/blob/
|
|
148
|
+
Defined in: [packages/client/src/errors.ts:15](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L15)
|
|
149
149
|
|
|
150
150
|
Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
|
|
151
151
|
before the RPC server publishes a reply with the matching `correlationId`.
|
|
@@ -172,7 +172,7 @@ client if a logger is configured). Carries a namespaced `_tag` of
|
|
|
172
172
|
new RpcTimeoutError(rpcName, timeoutMs): RpcTimeoutError;
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
Defined in: [packages/client/src/errors.ts:22](https://github.com/btravstack/amqp-contract/blob/
|
|
175
|
+
Defined in: [packages/client/src/errors.ts:22](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L22)
|
|
176
176
|
|
|
177
177
|
###### Parameters
|
|
178
178
|
|
|
@@ -201,19 +201,19 @@ TaggedError("@amqp-contract/RpcTimeoutError", {
|
|
|
201
201
|
|
|
202
202
|
| Property | Modifier | Type | Inherited from | Defined in |
|
|
203
203
|
| ------ | ------ | ------ | ------ | ------ |
|
|
204
|
-
| <a id="_tag-2"></a> `_tag` | `readonly` | `"@amqp-contract/RpcTimeoutError"` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", })._tag` | node\_modules/.pnpm/unthrown@0.
|
|
204
|
+
| <a id="_tag-2"></a> `_tag` | `readonly` | `"@amqp-contract/RpcTimeoutError"` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", })._tag` | node\_modules/.pnpm/unthrown@1.0.0/node\_modules/unthrown/dist/index.d.mts:778 |
|
|
205
205
|
| <a id="cause-2"></a> `cause?` | `public` | `unknown` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).cause` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:24 |
|
|
206
206
|
| <a id="message-2"></a> `message` | `public` | `string` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).message` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1075 |
|
|
207
207
|
| <a id="name-2"></a> `name` | `public` | `string` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).name` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1074 |
|
|
208
|
-
| <a id="rpcname-1"></a> `rpcName` | `readonly` | `string` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).rpcName` | [packages/client/src/errors.ts:19](https://github.com/btravstack/amqp-contract/blob/
|
|
208
|
+
| <a id="rpcname-1"></a> `rpcName` | `readonly` | `string` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).rpcName` | [packages/client/src/errors.ts:19](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L19) |
|
|
209
209
|
| <a id="stack-2"></a> `stack?` | `public` | `string` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).stack` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 |
|
|
210
|
-
| <a id="timeoutms"></a> `timeoutMs` | `readonly` | `number` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).timeoutMs` | [packages/client/src/errors.ts:20](https://github.com/btravstack/amqp-contract/blob/
|
|
210
|
+
| <a id="timeoutms"></a> `timeoutMs` | `readonly` | `number` | `TaggedError("@amqp-contract/RpcTimeoutError", { name: "RpcTimeoutError", }).timeoutMs` | [packages/client/src/errors.ts:20](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/errors.ts#L20) |
|
|
211
211
|
|
|
212
212
|
***
|
|
213
213
|
|
|
214
214
|
### TypedAmqpClient
|
|
215
215
|
|
|
216
|
-
Defined in: [packages/client/src/client.ts:125](https://github.com/btravstack/amqp-contract/blob/
|
|
216
|
+
Defined in: [packages/client/src/client.ts:125](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L125)
|
|
217
217
|
|
|
218
218
|
Type-safe AMQP client for publishing messages
|
|
219
219
|
|
|
@@ -238,7 +238,7 @@ call<TName>(
|
|
|
238
238
|
| RpcCancelledError>;
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
Defined in: [packages/client/src/client.ts:420](https://github.com/btravstack/amqp-contract/blob/
|
|
241
|
+
Defined in: [packages/client/src/client.ts:420](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L420)
|
|
242
242
|
|
|
243
243
|
Invoke an RPC defined via `defineRpc` and await the typed response.
|
|
244
244
|
|
|
@@ -288,7 +288,7 @@ result.match({
|
|
|
288
288
|
close(): AsyncResult<void, TechnicalError>;
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
-
Defined in: [packages/client/src/client.ts:574](https://github.com/btravstack/amqp-contract/blob/
|
|
291
|
+
Defined in: [packages/client/src/client.ts:574](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L574)
|
|
292
292
|
|
|
293
293
|
Close the channel and connection. Cancels the reply consumer (if any) and
|
|
294
294
|
rejects every in-flight RPC call with `RpcCancelledError`.
|
|
@@ -306,7 +306,7 @@ publish<TName>(
|
|
|
306
306
|
options?): AsyncResult<void, TechnicalError | MessageValidationError>;
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
Defined in: [packages/client/src/client.ts:308](https://github.com/btravstack/amqp-contract/blob/
|
|
309
|
+
Defined in: [packages/client/src/client.ts:308](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L308)
|
|
310
310
|
|
|
311
311
|
Publish a message using a defined publisher.
|
|
312
312
|
|
|
@@ -340,7 +340,7 @@ value already in options will be overwritten by the compression algorithm.
|
|
|
340
340
|
static create<TContract>(__namedParameters): AsyncResult<TypedAmqpClient<TContract>, TechnicalError>;
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
-
Defined in: [packages/client/src/client.ts:156](https://github.com/btravstack/amqp-contract/blob/
|
|
343
|
+
Defined in: [packages/client/src/client.ts:156](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L156)
|
|
344
344
|
|
|
345
345
|
Create a type-safe AMQP client from a contract.
|
|
346
346
|
|
|
@@ -375,7 +375,7 @@ connection options, following RabbitMQ best practices.
|
|
|
375
375
|
type CallOptions = object;
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
-
Defined in: [packages/client/src/client.ts:105](https://github.com/btravstack/amqp-contract/blob/
|
|
378
|
+
Defined in: [packages/client/src/client.ts:105](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L105)
|
|
379
379
|
|
|
380
380
|
Per-call options for `client.call()`.
|
|
381
381
|
|
|
@@ -383,8 +383,8 @@ Per-call options for `client.call()`.
|
|
|
383
383
|
|
|
384
384
|
| Property | Type | Description | Defined in |
|
|
385
385
|
| ------ | ------ | ------ | ------ |
|
|
386
|
-
| <a id="publishoptions"></a> `publishOptions?` | `Omit`<`AmqpClientPublishOptions`, `"replyTo"` \| `"correlationId"`> | Optional AMQP message properties to merge into the request. `replyTo` and `correlationId` are managed by the client and cannot be overridden. | [packages/client/src/client.ts:119](https://github.com/btravstack/amqp-contract/blob/
|
|
387
|
-
| <a id="timeoutms-1"></a> `timeoutMs` | `number` | Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves to `
|
|
386
|
+
| <a id="publishoptions"></a> `publishOptions?` | `Omit`<`AmqpClientPublishOptions`, `"replyTo"` \| `"correlationId"`> | Optional AMQP message properties to merge into the request. `replyTo` and `correlationId` are managed by the client and cannot be overridden. | [packages/client/src/client.ts:119](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L119) |
|
|
387
|
+
| <a id="timeoutms-1"></a> `timeoutMs` | `number` | Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves to `Err(RpcTimeoutError)` and the in-memory correlation entry is cleared. A late reply arriving after the timeout is silently dropped. Required: RPC without a timeout is a footgun. | [packages/client/src/client.ts:113](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L113) |
|
|
388
388
|
|
|
389
389
|
***
|
|
390
390
|
|
|
@@ -394,7 +394,7 @@ Per-call options for `client.call()`.
|
|
|
394
394
|
type ClientInferPublisherInput<TContract, TName> = PublisherInferInput<InferPublisher<TContract, TName>>;
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
Defined in: [packages/client/src/types.ts:43](https://github.com/btravstack/amqp-contract/blob/
|
|
397
|
+
Defined in: [packages/client/src/types.ts:43](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/types.ts#L43)
|
|
398
398
|
|
|
399
399
|
Input type accepted by `client.publish(name, ...)` for a specific publisher.
|
|
400
400
|
|
|
@@ -413,7 +413,7 @@ Input type accepted by `client.publish(name, ...)` for a specific publisher.
|
|
|
413
413
|
type ClientInferRpcRequestInput<TContract, TName> = InferRpc<TContract, TName> extends RpcDefinition<infer TRequest, MessageDefinition> ? TRequest extends MessageDefinition ? InferSchemaInput<TRequest["payload"]> : never : never;
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
-
Defined in: [packages/client/src/types.ts:61](https://github.com/btravstack/amqp-contract/blob/
|
|
416
|
+
Defined in: [packages/client/src/types.ts:61](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/types.ts#L61)
|
|
417
417
|
|
|
418
418
|
Input type accepted by `client.call(name, request, ...)`.
|
|
419
419
|
|
|
@@ -432,7 +432,7 @@ Input type accepted by `client.call(name, request, ...)`.
|
|
|
432
432
|
type ClientInferRpcResponseOutput<TContract, TName> = InferRpc<TContract, TName> extends RpcDefinition<MessageDefinition, infer TResponse> ? TResponse extends MessageDefinition ? InferSchemaOutput<TResponse["payload"]> : never : never;
|
|
433
433
|
```
|
|
434
434
|
|
|
435
|
-
Defined in: [packages/client/src/types.ts:74](https://github.com/btravstack/amqp-contract/blob/
|
|
435
|
+
Defined in: [packages/client/src/types.ts:74](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/types.ts#L74)
|
|
436
436
|
|
|
437
437
|
Output (validated) response type returned by `client.call(name, ...)`.
|
|
438
438
|
|
|
@@ -451,7 +451,7 @@ Output (validated) response type returned by `client.call(name, ...)`.
|
|
|
451
451
|
type CreateClientOptions<TContract> = object;
|
|
452
452
|
```
|
|
453
453
|
|
|
454
|
-
Defined in: [packages/client/src/client.ts:76](https://github.com/btravstack/amqp-contract/blob/
|
|
454
|
+
Defined in: [packages/client/src/client.ts:76](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L76)
|
|
455
455
|
|
|
456
456
|
Options for creating a client
|
|
457
457
|
|
|
@@ -465,13 +465,13 @@ Options for creating a client
|
|
|
465
465
|
|
|
466
466
|
| Property | Type | Description | Defined in |
|
|
467
467
|
| ------ | ------ | ------ | ------ |
|
|
468
|
-
| <a id="connectionoptions"></a> `connectionOptions?` | `AmqpConnectionManagerOptions` | - | [packages/client/src/client.ts:79](https://github.com/btravstack/amqp-contract/blob/
|
|
469
|
-
| <a id="connecttimeoutms"></a> `connectTimeoutMs?` | `number` \| `null` | Maximum time in ms to wait for the AMQP connection to become ready before `create()` resolves to an `
|
|
470
|
-
| <a id="contract"></a> `contract` | `TContract` | - | [packages/client/src/client.ts:77](https://github.com/btravstack/amqp-contract/blob/
|
|
471
|
-
| <a id="defaultpublishoptions"></a> `defaultPublishOptions?` | [`PublishOptions`](#publishoptions-1) | Default publish options that will be applied to all publish operations. These can be overridden by options passed to the publish method. By default, persistent is set to true for message durability. | [packages/client/src/client.ts:92](https://github.com/btravstack/amqp-contract/blob/
|
|
472
|
-
| <a id="logger"></a> `logger?` | `Logger` | - | [packages/client/src/client.ts:80](https://github.com/btravstack/amqp-contract/blob/
|
|
473
|
-
| <a id="telemetry"></a> `telemetry?` | `TelemetryProvider` | Optional telemetry provider for tracing and metrics. If not provided, uses the default provider which attempts to load OpenTelemetry. OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed. | [packages/client/src/client.ts:86](https://github.com/btravstack/amqp-contract/blob/
|
|
474
|
-
| <a id="urls"></a> `urls` | `ConnectionUrl`[] | - | [packages/client/src/client.ts:78](https://github.com/btravstack/amqp-contract/blob/
|
|
468
|
+
| <a id="connectionoptions"></a> `connectionOptions?` | `AmqpConnectionManagerOptions` | - | [packages/client/src/client.ts:79](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L79) |
|
|
469
|
+
| <a id="connecttimeoutms"></a> `connectTimeoutMs?` | `number` \| `null` | Maximum time in ms to wait for the AMQP connection to become ready before `create()` resolves to an `Err(TechnicalError)`. Defaults to 30s (the [AmqpClient](https://btravstack.github.io/amqp-contract/api/core#amqpclient)'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to disable the timeout and let amqp-connection-manager retry indefinitely. | [packages/client/src/client.ts:99](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L99) |
|
|
470
|
+
| <a id="contract"></a> `contract` | `TContract` | - | [packages/client/src/client.ts:77](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L77) |
|
|
471
|
+
| <a id="defaultpublishoptions"></a> `defaultPublishOptions?` | [`PublishOptions`](#publishoptions-1) | Default publish options that will be applied to all publish operations. These can be overridden by options passed to the publish method. By default, persistent is set to true for message durability. | [packages/client/src/client.ts:92](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L92) |
|
|
472
|
+
| <a id="logger"></a> `logger?` | `Logger` | - | [packages/client/src/client.ts:80](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L80) |
|
|
473
|
+
| <a id="telemetry"></a> `telemetry?` | `TelemetryProvider` | Optional telemetry provider for tracing and metrics. If not provided, uses the default provider which attempts to load OpenTelemetry. OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed. | [packages/client/src/client.ts:86](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L86) |
|
|
474
|
+
| <a id="urls"></a> `urls` | `ConnectionUrl`[] | - | [packages/client/src/client.ts:78](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L78) |
|
|
475
475
|
|
|
476
476
|
***
|
|
477
477
|
|
|
@@ -481,7 +481,7 @@ Options for creating a client
|
|
|
481
481
|
type PublishOptions = AmqpClientPublishOptions & object;
|
|
482
482
|
```
|
|
483
483
|
|
|
484
|
-
Defined in: [packages/client/src/client.ts:64](https://github.com/btravstack/amqp-contract/blob/
|
|
484
|
+
Defined in: [packages/client/src/client.ts:64](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L64)
|
|
485
485
|
|
|
486
486
|
Publish options that extend amqp-client's PublishOptions with optional compression support.
|
|
487
487
|
|
|
@@ -489,4 +489,4 @@ Publish options that extend amqp-client's PublishOptions with optional compressi
|
|
|
489
489
|
|
|
490
490
|
| Name | Type | Description | Defined in |
|
|
491
491
|
| ------ | ------ | ------ | ------ |
|
|
492
|
-
| `compression?` | `CompressionAlgorithm` | Optional compression algorithm to use for the message payload. When specified, the message will be compressed using the chosen algorithm and the contentEncoding header will be set automatically. | [packages/client/src/client.ts:70](https://github.com/btravstack/amqp-contract/blob/
|
|
492
|
+
| `compression?` | `CompressionAlgorithm` | Optional compression algorithm to use for the message payload. When specified, the message will be compressed using the chosen algorithm and the contentEncoding header will be set automatically. | [packages/client/src/client.ts:70](https://github.com/btravstack/amqp-contract/blob/cbafe3c4c44a5efd57ab3b8ebb53564b509d9d31/packages/client/src/client.ts#L70) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amqp-contract/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Client utilities for publishing messages using amqp-contract",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"amqp",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@standard-schema/spec": "1.1.0",
|
|
54
54
|
"ts-pattern": "5.9.0",
|
|
55
|
-
"unthrown": "0.
|
|
56
|
-
"@amqp-contract/contract": "
|
|
57
|
-
"@amqp-contract/core": "
|
|
55
|
+
"unthrown": "1.0.0",
|
|
56
|
+
"@amqp-contract/contract": "2.0.0",
|
|
57
|
+
"@amqp-contract/core": "2.0.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "24.13.2",
|
|
61
|
-
"@unthrown/vitest": "0.
|
|
61
|
+
"@unthrown/vitest": "1.0.0",
|
|
62
62
|
"@vitest/coverage-v8": "4.1.9",
|
|
63
63
|
"amqp-connection-manager": "5.0.0",
|
|
64
64
|
"amqplib": "2.0.1",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"typescript": "6.0.3",
|
|
69
69
|
"vitest": "4.1.9",
|
|
70
70
|
"zod": "4.4.3",
|
|
71
|
-
"@amqp-contract/testing": "
|
|
71
|
+
"@amqp-contract/testing": "2.0.0",
|
|
72
72
|
"@amqp-contract/tsconfig": "0.1.0",
|
|
73
73
|
"@amqp-contract/typedoc": "0.1.0"
|
|
74
74
|
},
|