@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.
- package/README.md +281 -0
- package/dist/__tests__/decorators.test.d.ts +5 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +72 -0
- package/dist/__tests__/discovery-client.test.d.ts +5 -0
- package/dist/__tests__/discovery-client.test.d.ts.map +1 -0
- package/dist/__tests__/discovery-client.test.js +142 -0
- package/dist/__tests__/load-balancer-strategies.test.d.ts +5 -0
- package/dist/__tests__/load-balancer-strategies.test.d.ts.map +1 -0
- package/dist/__tests__/load-balancer-strategies.test.js +234 -0
- package/dist/__tests__/memory-backend.test.d.ts +5 -0
- package/dist/__tests__/memory-backend.test.d.ts.map +1 -0
- package/dist/__tests__/memory-backend.test.js +246 -0
- package/dist/__tests__/service-client.test.d.ts +5 -0
- package/dist/__tests__/service-client.test.d.ts.map +1 -0
- package/dist/__tests__/service-client.test.js +215 -0
- package/dist/__tests__/service-registry.test.d.ts +5 -0
- package/dist/__tests__/service-registry.test.d.ts.map +1 -0
- package/dist/__tests__/service-registry.test.js +65 -0
- package/dist/backends/consul-backend.d.ts +76 -0
- package/dist/backends/consul-backend.d.ts.map +1 -0
- package/dist/backends/consul-backend.js +275 -0
- package/dist/backends/kubernetes-backend.d.ts +65 -0
- package/dist/backends/kubernetes-backend.d.ts.map +1 -0
- package/dist/backends/kubernetes-backend.js +174 -0
- package/dist/backends/memory-backend.d.ts +22 -0
- package/dist/backends/memory-backend.d.ts.map +1 -0
- package/dist/backends/memory-backend.js +115 -0
- package/dist/backends/redis-backend.d.ts +71 -0
- package/dist/backends/redis-backend.d.ts.map +1 -0
- package/dist/backends/redis-backend.js +200 -0
- package/dist/backends/registry-backend.d.ts +39 -0
- package/dist/backends/registry-backend.d.ts.map +1 -0
- package/dist/backends/registry-backend.js +5 -0
- package/dist/client/discovery-client.d.ts +47 -0
- package/dist/client/discovery-client.d.ts.map +1 -0
- package/dist/client/discovery-client.js +123 -0
- package/dist/client/service-client.d.ts +52 -0
- package/dist/client/service-client.d.ts.map +1 -0
- package/dist/client/service-client.js +95 -0
- package/dist/decorators/inject-service-client.decorator.d.ts +16 -0
- package/dist/decorators/inject-service-client.decorator.d.ts.map +1 -0
- package/dist/decorators/inject-service-client.decorator.js +24 -0
- package/dist/decorators/service-registry.decorator.d.ts +11 -0
- package/dist/decorators/service-registry.decorator.d.ts.map +1 -0
- package/dist/decorators/service-registry.decorator.js +20 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/load-balancer/strategies.d.ts +82 -0
- package/dist/load-balancer/strategies.d.ts.map +1 -0
- package/dist/load-balancer/strategies.js +209 -0
- package/dist/registry/service-registry.d.ts +51 -0
- package/dist/registry/service-registry.d.ts.map +1 -0
- package/dist/registry/service-registry.js +148 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- 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 @@
|
|
|
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;
|