@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,246 @@
1
+ "use strict";
2
+ /**
3
+ * Memory Backend Tests
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const memory_backend_1 = require("../backends/memory-backend");
7
+ const types_1 = require("../types");
8
+ describe('MemoryRegistryBackend', () => {
9
+ let backend;
10
+ beforeEach(() => {
11
+ backend = new memory_backend_1.MemoryRegistryBackend();
12
+ });
13
+ const createInstance = (id, name = 'test-service') => ({
14
+ id,
15
+ name,
16
+ host: 'localhost',
17
+ port: 3000,
18
+ status: types_1.ServiceStatus.UP,
19
+ lastHeartbeat: new Date(),
20
+ registeredAt: new Date(),
21
+ });
22
+ describe('register', () => {
23
+ it('should register a service instance', async () => {
24
+ const instance = createInstance('1');
25
+ await backend.register(instance);
26
+ const retrieved = await backend.getInstance('1');
27
+ expect(retrieved).toEqual(instance);
28
+ });
29
+ it('should index instances by service name', async () => {
30
+ const instance1 = createInstance('1', 'service-a');
31
+ const instance2 = createInstance('2', 'service-a');
32
+ const instance3 = createInstance('3', 'service-b');
33
+ await backend.register(instance1);
34
+ await backend.register(instance2);
35
+ await backend.register(instance3);
36
+ const instancesA = await backend.getInstances('service-a');
37
+ const instancesB = await backend.getInstances('service-b');
38
+ expect(instancesA).toHaveLength(2);
39
+ expect(instancesB).toHaveLength(1);
40
+ });
41
+ });
42
+ describe('deregister', () => {
43
+ it('should remove a registered instance', async () => {
44
+ const instance = createInstance('1');
45
+ await backend.register(instance);
46
+ await backend.deregister('1');
47
+ const retrieved = await backend.getInstance('1');
48
+ expect(retrieved).toBeNull();
49
+ });
50
+ it('should clean up service index when last instance is removed', async () => {
51
+ const instance = createInstance('1', 'service-a');
52
+ await backend.register(instance);
53
+ const servicesBefore = await backend.getAllServices();
54
+ expect(servicesBefore).toContain('service-a');
55
+ await backend.deregister('1');
56
+ const servicesAfter = await backend.getAllServices();
57
+ expect(servicesAfter).not.toContain('service-a');
58
+ });
59
+ });
60
+ describe('heartbeat', () => {
61
+ it('should update lastHeartbeat and set status to UP', async () => {
62
+ const instance = createInstance('1');
63
+ instance.status = types_1.ServiceStatus.DOWN;
64
+ await backend.register(instance);
65
+ const before = await backend.getInstance('1');
66
+ const beforeTime = before.lastHeartbeat.getTime();
67
+ await new Promise((resolve) => setTimeout(resolve, 10));
68
+ await backend.heartbeat('1');
69
+ const after = await backend.getInstance('1');
70
+ expect(after.lastHeartbeat.getTime()).toBeGreaterThan(beforeTime);
71
+ expect(after.status).toBe(types_1.ServiceStatus.UP);
72
+ });
73
+ });
74
+ describe('getInstances', () => {
75
+ it('should return all instances for a service', async () => {
76
+ const instance1 = createInstance('1', 'service-a');
77
+ const instance2 = createInstance('2', 'service-a');
78
+ const instance3 = createInstance('3', 'service-b');
79
+ await backend.register(instance1);
80
+ await backend.register(instance2);
81
+ await backend.register(instance3);
82
+ const instances = await backend.getInstances('service-a');
83
+ expect(instances).toHaveLength(2);
84
+ expect(instances.map((i) => i.id)).toEqual(['1', '2']);
85
+ });
86
+ it('should return empty array for non-existent service', async () => {
87
+ const instances = await backend.getInstances('non-existent');
88
+ expect(instances).toEqual([]);
89
+ });
90
+ it('should filter by zone', async () => {
91
+ const instance1 = { ...createInstance('1'), zone: 'us-east-1' };
92
+ const instance2 = { ...createInstance('2'), zone: 'us-west-1' };
93
+ const instance3 = { ...createInstance('3'), zone: 'us-east-1' };
94
+ await backend.register(instance1);
95
+ await backend.register(instance2);
96
+ await backend.register(instance3);
97
+ const instances = await backend.getInstances('test-service', {
98
+ zone: 'us-east-1',
99
+ });
100
+ expect(instances).toHaveLength(2);
101
+ expect(instances.every((i) => i.zone === 'us-east-1')).toBe(true);
102
+ });
103
+ it('should filter by status', async () => {
104
+ const instance1 = { ...createInstance('1'), status: types_1.ServiceStatus.UP };
105
+ const instance2 = { ...createInstance('2'), status: types_1.ServiceStatus.DOWN };
106
+ const instance3 = { ...createInstance('3'), status: types_1.ServiceStatus.UP };
107
+ await backend.register(instance1);
108
+ await backend.register(instance2);
109
+ await backend.register(instance3);
110
+ const instances = await backend.getInstances('test-service', {
111
+ status: types_1.ServiceStatus.UP,
112
+ });
113
+ expect(instances).toHaveLength(2);
114
+ expect(instances.every((i) => i.status === types_1.ServiceStatus.UP)).toBe(true);
115
+ });
116
+ it('should filter by tags', async () => {
117
+ const instance1 = { ...createInstance('1'), tags: ['web', 'api'] };
118
+ const instance2 = { ...createInstance('2'), tags: ['api'] };
119
+ const instance3 = { ...createInstance('3'), tags: ['web'] };
120
+ await backend.register(instance1);
121
+ await backend.register(instance2);
122
+ await backend.register(instance3);
123
+ const instances = await backend.getInstances('test-service', {
124
+ tags: ['web', 'api'],
125
+ });
126
+ expect(instances).toHaveLength(1);
127
+ expect(instances[0].id).toBe('1');
128
+ });
129
+ it('should filter by metadata', async () => {
130
+ const instance1 = {
131
+ ...createInstance('1'),
132
+ metadata: { version: '1.0.0', env: 'prod' },
133
+ };
134
+ const instance2 = {
135
+ ...createInstance('2'),
136
+ metadata: { version: '2.0.0', env: 'prod' },
137
+ };
138
+ const instance3 = {
139
+ ...createInstance('3'),
140
+ metadata: { version: '1.0.0', env: 'dev' },
141
+ };
142
+ await backend.register(instance1);
143
+ await backend.register(instance2);
144
+ await backend.register(instance3);
145
+ const instances = await backend.getInstances('test-service', {
146
+ metadata: { version: '1.0.0' },
147
+ });
148
+ expect(instances).toHaveLength(2);
149
+ expect(instances.every((i) => i.metadata?.version === '1.0.0')).toBe(true);
150
+ });
151
+ it('should combine multiple filters', async () => {
152
+ const instance1 = {
153
+ ...createInstance('1'),
154
+ zone: 'us-east-1',
155
+ status: types_1.ServiceStatus.UP,
156
+ tags: ['web'],
157
+ };
158
+ const instance2 = {
159
+ ...createInstance('2'),
160
+ zone: 'us-east-1',
161
+ status: types_1.ServiceStatus.DOWN,
162
+ tags: ['web'],
163
+ };
164
+ const instance3 = {
165
+ ...createInstance('3'),
166
+ zone: 'us-west-1',
167
+ status: types_1.ServiceStatus.UP,
168
+ tags: ['web'],
169
+ };
170
+ await backend.register(instance1);
171
+ await backend.register(instance2);
172
+ await backend.register(instance3);
173
+ const instances = await backend.getInstances('test-service', {
174
+ zone: 'us-east-1',
175
+ status: types_1.ServiceStatus.UP,
176
+ tags: ['web'],
177
+ });
178
+ expect(instances).toHaveLength(1);
179
+ expect(instances[0].id).toBe('1');
180
+ });
181
+ });
182
+ describe('getInstance', () => {
183
+ it('should return instance by ID', async () => {
184
+ const instance = createInstance('1');
185
+ await backend.register(instance);
186
+ const retrieved = await backend.getInstance('1');
187
+ expect(retrieved).toEqual(instance);
188
+ });
189
+ it('should return null for non-existent instance', async () => {
190
+ const retrieved = await backend.getInstance('non-existent');
191
+ expect(retrieved).toBeNull();
192
+ });
193
+ });
194
+ describe('getAllServices', () => {
195
+ it('should return all registered service names', async () => {
196
+ await backend.register(createInstance('1', 'service-a'));
197
+ await backend.register(createInstance('2', 'service-a'));
198
+ await backend.register(createInstance('3', 'service-b'));
199
+ const services = await backend.getAllServices();
200
+ expect(services.sort()).toEqual(['service-a', 'service-b']);
201
+ });
202
+ it('should return empty array when no services registered', async () => {
203
+ const services = await backend.getAllServices();
204
+ expect(services).toEqual([]);
205
+ });
206
+ });
207
+ describe('updateStatus', () => {
208
+ it('should update instance status', async () => {
209
+ const instance = createInstance('1');
210
+ await backend.register(instance);
211
+ await backend.updateStatus('1', types_1.ServiceStatus.DOWN);
212
+ const updated = await backend.getInstance('1');
213
+ expect(updated.status).toBe(types_1.ServiceStatus.DOWN);
214
+ await backend.updateStatus('1', types_1.ServiceStatus.UP);
215
+ const updated2 = await backend.getInstance('1');
216
+ expect(updated2.status).toBe(types_1.ServiceStatus.UP);
217
+ });
218
+ it('should not throw error for non-existent instance', async () => {
219
+ await expect(backend.updateStatus('non-existent', types_1.ServiceStatus.DOWN)).resolves.not.toThrow();
220
+ });
221
+ });
222
+ describe('cleanup', () => {
223
+ it('should remove expired instances', async () => {
224
+ const backend = new memory_backend_1.MemoryRegistryBackend(100); // 100ms expiration
225
+ const instance = createInstance('1');
226
+ await backend.register(instance);
227
+ // Wait for expiration
228
+ await new Promise((resolve) => setTimeout(resolve, 150));
229
+ await backend.cleanup();
230
+ const retrieved = await backend.getInstance('1');
231
+ expect(retrieved).toBeNull();
232
+ });
233
+ it('should not remove active instances', async () => {
234
+ const backend = new memory_backend_1.MemoryRegistryBackend(100);
235
+ const instance = createInstance('1');
236
+ await backend.register(instance);
237
+ // Update heartbeat before expiration
238
+ await new Promise((resolve) => setTimeout(resolve, 50));
239
+ await backend.heartbeat('1');
240
+ await new Promise((resolve) => setTimeout(resolve, 50));
241
+ await backend.cleanup();
242
+ const retrieved = await backend.getInstance('1');
243
+ expect(retrieved).toBeDefined();
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Redis Backend Tests
3
+ * Uses mocked ioredis client
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=redis-backend.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/redis-backend.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ /**
3
+ * Redis Backend Tests
4
+ * Uses mocked ioredis client
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const redis_backend_1 = require("../backends/redis-backend");
8
+ const types_1 = require("../types");
9
+ const logger_1 = require("../utils/logger");
10
+ // Suppress console logs during tests
11
+ beforeAll(() => {
12
+ logger_1.DiscoveryLogger.setLogger({
13
+ debug: jest.fn(),
14
+ info: jest.fn(),
15
+ warn: jest.fn(),
16
+ error: jest.fn(),
17
+ });
18
+ });
19
+ afterAll(() => {
20
+ logger_1.DiscoveryLogger.resetLogger();
21
+ });
22
+ /** Helper to create a mock Redis instance */
23
+ function createMockRedis() {
24
+ const store = new Map();
25
+ const sets = new Map();
26
+ const ttls = new Map();
27
+ const listeners = new Map();
28
+ const mock = {
29
+ on: jest.fn((event, handler) => {
30
+ if (!listeners.has(event))
31
+ listeners.set(event, []);
32
+ listeners.get(event).push(handler);
33
+ }),
34
+ setex: jest.fn(async (key, ttl, value) => {
35
+ store.set(key, value);
36
+ ttls.set(key, ttl);
37
+ }),
38
+ get: jest.fn(async (key) => store.get(key) || null),
39
+ del: jest.fn(async (key) => {
40
+ store.delete(key);
41
+ ttls.delete(key);
42
+ return 1;
43
+ }),
44
+ mget: jest.fn(async (...keys) => {
45
+ // mget can receive keys as individual args or a single array
46
+ const flatKeys = keys.flat();
47
+ return flatKeys.map((k) => store.get(k) || null);
48
+ }),
49
+ sadd: jest.fn(async (key, member) => {
50
+ if (!sets.has(key))
51
+ sets.set(key, new Set());
52
+ sets.get(key).add(member);
53
+ return 1;
54
+ }),
55
+ srem: jest.fn(async (key, member) => {
56
+ sets.get(key)?.delete(member);
57
+ return 1;
58
+ }),
59
+ smembers: jest.fn(async (key) => {
60
+ return Array.from(sets.get(key) || []);
61
+ }),
62
+ scard: jest.fn(async (key) => {
63
+ return sets.get(key)?.size || 0;
64
+ }),
65
+ exists: jest.fn(async (key) => {
66
+ return store.has(key) ? 1 : 0;
67
+ }),
68
+ expire: jest.fn(async () => 1),
69
+ scan: jest.fn(async (_cursor, _match, pattern) => {
70
+ const keys = Array.from(sets.keys()).filter((k) => {
71
+ // Simple glob match: convert "prefix*" to regex
72
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
73
+ return regex.test(k);
74
+ });
75
+ return ['0', keys];
76
+ }),
77
+ quit: jest.fn(async () => 'OK'),
78
+ // Helper to emit events during tests
79
+ _emit: (event, ...args) => {
80
+ const handlers = listeners.get(event) || [];
81
+ handlers.forEach((h) => h(...args));
82
+ },
83
+ };
84
+ return mock;
85
+ }
86
+ function createInstance(id, name = 'test-service', overrides = {}) {
87
+ return {
88
+ id,
89
+ name,
90
+ host: 'localhost',
91
+ port: 3000,
92
+ status: types_1.ServiceStatus.UP,
93
+ lastHeartbeat: new Date('2025-01-01T00:00:00Z'),
94
+ registeredAt: new Date('2025-01-01T00:00:00Z'),
95
+ ...overrides,
96
+ };
97
+ }
98
+ describe('RedisRegistryBackend', () => {
99
+ let redis;
100
+ let backend;
101
+ beforeEach(() => {
102
+ redis = createMockRedis();
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ backend = new redis_backend_1.RedisRegistryBackend(redis);
105
+ });
106
+ describe('constructor', () => {
107
+ it('should use default config values', () => {
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ const b = new redis_backend_1.RedisRegistryBackend(redis);
110
+ expect(b).toBeDefined();
111
+ });
112
+ it('should accept custom config', () => {
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ const b = new redis_backend_1.RedisRegistryBackend(redis, {
115
+ keyPrefix: 'custom:',
116
+ ttl: 120,
117
+ });
118
+ expect(b).toBeDefined();
119
+ });
120
+ it('should throw on invalid TTL', () => {
121
+ expect(() => {
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ new redis_backend_1.RedisRegistryBackend(redis, { ttl: -5 });
124
+ }).toThrow();
125
+ });
126
+ it('should register connection event handlers', () => {
127
+ expect(redis.on).toHaveBeenCalledWith('error', expect.any(Function));
128
+ expect(redis.on).toHaveBeenCalledWith('connect', expect.any(Function));
129
+ expect(redis.on).toHaveBeenCalledWith('reconnecting', expect.any(Function));
130
+ expect(redis.on).toHaveBeenCalledWith('close', expect.any(Function));
131
+ });
132
+ });
133
+ describe('register', () => {
134
+ it('should store the instance with TTL', async () => {
135
+ const instance = createInstance('1');
136
+ await backend.register(instance);
137
+ expect(redis.setex).toHaveBeenCalledWith('hazeljs:discovery:instance:1', 90, JSON.stringify(instance));
138
+ });
139
+ it('should add instance to service set', async () => {
140
+ const instance = createInstance('1', 'my-service');
141
+ await backend.register(instance);
142
+ expect(redis.sadd).toHaveBeenCalledWith('hazeljs:discovery:service:my-service', '1');
143
+ });
144
+ it('should set expiration on service set', async () => {
145
+ const instance = createInstance('1');
146
+ await backend.register(instance);
147
+ expect(redis.expire).toHaveBeenCalledWith('hazeljs:discovery:service:test-service', 180);
148
+ });
149
+ });
150
+ describe('deregister', () => {
151
+ it('should remove instance from Redis', async () => {
152
+ const instance = createInstance('1');
153
+ await backend.register(instance);
154
+ await backend.deregister('1');
155
+ expect(redis.srem).toHaveBeenCalledWith('hazeljs:discovery:service:test-service', '1');
156
+ expect(redis.del).toHaveBeenCalledWith('hazeljs:discovery:instance:1');
157
+ });
158
+ it('should do nothing if instance does not exist', async () => {
159
+ await backend.deregister('non-existent');
160
+ expect(redis.del).not.toHaveBeenCalled();
161
+ });
162
+ });
163
+ describe('heartbeat', () => {
164
+ it('should update heartbeat timestamp and status', async () => {
165
+ const instance = createInstance('1');
166
+ instance.status = types_1.ServiceStatus.DOWN;
167
+ await backend.register(instance);
168
+ await backend.heartbeat('1');
169
+ // Should have been called again with updated data
170
+ expect(redis.setex).toHaveBeenCalledTimes(2);
171
+ const lastCall = redis.setex.mock.calls[1];
172
+ const storedInstance = JSON.parse(lastCall[2]);
173
+ expect(storedInstance.status).toBe(types_1.ServiceStatus.UP);
174
+ });
175
+ it('should do nothing if instance does not exist', async () => {
176
+ await backend.heartbeat('non-existent');
177
+ // setex should not be called
178
+ expect(redis.setex).not.toHaveBeenCalled();
179
+ });
180
+ });
181
+ describe('getInstances', () => {
182
+ it('should return all instances for a service using MGET', async () => {
183
+ const i1 = createInstance('1', 'svc');
184
+ const i2 = createInstance('2', 'svc');
185
+ await backend.register(i1);
186
+ await backend.register(i2);
187
+ const instances = await backend.getInstances('svc');
188
+ expect(instances).toHaveLength(2);
189
+ expect(redis.mget).toHaveBeenCalled();
190
+ });
191
+ it('should return empty array for non-existent service', async () => {
192
+ const instances = await backend.getInstances('non-existent');
193
+ expect(instances).toEqual([]);
194
+ });
195
+ it('should apply filter', async () => {
196
+ const i1 = createInstance('1', 'svc', { zone: 'us-east-1' });
197
+ const i2 = createInstance('2', 'svc', { zone: 'us-west-1' });
198
+ await backend.register(i1);
199
+ await backend.register(i2);
200
+ const instances = await backend.getInstances('svc', { zone: 'us-east-1' });
201
+ expect(instances).toHaveLength(1);
202
+ expect(instances[0].zone).toBe('us-east-1');
203
+ });
204
+ });
205
+ describe('getInstance', () => {
206
+ it('should return a specific instance', async () => {
207
+ const instance = createInstance('1');
208
+ await backend.register(instance);
209
+ const retrieved = await backend.getInstance('1');
210
+ expect(retrieved).toBeDefined();
211
+ expect(retrieved.id).toBe('1');
212
+ });
213
+ it('should return null for non-existent instance', async () => {
214
+ const result = await backend.getInstance('non-existent');
215
+ expect(result).toBeNull();
216
+ });
217
+ it('should convert dates back from strings', async () => {
218
+ const instance = createInstance('1');
219
+ await backend.register(instance);
220
+ const retrieved = await backend.getInstance('1');
221
+ expect(retrieved.lastHeartbeat).toBeInstanceOf(Date);
222
+ expect(retrieved.registeredAt).toBeInstanceOf(Date);
223
+ });
224
+ });
225
+ describe('getAllServices', () => {
226
+ it('should use SCAN instead of KEYS', async () => {
227
+ const i1 = createInstance('1', 'svc-a');
228
+ const i2 = createInstance('2', 'svc-b');
229
+ await backend.register(i1);
230
+ await backend.register(i2);
231
+ const services = await backend.getAllServices();
232
+ expect(redis.scan).toHaveBeenCalled();
233
+ expect(services).toHaveLength(2);
234
+ });
235
+ });
236
+ describe('updateStatus', () => {
237
+ it('should update the status of an instance', async () => {
238
+ const instance = createInstance('1');
239
+ await backend.register(instance);
240
+ await backend.updateStatus('1', types_1.ServiceStatus.DOWN);
241
+ const retrieved = await backend.getInstance('1');
242
+ expect(retrieved.status).toBe(types_1.ServiceStatus.DOWN);
243
+ });
244
+ it('should do nothing for non-existent instance', async () => {
245
+ await backend.updateStatus('non-existent', types_1.ServiceStatus.DOWN);
246
+ expect(redis.setex).not.toHaveBeenCalled();
247
+ });
248
+ });
249
+ describe('cleanup', () => {
250
+ it('should remove stale entries from service sets', async () => {
251
+ const instance = createInstance('1', 'svc');
252
+ await backend.register(instance);
253
+ // Simulate the instance key expiring in Redis
254
+ redis.exists.mockResolvedValueOnce(0);
255
+ await backend.cleanup();
256
+ expect(redis.srem).toHaveBeenCalledWith('hazeljs:discovery:service:svc', '1');
257
+ });
258
+ });
259
+ describe('connection error handling', () => {
260
+ it('should track connection state via events', () => {
261
+ redis._emit('close');
262
+ // Backend should mark as disconnected
263
+ // Next operation should throw
264
+ expect(backend.getInstances('svc')).rejects.toThrow('Redis backend is not connected');
265
+ });
266
+ it('should recover on connect event', async () => {
267
+ redis._emit('close');
268
+ redis._emit('connect');
269
+ // Should work again
270
+ const result = await backend.getInstances('svc');
271
+ expect(result).toEqual([]);
272
+ });
273
+ });
274
+ describe('close', () => {
275
+ it('should call redis.quit()', async () => {
276
+ await backend.close();
277
+ expect(redis.quit).toHaveBeenCalled();
278
+ });
279
+ });
280
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Service Client Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=service-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service-client.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}