@hazeljs/discovery 0.2.0-alpha.1

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.
Files changed (78) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +450 -0
  3. package/dist/__tests__/consul-backend.test.d.ts +6 -0
  4. package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
  5. package/dist/__tests__/consul-backend.test.js +300 -0
  6. package/dist/__tests__/decorators.test.d.ts +5 -0
  7. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  8. package/dist/__tests__/decorators.test.js +72 -0
  9. package/dist/__tests__/discovery-client.test.d.ts +5 -0
  10. package/dist/__tests__/discovery-client.test.d.ts.map +1 -0
  11. package/dist/__tests__/discovery-client.test.js +142 -0
  12. package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
  13. package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
  14. package/dist/__tests__/kubernetes-backend.test.js +261 -0
  15. package/dist/__tests__/load-balancer-strategies.test.d.ts +5 -0
  16. package/dist/__tests__/load-balancer-strategies.test.d.ts.map +1 -0
  17. package/dist/__tests__/load-balancer-strategies.test.js +234 -0
  18. package/dist/__tests__/memory-backend.test.d.ts +5 -0
  19. package/dist/__tests__/memory-backend.test.d.ts.map +1 -0
  20. package/dist/__tests__/memory-backend.test.js +246 -0
  21. package/dist/__tests__/redis-backend.test.d.ts +6 -0
  22. package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
  23. package/dist/__tests__/redis-backend.test.js +280 -0
  24. package/dist/__tests__/service-client.test.d.ts +5 -0
  25. package/dist/__tests__/service-client.test.d.ts.map +1 -0
  26. package/dist/__tests__/service-client.test.js +216 -0
  27. package/dist/__tests__/service-registry.test.d.ts +5 -0
  28. package/dist/__tests__/service-registry.test.d.ts.map +1 -0
  29. package/dist/__tests__/service-registry.test.js +65 -0
  30. package/dist/backends/consul-backend.d.ts +115 -0
  31. package/dist/backends/consul-backend.d.ts.map +1 -0
  32. package/dist/backends/consul-backend.js +259 -0
  33. package/dist/backends/kubernetes-backend.d.ts +103 -0
  34. package/dist/backends/kubernetes-backend.d.ts.map +1 -0
  35. package/dist/backends/kubernetes-backend.js +153 -0
  36. package/dist/backends/memory-backend.d.ts +21 -0
  37. package/dist/backends/memory-backend.d.ts.map +1 -0
  38. package/dist/backends/memory-backend.js +86 -0
  39. package/dist/backends/redis-backend.d.ts +76 -0
  40. package/dist/backends/redis-backend.d.ts.map +1 -0
  41. package/dist/backends/redis-backend.js +220 -0
  42. package/dist/backends/registry-backend.d.ts +39 -0
  43. package/dist/backends/registry-backend.d.ts.map +1 -0
  44. package/dist/backends/registry-backend.js +5 -0
  45. package/dist/client/discovery-client.d.ts +49 -0
  46. package/dist/client/discovery-client.d.ts.map +1 -0
  47. package/dist/client/discovery-client.js +123 -0
  48. package/dist/client/service-client.d.ts +48 -0
  49. package/dist/client/service-client.d.ts.map +1 -0
  50. package/dist/client/service-client.js +155 -0
  51. package/dist/decorators/inject-service-client.decorator.d.ts +16 -0
  52. package/dist/decorators/inject-service-client.decorator.d.ts.map +1 -0
  53. package/dist/decorators/inject-service-client.decorator.js +24 -0
  54. package/dist/decorators/service-registry.decorator.d.ts +11 -0
  55. package/dist/decorators/service-registry.decorator.d.ts.map +1 -0
  56. package/dist/decorators/service-registry.decorator.js +20 -0
  57. package/dist/index.d.ts +21 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +51 -0
  60. package/dist/load-balancer/strategies.d.ts +82 -0
  61. package/dist/load-balancer/strategies.d.ts.map +1 -0
  62. package/dist/load-balancer/strategies.js +209 -0
  63. package/dist/registry/service-registry.d.ts +51 -0
  64. package/dist/registry/service-registry.d.ts.map +1 -0
  65. package/dist/registry/service-registry.js +159 -0
  66. package/dist/types/index.d.ts +61 -0
  67. package/dist/types/index.d.ts.map +1 -0
  68. package/dist/types/index.js +14 -0
  69. package/dist/utils/filter.d.ts +10 -0
  70. package/dist/utils/filter.d.ts.map +1 -0
  71. package/dist/utils/filter.js +39 -0
  72. package/dist/utils/logger.d.ts +21 -0
  73. package/dist/utils/logger.d.ts.map +1 -0
  74. package/dist/utils/logger.js +34 -0
  75. package/dist/utils/validation.d.ts +36 -0
  76. package/dist/utils/validation.d.ts.map +1 -0
  77. package/dist/utils/validation.js +109 -0
  78. package/package.json +81 -0
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Service Client Tests
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const service_client_1 = require("../client/service-client");
10
+ const discovery_client_1 = require("../client/discovery-client");
11
+ const memory_backend_1 = require("../backends/memory-backend");
12
+ const types_1 = require("../types");
13
+ const axios_1 = __importDefault(require("axios"));
14
+ // Mock axios
15
+ jest.mock('axios');
16
+ const mockedAxios = axios_1.default;
17
+ describe('ServiceClient', () => {
18
+ let serviceClient;
19
+ let discoveryClient;
20
+ let backend;
21
+ beforeEach(() => {
22
+ backend = new memory_backend_1.MemoryRegistryBackend();
23
+ discoveryClient = new discovery_client_1.DiscoveryClient({}, backend);
24
+ serviceClient = new service_client_1.ServiceClient(discoveryClient, {
25
+ serviceName: 'test-service',
26
+ timeout: 5000,
27
+ retries: 3,
28
+ retryDelay: 100,
29
+ });
30
+ // Reset mocks
31
+ jest.clearAllMocks();
32
+ });
33
+ const createInstance = (id) => ({
34
+ id,
35
+ name: 'test-service',
36
+ host: 'localhost',
37
+ port: 3000,
38
+ protocol: 'http',
39
+ status: types_1.ServiceStatus.UP,
40
+ lastHeartbeat: new Date(),
41
+ registeredAt: new Date(),
42
+ });
43
+ describe('GET requests', () => {
44
+ it('should make GET request to discovered service', async () => {
45
+ const instance = createInstance('1');
46
+ await backend.register(instance);
47
+ const mockResponse = { data: { message: 'success' }, status: 200 };
48
+ mockedAxios.create.mockReturnValue({
49
+ request: jest.fn().mockResolvedValue(mockResponse),
50
+ });
51
+ const client = new service_client_1.ServiceClient(discoveryClient, {
52
+ serviceName: 'test-service',
53
+ });
54
+ // Mock the axios instance request method
55
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
56
+ client.axiosInstance = { request: mockRequest };
57
+ await client.get('/api/users');
58
+ expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
59
+ method: 'GET',
60
+ url: '/api/users',
61
+ }));
62
+ });
63
+ });
64
+ describe('POST requests', () => {
65
+ it('should make POST request with data', async () => {
66
+ const instance = createInstance('1');
67
+ await backend.register(instance);
68
+ const mockResponse = { data: { id: 1 }, status: 201 };
69
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
70
+ serviceClient.axiosInstance = { request: mockRequest };
71
+ await serviceClient.post('/api/users', { name: 'John' });
72
+ expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
73
+ method: 'POST',
74
+ url: '/api/users',
75
+ data: { name: 'John' },
76
+ }));
77
+ });
78
+ });
79
+ describe('PUT requests', () => {
80
+ it('should make PUT request with data', async () => {
81
+ const instance = createInstance('1');
82
+ await backend.register(instance);
83
+ const mockResponse = { data: { updated: true }, status: 200 };
84
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
85
+ serviceClient.axiosInstance = { request: mockRequest };
86
+ await serviceClient.put('/api/users/1', { name: 'Jane' });
87
+ expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
88
+ method: 'PUT',
89
+ url: '/api/users/1',
90
+ data: { name: 'Jane' },
91
+ }));
92
+ });
93
+ });
94
+ describe('DELETE requests', () => {
95
+ it('should make DELETE request', async () => {
96
+ const instance = createInstance('1');
97
+ await backend.register(instance);
98
+ const mockResponse = { data: {}, status: 204 };
99
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
100
+ serviceClient.axiosInstance = { request: mockRequest };
101
+ await serviceClient.delete('/api/users/1');
102
+ expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
103
+ method: 'DELETE',
104
+ url: '/api/users/1',
105
+ }));
106
+ });
107
+ });
108
+ describe('PATCH requests', () => {
109
+ it('should make PATCH request with data', async () => {
110
+ const instance = createInstance('1');
111
+ await backend.register(instance);
112
+ const mockResponse = { data: { patched: true }, status: 200 };
113
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
114
+ serviceClient.axiosInstance = { request: mockRequest };
115
+ await serviceClient.patch('/api/users/1', { name: 'Updated' });
116
+ expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
117
+ method: 'PATCH',
118
+ url: '/api/users/1',
119
+ data: { name: 'Updated' },
120
+ }));
121
+ });
122
+ });
123
+ describe('retry logic', () => {
124
+ it('should retry on failure', async () => {
125
+ const instance = createInstance('1');
126
+ await backend.register(instance);
127
+ const mockRequest = jest
128
+ .fn()
129
+ .mockRejectedValueOnce(new Error('Network error'))
130
+ .mockRejectedValueOnce(new Error('Network error'))
131
+ .mockResolvedValue({ data: { success: true }, status: 200 });
132
+ serviceClient.axiosInstance = { request: mockRequest };
133
+ await serviceClient.get('/api/users');
134
+ expect(mockRequest).toHaveBeenCalledTimes(3);
135
+ });
136
+ it('should throw error after max retries', async () => {
137
+ const instance = createInstance('1');
138
+ await backend.register(instance);
139
+ const mockRequest = jest.fn().mockRejectedValue(new Error('Network error'));
140
+ serviceClient.axiosInstance = { request: mockRequest };
141
+ await expect(serviceClient.get('/api/users')).rejects.toThrow();
142
+ // retries: 3 → RetryPolicy maxAttempts: 3 → 1 initial + 3 retries = 4 total
143
+ expect(mockRequest).toHaveBeenCalledTimes(4);
144
+ });
145
+ it('should respect retry delay', async () => {
146
+ const client = new service_client_1.ServiceClient(discoveryClient, {
147
+ serviceName: 'test-service',
148
+ retries: 2,
149
+ retryDelay: 50,
150
+ });
151
+ const instance = createInstance('1');
152
+ await backend.register(instance);
153
+ const mockRequest = jest
154
+ .fn()
155
+ .mockRejectedValueOnce(new Error('Error 1'))
156
+ .mockResolvedValue({ data: {}, status: 200 });
157
+ client.axiosInstance = { request: mockRequest };
158
+ const start = Date.now();
159
+ await client.get('/api/users');
160
+ const duration = Date.now() - start;
161
+ // Should have waited at least 50ms for retry
162
+ expect(duration).toBeGreaterThanOrEqual(40); // Allow some margin
163
+ });
164
+ });
165
+ describe('service discovery integration', () => {
166
+ it('should use discovered service instance', async () => {
167
+ const instance1 = createInstance('1');
168
+ const instance2 = createInstance('2');
169
+ await backend.register(instance1);
170
+ await backend.register(instance2);
171
+ const mockRequest = jest.fn().mockResolvedValue({ data: {}, status: 200 });
172
+ serviceClient.axiosInstance = { request: mockRequest };
173
+ await serviceClient.get('/api/users');
174
+ // Should have used one of the instances
175
+ const call = mockRequest.mock.calls[0][0];
176
+ expect(call.baseURL).toMatch(/http:\/\/localhost:3000/);
177
+ });
178
+ it('should throw error when no instances available', async () => {
179
+ const mockRequest = jest.fn();
180
+ serviceClient.axiosInstance = { request: mockRequest };
181
+ await expect(serviceClient.get('/api/users')).rejects.toThrow('No instances available for service: test-service');
182
+ });
183
+ });
184
+ describe('load balancing strategy', () => {
185
+ it('should use specified load balancing strategy', async () => {
186
+ const instance1 = createInstance('1');
187
+ const instance2 = createInstance('2');
188
+ await backend.register(instance1);
189
+ await backend.register(instance2);
190
+ const client = new service_client_1.ServiceClient(discoveryClient, {
191
+ serviceName: 'test-service',
192
+ loadBalancingStrategy: 'random',
193
+ });
194
+ const mockRequest = jest.fn().mockResolvedValue({ data: {}, status: 200 });
195
+ client.axiosInstance = { request: mockRequest };
196
+ await client.get('/api/users');
197
+ expect(mockRequest).toHaveBeenCalled();
198
+ });
199
+ });
200
+ describe('filters', () => {
201
+ it('should apply service filters', async () => {
202
+ const instance1 = { ...createInstance('1'), zone: 'us-east-1' };
203
+ const instance2 = { ...createInstance('2'), zone: 'us-west-1' };
204
+ await backend.register(instance1);
205
+ await backend.register(instance2);
206
+ const client = new service_client_1.ServiceClient(discoveryClient, {
207
+ serviceName: 'test-service',
208
+ filter: { zone: 'us-east-1' },
209
+ });
210
+ const mockRequest = jest.fn().mockResolvedValue({ data: {}, status: 200 });
211
+ client.axiosInstance = { request: mockRequest };
212
+ await client.get('/api/users');
213
+ expect(mockRequest).toHaveBeenCalled();
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Service Registry Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=service-registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-registry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service-registry.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Service Registry Tests
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const service_registry_1 = require("../registry/service-registry");
7
+ const memory_backend_1 = require("../backends/memory-backend");
8
+ const types_1 = require("../types");
9
+ describe('ServiceRegistry', () => {
10
+ let registry;
11
+ let backend;
12
+ beforeEach(() => {
13
+ backend = new memory_backend_1.MemoryRegistryBackend();
14
+ registry = new service_registry_1.ServiceRegistry({
15
+ name: 'test-service',
16
+ port: 3000,
17
+ host: 'localhost',
18
+ healthCheckPath: '/health',
19
+ metadata: { version: '1.0.0' },
20
+ zone: 'us-east-1',
21
+ tags: ['test'],
22
+ }, backend);
23
+ });
24
+ afterEach(async () => {
25
+ await registry.deregister();
26
+ });
27
+ describe('register', () => {
28
+ it('should register a service instance', async () => {
29
+ await registry.register();
30
+ const instance = registry.getInstance();
31
+ expect(instance).toBeDefined();
32
+ expect(instance?.name).toBe('test-service');
33
+ expect(instance?.port).toBe(3000);
34
+ expect(instance?.zone).toBe('us-east-1');
35
+ });
36
+ it('should set initial status to STARTING or DOWN after health check', async () => {
37
+ await registry.register();
38
+ const instance = registry.getInstance();
39
+ // Status will be STARTING initially, but health check runs immediately
40
+ // Since there's no actual server, it will become DOWN
41
+ expect([types_1.ServiceStatus.STARTING, types_1.ServiceStatus.DOWN]).toContain(instance?.status);
42
+ });
43
+ });
44
+ describe('deregister', () => {
45
+ it('should deregister a service instance', async () => {
46
+ await registry.register();
47
+ const instanceId = registry.getInstance()?.id;
48
+ await registry.deregister();
49
+ const instance = await backend.getInstance(instanceId);
50
+ expect(instance).toBeNull();
51
+ });
52
+ });
53
+ describe('getInstance', () => {
54
+ it('should return the registered instance', async () => {
55
+ await registry.register();
56
+ const instance = registry.getInstance();
57
+ expect(instance).toBeDefined();
58
+ expect(instance?.name).toBe('test-service');
59
+ });
60
+ it('should return null before registration', () => {
61
+ const instance = registry.getInstance();
62
+ expect(instance).toBeNull();
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Consul Registry Backend
3
+ * Integrates with HashiCorp Consul for service discovery
4
+ */
5
+ import { RegistryBackend } from './registry-backend';
6
+ import { ServiceInstance, ServiceFilter } from '../types';
7
+ /**
8
+ * Minimal type definitions for the Consul client API surface we use.
9
+ * These mirror the shapes exposed by the `consul` npm package.
10
+ */
11
+ export interface ConsulClient {
12
+ agent: {
13
+ service: {
14
+ register(opts: Record<string, unknown>): Promise<void>;
15
+ deregister(serviceId: string): Promise<void>;
16
+ list(): Promise<Record<string, ConsulServiceEntry>>;
17
+ };
18
+ check: {
19
+ pass(checkId: string): Promise<void>;
20
+ fail(checkId: string): Promise<void>;
21
+ warn(checkId: string): Promise<void>;
22
+ list(): Promise<Record<string, ConsulCheckEntry>>;
23
+ };
24
+ };
25
+ health: {
26
+ service(opts: {
27
+ service: string;
28
+ passing?: boolean;
29
+ }): Promise<ConsulHealthEntry[]>;
30
+ };
31
+ catalog: {
32
+ service: {
33
+ list(): Promise<Record<string, string[]>>;
34
+ };
35
+ };
36
+ }
37
+ export interface ConsulServiceEntry {
38
+ ID: string;
39
+ Service: string;
40
+ Address: string;
41
+ Port: number;
42
+ Meta?: Record<string, string>;
43
+ Tags?: string[];
44
+ }
45
+ export interface ConsulCheckEntry {
46
+ Status: string;
47
+ }
48
+ export interface ConsulHealthEntry {
49
+ Service: ConsulServiceEntry;
50
+ Checks?: ConsulCheckEntry[];
51
+ }
52
+ export interface ConsulBackendConfig {
53
+ host?: string;
54
+ port?: number;
55
+ secure?: boolean;
56
+ token?: string;
57
+ datacenter?: string;
58
+ ttl?: string;
59
+ }
60
+ export declare class ConsulRegistryBackend implements RegistryBackend {
61
+ private consul;
62
+ private readonly ttl;
63
+ private checkIntervals;
64
+ constructor(consul: ConsulClient, config?: ConsulBackendConfig);
65
+ /**
66
+ * Register a service instance with Consul
67
+ */
68
+ register(instance: ServiceInstance): Promise<void>;
69
+ /**
70
+ * Deregister a service instance
71
+ */
72
+ deregister(instanceId: string): Promise<void>;
73
+ /**
74
+ * Update service instance heartbeat
75
+ */
76
+ heartbeat(instanceId: string): Promise<void>;
77
+ /**
78
+ * Get all instances of a service
79
+ */
80
+ getInstances(serviceName: string, filter?: ServiceFilter): Promise<ServiceInstance[]>;
81
+ /**
82
+ * Get a specific service instance
83
+ */
84
+ getInstance(instanceId: string): Promise<ServiceInstance | null>;
85
+ /**
86
+ * Get all registered services
87
+ */
88
+ getAllServices(): Promise<string[]>;
89
+ /**
90
+ * Update service instance status
91
+ */
92
+ updateStatus(instanceId: string, status: string): Promise<void>;
93
+ /**
94
+ * Clean up expired instances
95
+ * Consul handles this automatically via TTL checks
96
+ */
97
+ cleanup(): Promise<void>;
98
+ /**
99
+ * Close Consul connection and stop all TTL checks
100
+ */
101
+ close(): Promise<void>;
102
+ /**
103
+ * Start TTL check updates for a service
104
+ */
105
+ private startTTLCheck;
106
+ /**
107
+ * Stop TTL check updates for a service
108
+ */
109
+ private stopTTLCheck;
110
+ /**
111
+ * Parse TTL string to seconds
112
+ */
113
+ private parseTTL;
114
+ }
115
+ //# sourceMappingURL=consul-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consul-backend.d.ts","sourceRoot":"","sources":["../../src/backends/consul-backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ /**
3
+ * Consul Registry Backend
4
+ * Integrates with HashiCorp Consul for service discovery
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ConsulRegistryBackend = void 0;
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");
12
+ class ConsulRegistryBackend {
13
+ constructor(consul, config = {}) {
14
+ this.checkIntervals = new Map();
15
+ (0, validation_1.validateConsulBackendConfig)(config);
16
+ this.consul = consul;
17
+ this.ttl = config.ttl || '30s';
18
+ }
19
+ /**
20
+ * Register a service instance with Consul
21
+ */
22
+ async register(instance) {
23
+ const serviceId = instance.id;
24
+ const checkId = `service:${serviceId}`;
25
+ // Register service
26
+ await this.consul.agent.service.register({
27
+ id: serviceId,
28
+ name: instance.name,
29
+ address: instance.host,
30
+ port: instance.port,
31
+ tags: instance.tags || [],
32
+ meta: {
33
+ ...instance.metadata,
34
+ zone: instance.zone || '',
35
+ registeredAt: instance.registeredAt.toISOString(),
36
+ },
37
+ check: {
38
+ ttl: this.ttl,
39
+ deregister_critical_service_after: '90s',
40
+ },
41
+ });
42
+ // Start TTL check updates
43
+ this.startTTLCheck(serviceId, checkId);
44
+ }
45
+ /**
46
+ * Deregister a service instance
47
+ */
48
+ async deregister(instanceId) {
49
+ // Stop TTL check
50
+ this.stopTTLCheck(instanceId);
51
+ // Deregister from Consul
52
+ await this.consul.agent.service.deregister(instanceId);
53
+ }
54
+ /**
55
+ * Update service instance heartbeat
56
+ */
57
+ async heartbeat(instanceId) {
58
+ const logger = logger_1.DiscoveryLogger.getLogger();
59
+ const checkId = `service:${instanceId}`;
60
+ try {
61
+ // Pass TTL check
62
+ await this.consul.agent.check.pass(checkId);
63
+ }
64
+ catch (error) {
65
+ logger.warn(`Consul heartbeat failed for ${instanceId}, will retry on next heartbeat`, error);
66
+ }
67
+ }
68
+ /**
69
+ * Get all instances of a service
70
+ */
71
+ async getInstances(serviceName, filter) {
72
+ const logger = logger_1.DiscoveryLogger.getLogger();
73
+ try {
74
+ const result = await this.consul.health.service({
75
+ service: serviceName,
76
+ passing: filter?.status === types_1.ServiceStatus.UP,
77
+ });
78
+ const instances = result.map((entry) => {
79
+ const service = entry.Service;
80
+ const checks = entry.Checks || [];
81
+ // Determine status from checks
82
+ let status = types_1.ServiceStatus.UP;
83
+ for (const check of checks) {
84
+ if (check.Status === 'critical') {
85
+ status = types_1.ServiceStatus.DOWN;
86
+ break;
87
+ }
88
+ else if (check.Status === 'warning') {
89
+ status = types_1.ServiceStatus.STARTING;
90
+ }
91
+ }
92
+ return {
93
+ id: service.ID,
94
+ name: service.Service,
95
+ host: service.Address,
96
+ port: service.Port,
97
+ status,
98
+ metadata: service.Meta || {},
99
+ tags: service.Tags || [],
100
+ zone: service.Meta?.zone || undefined,
101
+ lastHeartbeat: new Date(),
102
+ registeredAt: service.Meta?.registeredAt
103
+ ? new Date(service.Meta.registeredAt)
104
+ : new Date(),
105
+ };
106
+ });
107
+ // Apply additional filters
108
+ return (0, filter_1.applyServiceFilter)(instances, filter);
109
+ }
110
+ catch (error) {
111
+ logger.error(`Failed to get instances for service "${serviceName}" from Consul`, error);
112
+ return [];
113
+ }
114
+ }
115
+ /**
116
+ * Get a specific service instance
117
+ */
118
+ async getInstance(instanceId) {
119
+ const logger = logger_1.DiscoveryLogger.getLogger();
120
+ try {
121
+ const services = await this.consul.agent.service.list();
122
+ const service = services[instanceId];
123
+ if (!service) {
124
+ return null;
125
+ }
126
+ // Get health status
127
+ const checks = await this.consul.agent.check.list();
128
+ const checkId = `service:${instanceId}`;
129
+ const check = checks[checkId];
130
+ let status = types_1.ServiceStatus.UP;
131
+ if (check) {
132
+ if (check.Status === 'critical') {
133
+ status = types_1.ServiceStatus.DOWN;
134
+ }
135
+ else if (check.Status === 'warning') {
136
+ status = types_1.ServiceStatus.STARTING;
137
+ }
138
+ }
139
+ return {
140
+ id: service.ID,
141
+ name: service.Service,
142
+ host: service.Address,
143
+ port: service.Port,
144
+ status,
145
+ metadata: service.Meta || {},
146
+ tags: service.Tags || [],
147
+ zone: service.Meta?.zone || undefined,
148
+ lastHeartbeat: new Date(),
149
+ registeredAt: service.Meta?.registeredAt ? new Date(service.Meta.registeredAt) : new Date(),
150
+ };
151
+ }
152
+ catch (error) {
153
+ logger.error(`Failed to get instance "${instanceId}" from Consul`, error);
154
+ return null;
155
+ }
156
+ }
157
+ /**
158
+ * Get all registered services
159
+ */
160
+ async getAllServices() {
161
+ const logger = logger_1.DiscoveryLogger.getLogger();
162
+ try {
163
+ const services = await this.consul.catalog.service.list();
164
+ return Object.keys(services);
165
+ }
166
+ catch (error) {
167
+ logger.error('Failed to list services from Consul', error);
168
+ return [];
169
+ }
170
+ }
171
+ /**
172
+ * Update service instance status
173
+ */
174
+ async updateStatus(instanceId, status) {
175
+ const logger = logger_1.DiscoveryLogger.getLogger();
176
+ const checkId = `service:${instanceId}`;
177
+ try {
178
+ if (status === types_1.ServiceStatus.UP) {
179
+ await this.consul.agent.check.pass(checkId);
180
+ }
181
+ else if (status === types_1.ServiceStatus.DOWN) {
182
+ await this.consul.agent.check.fail(checkId);
183
+ }
184
+ else if (status === types_1.ServiceStatus.STARTING) {
185
+ await this.consul.agent.check.warn(checkId);
186
+ }
187
+ }
188
+ catch (error) {
189
+ logger.warn(`Failed to update status for ${instanceId} in Consul`, error);
190
+ }
191
+ }
192
+ /**
193
+ * Clean up expired instances
194
+ * Consul handles this automatically via TTL checks
195
+ */
196
+ async cleanup() {
197
+ // Consul automatically deregisters services that fail TTL checks
198
+ // No manual cleanup needed
199
+ }
200
+ /**
201
+ * Close Consul connection and stop all TTL checks
202
+ */
203
+ async close() {
204
+ // Stop all TTL check intervals
205
+ for (const [instanceId] of this.checkIntervals) {
206
+ this.stopTTLCheck(instanceId);
207
+ }
208
+ }
209
+ /**
210
+ * Start TTL check updates for a service
211
+ */
212
+ startTTLCheck(instanceId, checkId) {
213
+ const logger = logger_1.DiscoveryLogger.getLogger();
214
+ // Parse TTL to get interval (update at 2/3 of TTL)
215
+ const ttlSeconds = this.parseTTL(this.ttl);
216
+ const intervalMs = (ttlSeconds * 1000 * 2) / 3;
217
+ const interval = setInterval(async () => {
218
+ try {
219
+ await this.consul.agent.check.pass(checkId);
220
+ }
221
+ catch (error) {
222
+ logger.warn(`TTL check pass failed for ${instanceId}`, error);
223
+ }
224
+ }, intervalMs);
225
+ this.checkIntervals.set(instanceId, interval);
226
+ }
227
+ /**
228
+ * Stop TTL check updates for a service
229
+ */
230
+ stopTTLCheck(instanceId) {
231
+ const interval = this.checkIntervals.get(instanceId);
232
+ if (interval) {
233
+ clearInterval(interval);
234
+ this.checkIntervals.delete(instanceId);
235
+ }
236
+ }
237
+ /**
238
+ * Parse TTL string to seconds
239
+ */
240
+ parseTTL(ttl) {
241
+ const match = ttl.match(/^(\d+)([smh])$/);
242
+ if (!match) {
243
+ return 30; // default 30 seconds
244
+ }
245
+ const value = parseInt(match[1]);
246
+ const unit = match[2];
247
+ switch (unit) {
248
+ case 's':
249
+ return value;
250
+ case 'm':
251
+ return value * 60;
252
+ case 'h':
253
+ return value * 3600;
254
+ default:
255
+ return 30;
256
+ }
257
+ }
258
+ }
259
+ exports.ConsulRegistryBackend = ConsulRegistryBackend;