@hazeljs/discovery 0.2.0-beta.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 (59) hide show
  1. package/README.md +281 -0
  2. package/dist/__tests__/decorators.test.d.ts +5 -0
  3. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  4. package/dist/__tests__/decorators.test.js +72 -0
  5. package/dist/__tests__/discovery-client.test.d.ts +5 -0
  6. package/dist/__tests__/discovery-client.test.d.ts.map +1 -0
  7. package/dist/__tests__/discovery-client.test.js +142 -0
  8. package/dist/__tests__/load-balancer-strategies.test.d.ts +5 -0
  9. package/dist/__tests__/load-balancer-strategies.test.d.ts.map +1 -0
  10. package/dist/__tests__/load-balancer-strategies.test.js +234 -0
  11. package/dist/__tests__/memory-backend.test.d.ts +5 -0
  12. package/dist/__tests__/memory-backend.test.d.ts.map +1 -0
  13. package/dist/__tests__/memory-backend.test.js +246 -0
  14. package/dist/__tests__/service-client.test.d.ts +5 -0
  15. package/dist/__tests__/service-client.test.d.ts.map +1 -0
  16. package/dist/__tests__/service-client.test.js +215 -0
  17. package/dist/__tests__/service-registry.test.d.ts +5 -0
  18. package/dist/__tests__/service-registry.test.d.ts.map +1 -0
  19. package/dist/__tests__/service-registry.test.js +65 -0
  20. package/dist/backends/consul-backend.d.ts +76 -0
  21. package/dist/backends/consul-backend.d.ts.map +1 -0
  22. package/dist/backends/consul-backend.js +275 -0
  23. package/dist/backends/kubernetes-backend.d.ts +65 -0
  24. package/dist/backends/kubernetes-backend.d.ts.map +1 -0
  25. package/dist/backends/kubernetes-backend.js +174 -0
  26. package/dist/backends/memory-backend.d.ts +22 -0
  27. package/dist/backends/memory-backend.d.ts.map +1 -0
  28. package/dist/backends/memory-backend.js +115 -0
  29. package/dist/backends/redis-backend.d.ts +71 -0
  30. package/dist/backends/redis-backend.d.ts.map +1 -0
  31. package/dist/backends/redis-backend.js +200 -0
  32. package/dist/backends/registry-backend.d.ts +39 -0
  33. package/dist/backends/registry-backend.d.ts.map +1 -0
  34. package/dist/backends/registry-backend.js +5 -0
  35. package/dist/client/discovery-client.d.ts +47 -0
  36. package/dist/client/discovery-client.d.ts.map +1 -0
  37. package/dist/client/discovery-client.js +123 -0
  38. package/dist/client/service-client.d.ts +52 -0
  39. package/dist/client/service-client.d.ts.map +1 -0
  40. package/dist/client/service-client.js +95 -0
  41. package/dist/decorators/inject-service-client.decorator.d.ts +16 -0
  42. package/dist/decorators/inject-service-client.decorator.d.ts.map +1 -0
  43. package/dist/decorators/inject-service-client.decorator.js +24 -0
  44. package/dist/decorators/service-registry.decorator.d.ts +11 -0
  45. package/dist/decorators/service-registry.decorator.d.ts.map +1 -0
  46. package/dist/decorators/service-registry.decorator.js +20 -0
  47. package/dist/index.d.ts +18 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +44 -0
  50. package/dist/load-balancer/strategies.d.ts +82 -0
  51. package/dist/load-balancer/strategies.d.ts.map +1 -0
  52. package/dist/load-balancer/strategies.js +209 -0
  53. package/dist/registry/service-registry.d.ts +51 -0
  54. package/dist/registry/service-registry.d.ts.map +1 -0
  55. package/dist/registry/service-registry.js +148 -0
  56. package/dist/types/index.d.ts +61 -0
  57. package/dist/types/index.d.ts.map +1 -0
  58. package/dist/types/index.js +14 -0
  59. package/package.json +78 -0
