@hazeljs/discovery 0.2.0-beta.39 → 0.2.0-beta.42
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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Service Discovery and Registry for HazelJS microservices - inspired by Netflix Eureka and Consul.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@hazeljs/discovery)
|
|
6
|
+
[](https://www.npmjs.com/package/@hazeljs/discovery)
|
|
6
7
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
7
8
|
|
|
8
9
|
## Features
|
|
@@ -139,7 +139,8 @@ describe('ServiceClient', () => {
|
|
|
139
139
|
const mockRequest = jest.fn().mockRejectedValue(new Error('Network error'));
|
|
140
140
|
serviceClient.axiosInstance = { request: mockRequest };
|
|
141
141
|
await expect(serviceClient.get('/api/users')).rejects.toThrow();
|
|
142
|
-
|
|
142
|
+
// retries: 3 → RetryPolicy maxAttempts: 3 → 1 initial + 3 retries = 4 total
|
|
143
|
+
expect(mockRequest).toHaveBeenCalledTimes(4);
|
|
143
144
|
});
|
|
144
145
|
it('should respect retry delay', async () => {
|
|
145
146
|
const client = new service_client_1.ServiceClient(discoveryClient, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service Client
|
|
3
|
-
* HTTP client with automatic service discovery and load balancing
|
|
3
|
+
* HTTP client with automatic service discovery and load balancing.
|
|
4
|
+
* Uses @hazeljs/resilience RetryPolicy for retry logic.
|
|
4
5
|
*/
|
|
5
6
|
import { DiscoveryClient } from './discovery-client';
|
|
6
7
|
import { ServiceFilter } from '../types';
|
|
@@ -17,8 +18,7 @@ export declare class ServiceClient {
|
|
|
17
18
|
private discoveryClient;
|
|
18
19
|
private config;
|
|
19
20
|
private axiosInstance;
|
|
20
|
-
private
|
|
21
|
-
private retryDelay;
|
|
21
|
+
private retryPolicy;
|
|
22
22
|
constructor(discoveryClient: DiscoveryClient, config: ServiceClientConfig);
|
|
23
23
|
/**
|
|
24
24
|
* GET request
|
|
@@ -41,12 +41,8 @@ export declare class ServiceClient {
|
|
|
41
41
|
*/
|
|
42
42
|
patch<T = unknown>(path: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
|
|
43
43
|
/**
|
|
44
|
-
* Generic request with service discovery
|
|
44
|
+
* Generic request with service discovery and resilience-backed retry
|
|
45
45
|
*/
|
|
46
46
|
private request;
|
|
47
|
-
/**
|
|
48
|
-
* Sleep utility
|
|
49
|
-
*/
|
|
50
|
-
private sleep;
|
|
51
47
|
}
|
|
52
48
|
//# sourceMappingURL=service-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-client.d.ts","sourceRoot":"","sources":["../../src/client/service-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"service-client.d.ts","sourceRoot":"","sources":["../../src/client/service-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKzC,OAAc,EAAiB,kBAAkB,EAAE,aAAa,EAAc,MAAM,OAAO,CAAC;AAE5F,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyBD,qBAAa,aAAa;IAKtB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,MAAM;IALhB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAc;gBAGvB,eAAe,EAAE,eAAe,EAChC,MAAM,EAAE,mBAAmB;IAyBrC;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5F;;OAEG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;IACG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI/F;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;YACW,OAAO;CAyCtB"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Service Client
|
|
4
|
-
* HTTP client with automatic service discovery and load balancing
|
|
4
|
+
* HTTP client with automatic service discovery and load balancing.
|
|
5
|
+
* Uses @hazeljs/resilience RetryPolicy for retry logic.
|
|
5
6
|
*/
|
|
6
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
8
|
if (k2 === undefined) k2 = k;
|
|
@@ -41,6 +42,7 @@ exports.ServiceClient = void 0;
|
|
|
41
42
|
const strategies_1 = require("../load-balancer/strategies");
|
|
42
43
|
const logger_1 = require("../utils/logger");
|
|
43
44
|
const validation_1 = require("../utils/validation");
|
|
45
|
+
const resilience_1 = require("@hazeljs/resilience");
|
|
44
46
|
const axios_1 = __importStar(require("axios"));
|
|
45
47
|
/** HTTP status codes that are safe to retry */
|
|
46
48
|
const RETRYABLE_STATUS_CODES = new Set([502, 503, 504, 408, 429]);
|
|
@@ -55,7 +57,11 @@ function isRetryableError(error) {
|
|
|
55
57
|
return true;
|
|
56
58
|
return RETRYABLE_STATUS_CODES.has(error.response.status);
|
|
57
59
|
}
|
|
58
|
-
//
|
|
60
|
+
// "No instances available" is not transient - do not retry
|
|
61
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
62
|
+
if (msg.includes('No instances available'))
|
|
63
|
+
return false;
|
|
64
|
+
// Other non-Axios errors (e.g. network) are retryable
|
|
59
65
|
return true;
|
|
60
66
|
}
|
|
61
67
|
class ServiceClient {
|
|
@@ -63,11 +69,21 @@ class ServiceClient {
|
|
|
63
69
|
this.discoveryClient = discoveryClient;
|
|
64
70
|
this.config = config;
|
|
65
71
|
(0, validation_1.validateServiceClientConfig)(config);
|
|
66
|
-
this.retries = config.retries ?? 3;
|
|
67
|
-
this.retryDelay = config.retryDelay ?? 1000;
|
|
68
72
|
this.axiosInstance = axios_1.default.create({
|
|
69
73
|
timeout: config.timeout || 5000,
|
|
70
74
|
});
|
|
75
|
+
// Delegate retry logic to @hazeljs/resilience RetryPolicy
|
|
76
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
77
|
+
this.retryPolicy = new resilience_1.RetryPolicy({
|
|
78
|
+
maxAttempts: config.retries ?? 3,
|
|
79
|
+
backoff: 'fixed',
|
|
80
|
+
baseDelay: config.retryDelay ?? 1000,
|
|
81
|
+
jitter: false,
|
|
82
|
+
retryPredicate: isRetryableError,
|
|
83
|
+
onRetry: (error, attempt) => {
|
|
84
|
+
logger.warn(`Request to ${config.serviceName} failed (attempt ${attempt}/${config.retries ?? 3})`, error);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
71
87
|
}
|
|
72
88
|
/**
|
|
73
89
|
* GET request
|
|
@@ -100,13 +116,11 @@ class ServiceClient {
|
|
|
100
116
|
return this.request({ ...config, method: 'PATCH', url: path, data });
|
|
101
117
|
}
|
|
102
118
|
/**
|
|
103
|
-
* Generic request with service discovery
|
|
119
|
+
* Generic request with service discovery and resilience-backed retry
|
|
104
120
|
*/
|
|
105
121
|
async request(config) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (let attempt = 0; attempt < this.retries; attempt++) {
|
|
109
|
-
// Discover service instance
|
|
122
|
+
return this.retryPolicy.execute(async () => {
|
|
123
|
+
// Discover service instance on each attempt (may pick a different one)
|
|
110
124
|
const instance = await this.discoveryClient.getInstance(this.config.serviceName, this.config.loadBalancingStrategy, this.config.filter);
|
|
111
125
|
if (!instance) {
|
|
112
126
|
throw new Error(`No instances available for service: ${this.config.serviceName}`);
|
|
@@ -130,31 +144,12 @@ class ServiceClient {
|
|
|
130
144
|
const response = await this.axiosInstance.request(fullConfig);
|
|
131
145
|
return response;
|
|
132
146
|
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
lastError = error;
|
|
135
|
-
logger.warn(`Request to ${this.config.serviceName} failed (attempt ${attempt + 1}/${this.retries})`, error);
|
|
136
|
-
// Only retry on transient / network errors
|
|
137
|
-
if (!isRetryableError(error)) {
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
// Wait before retry (skip delay on last attempt since we'll throw)
|
|
141
|
-
if (attempt < this.retries - 1) {
|
|
142
|
-
await this.sleep(this.retryDelay);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
147
|
finally {
|
|
146
148
|
if (isLeastConn) {
|
|
147
149
|
lbStrategy.decrementConnections(instance.id);
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
|
-
}
|
|
151
|
-
throw lastError || new Error('Request failed after all retries');
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Sleep utility
|
|
155
|
-
*/
|
|
156
|
-
sleep(ms) {
|
|
157
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
});
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
155
|
exports.ServiceClient = ServiceClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/discovery",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.42",
|
|
4
4
|
"description": "Service discovery and registry for HazelJS microservices - Eureka-inspired with multiple backend support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"clean": "rm -rf dist"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@hazeljs/core": "^0.2.0-beta.42",
|
|
22
|
+
"@hazeljs/resilience": "^0.2.0-beta.42",
|
|
21
23
|
"axios": "^1.6.0",
|
|
22
24
|
"reflect-metadata": "^0.2.2"
|
|
23
25
|
},
|
|
@@ -75,5 +77,5 @@
|
|
|
75
77
|
"url": "https://github.com/hazeljs/hazel-js/issues"
|
|
76
78
|
},
|
|
77
79
|
"homepage": "https://hazeljs.com",
|
|
78
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "00db05d3763e93f2caa5bc5a3540b5c81d8b3faf"
|
|
79
81
|
}
|