@amqp-contract/client 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,38 +1,61 @@
1
1
  # @amqp-contract/client
2
2
 
3
- Type-safe AMQP client for publishing messages using amqp-contract.
3
+ Type-safe AMQP client for publishing messages using amqp-contract with explicit error handling via `Result` types.
4
4
 
5
5
  📖 **[Full documentation →](https://btravers.github.io/amqp-contract/api/client)**
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- pnpm add @amqp-contract/client amqplib
10
+ pnpm add @amqp-contract/client
11
11
  ```
12
12
 
13
13
  ## Usage
14
14
 
15
15
  ```typescript
16
16
  import { TypedAmqpClient } from '@amqp-contract/client';
17
- import { connect } from 'amqplib';
18
17
  import { contract } from './contract';
19
18
 
20
- // Connect to RabbitMQ
21
- const connection = await connect('amqp://localhost');
22
-
23
19
  // Create client from contract (automatically connects)
24
- const client = await TypedAmqpClient.create({ contract, connection });
20
+ const client = await TypedAmqpClient.create({
21
+ contract,
22
+ connection: 'amqp://localhost'
23
+ });
25
24
 
26
- // Publish message with type safety
27
- await client.publish('orderCreated', {
25
+ // Publish message with explicit error handling
26
+ const result = client.publish('orderCreated', {
28
27
  orderId: 'ORD-123',
29
28
  amount: 99.99,
30
29
  });
31
30
 
31
+ // Handle errors explicitly - no exceptions thrown
32
+ if (result.isError()) {
33
+ console.error('Failed to publish:', result.error);
34
+ // result.error is either TechnicalError or MessageValidationError
35
+ return;
36
+ }
37
+
38
+ console.log('Published successfully');
39
+
32
40
  // Clean up
33
41
  await client.close();
34
42
  ```
35
43
 
44
+ ## Error Handling
45
+
46
+ The client uses `Result` types from [@swan-io/boxed](https://github.com/swan-io/boxed) for explicit error handling. Runtime errors are part of the type signature:
47
+
48
+ ```typescript
49
+ publish(): Result<boolean, TechnicalError | MessageValidationError>
50
+ ```
51
+
52
+ **Error Types:**
53
+
54
+ - `TechnicalError` - Runtime failures (channel buffer full, network issues, etc.)
55
+ - `MessageValidationError` - Message fails schema validation
56
+
57
+ **Programming Errors** (client not initialized, invalid publisher name) throw exceptions since they indicate bugs caught by TypeScript at compile-time.
58
+
36
59
  ## API
37
60
 
38
61
  ### `TypedAmqpClient.create(options)`
@@ -42,16 +65,48 @@ Create a type-safe AMQP client from a contract. Automatically connects to Rabbit
42
65
  **Parameters:**
43
66
 
44
67
  - `options.contract` - Contract definition
45
- - `options.connection` - amqplib Connection object
68
+ - `options.connection` - AMQP connection URL (string) or connection options (Options.Connect)
69
+
70
+ **Returns:** `Promise<TypedAmqpClient>`
71
+
72
+ **Throws:** Connection errors (programming errors)
46
73
 
47
74
  ### `TypedAmqpClient.publish(publisherName, message, options?)`
48
75
 
49
76
  Publish a message using a defined publisher. The message will be validated against the schema and type-checked at compile time.
50
77
 
78
+ **Parameters:**
79
+
80
+ - `publisherName` - Name of the publisher (type-checked against contract)
81
+ - `message` - Message payload (type-checked against publisher schema)
82
+ - `options` - Optional publish options (e.g., headers, priority)
83
+
84
+ **Returns:** `Result<boolean, TechnicalError | MessageValidationError>`
85
+
86
+ **Example:**
87
+
88
+ ```typescript
89
+ const result = client.publish('orderCreated', { orderId: '123' });
90
+
91
+ if (result.isOk()) {
92
+ // Message published successfully
93
+ console.log('Published:', result.value); // true
94
+ } else {
95
+ // Handle specific error types
96
+ if (result.error instanceof MessageValidationError) {
97
+ console.error('Validation failed:', result.error.issues);
98
+ } else if (result.error instanceof TechnicalError) {
99
+ console.error('Technical error:', result.error.message);
100
+ }
101
+ }
102
+ ```
103
+
51
104
  ### `TypedAmqpClient.close()`
52
105
 
53
106
  Close the channel and connection.
54
107
 
108
+ **Returns:** `Promise<void>`
109
+
55
110
  ## License
56
111
 
57
112
  MIT
package/dist/index.cjs CHANGED
@@ -1,13 +1,52 @@
1
+ let amqplib = require("amqplib");
2
+ let _swan_io_boxed = require("@swan-io/boxed");
1
3
 
4
+ //#region src/errors.ts
5
+ /**
6
+ * Base error class for client errors
7
+ */
8
+ var ClientError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ClientError";
12
+ const ErrorConstructor = Error;
13
+ if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
14
+ }
15
+ };
16
+ /**
17
+ * Error for technical/runtime failures that cannot be prevented by TypeScript
18
+ * This includes validation failures and AMQP channel issues
19
+ */
20
+ var TechnicalError = class extends ClientError {
21
+ constructor(message, cause) {
22
+ super(message);
23
+ this.cause = cause;
24
+ this.name = "TechnicalError";
25
+ }
26
+ };
27
+ /**
28
+ * Error thrown when message validation fails
29
+ */
30
+ var MessageValidationError = class extends ClientError {
31
+ constructor(publisherName, issues) {
32
+ super(`Message validation failed for publisher "${publisherName}"`);
33
+ this.publisherName = publisherName;
34
+ this.issues = issues;
35
+ this.name = "MessageValidationError";
36
+ }
37
+ };
38
+
39
+ //#endregion
2
40
  //#region src/client.ts
3
41
  /**
4
42
  * Type-safe AMQP client for publishing messages
5
43
  */
6
44
  var TypedAmqpClient = class TypedAmqpClient {
7
45
  channel = null;
8
- constructor(contract, connection) {
46
+ connection = null;
47
+ constructor(contract, connectionOptions) {
9
48
  this.contract = contract;
10
- this.connection = connection;
49
+ this.connectionOptions = connectionOptions;
11
50
  }
12
51
  /**
13
52
  * Create a type-safe AMQP client from a contract
@@ -20,8 +59,9 @@ var TypedAmqpClient = class TypedAmqpClient {
20
59
  }
21
60
  /**
22
61
  * Publish a message using a defined publisher
62
+ * Returns Result.Ok(true) on success, or Result.Error with specific error on failure
23
63
  */
24
- async publish(publisherName, message, options) {
64
+ publish(publisherName, message, options) {
25
65
  if (!this.channel) throw new Error("Client not initialized. Create the client using TypedAmqpClient.create() to establish a connection.");
26
66
  const publishers = this.contract.publishers;
27
67
  if (!publishers) throw new Error("No publishers defined in contract");
@@ -29,23 +69,32 @@ var TypedAmqpClient = class TypedAmqpClient {
29
69
  if (!publisher || typeof publisher !== "object") throw new Error(`Publisher "${String(publisherName)}" not found in contract`);
30
70
  const publisherDef = publisher;
31
71
  const validation = publisherDef.message["~standard"].validate(message);
32
- if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) throw new Error(`Message validation failed: ${JSON.stringify(validation.issues)}`);
72
+ if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) return _swan_io_boxed.Result.Error(new MessageValidationError(String(publisherName), validation.issues));
33
73
  const validatedMessage = typeof validation === "object" && validation !== null && "value" in validation ? validation.value : message;
34
74
  const routingKey = options?.routingKey ?? publisherDef.routingKey ?? "";
35
75
  const content = Buffer.from(JSON.stringify(validatedMessage));
36
- return this.channel.publish(publisherDef.exchange, routingKey, content, options?.options);
76
+ const published = this.channel.publish(publisherDef.exchange, routingKey, content, options?.options);
77
+ if (!published) return _swan_io_boxed.Result.Error(new TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
78
+ return _swan_io_boxed.Result.Ok(published);
37
79
  }
38
80
  /**
39
- * Close the connection
81
+ * Close the channel and connection
40
82
  */
41
83
  async close() {
42
- if (this.channel) await this.channel.close();
43
- await this.connection.close();
84
+ if (this.channel) {
85
+ await this.channel.close();
86
+ this.channel = null;
87
+ }
88
+ if (this.connection) {
89
+ await this.connection.close();
90
+ this.connection = null;
91
+ }
44
92
  }
45
93
  /**
46
94
  * Connect to AMQP broker
47
95
  */
48
96
  async init() {
97
+ this.connection = await (0, amqplib.connect)(this.connectionOptions);
49
98
  this.channel = await this.connection.createChannel();
50
99
  if (this.contract.exchanges) for (const exchange of Object.values(this.contract.exchanges)) await this.channel.assertExchange(exchange.name, exchange.type, {
51
100
  durable: exchange.durable,
@@ -64,4 +113,6 @@ var TypedAmqpClient = class TypedAmqpClient {
64
113
  };
65
114
 
66
115
  //#endregion
116
+ exports.MessageValidationError = MessageValidationError;
117
+ exports.TechnicalError = TechnicalError;
67
118
  exports.TypedAmqpClient = TypedAmqpClient;
package/dist/index.d.cts CHANGED
@@ -1,14 +1,38 @@
1
- import { ChannelModel, Options } from "amqplib";
1
+ import { Options } from "amqplib";
2
2
  import { ClientInferPublisherInput, ContractDefinition, InferPublisherNames } from "@amqp-contract/contract";
3
+ import { Result } from "@swan-io/boxed";
3
4
 
5
+ //#region src/errors.d.ts
6
+ /**
7
+ * Base error class for client errors
8
+ */
9
+ declare abstract class ClientError extends Error {
10
+ protected constructor(message: string);
11
+ }
12
+ /**
13
+ * Error for technical/runtime failures that cannot be prevented by TypeScript
14
+ * This includes validation failures and AMQP channel issues
15
+ */
16
+ declare class TechnicalError extends ClientError {
17
+ readonly cause?: unknown | undefined;
18
+ constructor(message: string, cause?: unknown | undefined);
19
+ }
20
+ /**
21
+ * Error thrown when message validation fails
22
+ */
23
+ declare class MessageValidationError extends ClientError {
24
+ readonly publisherName: string;
25
+ readonly issues: unknown;
26
+ constructor(publisherName: string, issues: unknown);
27
+ }
28
+ //#endregion
4
29
  //#region src/client.d.ts
5
-
6
30
  /**
7
31
  * Options for creating a client
8
32
  */
9
33
  interface CreateClientOptions<TContract extends ContractDefinition> {
10
34
  contract: TContract;
11
- connection: ChannelModel;
35
+ connection: string | Options.Connect;
12
36
  }
13
37
  /**
14
38
  * Options for publishing a message
@@ -22,8 +46,9 @@ interface PublishOptions {
22
46
  */
23
47
  declare class TypedAmqpClient<TContract extends ContractDefinition> {
24
48
  private readonly contract;
25
- private readonly connection;
49
+ private readonly connectionOptions;
26
50
  private channel;
51
+ private connection;
27
52
  private constructor();
28
53
  /**
29
54
  * Create a type-safe AMQP client from a contract
@@ -32,10 +57,11 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
32
57
  static create<TContract extends ContractDefinition>(options: CreateClientOptions<TContract>): Promise<TypedAmqpClient<TContract>>;
33
58
  /**
34
59
  * Publish a message using a defined publisher
60
+ * Returns Result.Ok(true) on success, or Result.Error with specific error on failure
35
61
  */
36
- publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): Promise<boolean>;
62
+ publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): Result<boolean, TechnicalError | MessageValidationError>;
37
63
  /**
38
- * Close the connection
64
+ * Close the channel and connection
39
65
  */
40
66
  close(): Promise<void>;
41
67
  /**
@@ -44,5 +70,5 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
44
70
  private init;
45
71
  }
46
72
  //#endregion
47
- export { type CreateClientOptions, type PublishOptions, TypedAmqpClient };
73
+ export { type CreateClientOptions, MessageValidationError, type PublishOptions, TechnicalError, TypedAmqpClient };
48
74
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/client.ts"],"sourcesContent":[],"mappings":";;;;;;;AAUA;AAAuD,UAAtC,mBAAsC,CAAA,kBAAA,kBAAA,CAAA,CAAA;EAC3C,QAAA,EAAA,SAAA;EACE,UAAA,EAAA,YAAA;;AAMd;AAQA;;AAYwC,UApBvB,cAAA,CAoBuB;EACP,UAAA,CAAA,EAAA,MAAA;EAApB,OAAA,CAAA,EAnBD,OAAA,CAAQ,OAmBP;;;;;AAUiB,cAvBjB,eAuBiB,CAAA,kBAvBiB,kBAuBjB,CAAA,CAAA;EACX,iBAAA,QAAA;EACoB,iBAAA,UAAA;EAAW,QAAA,OAAA;EAArC,QAAA,WAAA,CAAA;EACC;;;;kCAd0B,6BAC3B,oBAAoB,aAC5B,QAAQ,gBAAgB;;;;wBASC,oBAAoB,2BAC/B,gBACN,0BAA0B,WAAW,kBACpC,iBACT;;;;WAiDY"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":[],"mappings":";;;;;;;;uBAGe,WAAA,SAAoB,KAAA;;;AAkBnC;AAaA;;;cAba,cAAA,SAAuB,WAAA;ECRnB,SAAA,KAAA,CAAA,EAAA,OAAmB,GAAA,SAAA;EAAmB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;;;;AAQvD;AAQa,cDKA,sBAAA,SAA+B,WAAA,CCLhB;EAAmB,SAAA,aAAA,EAAA,MAAA;EAaP,SAAA,MAAA,EAAA,OAAA;EACP,WAAA,CAAA,aAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA;;;;;;ADtBjC;AAaa,UCrBI,mBDqBmB,CAAA,kBCrBmB,kBDqBA,CAAA,CAAA;YCpB3C;uBACW,OAAA,CAAQ;;AAF/B;;;AAEuB,UAMN,cAAA,CANc;EAAO,UAAA,CAAA,EAAA,MAAA;EAMrB,OAAA,CAAA,EAEL,OAAA,CAAQ,OAFW;AAQ/B;;;;AAca,cAdA,eAcA,CAAA,kBAdkC,kBAclC,CAAA,CAAA;EACgB,iBAAA,QAAA;EAAhB,iBAAA,iBAAA;EAAR,QAAA,OAAA;EAUuC,QAAA,UAAA;EAApB,QAAA,WAAA,CAAA;EACL;;;;EAEL,OAAA,MAAA,CAAA,kBAf0B,kBAe1B,CAAA,CAAA,OAAA,EAdD,mBAcC,CAdmB,SAcnB,CAAA,CAAA,EAbT,OAaS,CAbD,eAaC,CAbe,SAaf,CAAA,CAAA;EACO;;;;EAgEG,OAAA,CAAA,cApEA,mBAoEA,CApEoB,SAoEpB,CAAA,CAAA,CAAA,aAAA,EAnEL,KAmEK,EAAA,OAAA,EAlEX,yBAkEW,CAlEe,SAkEf,EAlE0B,KAkE1B,CAAA,EAAA,OAAA,CAAA,EAjEV,cAiEU,CAAA,EAhEnB,MAgEmB,CAAA,OAAA,EAhEH,cAgEG,GAhEc,sBAgEd,CAAA;;;;WAAP"}
package/dist/index.d.mts CHANGED
@@ -1,14 +1,38 @@
1
- import { ChannelModel, Options } from "amqplib";
1
+ import { Options } from "amqplib";
2
+ import { Result } from "@swan-io/boxed";
2
3
  import { ClientInferPublisherInput, ContractDefinition, InferPublisherNames } from "@amqp-contract/contract";
3
4
 
5
+ //#region src/errors.d.ts
6
+ /**
7
+ * Base error class for client errors
8
+ */
9
+ declare abstract class ClientError extends Error {
10
+ protected constructor(message: string);
11
+ }
12
+ /**
13
+ * Error for technical/runtime failures that cannot be prevented by TypeScript
14
+ * This includes validation failures and AMQP channel issues
15
+ */
16
+ declare class TechnicalError extends ClientError {
17
+ readonly cause?: unknown | undefined;
18
+ constructor(message: string, cause?: unknown | undefined);
19
+ }
20
+ /**
21
+ * Error thrown when message validation fails
22
+ */
23
+ declare class MessageValidationError extends ClientError {
24
+ readonly publisherName: string;
25
+ readonly issues: unknown;
26
+ constructor(publisherName: string, issues: unknown);
27
+ }
28
+ //#endregion
4
29
  //#region src/client.d.ts
5
-
6
30
  /**
7
31
  * Options for creating a client
8
32
  */
9
33
  interface CreateClientOptions<TContract extends ContractDefinition> {
10
34
  contract: TContract;
11
- connection: ChannelModel;
35
+ connection: string | Options.Connect;
12
36
  }
13
37
  /**
14
38
  * Options for publishing a message
@@ -22,8 +46,9 @@ interface PublishOptions {
22
46
  */
23
47
  declare class TypedAmqpClient<TContract extends ContractDefinition> {
24
48
  private readonly contract;
25
- private readonly connection;
49
+ private readonly connectionOptions;
26
50
  private channel;
51
+ private connection;
27
52
  private constructor();
28
53
  /**
29
54
  * Create a type-safe AMQP client from a contract
@@ -32,10 +57,11 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
32
57
  static create<TContract extends ContractDefinition>(options: CreateClientOptions<TContract>): Promise<TypedAmqpClient<TContract>>;
33
58
  /**
34
59
  * Publish a message using a defined publisher
60
+ * Returns Result.Ok(true) on success, or Result.Error with specific error on failure
35
61
  */
36
- publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): Promise<boolean>;
62
+ publish<TName extends InferPublisherNames<TContract>>(publisherName: TName, message: ClientInferPublisherInput<TContract, TName>, options?: PublishOptions): Result<boolean, TechnicalError | MessageValidationError>;
37
63
  /**
38
- * Close the connection
64
+ * Close the channel and connection
39
65
  */
40
66
  close(): Promise<void>;
41
67
  /**
@@ -44,5 +70,5 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
44
70
  private init;
45
71
  }
46
72
  //#endregion
47
- export { type CreateClientOptions, type PublishOptions, TypedAmqpClient };
73
+ export { type CreateClientOptions, MessageValidationError, type PublishOptions, TechnicalError, TypedAmqpClient };
48
74
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts"],"sourcesContent":[],"mappings":";;;;;;;AAUA;AAAuD,UAAtC,mBAAsC,CAAA,kBAAA,kBAAA,CAAA,CAAA;EAC3C,QAAA,EAAA,SAAA;EACE,UAAA,EAAA,YAAA;;AAMd;AAQA;;AAYwC,UApBvB,cAAA,CAoBuB;EACP,UAAA,CAAA,EAAA,MAAA;EAApB,OAAA,CAAA,EAnBD,OAAA,CAAQ,OAmBP;;;;;AAUiB,cAvBjB,eAuBiB,CAAA,kBAvBiB,kBAuBjB,CAAA,CAAA;EACX,iBAAA,QAAA;EACoB,iBAAA,UAAA;EAAW,QAAA,OAAA;EAArC,QAAA,WAAA,CAAA;EACC;;;;kCAd0B,6BAC3B,oBAAoB,aAC5B,QAAQ,gBAAgB;;;;wBASC,oBAAoB,2BAC/B,gBACN,0BAA0B,WAAW,kBACpC,iBACT;;;;WAiDY"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":[],"mappings":";;;;;;;;uBAGe,WAAA,SAAoB,KAAA;;;AAkBnC;AAaA;;;cAba,cAAA,SAAuB,WAAA;ECRnB,SAAA,KAAA,CAAA,EAAA,OAAmB,GAAA,SAAA;EAAmB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;;;;AAQvD;AAQa,cDKA,sBAAA,SAA+B,WAAA,CCLhB;EAAmB,SAAA,aAAA,EAAA,MAAA;EAaP,SAAA,MAAA,EAAA,OAAA;EACP,WAAA,CAAA,aAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA;;;;;;ADtBjC;AAaa,UCrBI,mBDqBmB,CAAA,kBCrBmB,kBDqBA,CAAA,CAAA;YCpB3C;uBACW,OAAA,CAAQ;;AAF/B;;;AAEuB,UAMN,cAAA,CANc;EAAO,UAAA,CAAA,EAAA,MAAA;EAMrB,OAAA,CAAA,EAEL,OAAA,CAAQ,OAFW;AAQ/B;;;;AAca,cAdA,eAcA,CAAA,kBAdkC,kBAclC,CAAA,CAAA;EACgB,iBAAA,QAAA;EAAhB,iBAAA,iBAAA;EAAR,QAAA,OAAA;EAUuC,QAAA,UAAA;EAApB,QAAA,WAAA,CAAA;EACL;;;;EAEL,OAAA,MAAA,CAAA,kBAf0B,kBAe1B,CAAA,CAAA,OAAA,EAdD,mBAcC,CAdmB,SAcnB,CAAA,CAAA,EAbT,OAaS,CAbD,eAaC,CAbe,SAaf,CAAA,CAAA;EACO;;;;EAgEG,OAAA,CAAA,cApEA,mBAoEA,CApEoB,SAoEpB,CAAA,CAAA,CAAA,aAAA,EAnEL,KAmEK,EAAA,OAAA,EAlEX,yBAkEW,CAlEe,SAkEf,EAlE0B,KAkE1B,CAAA,EAAA,OAAA,CAAA,EAjEV,cAiEU,CAAA,EAhEnB,MAgEmB,CAAA,OAAA,EAhEH,cAgEG,GAhEc,sBAgEd,CAAA;;;;WAAP"}
package/dist/index.mjs CHANGED
@@ -1,12 +1,52 @@
1
+ import { connect } from "amqplib";
2
+ import { Result } from "@swan-io/boxed";
3
+
4
+ //#region src/errors.ts
5
+ /**
6
+ * Base error class for client errors
7
+ */
8
+ var ClientError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ClientError";
12
+ const ErrorConstructor = Error;
13
+ if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
14
+ }
15
+ };
16
+ /**
17
+ * Error for technical/runtime failures that cannot be prevented by TypeScript
18
+ * This includes validation failures and AMQP channel issues
19
+ */
20
+ var TechnicalError = class extends ClientError {
21
+ constructor(message, cause) {
22
+ super(message);
23
+ this.cause = cause;
24
+ this.name = "TechnicalError";
25
+ }
26
+ };
27
+ /**
28
+ * Error thrown when message validation fails
29
+ */
30
+ var MessageValidationError = class extends ClientError {
31
+ constructor(publisherName, issues) {
32
+ super(`Message validation failed for publisher "${publisherName}"`);
33
+ this.publisherName = publisherName;
34
+ this.issues = issues;
35
+ this.name = "MessageValidationError";
36
+ }
37
+ };
38
+
39
+ //#endregion
1
40
  //#region src/client.ts
2
41
  /**
3
42
  * Type-safe AMQP client for publishing messages
4
43
  */
5
44
  var TypedAmqpClient = class TypedAmqpClient {
6
45
  channel = null;
7
- constructor(contract, connection) {
46
+ connection = null;
47
+ constructor(contract, connectionOptions) {
8
48
  this.contract = contract;
9
- this.connection = connection;
49
+ this.connectionOptions = connectionOptions;
10
50
  }
11
51
  /**
12
52
  * Create a type-safe AMQP client from a contract
@@ -19,8 +59,9 @@ var TypedAmqpClient = class TypedAmqpClient {
19
59
  }
20
60
  /**
21
61
  * Publish a message using a defined publisher
62
+ * Returns Result.Ok(true) on success, or Result.Error with specific error on failure
22
63
  */
23
- async publish(publisherName, message, options) {
64
+ publish(publisherName, message, options) {
24
65
  if (!this.channel) throw new Error("Client not initialized. Create the client using TypedAmqpClient.create() to establish a connection.");
25
66
  const publishers = this.contract.publishers;
26
67
  if (!publishers) throw new Error("No publishers defined in contract");
@@ -28,23 +69,32 @@ var TypedAmqpClient = class TypedAmqpClient {
28
69
  if (!publisher || typeof publisher !== "object") throw new Error(`Publisher "${String(publisherName)}" not found in contract`);
29
70
  const publisherDef = publisher;
30
71
  const validation = publisherDef.message["~standard"].validate(message);
31
- if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) throw new Error(`Message validation failed: ${JSON.stringify(validation.issues)}`);
72
+ if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) return Result.Error(new MessageValidationError(String(publisherName), validation.issues));
32
73
  const validatedMessage = typeof validation === "object" && validation !== null && "value" in validation ? validation.value : message;
33
74
  const routingKey = options?.routingKey ?? publisherDef.routingKey ?? "";
34
75
  const content = Buffer.from(JSON.stringify(validatedMessage));
35
- return this.channel.publish(publisherDef.exchange, routingKey, content, options?.options);
76
+ const published = this.channel.publish(publisherDef.exchange, routingKey, content, options?.options);
77
+ if (!published) return Result.Error(new TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
78
+ return Result.Ok(published);
36
79
  }
37
80
  /**
38
- * Close the connection
81
+ * Close the channel and connection
39
82
  */
40
83
  async close() {
41
- if (this.channel) await this.channel.close();
42
- await this.connection.close();
84
+ if (this.channel) {
85
+ await this.channel.close();
86
+ this.channel = null;
87
+ }
88
+ if (this.connection) {
89
+ await this.connection.close();
90
+ this.connection = null;
91
+ }
43
92
  }
44
93
  /**
45
94
  * Connect to AMQP broker
46
95
  */
47
96
  async init() {
97
+ this.connection = await connect(this.connectionOptions);
48
98
  this.channel = await this.connection.createChannel();
49
99
  if (this.contract.exchanges) for (const exchange of Object.values(this.contract.exchanges)) await this.channel.assertExchange(exchange.name, exchange.type, {
50
100
  durable: exchange.durable,
@@ -63,5 +113,5 @@ var TypedAmqpClient = class TypedAmqpClient {
63
113
  };
64
114
 
65
115
  //#endregion
66
- export { TypedAmqpClient };
116
+ export { MessageValidationError, TechnicalError, TypedAmqpClient };
67
117
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["contract: TContract","connection: ChannelModel"],"sources":["../src/client.ts"],"sourcesContent":["import type { Channel, ChannelModel, Options } from \"amqplib\";\nimport type {\n ClientInferPublisherInput,\n ContractDefinition,\n InferPublisherNames,\n} from \"@amqp-contract/contract\";\n\n/**\n * Options for creating a client\n */\nexport interface CreateClientOptions<TContract extends ContractDefinition> {\n contract: TContract;\n connection: ChannelModel;\n}\n\n/**\n * Options for publishing a message\n */\nexport interface PublishOptions {\n routingKey?: string;\n options?: Options.Publish;\n}\n\n/**\n * Type-safe AMQP client for publishing messages\n */\nexport class TypedAmqpClient<TContract extends ContractDefinition> {\n private channel: Channel | null = null;\n\n private constructor(\n private readonly contract: TContract,\n private readonly connection: ChannelModel,\n ) {}\n\n /**\n * Create a type-safe AMQP client from a contract\n * The client will automatically connect to the AMQP broker\n */\n static async create<TContract extends ContractDefinition>(\n options: CreateClientOptions<TContract>,\n ): Promise<TypedAmqpClient<TContract>> {\n const client = new TypedAmqpClient(options.contract, options.connection);\n await client.init();\n return client;\n }\n\n /**\n * Publish a message using a defined publisher\n */\n async publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): Promise<boolean> {\n if (!this.channel) {\n throw new Error(\n \"Client not initialized. Create the client using TypedAmqpClient.create() to establish a connection.\",\n );\n }\n\n const publishers = this.contract.publishers as Record<string, unknown>;\n if (!publishers) {\n throw new Error(\"No publishers defined in contract\");\n }\n\n const publisher = publishers[publisherName as string];\n if (!publisher || typeof publisher !== \"object\") {\n throw new Error(`Publisher \"${String(publisherName)}\" not found in contract`);\n }\n\n const publisherDef = publisher as {\n exchange: string;\n routingKey?: string;\n message: { \"~standard\": { validate: (value: unknown) => unknown } };\n };\n\n // Validate message using schema\n const validation = publisherDef.message[\"~standard\"].validate(message);\n if (\n typeof validation === \"object\" &&\n validation !== null &&\n \"issues\" in validation &&\n validation.issues\n ) {\n throw new Error(`Message validation failed: ${JSON.stringify(validation.issues)}`);\n }\n\n const validatedMessage =\n typeof validation === \"object\" && validation !== null && \"value\" in validation\n ? validation.value\n : message;\n\n // Publish message\n const routingKey = options?.routingKey ?? publisherDef.routingKey ?? \"\";\n const content = Buffer.from(JSON.stringify(validatedMessage));\n\n return this.channel.publish(publisherDef.exchange, routingKey, content, options?.options);\n }\n\n /**\n * Close the connection\n */\n async close(): Promise<void> {\n if (this.channel) {\n await this.channel.close();\n }\n\n await this.connection.close();\n }\n\n /**\n * Connect to AMQP broker\n */\n private async init(): Promise<void> {\n this.channel = await this.connection.createChannel();\n\n // Setup exchanges\n if (this.contract.exchanges) {\n for (const exchange of Object.values(this.contract.exchanges)) {\n await this.channel.assertExchange(exchange.name, exchange.type, {\n durable: exchange.durable,\n autoDelete: exchange.autoDelete,\n internal: exchange.internal,\n arguments: exchange.arguments,\n });\n }\n }\n\n // Setup queues\n if (this.contract.queues) {\n for (const queue of Object.values(this.contract.queues)) {\n await this.channel.assertQueue(queue.name, {\n durable: queue.durable,\n exclusive: queue.exclusive,\n autoDelete: queue.autoDelete,\n arguments: queue.arguments,\n });\n }\n }\n\n // Setup bindings\n if (this.contract.bindings) {\n for (const binding of Object.values(this.contract.bindings)) {\n await this.channel.bindQueue(\n binding.queue,\n binding.exchange,\n binding.routingKey ?? \"\",\n binding.arguments,\n );\n }\n }\n }\n}\n"],"mappings":";;;;AA0BA,IAAa,kBAAb,MAAa,gBAAsD;CACjE,AAAQ,UAA0B;CAElC,AAAQ,YACN,AAAiBA,UACjB,AAAiBC,YACjB;EAFiB;EACA;;;;;;CAOnB,aAAa,OACX,SACqC;EACrC,MAAM,SAAS,IAAI,gBAAgB,QAAQ,UAAU,QAAQ,WAAW;AACxE,QAAM,OAAO,MAAM;AACnB,SAAO;;;;;CAMT,MAAM,QACJ,eACA,SACA,SACkB;AAClB,MAAI,CAAC,KAAK,QACR,OAAM,IAAI,MACR,sGACD;EAGH,MAAM,aAAa,KAAK,SAAS;AACjC,MAAI,CAAC,WACH,OAAM,IAAI,MAAM,oCAAoC;EAGtD,MAAM,YAAY,WAAW;AAC7B,MAAI,CAAC,aAAa,OAAO,cAAc,SACrC,OAAM,IAAI,MAAM,cAAc,OAAO,cAAc,CAAC,yBAAyB;EAG/E,MAAM,eAAe;EAOrB,MAAM,aAAa,aAAa,QAAQ,aAAa,SAAS,QAAQ;AACtE,MACE,OAAO,eAAe,YACtB,eAAe,QACf,YAAY,cACZ,WAAW,OAEX,OAAM,IAAI,MAAM,8BAA8B,KAAK,UAAU,WAAW,OAAO,GAAG;EAGpF,MAAM,mBACJ,OAAO,eAAe,YAAY,eAAe,QAAQ,WAAW,aAChE,WAAW,QACX;EAGN,MAAM,aAAa,SAAS,cAAc,aAAa,cAAc;EACrE,MAAM,UAAU,OAAO,KAAK,KAAK,UAAU,iBAAiB,CAAC;AAE7D,SAAO,KAAK,QAAQ,QAAQ,aAAa,UAAU,YAAY,SAAS,SAAS,QAAQ;;;;;CAM3F,MAAM,QAAuB;AAC3B,MAAI,KAAK,QACP,OAAM,KAAK,QAAQ,OAAO;AAG5B,QAAM,KAAK,WAAW,OAAO;;;;;CAM/B,MAAc,OAAsB;AAClC,OAAK,UAAU,MAAM,KAAK,WAAW,eAAe;AAGpD,MAAI,KAAK,SAAS,UAChB,MAAK,MAAM,YAAY,OAAO,OAAO,KAAK,SAAS,UAAU,CAC3D,OAAM,KAAK,QAAQ,eAAe,SAAS,MAAM,SAAS,MAAM;GAC9D,SAAS,SAAS;GAClB,YAAY,SAAS;GACrB,UAAU,SAAS;GACnB,WAAW,SAAS;GACrB,CAAC;AAKN,MAAI,KAAK,SAAS,OAChB,MAAK,MAAM,SAAS,OAAO,OAAO,KAAK,SAAS,OAAO,CACrD,OAAM,KAAK,QAAQ,YAAY,MAAM,MAAM;GACzC,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,WAAW,MAAM;GAClB,CAAC;AAKN,MAAI,KAAK,SAAS,SAChB,MAAK,MAAM,WAAW,OAAO,OAAO,KAAK,SAAS,SAAS,CACzD,OAAM,KAAK,QAAQ,UACjB,QAAQ,OACR,QAAQ,UACR,QAAQ,cAAc,IACtB,QAAQ,UACT"}
1
+ {"version":3,"file":"index.mjs","names":["cause?: unknown","publisherName: string","issues: unknown","contract: TContract","connectionOptions: string | Options.Connect"],"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * Base error class for client errors\n */\nabstract class ClientError extends Error {\n protected constructor(message: string) {\n super(message);\n this.name = \"ClientError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error for technical/runtime failures that cannot be prevented by TypeScript\n * This includes validation failures and AMQP channel issues\n */\nexport class TechnicalError extends ClientError {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"TechnicalError\";\n }\n}\n\n/**\n * Error thrown when message validation fails\n */\nexport class MessageValidationError extends ClientError {\n constructor(\n public readonly publisherName: string,\n public readonly issues: unknown,\n ) {\n super(`Message validation failed for publisher \"${publisherName}\"`);\n this.name = \"MessageValidationError\";\n }\n}\n","import { connect } from \"amqplib\";\nimport type { Channel, ChannelModel, Options } from \"amqplib\";\nimport type {\n ClientInferPublisherInput,\n ContractDefinition,\n InferPublisherNames,\n} from \"@amqp-contract/contract\";\nimport { Result } from \"@swan-io/boxed\";\nimport { MessageValidationError, TechnicalError } from \"./errors.js\";\n\n/**\n * Options for creating a client\n */\nexport interface CreateClientOptions<TContract extends ContractDefinition> {\n contract: TContract;\n connection: string | Options.Connect;\n}\n\n/**\n * Options for publishing a message\n */\nexport interface PublishOptions {\n routingKey?: string;\n options?: Options.Publish;\n}\n\n/**\n * Type-safe AMQP client for publishing messages\n */\nexport class TypedAmqpClient<TContract extends ContractDefinition> {\n private channel: Channel | null = null;\n private connection: ChannelModel | null = null;\n\n private constructor(\n private readonly contract: TContract,\n private readonly connectionOptions: string | Options.Connect,\n ) {}\n\n /**\n * Create a type-safe AMQP client from a contract\n * The client will automatically connect to the AMQP broker\n */\n static async create<TContract extends ContractDefinition>(\n options: CreateClientOptions<TContract>,\n ): Promise<TypedAmqpClient<TContract>> {\n const client = new TypedAmqpClient(options.contract, options.connection);\n await client.init();\n return client;\n }\n\n /**\n * Publish a message using a defined publisher\n * Returns Result.Ok(true) on success, or Result.Error with specific error on failure\n */\n publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): Result<boolean, TechnicalError | MessageValidationError> {\n if (!this.channel) {\n throw new Error(\n \"Client not initialized. Create the client using TypedAmqpClient.create() to establish a connection.\",\n );\n }\n\n const publishers = this.contract.publishers as Record<string, unknown>;\n if (!publishers) {\n throw new Error(\"No publishers defined in contract\");\n }\n\n const publisher = publishers[publisherName as string];\n if (!publisher || typeof publisher !== \"object\") {\n throw new Error(`Publisher \"${String(publisherName)}\" not found in contract`);\n }\n\n const publisherDef = publisher as {\n exchange: string;\n routingKey?: string;\n message: { \"~standard\": { validate: (value: unknown) => unknown } };\n };\n\n // Validate message using schema\n const validation = publisherDef.message[\"~standard\"].validate(message);\n if (\n typeof validation === \"object\" &&\n validation !== null &&\n \"issues\" in validation &&\n validation.issues\n ) {\n return Result.Error(new MessageValidationError(String(publisherName), validation.issues));\n }\n\n const validatedMessage =\n typeof validation === \"object\" && validation !== null && \"value\" in validation\n ? validation.value\n : message;\n\n // Publish message\n const routingKey = options?.routingKey ?? publisherDef.routingKey ?? \"\";\n const content = Buffer.from(JSON.stringify(validatedMessage));\n\n const published = this.channel.publish(\n publisherDef.exchange,\n routingKey,\n content,\n options?.options,\n );\n\n if (!published) {\n return Result.Error(\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 return Result.Ok(published);\n }\n\n /**\n * Close the channel and connection\n */\n async close(): Promise<void> {\n if (this.channel) {\n await this.channel.close();\n this.channel = null;\n }\n if (this.connection) {\n await this.connection.close();\n this.connection = null;\n }\n }\n\n /**\n * Connect to AMQP broker\n */\n private async init(): Promise<void> {\n this.connection = await connect(this.connectionOptions);\n this.channel = await this.connection.createChannel();\n\n // Setup exchanges\n if (this.contract.exchanges) {\n for (const exchange of Object.values(this.contract.exchanges)) {\n await this.channel.assertExchange(exchange.name, exchange.type, {\n durable: exchange.durable,\n autoDelete: exchange.autoDelete,\n internal: exchange.internal,\n arguments: exchange.arguments,\n });\n }\n }\n\n // Setup queues\n if (this.contract.queues) {\n for (const queue of Object.values(this.contract.queues)) {\n await this.channel.assertQueue(queue.name, {\n durable: queue.durable,\n exclusive: queue.exclusive,\n autoDelete: queue.autoDelete,\n arguments: queue.arguments,\n });\n }\n }\n\n // Setup bindings\n if (this.contract.bindings) {\n for (const binding of Object.values(this.contract.bindings)) {\n await this.channel.bindQueue(\n binding.queue,\n binding.exchange,\n binding.routingKey ?? \"\",\n binding.arguments,\n );\n }\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amqp-contract/client",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "description": "Client utilities for publishing messages using amqp-contract",
5
5
  "keywords": [
6
6
  "amqp",
@@ -41,7 +41,8 @@
41
41
  "dist"
42
42
  ],
43
43
  "dependencies": {
44
- "@amqp-contract/contract": "0.0.5"
44
+ "@swan-io/boxed": "3.2.1",
45
+ "@amqp-contract/contract": "0.1.0"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/amqplib": "0.10.8",
@@ -52,7 +53,7 @@
52
53
  "typescript": "5.9.3",
53
54
  "vitest": "4.0.16",
54
55
  "zod": "4.2.1",
55
- "@amqp-contract/testing": "0.0.5",
56
+ "@amqp-contract/testing": "0.1.0",
56
57
  "@amqp-contract/tsconfig": "0.0.0"
57
58
  },
58
59
  "peerDependencies": {