@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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes Registry Backend
|
|
3
|
+
* Uses Kubernetes Service Discovery
|
|
4
|
+
*/
|
|
5
|
+
import { RegistryBackend } from './registry-backend';
|
|
6
|
+
import { ServiceInstance, ServiceFilter } from '../types';
|
|
7
|
+
type KubeConfig = any;
|
|
8
|
+
export interface KubernetesBackendConfig {
|
|
9
|
+
namespace?: string;
|
|
10
|
+
labelSelector?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class KubernetesRegistryBackend implements RegistryBackend {
|
|
13
|
+
private k8sApi;
|
|
14
|
+
private readonly namespace;
|
|
15
|
+
private readonly labelSelector;
|
|
16
|
+
constructor(kubeConfig: KubeConfig, config?: KubernetesBackendConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Register a service instance
|
|
19
|
+
* In Kubernetes, services are registered via Service/Endpoints objects
|
|
20
|
+
* This is typically handled by K8s itself
|
|
21
|
+
*/
|
|
22
|
+
register(_instance: ServiceInstance): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Deregister a service instance
|
|
25
|
+
* In Kubernetes, this is handled automatically when pods terminate
|
|
26
|
+
*/
|
|
27
|
+
deregister(_instanceId: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Update service instance heartbeat
|
|
30
|
+
* Kubernetes handles liveness/readiness probes
|
|
31
|
+
*/
|
|
32
|
+
heartbeat(_instanceId: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Get all instances of a service from Kubernetes Endpoints
|
|
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 in the namespace
|
|
43
|
+
*/
|
|
44
|
+
getAllServices(): Promise<string[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Update service instance status
|
|
47
|
+
* In Kubernetes, status is managed by readiness probes
|
|
48
|
+
*/
|
|
49
|
+
updateStatus(_instanceId: string, _status: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Clean up expired instances
|
|
52
|
+
* Kubernetes handles this automatically
|
|
53
|
+
*/
|
|
54
|
+
cleanup(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Create a ServiceInstance from Kubernetes endpoint data
|
|
57
|
+
*/
|
|
58
|
+
private createServiceInstance;
|
|
59
|
+
/**
|
|
60
|
+
* Apply filter to instances
|
|
61
|
+
*/
|
|
62
|
+
private applyFilter;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=kubernetes-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kubernetes-backend.d.ts","sourceRoot":"","sources":["../../src/backends/kubernetes-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,UAAU,GAAG,GAAG,CAAC;AAItB,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,yBAA0B,YAAW,eAAe;IAC/D,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,UAAU,EAAE,UAAU,EAAE,MAAM,GAAE,uBAA4B;IASxE;;;;OAIG;IACG,QAAQ,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzD;;;OAGG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpD;;;OAGG;IACG,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD;;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;IAQtE;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBzC;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAsC7B;;OAEG;IACH,OAAO,CAAC,WAAW;CA2BpB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Kubernetes Registry Backend
|
|
4
|
+
* Uses Kubernetes Service Discovery
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.KubernetesRegistryBackend = void 0;
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
class KubernetesRegistryBackend {
|
|
10
|
+
constructor(kubeConfig, config = {}) {
|
|
11
|
+
// Import CoreV1Api dynamically to avoid build errors when @kubernetes/client-node is not installed
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
const { CoreV1Api: CoreV1ApiClass } = require('@kubernetes/client-node');
|
|
14
|
+
this.k8sApi = kubeConfig.makeApiClient(CoreV1ApiClass);
|
|
15
|
+
this.namespace = config.namespace || 'default';
|
|
16
|
+
this.labelSelector = config.labelSelector || 'app.kubernetes.io/managed-by=hazeljs';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register a service instance
|
|
20
|
+
* In Kubernetes, services are registered via Service/Endpoints objects
|
|
21
|
+
* This is typically handled by K8s itself
|
|
22
|
+
*/
|
|
23
|
+
async register(_instance) {
|
|
24
|
+
// In Kubernetes, service registration is handled by the platform
|
|
25
|
+
// We can optionally create/update annotations on the pod
|
|
26
|
+
// Service registration is managed by Kubernetes
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Deregister a service instance
|
|
30
|
+
* In Kubernetes, this is handled automatically when pods terminate
|
|
31
|
+
*/
|
|
32
|
+
async deregister(_instanceId) {
|
|
33
|
+
// Service deregistration is managed by Kubernetes
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Update service instance heartbeat
|
|
37
|
+
* Kubernetes handles liveness/readiness probes
|
|
38
|
+
*/
|
|
39
|
+
async heartbeat(_instanceId) {
|
|
40
|
+
// Kubernetes handles health checks via probes
|
|
41
|
+
// No manual heartbeat needed
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get all instances of a service from Kubernetes Endpoints
|
|
45
|
+
*/
|
|
46
|
+
async getInstances(serviceName, filter) {
|
|
47
|
+
try {
|
|
48
|
+
// Get service endpoints
|
|
49
|
+
const endpointsResponse = await this.k8sApi.readNamespacedEndpoints(serviceName, this.namespace);
|
|
50
|
+
const endpoints = endpointsResponse.body;
|
|
51
|
+
const instances = [];
|
|
52
|
+
if (!endpoints.subsets) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
// Process each subset
|
|
56
|
+
for (const subset of endpoints.subsets) {
|
|
57
|
+
const ports = subset.ports || [];
|
|
58
|
+
const addresses = subset.addresses || [];
|
|
59
|
+
const notReadyAddresses = subset.notReadyAddresses || [];
|
|
60
|
+
// Process ready addresses
|
|
61
|
+
for (const address of addresses) {
|
|
62
|
+
for (const port of ports) {
|
|
63
|
+
const instance = this.createServiceInstance(serviceName, address, port, types_1.ServiceStatus.UP, endpoints.metadata);
|
|
64
|
+
instances.push(instance);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Process not-ready addresses
|
|
68
|
+
for (const address of notReadyAddresses) {
|
|
69
|
+
for (const port of ports) {
|
|
70
|
+
const instance = this.createServiceInstance(serviceName, address, port, types_1.ServiceStatus.STARTING, endpoints.metadata);
|
|
71
|
+
instances.push(instance);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Apply filters
|
|
76
|
+
if (filter) {
|
|
77
|
+
return this.applyFilter(instances, filter);
|
|
78
|
+
}
|
|
79
|
+
return instances;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get a specific service instance
|
|
87
|
+
*/
|
|
88
|
+
async getInstance(instanceId) {
|
|
89
|
+
// Parse instanceId to get service name and address
|
|
90
|
+
const [serviceName] = instanceId.split(':');
|
|
91
|
+
const instances = await this.getInstances(serviceName);
|
|
92
|
+
return instances.find((i) => i.id === instanceId) || null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get all registered services in the namespace
|
|
96
|
+
*/
|
|
97
|
+
async getAllServices() {
|
|
98
|
+
try {
|
|
99
|
+
const servicesResponse = await this.k8sApi.listNamespacedService(this.namespace, undefined, undefined, undefined, undefined, this.labelSelector);
|
|
100
|
+
return servicesResponse.body.items.map((service) => service.metadata?.name || '');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Update service instance status
|
|
108
|
+
* In Kubernetes, status is managed by readiness probes
|
|
109
|
+
*/
|
|
110
|
+
async updateStatus(_instanceId, _status) {
|
|
111
|
+
// Kubernetes manages status via probes
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Clean up expired instances
|
|
115
|
+
* Kubernetes handles this automatically
|
|
116
|
+
*/
|
|
117
|
+
async cleanup() {
|
|
118
|
+
// Kubernetes automatically removes terminated pods
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a ServiceInstance from Kubernetes endpoint data
|
|
122
|
+
*/
|
|
123
|
+
createServiceInstance(serviceName, address, port, status, metadata) {
|
|
124
|
+
const host = address.ip;
|
|
125
|
+
const portNumber = port.port;
|
|
126
|
+
const instanceId = `${serviceName}:${host}:${portNumber}`;
|
|
127
|
+
// Extract metadata from annotations and labels
|
|
128
|
+
const annotations = metadata?.annotations || {};
|
|
129
|
+
const labels = metadata?.labels || {};
|
|
130
|
+
return {
|
|
131
|
+
id: instanceId,
|
|
132
|
+
name: serviceName,
|
|
133
|
+
host,
|
|
134
|
+
port: portNumber,
|
|
135
|
+
status,
|
|
136
|
+
metadata: {
|
|
137
|
+
...annotations,
|
|
138
|
+
podName: address.targetRef?.name,
|
|
139
|
+
nodeName: address.nodeName,
|
|
140
|
+
},
|
|
141
|
+
tags: Object.keys(labels),
|
|
142
|
+
zone: labels['topology.kubernetes.io/zone'] || labels['failure-domain.beta.kubernetes.io/zone'],
|
|
143
|
+
lastHeartbeat: new Date(),
|
|
144
|
+
registeredAt: new Date(metadata?.creationTimestamp || Date.now()),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Apply filter to instances
|
|
149
|
+
*/
|
|
150
|
+
applyFilter(instances, filter) {
|
|
151
|
+
return instances.filter((instance) => {
|
|
152
|
+
if (filter.zone && instance.zone !== filter.zone) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (filter.status && instance.status !== filter.status) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
159
|
+
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (filter.metadata) {
|
|
164
|
+
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
165
|
+
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.KubernetesRegistryBackend = KubernetesRegistryBackend;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Registry Backend
|
|
3
|
+
* For development and testing
|
|
4
|
+
*/
|
|
5
|
+
import { RegistryBackend } from './registry-backend';
|
|
6
|
+
import { ServiceInstance, ServiceFilter } from '../types';
|
|
7
|
+
export declare class MemoryRegistryBackend implements RegistryBackend {
|
|
8
|
+
private instances;
|
|
9
|
+
private serviceIndex;
|
|
10
|
+
private readonly expirationTime;
|
|
11
|
+
constructor(expirationTime?: number);
|
|
12
|
+
register(instance: ServiceInstance): Promise<void>;
|
|
13
|
+
deregister(instanceId: string): Promise<void>;
|
|
14
|
+
heartbeat(instanceId: string): Promise<void>;
|
|
15
|
+
getInstances(serviceName: string, filter?: ServiceFilter): Promise<ServiceInstance[]>;
|
|
16
|
+
getInstance(instanceId: string): Promise<ServiceInstance | null>;
|
|
17
|
+
getAllServices(): Promise<string[]>;
|
|
18
|
+
updateStatus(instanceId: string, status: string): Promise<void>;
|
|
19
|
+
cleanup(): Promise<void>;
|
|
20
|
+
private applyFilter;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=memory-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-backend.d.ts","sourceRoot":"","sources":["../../src/backends/memory-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,cAAc,SAAQ;IAK5B,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlD,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB7C,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAoBrF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAIhE,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAInC,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB9B,OAAO,CAAC,WAAW;CA+BpB"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* In-Memory Registry Backend
|
|
4
|
+
* For development and testing
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MemoryRegistryBackend = void 0;
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
class MemoryRegistryBackend {
|
|
10
|
+
constructor(expirationTime = 90000) {
|
|
11
|
+
this.instances = new Map();
|
|
12
|
+
this.serviceIndex = new Map();
|
|
13
|
+
// 90 seconds default
|
|
14
|
+
this.expirationTime = expirationTime;
|
|
15
|
+
}
|
|
16
|
+
async register(instance) {
|
|
17
|
+
this.instances.set(instance.id, instance);
|
|
18
|
+
// Update service index
|
|
19
|
+
if (!this.serviceIndex.has(instance.name)) {
|
|
20
|
+
this.serviceIndex.set(instance.name, new Set());
|
|
21
|
+
}
|
|
22
|
+
this.serviceIndex.get(instance.name).add(instance.id);
|
|
23
|
+
}
|
|
24
|
+
async deregister(instanceId) {
|
|
25
|
+
const instance = this.instances.get(instanceId);
|
|
26
|
+
if (instance) {
|
|
27
|
+
this.instances.delete(instanceId);
|
|
28
|
+
// Update service index
|
|
29
|
+
const serviceInstances = this.serviceIndex.get(instance.name);
|
|
30
|
+
if (serviceInstances) {
|
|
31
|
+
serviceInstances.delete(instanceId);
|
|
32
|
+
if (serviceInstances.size === 0) {
|
|
33
|
+
this.serviceIndex.delete(instance.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async heartbeat(instanceId) {
|
|
39
|
+
const instance = this.instances.get(instanceId);
|
|
40
|
+
if (instance) {
|
|
41
|
+
instance.lastHeartbeat = new Date();
|
|
42
|
+
instance.status = types_1.ServiceStatus.UP;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async getInstances(serviceName, filter) {
|
|
46
|
+
const instanceIds = this.serviceIndex.get(serviceName);
|
|
47
|
+
if (!instanceIds)
|
|
48
|
+
return [];
|
|
49
|
+
let instances = [];
|
|
50
|
+
for (const id of instanceIds) {
|
|
51
|
+
const instance = this.instances.get(id);
|
|
52
|
+
if (instance) {
|
|
53
|
+
instances.push(instance);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Apply filters
|
|
57
|
+
if (filter) {
|
|
58
|
+
instances = this.applyFilter(instances, filter);
|
|
59
|
+
}
|
|
60
|
+
return instances;
|
|
61
|
+
}
|
|
62
|
+
async getInstance(instanceId) {
|
|
63
|
+
return this.instances.get(instanceId) || null;
|
|
64
|
+
}
|
|
65
|
+
async getAllServices() {
|
|
66
|
+
return Array.from(this.serviceIndex.keys());
|
|
67
|
+
}
|
|
68
|
+
async updateStatus(instanceId, status) {
|
|
69
|
+
const instance = this.instances.get(instanceId);
|
|
70
|
+
if (instance) {
|
|
71
|
+
instance.status = status;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async cleanup() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const expiredIds = [];
|
|
77
|
+
for (const [id, instance] of this.instances) {
|
|
78
|
+
const timeSinceHeartbeat = now - instance.lastHeartbeat.getTime();
|
|
79
|
+
if (timeSinceHeartbeat > this.expirationTime) {
|
|
80
|
+
expiredIds.push(id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const id of expiredIds) {
|
|
84
|
+
await this.deregister(id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
applyFilter(instances, filter) {
|
|
88
|
+
return instances.filter((instance) => {
|
|
89
|
+
// Filter by zone
|
|
90
|
+
if (filter.zone && instance.zone !== filter.zone) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Filter by status
|
|
94
|
+
if (filter.status && instance.status !== filter.status) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
// Filter by tags
|
|
98
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
99
|
+
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Filter by metadata
|
|
104
|
+
if (filter.metadata) {
|
|
105
|
+
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
106
|
+
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.MemoryRegistryBackend = MemoryRegistryBackend;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Registry Backend
|
|
3
|
+
* For production distributed service registry
|
|
4
|
+
*/
|
|
5
|
+
import { RegistryBackend } from './registry-backend';
|
|
6
|
+
import { ServiceInstance, ServiceFilter } from '../types';
|
|
7
|
+
import type { Redis } from 'ioredis';
|
|
8
|
+
export interface RedisBackendConfig {
|
|
9
|
+
host?: string;
|
|
10
|
+
port?: number;
|
|
11
|
+
password?: string;
|
|
12
|
+
db?: number;
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
ttl?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class RedisRegistryBackend implements RegistryBackend {
|
|
17
|
+
private redis;
|
|
18
|
+
private readonly keyPrefix;
|
|
19
|
+
private readonly ttl;
|
|
20
|
+
constructor(redis: Redis, config?: RedisBackendConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Register a service instance
|
|
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
|
+
* Note: Redis handles expiration automatically via TTL
|
|
52
|
+
*/
|
|
53
|
+
cleanup(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Close Redis connection
|
|
56
|
+
*/
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Get Redis key for instance
|
|
60
|
+
*/
|
|
61
|
+
private getInstanceKey;
|
|
62
|
+
/**
|
|
63
|
+
* Get Redis key for service set
|
|
64
|
+
*/
|
|
65
|
+
private getServiceSetKey;
|
|
66
|
+
/**
|
|
67
|
+
* Apply filter to instances
|
|
68
|
+
*/
|
|
69
|
+
private applyFilter;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=redis-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-backend.d.ts","sourceRoot":"","sources":["../../src/backends/redis-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,KAAK,EAAE,KAAK,EAAE,MAAM,GAAE,kBAAuB;IAMzD;;OAEG;IACG,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD;;OAEG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBnD;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBlD;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA2B3F;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAiBtE;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAUzC;;OAEG;IACG,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYrE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B9B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAItB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAIxB;;OAEG;IACH,OAAO,CAAC,WAAW;CA+BpB"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis Registry Backend
|
|
4
|
+
* For production distributed service registry
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RedisRegistryBackend = void 0;
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
class RedisRegistryBackend {
|
|
10
|
+
constructor(redis, config = {}) {
|
|
11
|
+
this.redis = redis;
|
|
12
|
+
this.keyPrefix = config.keyPrefix || 'hazeljs:discovery:';
|
|
13
|
+
this.ttl = config.ttl || 90; // 90 seconds default
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Register a service instance
|
|
17
|
+
*/
|
|
18
|
+
async register(instance) {
|
|
19
|
+
const key = this.getInstanceKey(instance.id);
|
|
20
|
+
const serviceSetKey = this.getServiceSetKey(instance.name);
|
|
21
|
+
// Store instance data
|
|
22
|
+
await this.redis.setex(key, this.ttl, JSON.stringify(instance));
|
|
23
|
+
// Add to service set
|
|
24
|
+
await this.redis.sadd(serviceSetKey, instance.id);
|
|
25
|
+
// Set expiration on service set (will be refreshed on heartbeat)
|
|
26
|
+
await this.redis.expire(serviceSetKey, this.ttl * 2);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Deregister a service instance
|
|
30
|
+
*/
|
|
31
|
+
async deregister(instanceId) {
|
|
32
|
+
const key = this.getInstanceKey(instanceId);
|
|
33
|
+
// Get instance to find service name
|
|
34
|
+
const data = await this.redis.get(key);
|
|
35
|
+
if (data) {
|
|
36
|
+
const instance = JSON.parse(data);
|
|
37
|
+
const serviceSetKey = this.getServiceSetKey(instance.name);
|
|
38
|
+
// Remove from service set
|
|
39
|
+
await this.redis.srem(serviceSetKey, instanceId);
|
|
40
|
+
// Delete instance data
|
|
41
|
+
await this.redis.del(key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Update service instance heartbeat
|
|
46
|
+
*/
|
|
47
|
+
async heartbeat(instanceId) {
|
|
48
|
+
const key = this.getInstanceKey(instanceId);
|
|
49
|
+
// Get current instance
|
|
50
|
+
const data = await this.redis.get(key);
|
|
51
|
+
if (data) {
|
|
52
|
+
const instance = JSON.parse(data);
|
|
53
|
+
// Update heartbeat and status
|
|
54
|
+
instance.lastHeartbeat = new Date();
|
|
55
|
+
instance.status = types_1.ServiceStatus.UP;
|
|
56
|
+
// Update with new TTL
|
|
57
|
+
await this.redis.setex(key, this.ttl, JSON.stringify(instance));
|
|
58
|
+
// Refresh service set TTL
|
|
59
|
+
const serviceSetKey = this.getServiceSetKey(instance.name);
|
|
60
|
+
await this.redis.expire(serviceSetKey, this.ttl * 2);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get all instances of a service
|
|
65
|
+
*/
|
|
66
|
+
async getInstances(serviceName, filter) {
|
|
67
|
+
const serviceSetKey = this.getServiceSetKey(serviceName);
|
|
68
|
+
// Get all instance IDs for this service
|
|
69
|
+
const instanceIds = await this.redis.smembers(serviceSetKey);
|
|
70
|
+
if (instanceIds.length === 0) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
// Get all instance data
|
|
74
|
+
const instances = [];
|
|
75
|
+
for (const id of instanceIds) {
|
|
76
|
+
const instance = await this.getInstance(id);
|
|
77
|
+
if (instance) {
|
|
78
|
+
instances.push(instance);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Apply filters
|
|
82
|
+
if (filter) {
|
|
83
|
+
return this.applyFilter(instances, filter);
|
|
84
|
+
}
|
|
85
|
+
return instances;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get a specific service instance
|
|
89
|
+
*/
|
|
90
|
+
async getInstance(instanceId) {
|
|
91
|
+
const key = this.getInstanceKey(instanceId);
|
|
92
|
+
const data = await this.redis.get(key);
|
|
93
|
+
if (!data) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const instance = JSON.parse(data);
|
|
97
|
+
// Convert date strings back to Date objects
|
|
98
|
+
instance.lastHeartbeat = new Date(instance.lastHeartbeat);
|
|
99
|
+
instance.registeredAt = new Date(instance.registeredAt);
|
|
100
|
+
return instance;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get all registered services
|
|
104
|
+
*/
|
|
105
|
+
async getAllServices() {
|
|
106
|
+
const pattern = `${this.keyPrefix}service:*`;
|
|
107
|
+
const keys = await this.redis.keys(pattern);
|
|
108
|
+
return keys.map((key) => {
|
|
109
|
+
const serviceName = key.replace(`${this.keyPrefix}service:`, '');
|
|
110
|
+
return serviceName;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Update service instance status
|
|
115
|
+
*/
|
|
116
|
+
async updateStatus(instanceId, status) {
|
|
117
|
+
const key = this.getInstanceKey(instanceId);
|
|
118
|
+
const data = await this.redis.get(key);
|
|
119
|
+
if (data) {
|
|
120
|
+
const instance = JSON.parse(data);
|
|
121
|
+
instance.status = status;
|
|
122
|
+
await this.redis.setex(key, this.ttl, JSON.stringify(instance));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Clean up expired instances
|
|
127
|
+
* Note: Redis handles expiration automatically via TTL
|
|
128
|
+
*/
|
|
129
|
+
async cleanup() {
|
|
130
|
+
// Redis automatically removes expired keys
|
|
131
|
+
// This method can be used for additional cleanup if needed
|
|
132
|
+
const services = await this.getAllServices();
|
|
133
|
+
for (const serviceName of services) {
|
|
134
|
+
const serviceSetKey = this.getServiceSetKey(serviceName);
|
|
135
|
+
const instanceIds = await this.redis.smembers(serviceSetKey);
|
|
136
|
+
// Check each instance and remove if expired
|
|
137
|
+
for (const id of instanceIds) {
|
|
138
|
+
const exists = await this.redis.exists(this.getInstanceKey(id));
|
|
139
|
+
if (!exists) {
|
|
140
|
+
// Instance key expired but still in set, remove it
|
|
141
|
+
await this.redis.srem(serviceSetKey, id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Remove empty service sets
|
|
145
|
+
const count = await this.redis.scard(serviceSetKey);
|
|
146
|
+
if (count === 0) {
|
|
147
|
+
await this.redis.del(serviceSetKey);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Close Redis connection
|
|
153
|
+
*/
|
|
154
|
+
async close() {
|
|
155
|
+
await this.redis.quit();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get Redis key for instance
|
|
159
|
+
*/
|
|
160
|
+
getInstanceKey(instanceId) {
|
|
161
|
+
return `${this.keyPrefix}instance:${instanceId}`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get Redis key for service set
|
|
165
|
+
*/
|
|
166
|
+
getServiceSetKey(serviceName) {
|
|
167
|
+
return `${this.keyPrefix}service:${serviceName}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Apply filter to instances
|
|
171
|
+
*/
|
|
172
|
+
applyFilter(instances, filter) {
|
|
173
|
+
return instances.filter((instance) => {
|
|
174
|
+
// Filter by zone
|
|
175
|
+
if (filter.zone && instance.zone !== filter.zone) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
// Filter by status
|
|
179
|
+
if (filter.status && instance.status !== filter.status) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Filter by tags
|
|
183
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
184
|
+
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Filter by metadata
|
|
189
|
+
if (filter.metadata) {
|
|
190
|
+
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
191
|
+
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
exports.RedisRegistryBackend = RedisRegistryBackend;
|