@@ -0,0 +1,215 @@
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
+ expect(mockRequest).toHaveBeenCalledTimes(3); // Default retries
143
+ });
144
+ it('should respect retry delay', async () => {
145
+ const client = new service_client_1.ServiceClient(discoveryClient, {
146
+ serviceName: 'test-service',
147
+ retries: 2,
148
+ retryDelay: 50,
149
+ });
150
+ const instance = createInstance('1');
151
+ await backend.register(instance);
152
+ const mockRequest = jest
153
+ .fn()
154
+ .mockRejectedValueOnce(new Error('Error 1'))
155
+ .mockResolvedValue({ data: {}, status: 200 });
156
+ client.axiosInstance = { request: mockRequest };
157
+ const start = Date.now();
158
+ await client.get('/api/users');
159
+ const duration = Date.now() - start;
160
+ // Should have waited at least 50ms for retry
161
+ expect(duration).toBeGreaterThanOrEqual(40); // Allow some margin
162
+ });
163
+ });
164
+ describe('service discovery integration', () => {
165
+ it('should use discovered service instance', async () => {
166
+ const instance1 = createInstance('1');
167
+ const instance2 = createInstance('2');
168
+ await backend.register(instance1);
169
+ await backend.register(instance2);
170
+ const mockRequest = jest.fn().mockResolvedValue({ data: {}, status: 200 });
171
+ serviceClient.axiosInstance = { request: mockRequest };
172
+ await serviceClient.get('/api/users');
173
+ // Should have used one of the instances
174
+ const call = mockRequest.mock.calls[0][0];
175
+ expect(call.baseURL).toMatch(/http:\/\/localhost:3000/);
176
+ });
177
+ it('should throw error when no instances available', async () => {
178
+ const mockRequest = jest.fn();
179
+ serviceClient.axiosInstance = { request: mockRequest };
180
+ await expect(serviceClient.get('/api/users')).rejects.toThrow('No instances available for service: test-service');
181
+ });
182
+ });
183
+ describe('load balancing strategy', () => {
184
+ it('should use specified load balancing strategy', async () => {
185
+ const instance1 = createInstance('1');
186
+ const instance2 = createInstance('2');
187
+ await backend.register(instance1);
188
+ await backend.register(instance2);
189
+ const client = new service_client_1.ServiceClient(discoveryClient, {
190
+ serviceName: 'test-service',
191
+ loadBalancingStrategy: 'random',
192
+ });
193
+ const mockRequest = jest.fn().mockResolvedValue({ data: {}, status: 200 });
194
+ client.axiosInstance = { request: mockRequest };
195
+ await client.get('/api/users');
196
+ expect(mockRequest).toHaveBeenCalled();
197
+ });
198
+ });
199
+ describe('filters', () => {
200
+ it('should apply service filters', async () => {
201
+ const instance1 = { ...createInstance('1'), zone: 'us-east-1' };
202
+ const instance2 = { ...createInstance('2'), zone: 'us-west-1' };
203
+ await backend.register(instance1);
204
+ await backend.register(instance2);
205
+ const client = new service_client_1.ServiceClient(discoveryClient, {
206
+ serviceName: 'test-service',
207
+ filter: { zone: 'us-east-1' },
208
+ });
209
+ const mockRequest = jest.fn().mockResolvedValue({ data: {}, status: 200 });
210
+ client.axiosInstance = { request: mockRequest };
211
+ await client.get('/api/users');
212
+ expect(mockRequest).toHaveBeenCalled();
213
+ });
214
+ });
215
+ });
@@ -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,76 @@
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
+ type Consul = any;
8
+ export interface ConsulBackendConfig {
9
+ host?: string;
10
+ port?: number;
11
+ secure?: boolean;
12
+ token?: string;
13
+ datacenter?: string;
14
+ ttl?: string;
15
+ }
16
+ export declare class ConsulRegistryBackend implements RegistryBackend {
17
+ private consul;
18
+ private readonly ttl;
19
+ private checkIntervals;
20
+ constructor(consul: Consul, config?: ConsulBackendConfig);
21
+ /**
22
+ * Register a service instance with Consul
23
+ */
24
+ register(instance: ServiceInstance): Promise<void>;
25
+ /**
26
+ * Deregister a service instance
27
+ */
28
+ deregister(instanceId: string): Promise<void>;
29
+ /**
30
+ * Update service instance heartbeat
31
+ */
32
+ heartbeat(instanceId: string): Promise<void>;
33
+ /**
34
+ * Get all instances of a service
35
+ */
36
+ getInstances(serviceName: string, filter?: ServiceFilter): Promise<ServiceInstance[]>;
37
+ /**
38
+ * Get a specific service instance
39
+ */
40
+ getInstance(instanceId: string): Promise<ServiceInstance | null>;
41
+ /**
42
+ * Get all registered services
43
+ */
44
+ getAllServices(): Promise<string[]>;
45
+ /**
46
+ * Update service instance status
47
+ */
48
+ updateStatus(instanceId: string, status: string): Promise<void>;
49
+ /**
50
+ * Clean up expired instances
51
+ * Consul handles this automatically via TTL checks
52
+ */
53
+ cleanup(): Promise<void>;
54
+ /**
55
+ * Close Consul connection and stop all TTL checks
56
+ */
57
+ close(): Promise<void>;
58
+ /**
59
+ * Start TTL check updates for a service
60
+ */
61
+ private startTTLCheck;
62
+ /**
63
+ * Stop TTL check updates for a service
64
+ */
65
+ private stopTTLCheck;
66
+ /**
67
+ * Parse TTL string to seconds
68
+ */
69
+ private parseTTL;
70
+ /**
71
+ * Apply filter to instances
72
+ */
73
+ private applyFilter;
74
+ }
75
+ export {};
76
+ //# 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":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAIzE,KAAK,MAAM,GAAG,GAAG,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,cAAc,CAA0C;gBAEpD,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,mBAAwB;IAK5D;;OAEG;IACG,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BxD;;OAEG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA6D3F;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAwCtE;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IASzC;;OAEG;IACG,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBrE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAK9B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;OAEG;IACH,OAAO,CAAC,aAAa;IAgBrB;;OAEG;IACH,OAAO,CAAC,YAAY;IAQpB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAqBhB;;OAEG;IACH,OAAO,CAAC,WAAW;CA2BpB"}
@@ -0,0 +1,275 @@
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
+ class ConsulRegistryBackend {
10
+ constructor(consul, config = {}) {
11
+ this.checkIntervals = new Map();
12
+ this.consul = consul;
13
+ this.ttl = config.ttl || '30s';
14
+ }
15
+ /**
16
+ * Register a service instance with Consul
17
+ */
18
+ async register(instance) {
19
+ const serviceId = instance.id;
20
+ const checkId = `service:${serviceId}`;
21
+ // Register service
22
+ await this.consul.agent.service.register({
23
+ id: serviceId,
24
+ name: instance.name,
25
+ address: instance.host,
26
+ port: instance.port,
27
+ tags: instance.tags || [],
28
+ meta: {
29
+ ...instance.metadata,
30
+ zone: instance.zone || '',
31
+ registeredAt: instance.registeredAt.toISOString(),
32
+ },
33
+ check: {
34
+ ttl: this.ttl,
35
+ deregister_critical_service_after: '90s',
36
+ },
37
+ });
38
+ // Start TTL check updates
39
+ this.startTTLCheck(serviceId, checkId);
40
+ }
41
+ /**
42
+ * Deregister a service instance
43
+ */
44
+ async deregister(instanceId) {
45
+ // Stop TTL check
46
+ this.stopTTLCheck(instanceId);
47
+ // Deregister from Consul
48
+ await this.consul.agent.service.deregister(instanceId);
49
+ }
50
+ /**
51
+ * Update service instance heartbeat
52
+ */
53
+ async heartbeat(instanceId) {
54
+ const checkId = `service:${instanceId}`;
55
+ try {
56
+ // Pass TTL check
57
+ await this.consul.agent.check.pass(checkId);
58
+ }
59
+ catch {
60
+ // Silently fail - will be retried on next heartbeat
61
+ }
62
+ }
63
+ /**
64
+ * Get all instances of a service
65
+ */
66
+ async getInstances(serviceName, filter) {
67
+ try {
68
+ const result = await this.consul.health.service({
69
+ service: serviceName,
70
+ passing: filter?.status === types_1.ServiceStatus.UP,
71
+ });
72
+ const instances = result.map((entry) => {
73
+ const service = entry.Service;
74
+ const checks = entry.Checks || [];
75
+ // Determine status from checks
76
+ let status = types_1.ServiceStatus.UP;
77
+ for (const check of checks) {
78
+ if (check.Status === 'critical') {
79
+ status = types_1.ServiceStatus.DOWN;
80
+ break;
81
+ }
82
+ else if (check.Status === 'warning') {
83
+ status = types_1.ServiceStatus.STARTING;
84
+ }
85
+ }
86
+ return {
87
+ id: service.ID,
88
+ name: service.Service,
89
+ host: service.Address,
90
+ port: service.Port,
91
+ status,
92
+ metadata: service.Meta || {},
93
+ tags: service.Tags || [],
94
+ zone: service.Meta?.zone || undefined,
95
+ lastHeartbeat: new Date(),
96
+ registeredAt: service.Meta?.registeredAt
97
+ ? new Date(service.Meta.registeredAt)
98
+ : new Date(),
99
+ };
100
+ });
101
+ // Apply additional filters
102
+ if (filter) {
103
+ return this.applyFilter(instances, filter);
104
+ }
105
+ return instances;
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ }
111
+ /**
112
+ * Get a specific service instance
113
+ */
114
+ async getInstance(instanceId) {
115
+ try {
116
+ const services = await this.consul.agent.service.list();
117
+ const service = services[instanceId];
118
+ if (!service) {
119
+ return null;
120
+ }
121
+ // Get health status
122
+ const checks = await this.consul.agent.check.list();
123
+ const checkId = `service:${instanceId}`;
124
+ const check = checks[checkId];
125
+ let status = types_1.ServiceStatus.UP;
126
+ if (check) {
127
+ if (check.Status === 'critical') {
128
+ status = types_1.ServiceStatus.DOWN;
129
+ }
130
+ else if (check.Status === 'warning') {
131
+ status = types_1.ServiceStatus.STARTING;
132
+ }
133
+ }
134
+ return {
135
+ id: service.ID,
136
+ name: service.Service,
137
+ host: service.Address,
138
+ port: service.Port,
139
+ status,
140
+ metadata: service.Meta || {},
141
+ tags: service.Tags || [],
142
+ zone: service.Meta?.zone || undefined,
143
+ lastHeartbeat: new Date(),
144
+ registeredAt: service.Meta?.registeredAt ? new Date(service.Meta.registeredAt) : new Date(),
145
+ };
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * Get all registered services
153
+ */
154
+ async getAllServices() {
155
+ try {
156
+ const services = await this.consul.catalog.service.list();
157
+ return Object.keys(services);
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ }
163
+ /**
164
+ * Update service instance status
165
+ */
166
+ async updateStatus(instanceId, status) {
167
+ const checkId = `service:${instanceId}`;
168
+ try {
169
+ if (status === types_1.ServiceStatus.UP) {
170
+ await this.consul.agent.check.pass(checkId);
171
+ }
172
+ else if (status === types_1.ServiceStatus.DOWN) {
173
+ await this.consul.agent.check.fail(checkId);
174
+ }
175
+ else if (status === types_1.ServiceStatus.STARTING) {
176
+ await this.consul.agent.check.warn(checkId);
177
+ }
178
+ }
179
+ catch {
180
+ // Silently fail - status will be updated on next heartbeat
181
+ }
182
+ }
183
+ /**
184
+ * Clean up expired instances
185
+ * Consul handles this automatically via TTL checks
186
+ */
187
+ async cleanup() {
188
+ // Consul automatically deregisters services that fail TTL checks
189
+ // No manual cleanup needed
190
+ }
191
+ /**
192
+ * Close Consul connection and stop all TTL checks
193
+ */
194
+ async close() {
195
+ // Stop all TTL check intervals
196
+ for (const [instanceId] of this.checkIntervals) {
197
+ this.stopTTLCheck(instanceId);
198
+ }
199
+ }
200
+ /**
201
+ * Start TTL check updates for a service
202
+ */
203
+ startTTLCheck(instanceId, checkId) {
204
+ // Parse TTL to get interval (update at 2/3 of TTL)
205
+ const ttlSeconds = this.parseTTL(this.ttl);
206
+ const intervalMs = (ttlSeconds * 1000 * 2) / 3;
207
+ const interval = setInterval(async () => {
208
+ try {
209
+ await this.consul.agent.check.pass(checkId);
210
+ }
211
+ catch {
212
+ // Silently fail - will be retried on next interval
213
+ }
214
+ }, intervalMs);
215
+ this.checkIntervals.set(instanceId, interval);
216
+ }
217
+ /**
218
+ * Stop TTL check updates for a service
219
+ */
220
+ stopTTLCheck(instanceId) {
221
+ const interval = this.checkIntervals.get(instanceId);
222
+ if (interval) {
223
+ clearInterval(interval);
224
+ this.checkIntervals.delete(instanceId);
225
+ }
226
+ }
227
+ /**
228
+ * Parse TTL string to seconds
229
+ */
230
+ parseTTL(ttl) {
231
+ const match = ttl.match(/^(\d+)([smh])$/);
232
+ if (!match) {
233
+ return 30; // default 30 seconds
234
+ }
235
+ const value = parseInt(match[1]);
236
+ const unit = match[2];
237
+ switch (unit) {
238
+ case 's':
239
+ return value;
240
+ case 'm':
241
+ return value * 60;
242
+ case 'h':
243
+ return value * 3600;
244
+ default:
245
+ return 30;
246
+ }
247
+ }
248
+ /**
249
+ * Apply filter to instances
250
+ */
251
+ applyFilter(instances, filter) {
252
+ return instances.filter((instance) => {
253
+ if (filter.zone && instance.zone !== filter.zone) {
254
+ return false;
255
+ }
256
+ if (filter.status && instance.status !== filter.status) {
257
+ return false;
258
+ }
259
+ if (filter.tags && filter.tags.length > 0) {
260
+ if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
261
+ return false;
262
+ }
263
+ }
264
+ if (filter.metadata) {
265
+ for (const [key, value] of Object.entries(filter.metadata)) {
266
+ if (!instance.metadata || instance.metadata[key] !== value) {
267
+ return false;
268
+ }
269
+ }
270
+ }
271
+ return true;
272
+ });
273
+ }
274
+ }
275
+ exports.ConsulRegistryBackend = ConsulRegistryBackend;