@hazeljs/discovery 0.2.0-beta.8 → 0.2.0-beta.80
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/LICENSE +192 -21
- package/README.md +190 -21
- package/dist/__tests__/consul-backend.test.d.ts +6 -0
- package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
- package/dist/__tests__/consul-backend.test.js +300 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
- package/dist/__tests__/kubernetes-backend.test.js +261 -0
- package/dist/__tests__/redis-backend.test.d.ts +6 -0
- package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
- package/dist/__tests__/redis-backend.test.js +280 -0
- package/dist/__tests__/service-client.test.js +2 -1
- package/dist/backends/consul-backend.d.ts +46 -7
- package/dist/backends/consul-backend.d.ts.map +1 -1
- package/dist/backends/consul-backend.js +23 -39
- package/dist/backends/kubernetes-backend.d.ts +44 -6
- package/dist/backends/kubernetes-backend.d.ts.map +1 -1
- package/dist/backends/kubernetes-backend.js +11 -32
- package/dist/backends/memory-backend.d.ts +0 -1
- package/dist/backends/memory-backend.d.ts.map +1 -1
- package/dist/backends/memory-backend.js +3 -32
- package/dist/backends/redis-backend.d.ts +11 -6
- package/dist/backends/redis-backend.d.ts.map +1 -1
- package/dist/backends/redis-backend.js +66 -46
- package/dist/client/discovery-client.d.ts +6 -4
- package/dist/client/discovery-client.d.ts.map +1 -1
- package/dist/client/discovery-client.js +30 -30
- package/dist/client/service-client.d.ts +4 -8
- package/dist/client/service-client.d.ts.map +1 -1
- package/dist/client/service-client.js +94 -34
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/registry/service-registry.d.ts.map +1 -1
- package/dist/registry/service-registry.js +13 -2
- package/dist/utils/filter.d.ts +10 -0
- package/dist/utils/filter.d.ts.map +1 -0
- package/dist/utils/filter.js +39 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +34 -0
- package/dist/utils/validation.d.ts +36 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +109 -0
- package/package.json +7 -5
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.KubernetesRegistryBackend = void 0;
|
|
8
8
|
const types_1 = require("../types");
|
|
9
|
+
const filter_1 = require("../utils/filter");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
const validation_1 = require("../utils/validation");
|
|
9
12
|
class KubernetesRegistryBackend {
|
|
10
13
|
constructor(kubeConfig, config = {}) {
|
|
14
|
+
(0, validation_1.validateKubernetesBackendConfig)(config);
|
|
11
15
|
// Import CoreV1Api dynamically to avoid build errors when @kubernetes/client-node is not installed
|
|
12
16
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
17
|
const { CoreV1Api: CoreV1ApiClass } = require('@kubernetes/client-node');
|
|
@@ -44,6 +48,7 @@ class KubernetesRegistryBackend {
|
|
|
44
48
|
* Get all instances of a service from Kubernetes Endpoints
|
|
45
49
|
*/
|
|
46
50
|
async getInstances(serviceName, filter) {
|
|
51
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
47
52
|
try {
|
|
48
53
|
// Get service endpoints
|
|
49
54
|
const endpointsResponse = await this.k8sApi.readNamespacedEndpoints(serviceName, this.namespace);
|
|
@@ -73,12 +78,10 @@ class KubernetesRegistryBackend {
|
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
// Apply filters
|
|
76
|
-
|
|
77
|
-
return this.applyFilter(instances, filter);
|
|
78
|
-
}
|
|
79
|
-
return instances;
|
|
81
|
+
return (0, filter_1.applyServiceFilter)(instances, filter);
|
|
80
82
|
}
|
|
81
|
-
catch {
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error(`Failed to get instances for service "${serviceName}" from Kubernetes`, error);
|
|
82
85
|
return [];
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -95,11 +98,13 @@ class KubernetesRegistryBackend {
|
|
|
95
98
|
* Get all registered services in the namespace
|
|
96
99
|
*/
|
|
97
100
|
async getAllServices() {
|
|
101
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
98
102
|
try {
|
|
99
103
|
const servicesResponse = await this.k8sApi.listNamespacedService(this.namespace, undefined, undefined, undefined, undefined, this.labelSelector);
|
|
100
104
|
return servicesResponse.body.items.map((service) => service.metadata?.name || '');
|
|
101
105
|
}
|
|
102
|
-
catch {
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.error('Failed to list services from Kubernetes', error);
|
|
103
108
|
return [];
|
|
104
109
|
}
|
|
105
110
|
}
|
|
@@ -144,31 +149,5 @@ class KubernetesRegistryBackend {
|
|
|
144
149
|
registeredAt: new Date(metadata?.creationTimestamp || Date.now()),
|
|
145
150
|
};
|
|
146
151
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Apply filter to instances
|
|
149
|
-
*/
|
|
150
|
-
applyFilter(instances, filter) {
|
|
151
|
-
return instances.filter((instance) => {
|
|
152
|
-
if (filter.zone && instance.zone !== filter.zone) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
if (filter.status && instance.status !== filter.status) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
159
|
-
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (filter.metadata) {
|
|
164
|
-
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
165
|
-
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return true;
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
152
|
}
|
|
174
153
|
exports.KubernetesRegistryBackend = KubernetesRegistryBackend;
|
|
@@ -17,6 +17,5 @@ export declare class MemoryRegistryBackend implements RegistryBackend {
|
|
|
17
17
|
getAllServices(): Promise<string[]>;
|
|
18
18
|
updateStatus(instanceId: string, status: string): Promise<void>;
|
|
19
19
|
cleanup(): Promise<void>;
|
|
20
|
-
private applyFilter;
|
|
21
20
|
}
|
|
22
21
|
//# sourceMappingURL=memory-backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-backend.d.ts","sourceRoot":"","sources":["../../src/backends/memory-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-backend.d.ts","sourceRoot":"","sources":["../../src/backends/memory-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAGzE,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,cAAc,SAAQ;IAK5B,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlD,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB7C,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAgBrF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAIhE,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAInC,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAe/B"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.MemoryRegistryBackend = void 0;
|
|
8
8
|
const types_1 = require("../types");
|
|
9
|
+
const filter_1 = require("../utils/filter");
|
|
9
10
|
class MemoryRegistryBackend {
|
|
10
11
|
constructor(expirationTime = 90000) {
|
|
11
12
|
this.instances = new Map();
|
|
@@ -46,7 +47,7 @@ class MemoryRegistryBackend {
|
|
|
46
47
|
const instanceIds = this.serviceIndex.get(serviceName);
|
|
47
48
|
if (!instanceIds)
|
|
48
49
|
return [];
|
|
49
|
-
|
|
50
|
+
const instances = [];
|
|
50
51
|
for (const id of instanceIds) {
|
|
51
52
|
const instance = this.instances.get(id);
|
|
52
53
|
if (instance) {
|
|
@@ -54,10 +55,7 @@ class MemoryRegistryBackend {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
// Apply filters
|
|
57
|
-
|
|
58
|
-
instances = this.applyFilter(instances, filter);
|
|
59
|
-
}
|
|
60
|
-
return instances;
|
|
58
|
+
return (0, filter_1.applyServiceFilter)(instances, filter);
|
|
61
59
|
}
|
|
62
60
|
async getInstance(instanceId) {
|
|
63
61
|
return this.instances.get(instanceId) || null;
|
|
@@ -84,32 +82,5 @@ class MemoryRegistryBackend {
|
|
|
84
82
|
await this.deregister(id);
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
|
-
applyFilter(instances, filter) {
|
|
88
|
-
return instances.filter((instance) => {
|
|
89
|
-
// Filter by zone
|
|
90
|
-
if (filter.zone && instance.zone !== filter.zone) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
// Filter by status
|
|
94
|
-
if (filter.status && instance.status !== filter.status) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
// Filter by tags
|
|
98
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
99
|
-
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Filter by metadata
|
|
104
|
-
if (filter.metadata) {
|
|
105
|
-
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
106
|
-
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
85
|
}
|
|
115
86
|
exports.MemoryRegistryBackend = MemoryRegistryBackend;
|
|
@@ -17,7 +17,16 @@ export declare class RedisRegistryBackend implements RegistryBackend {
|
|
|
17
17
|
private redis;
|
|
18
18
|
private readonly keyPrefix;
|
|
19
19
|
private readonly ttl;
|
|
20
|
+
private connected;
|
|
20
21
|
constructor(redis: Redis, config?: RedisBackendConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Set up Redis connection event handlers for resilience
|
|
24
|
+
*/
|
|
25
|
+
private setupConnectionHandlers;
|
|
26
|
+
/**
|
|
27
|
+
* Check Redis connectivity before operations
|
|
28
|
+
*/
|
|
29
|
+
private ensureConnected;
|
|
21
30
|
/**
|
|
22
31
|
* Register a service instance
|
|
23
32
|
*/
|
|
@@ -31,7 +40,7 @@ export declare class RedisRegistryBackend implements RegistryBackend {
|
|
|
31
40
|
*/
|
|
32
41
|
heartbeat(instanceId: string): Promise<void>;
|
|
33
42
|
/**
|
|
34
|
-
* Get all instances of a service
|
|
43
|
+
* Get all instances of a service (uses MGET for efficiency)
|
|
35
44
|
*/
|
|
36
45
|
getInstances(serviceName: string, filter?: ServiceFilter): Promise<ServiceInstance[]>;
|
|
37
46
|
/**
|
|
@@ -39,7 +48,7 @@ export declare class RedisRegistryBackend implements RegistryBackend {
|
|
|
39
48
|
*/
|
|
40
49
|
getInstance(instanceId: string): Promise<ServiceInstance | null>;
|
|
41
50
|
/**
|
|
42
|
-
* Get all registered services
|
|
51
|
+
* Get all registered services using SCAN (safe for production)
|
|
43
52
|
*/
|
|
44
53
|
getAllServices(): Promise<string[]>;
|
|
45
54
|
/**
|
|
@@ -63,9 +72,5 @@ export declare class RedisRegistryBackend implements RegistryBackend {
|
|
|
63
72
|
* Get Redis key for service set
|
|
64
73
|
*/
|
|
65
74
|
private getServiceSetKey;
|
|
66
|
-
/**
|
|
67
|
-
* Apply filter to instances
|
|
68
|
-
*/
|
|
69
|
-
private applyFilter;
|
|
70
75
|
}
|
|
71
76
|
//# sourceMappingURL=redis-backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-backend.d.ts","sourceRoot":"","sources":["../../src/backends/redis-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"redis-backend.d.ts","sourceRoot":"","sources":["../../src/backends/redis-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAIzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAQ;gBAEb,KAAK,EAAE,KAAK,EAAE,MAAM,GAAE,kBAAuB;IAUzD;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAuB/B;;OAEG;IACH,OAAO,CAAC,eAAe;IAMvB;;OAEG;IACG,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxD;;OAEG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBnD;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlD;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA8B3F;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAmBtE;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAoBzC;;OAEG;IACG,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B9B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAItB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAGzB"}
|
|
@@ -6,16 +6,52 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.RedisRegistryBackend = void 0;
|
|
8
8
|
const types_1 = require("../types");
|
|
9
|
+
const filter_1 = require("../utils/filter");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
const validation_1 = require("../utils/validation");
|
|
9
12
|
class RedisRegistryBackend {
|
|
10
13
|
constructor(redis, config = {}) {
|
|
14
|
+
this.connected = true;
|
|
15
|
+
(0, validation_1.validateRedisBackendConfig)(config);
|
|
11
16
|
this.redis = redis;
|
|
12
17
|
this.keyPrefix = config.keyPrefix || 'hazeljs:discovery:';
|
|
13
18
|
this.ttl = config.ttl || 90; // 90 seconds default
|
|
19
|
+
this.setupConnectionHandlers();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Set up Redis connection event handlers for resilience
|
|
23
|
+
*/
|
|
24
|
+
setupConnectionHandlers() {
|
|
25
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
26
|
+
this.redis.on('error', (err) => {
|
|
27
|
+
this.connected = false;
|
|
28
|
+
logger.error('Redis connection error', err);
|
|
29
|
+
});
|
|
30
|
+
this.redis.on('connect', () => {
|
|
31
|
+
this.connected = true;
|
|
32
|
+
logger.info('Redis connected');
|
|
33
|
+
});
|
|
34
|
+
this.redis.on('reconnecting', () => {
|
|
35
|
+
logger.warn('Redis reconnecting...');
|
|
36
|
+
});
|
|
37
|
+
this.redis.on('close', () => {
|
|
38
|
+
this.connected = false;
|
|
39
|
+
logger.warn('Redis connection closed');
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check Redis connectivity before operations
|
|
44
|
+
*/
|
|
45
|
+
ensureConnected() {
|
|
46
|
+
if (!this.connected) {
|
|
47
|
+
throw new Error('Redis backend is not connected');
|
|
48
|
+
}
|
|
14
49
|
}
|
|
15
50
|
/**
|
|
16
51
|
* Register a service instance
|
|
17
52
|
*/
|
|
18
53
|
async register(instance) {
|
|
54
|
+
this.ensureConnected();
|
|
19
55
|
const key = this.getInstanceKey(instance.id);
|
|
20
56
|
const serviceSetKey = this.getServiceSetKey(instance.name);
|
|
21
57
|
// Store instance data
|
|
@@ -29,6 +65,7 @@ class RedisRegistryBackend {
|
|
|
29
65
|
* Deregister a service instance
|
|
30
66
|
*/
|
|
31
67
|
async deregister(instanceId) {
|
|
68
|
+
this.ensureConnected();
|
|
32
69
|
const key = this.getInstanceKey(instanceId);
|
|
33
70
|
// Get instance to find service name
|
|
34
71
|
const data = await this.redis.get(key);
|
|
@@ -45,6 +82,7 @@ class RedisRegistryBackend {
|
|
|
45
82
|
* Update service instance heartbeat
|
|
46
83
|
*/
|
|
47
84
|
async heartbeat(instanceId) {
|
|
85
|
+
this.ensureConnected();
|
|
48
86
|
const key = this.getInstanceKey(instanceId);
|
|
49
87
|
// Get current instance
|
|
50
88
|
const data = await this.redis.get(key);
|
|
@@ -61,33 +99,36 @@ class RedisRegistryBackend {
|
|
|
61
99
|
}
|
|
62
100
|
}
|
|
63
101
|
/**
|
|
64
|
-
* Get all instances of a service
|
|
102
|
+
* Get all instances of a service (uses MGET for efficiency)
|
|
65
103
|
*/
|
|
66
104
|
async getInstances(serviceName, filter) {
|
|
105
|
+
this.ensureConnected();
|
|
67
106
|
const serviceSetKey = this.getServiceSetKey(serviceName);
|
|
68
107
|
// Get all instance IDs for this service
|
|
69
108
|
const instanceIds = await this.redis.smembers(serviceSetKey);
|
|
70
109
|
if (instanceIds.length === 0) {
|
|
71
110
|
return [];
|
|
72
111
|
}
|
|
73
|
-
//
|
|
112
|
+
// Batch-fetch all instances with MGET
|
|
113
|
+
const keys = instanceIds.map((id) => this.getInstanceKey(id));
|
|
114
|
+
const results = await this.redis.mget(...keys);
|
|
74
115
|
const instances = [];
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
for (const data of results) {
|
|
117
|
+
if (data) {
|
|
118
|
+
const instance = JSON.parse(data);
|
|
119
|
+
instance.lastHeartbeat = new Date(instance.lastHeartbeat);
|
|
120
|
+
instance.registeredAt = new Date(instance.registeredAt);
|
|
78
121
|
instances.push(instance);
|
|
79
122
|
}
|
|
80
123
|
}
|
|
81
124
|
// Apply filters
|
|
82
|
-
|
|
83
|
-
return this.applyFilter(instances, filter);
|
|
84
|
-
}
|
|
85
|
-
return instances;
|
|
125
|
+
return (0, filter_1.applyServiceFilter)(instances, filter);
|
|
86
126
|
}
|
|
87
127
|
/**
|
|
88
128
|
* Get a specific service instance
|
|
89
129
|
*/
|
|
90
130
|
async getInstance(instanceId) {
|
|
131
|
+
this.ensureConnected();
|
|
91
132
|
const key = this.getInstanceKey(instanceId);
|
|
92
133
|
const data = await this.redis.get(key);
|
|
93
134
|
if (!data) {
|
|
@@ -100,20 +141,28 @@ class RedisRegistryBackend {
|
|
|
100
141
|
return instance;
|
|
101
142
|
}
|
|
102
143
|
/**
|
|
103
|
-
* Get all registered services
|
|
144
|
+
* Get all registered services using SCAN (safe for production)
|
|
104
145
|
*/
|
|
105
146
|
async getAllServices() {
|
|
147
|
+
this.ensureConnected();
|
|
106
148
|
const pattern = `${this.keyPrefix}service:*`;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
149
|
+
const prefixLen = `${this.keyPrefix}service:`.length;
|
|
150
|
+
const services = [];
|
|
151
|
+
let cursor = '0';
|
|
152
|
+
do {
|
|
153
|
+
const [nextCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
|
154
|
+
cursor = nextCursor;
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
services.push(key.substring(prefixLen));
|
|
157
|
+
}
|
|
158
|
+
} while (cursor !== '0');
|
|
159
|
+
return services;
|
|
112
160
|
}
|
|
113
161
|
/**
|
|
114
162
|
* Update service instance status
|
|
115
163
|
*/
|
|
116
164
|
async updateStatus(instanceId, status) {
|
|
165
|
+
this.ensureConnected();
|
|
117
166
|
const key = this.getInstanceKey(instanceId);
|
|
118
167
|
const data = await this.redis.get(key);
|
|
119
168
|
if (data) {
|
|
@@ -127,8 +176,9 @@ class RedisRegistryBackend {
|
|
|
127
176
|
* Note: Redis handles expiration automatically via TTL
|
|
128
177
|
*/
|
|
129
178
|
async cleanup() {
|
|
179
|
+
this.ensureConnected();
|
|
130
180
|
// Redis automatically removes expired keys
|
|
131
|
-
// This method
|
|
181
|
+
// This method cleans up stale entries in service sets
|
|
132
182
|
const services = await this.getAllServices();
|
|
133
183
|
for (const serviceName of services) {
|
|
134
184
|
const serviceSetKey = this.getServiceSetKey(serviceName);
|
|
@@ -166,35 +216,5 @@ class RedisRegistryBackend {
|
|
|
166
216
|
getServiceSetKey(serviceName) {
|
|
167
217
|
return `${this.keyPrefix}service:${serviceName}`;
|
|
168
218
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Apply filter to instances
|
|
171
|
-
*/
|
|
172
|
-
applyFilter(instances, filter) {
|
|
173
|
-
return instances.filter((instance) => {
|
|
174
|
-
// Filter by zone
|
|
175
|
-
if (filter.zone && instance.zone !== filter.zone) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
// Filter by status
|
|
179
|
-
if (filter.status && instance.status !== filter.status) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
// Filter by tags
|
|
183
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
184
|
-
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// Filter by metadata
|
|
189
|
-
if (filter.metadata) {
|
|
190
|
-
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
191
|
-
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return true;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
219
|
}
|
|
200
220
|
exports.RedisRegistryBackend = RedisRegistryBackend;
|
|
@@ -10,6 +10,7 @@ export declare class DiscoveryClient {
|
|
|
10
10
|
private backend;
|
|
11
11
|
private cache;
|
|
12
12
|
private loadBalancerFactory;
|
|
13
|
+
private refreshIntervalHandle;
|
|
13
14
|
constructor(config?: DiscoveryClientConfig, backend?: RegistryBackend);
|
|
14
15
|
/**
|
|
15
16
|
* Get all instances of a service
|
|
@@ -36,12 +37,13 @@ export declare class DiscoveryClient {
|
|
|
36
37
|
*/
|
|
37
38
|
getLoadBalancerFactory(): LoadBalancerFactory;
|
|
38
39
|
/**
|
|
39
|
-
*
|
|
40
|
+
* Close the discovery client and release all resources.
|
|
41
|
+
* Stops the cache refresh interval and clears the cache.
|
|
40
42
|
*/
|
|
41
|
-
|
|
43
|
+
close(): void;
|
|
42
44
|
/**
|
|
43
|
-
*
|
|
45
|
+
* Start cache refresh interval
|
|
44
46
|
*/
|
|
45
|
-
private
|
|
47
|
+
private startRefreshInterval;
|
|
46
48
|
}
|
|
47
49
|
//# sourceMappingURL=discovery-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery-client.d.ts","sourceRoot":"","sources":["../../src/client/discovery-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"discovery-client.d.ts","sourceRoot":"","sources":["../../src/client/discovery-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAKlE,qBAAa,eAAe;IAOxB,OAAO,CAAC,MAAM;IANhB,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,KAAK,CAA0E;IACvF,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,qBAAqB,CAA+B;gBAGlD,MAAM,GAAE,qBAA0B,EAC1C,OAAO,CAAC,EAAE,eAAe;IAY3B;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA2B3F;;OAEG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,QAAQ,GAAE,MAAsB,EAChC,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAQlC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIzC;;OAEG;IACH,UAAU,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAQtC;;OAEG;IACH,UAAU,IAAI,eAAe;IAI7B;;OAEG;IACH,sBAAsB,IAAI,mBAAmB;IAI7C;;;OAGG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAkB7B"}
|
|
@@ -7,10 +7,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.DiscoveryClient = void 0;
|
|
8
8
|
const memory_backend_1 = require("../backends/memory-backend");
|
|
9
9
|
const strategies_1 = require("../load-balancer/strategies");
|
|
10
|
+
const filter_1 = require("../utils/filter");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
const validation_1 = require("../utils/validation");
|
|
10
13
|
class DiscoveryClient {
|
|
11
14
|
constructor(config = {}, backend) {
|
|
12
15
|
this.config = config;
|
|
13
16
|
this.cache = new Map();
|
|
17
|
+
this.refreshIntervalHandle = null;
|
|
18
|
+
(0, validation_1.validateDiscoveryClientConfig)(config);
|
|
14
19
|
this.backend = backend || new memory_backend_1.MemoryRegistryBackend();
|
|
15
20
|
this.loadBalancerFactory = new strategies_1.LoadBalancerFactory();
|
|
16
21
|
if (config.refreshInterval) {
|
|
@@ -28,7 +33,7 @@ class DiscoveryClient {
|
|
|
28
33
|
const age = Date.now() - cached.timestamp;
|
|
29
34
|
const ttl = this.config.cacheTTL || 30000;
|
|
30
35
|
if (age < ttl) {
|
|
31
|
-
return
|
|
36
|
+
return (0, filter_1.applyServiceFilter)(cached.instances, filter);
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
}
|
|
@@ -83,41 +88,36 @@ class DiscoveryClient {
|
|
|
83
88
|
return this.loadBalancerFactory;
|
|
84
89
|
}
|
|
85
90
|
/**
|
|
86
|
-
*
|
|
91
|
+
* Close the discovery client and release all resources.
|
|
92
|
+
* Stops the cache refresh interval and clears the cache.
|
|
87
93
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
instances,
|
|
95
|
-
timestamp: Date.now(),
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}, this.config.refreshInterval);
|
|
94
|
+
close() {
|
|
95
|
+
if (this.refreshIntervalHandle) {
|
|
96
|
+
clearInterval(this.refreshIntervalHandle);
|
|
97
|
+
this.refreshIntervalHandle = null;
|
|
98
|
+
}
|
|
99
|
+
this.cache.clear();
|
|
99
100
|
}
|
|
100
101
|
/**
|
|
101
|
-
*
|
|
102
|
+
* Start cache refresh interval
|
|
102
103
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
115
|
-
if (instance.metadata?.[key] !== value)
|
|
116
|
-
return false;
|
|
104
|
+
startRefreshInterval() {
|
|
105
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
106
|
+
this.refreshIntervalHandle = setInterval(async () => {
|
|
107
|
+
try {
|
|
108
|
+
const services = await this.getAllServices();
|
|
109
|
+
for (const service of services) {
|
|
110
|
+
const instances = await this.backend.getInstances(service);
|
|
111
|
+
this.cache.set(service, {
|
|
112
|
+
instances,
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
});
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error('Failed to refresh service cache', error);
|
|
119
|
+
}
|
|
120
|
+
}, this.config.refreshInterval);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
exports.DiscoveryClient = 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"}
|