@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.
- package/LICENSE +192 -0
- package/README.md +450 -0
- package/dist/__tests__/consul-backend.test.d.ts +6 -0
- package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
- package/dist/__tests__/consul-backend.test.js +300 -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__/kubernetes-backend.test.d.ts +6 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
- package/dist/__tests__/kubernetes-backend.test.js +261 -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__/redis-backend.test.d.ts +6 -0
- package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
- package/dist/__tests__/redis-backend.test.js +280 -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 +216 -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 +115 -0
- package/dist/backends/consul-backend.d.ts.map +1 -0
- package/dist/backends/consul-backend.js +259 -0
- package/dist/backends/kubernetes-backend.d.ts +103 -0
- package/dist/backends/kubernetes-backend.d.ts.map +1 -0
- package/dist/backends/kubernetes-backend.js +153 -0
- package/dist/backends/memory-backend.d.ts +21 -0
- package/dist/backends/memory-backend.d.ts.map +1 -0
- package/dist/backends/memory-backend.js +86 -0
- package/dist/backends/redis-backend.d.ts +76 -0
- package/dist/backends/redis-backend.d.ts.map +1 -0
- package/dist/backends/redis-backend.js +220 -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 +49 -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 +48 -0
- package/dist/client/service-client.d.ts.map +1 -0
- package/dist/client/service-client.js +155 -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 +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -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 +159 -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/dist/utils/filter.d.ts +10 -0
- package/dist/utils/filter.d.ts.map +1 -0
- package/dist/utils/filter.js +39 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +34 -0
- package/dist/utils/validation.d.ts +36 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +109 -0
- package/package.json +81 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load Balancing Strategies
|
|
3
|
+
*/
|
|
4
|
+
import { ServiceInstance, LoadBalancerStrategy } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Base strategy that filters healthy instances
|
|
7
|
+
*/
|
|
8
|
+
declare abstract class BaseStrategy implements LoadBalancerStrategy {
|
|
9
|
+
abstract name: string;
|
|
10
|
+
protected filterHealthy(instances: ServiceInstance[]): ServiceInstance[];
|
|
11
|
+
abstract choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Round Robin Strategy
|
|
15
|
+
*/
|
|
16
|
+
export declare class RoundRobinStrategy extends BaseStrategy {
|
|
17
|
+
name: string;
|
|
18
|
+
private currentIndex;
|
|
19
|
+
choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Random Strategy
|
|
23
|
+
*/
|
|
24
|
+
export declare class RandomStrategy extends BaseStrategy {
|
|
25
|
+
name: string;
|
|
26
|
+
choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Least Connections Strategy
|
|
30
|
+
* Tracks active connections per instance
|
|
31
|
+
*/
|
|
32
|
+
export declare class LeastConnectionsStrategy extends BaseStrategy {
|
|
33
|
+
name: string;
|
|
34
|
+
private connections;
|
|
35
|
+
choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
36
|
+
incrementConnections(instanceId: string): void;
|
|
37
|
+
decrementConnections(instanceId: string): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Weighted Round Robin Strategy
|
|
41
|
+
* Uses metadata.weight for weighted selection
|
|
42
|
+
*/
|
|
43
|
+
export declare class WeightedRoundRobinStrategy extends BaseStrategy {
|
|
44
|
+
name: string;
|
|
45
|
+
private currentIndex;
|
|
46
|
+
private currentWeight;
|
|
47
|
+
choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* IP Hash Strategy
|
|
51
|
+
* Consistent hashing based on client IP
|
|
52
|
+
*/
|
|
53
|
+
export declare class IPHashStrategy extends BaseStrategy {
|
|
54
|
+
name: string;
|
|
55
|
+
choose(instances: ServiceInstance[], clientIP?: string): ServiceInstance | null;
|
|
56
|
+
private hashCode;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Zone Aware Strategy
|
|
60
|
+
* Prefers instances in the same zone
|
|
61
|
+
*/
|
|
62
|
+
export declare class ZoneAwareStrategy extends BaseStrategy {
|
|
63
|
+
private preferredZone?;
|
|
64
|
+
name: string;
|
|
65
|
+
constructor(preferredZone?: string | undefined);
|
|
66
|
+
choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load Balancer Factory
|
|
70
|
+
*/
|
|
71
|
+
export declare class LoadBalancerFactory {
|
|
72
|
+
private strategies;
|
|
73
|
+
constructor();
|
|
74
|
+
private registerDefaultStrategies;
|
|
75
|
+
register(strategy: LoadBalancerStrategy): void;
|
|
76
|
+
get(name: string): LoadBalancerStrategy | undefined;
|
|
77
|
+
create(name: string, options?: {
|
|
78
|
+
zone?: string;
|
|
79
|
+
}): LoadBalancerStrategy;
|
|
80
|
+
}
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=strategies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategies.d.ts","sourceRoot":"","sources":["../../src/load-balancer/strategies.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAiB,MAAM,UAAU,CAAC;AAEhF;;GAEG;AACH,uBAAe,YAAa,YAAW,oBAAoB;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE;IAIxE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI;CACtE;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;IAClD,IAAI,SAAiB;IACrB,OAAO,CAAC,YAAY,CAAK;IAEzB,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI;CAQ7D;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,IAAI,SAAY;IAEhB,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI;CAO7D;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,YAAY;IACxD,IAAI,SAAuB;IAC3B,OAAO,CAAC,WAAW,CAA6B;IAEhD,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI;IAmB5D,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAK9C,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAI/C;AAED;;;GAGG;AACH,qBAAa,0BAA2B,SAAQ,YAAY;IAC1D,IAAI,SAA0B;IAC9B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,aAAa,CAAK;IAE1B,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI;CAoB7D;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,IAAI,SAAa;IAEjB,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAW/E,OAAO,CAAC,QAAQ;CASjB;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,YAAY;IAGrC,OAAO,CAAC,aAAa,CAAC;IAFlC,IAAI,SAAgB;gBAEA,aAAa,CAAC,EAAE,MAAM,YAAA;IAI1C,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI;CAe7D;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,UAAU,CAA2C;;IAM7D,OAAO,CAAC,yBAAyB;IAQjC,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAI9C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAInD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,oBAAoB;CAWxE"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Load Balancing Strategies
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LoadBalancerFactory = exports.ZoneAwareStrategy = exports.IPHashStrategy = exports.WeightedRoundRobinStrategy = exports.LeastConnectionsStrategy = exports.RandomStrategy = exports.RoundRobinStrategy = void 0;
|
|
7
|
+
const types_1 = require("../types");
|
|
8
|
+
/**
|
|
9
|
+
* Base strategy that filters healthy instances
|
|
10
|
+
*/
|
|
11
|
+
class BaseStrategy {
|
|
12
|
+
filterHealthy(instances) {
|
|
13
|
+
return instances.filter((instance) => instance.status === types_1.ServiceStatus.UP);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Round Robin Strategy
|
|
18
|
+
*/
|
|
19
|
+
class RoundRobinStrategy extends BaseStrategy {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.name = 'round-robin';
|
|
23
|
+
this.currentIndex = 0;
|
|
24
|
+
}
|
|
25
|
+
choose(instances) {
|
|
26
|
+
const healthy = this.filterHealthy(instances);
|
|
27
|
+
if (healthy.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
const instance = healthy[this.currentIndex % healthy.length];
|
|
30
|
+
this.currentIndex = (this.currentIndex + 1) % healthy.length;
|
|
31
|
+
return instance;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.RoundRobinStrategy = RoundRobinStrategy;
|
|
35
|
+
/**
|
|
36
|
+
* Random Strategy
|
|
37
|
+
*/
|
|
38
|
+
class RandomStrategy extends BaseStrategy {
|
|
39
|
+
constructor() {
|
|
40
|
+
super(...arguments);
|
|
41
|
+
this.name = 'random';
|
|
42
|
+
}
|
|
43
|
+
choose(instances) {
|
|
44
|
+
const healthy = this.filterHealthy(instances);
|
|
45
|
+
if (healthy.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
const randomIndex = Math.floor(Math.random() * healthy.length);
|
|
48
|
+
return healthy[randomIndex];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.RandomStrategy = RandomStrategy;
|
|
52
|
+
/**
|
|
53
|
+
* Least Connections Strategy
|
|
54
|
+
* Tracks active connections per instance
|
|
55
|
+
*/
|
|
56
|
+
class LeastConnectionsStrategy extends BaseStrategy {
|
|
57
|
+
constructor() {
|
|
58
|
+
super(...arguments);
|
|
59
|
+
this.name = 'least-connections';
|
|
60
|
+
this.connections = new Map();
|
|
61
|
+
}
|
|
62
|
+
choose(instances) {
|
|
63
|
+
const healthy = this.filterHealthy(instances);
|
|
64
|
+
if (healthy.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
// Find instance with least connections
|
|
67
|
+
let minConnections = Infinity;
|
|
68
|
+
let selectedInstance = null;
|
|
69
|
+
for (const instance of healthy) {
|
|
70
|
+
const connections = this.connections.get(instance.id) || 0;
|
|
71
|
+
if (connections < minConnections) {
|
|
72
|
+
minConnections = connections;
|
|
73
|
+
selectedInstance = instance;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return selectedInstance;
|
|
77
|
+
}
|
|
78
|
+
incrementConnections(instanceId) {
|
|
79
|
+
const current = this.connections.get(instanceId) || 0;
|
|
80
|
+
this.connections.set(instanceId, current + 1);
|
|
81
|
+
}
|
|
82
|
+
decrementConnections(instanceId) {
|
|
83
|
+
const current = this.connections.get(instanceId) || 0;
|
|
84
|
+
this.connections.set(instanceId, Math.max(0, current - 1));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.LeastConnectionsStrategy = LeastConnectionsStrategy;
|
|
88
|
+
/**
|
|
89
|
+
* Weighted Round Robin Strategy
|
|
90
|
+
* Uses metadata.weight for weighted selection
|
|
91
|
+
*/
|
|
92
|
+
class WeightedRoundRobinStrategy extends BaseStrategy {
|
|
93
|
+
constructor() {
|
|
94
|
+
super(...arguments);
|
|
95
|
+
this.name = 'weighted-round-robin';
|
|
96
|
+
this.currentIndex = 0;
|
|
97
|
+
this.currentWeight = 0;
|
|
98
|
+
}
|
|
99
|
+
choose(instances) {
|
|
100
|
+
const healthy = this.filterHealthy(instances);
|
|
101
|
+
if (healthy.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
// Build weighted list
|
|
104
|
+
const weighted = [];
|
|
105
|
+
for (const instance of healthy) {
|
|
106
|
+
const weightValue = instance.metadata?.weight;
|
|
107
|
+
const weight = typeof weightValue === 'number' && weightValue > 0 ? weightValue : 1;
|
|
108
|
+
for (let i = 0; i < weight; i++) {
|
|
109
|
+
weighted.push(instance);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (weighted.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
const instance = weighted[this.currentIndex % weighted.length];
|
|
115
|
+
this.currentIndex = (this.currentIndex + 1) % weighted.length;
|
|
116
|
+
return instance;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.WeightedRoundRobinStrategy = WeightedRoundRobinStrategy;
|
|
120
|
+
/**
|
|
121
|
+
* IP Hash Strategy
|
|
122
|
+
* Consistent hashing based on client IP
|
|
123
|
+
*/
|
|
124
|
+
class IPHashStrategy extends BaseStrategy {
|
|
125
|
+
constructor() {
|
|
126
|
+
super(...arguments);
|
|
127
|
+
this.name = 'ip-hash';
|
|
128
|
+
}
|
|
129
|
+
choose(instances, clientIP) {
|
|
130
|
+
const healthy = this.filterHealthy(instances);
|
|
131
|
+
if (healthy.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
if (!clientIP)
|
|
134
|
+
return healthy[0];
|
|
135
|
+
// Simple hash function
|
|
136
|
+
const hash = this.hashCode(clientIP);
|
|
137
|
+
const index = Math.abs(hash) % healthy.length;
|
|
138
|
+
return healthy[index];
|
|
139
|
+
}
|
|
140
|
+
hashCode(str) {
|
|
141
|
+
let hash = 0;
|
|
142
|
+
for (let i = 0; i < str.length; i++) {
|
|
143
|
+
const char = str.charCodeAt(i);
|
|
144
|
+
hash = (hash << 5) - hash + char;
|
|
145
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
146
|
+
}
|
|
147
|
+
return hash;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.IPHashStrategy = IPHashStrategy;
|
|
151
|
+
/**
|
|
152
|
+
* Zone Aware Strategy
|
|
153
|
+
* Prefers instances in the same zone
|
|
154
|
+
*/
|
|
155
|
+
class ZoneAwareStrategy extends BaseStrategy {
|
|
156
|
+
constructor(preferredZone) {
|
|
157
|
+
super();
|
|
158
|
+
this.preferredZone = preferredZone;
|
|
159
|
+
this.name = 'zone-aware';
|
|
160
|
+
}
|
|
161
|
+
choose(instances) {
|
|
162
|
+
const healthy = this.filterHealthy(instances);
|
|
163
|
+
if (healthy.length === 0)
|
|
164
|
+
return null;
|
|
165
|
+
// Try to find instance in preferred zone
|
|
166
|
+
if (this.preferredZone) {
|
|
167
|
+
const sameZone = healthy.filter((i) => i.zone === this.preferredZone);
|
|
168
|
+
if (sameZone.length > 0) {
|
|
169
|
+
return sameZone[Math.floor(Math.random() * sameZone.length)];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Fallback to random
|
|
173
|
+
return healthy[Math.floor(Math.random() * healthy.length)];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.ZoneAwareStrategy = ZoneAwareStrategy;
|
|
177
|
+
/**
|
|
178
|
+
* Load Balancer Factory
|
|
179
|
+
*/
|
|
180
|
+
class LoadBalancerFactory {
|
|
181
|
+
constructor() {
|
|
182
|
+
this.strategies = new Map();
|
|
183
|
+
this.registerDefaultStrategies();
|
|
184
|
+
}
|
|
185
|
+
registerDefaultStrategies() {
|
|
186
|
+
this.register(new RoundRobinStrategy());
|
|
187
|
+
this.register(new RandomStrategy());
|
|
188
|
+
this.register(new LeastConnectionsStrategy());
|
|
189
|
+
this.register(new WeightedRoundRobinStrategy());
|
|
190
|
+
this.register(new IPHashStrategy());
|
|
191
|
+
}
|
|
192
|
+
register(strategy) {
|
|
193
|
+
this.strategies.set(strategy.name, strategy);
|
|
194
|
+
}
|
|
195
|
+
get(name) {
|
|
196
|
+
return this.strategies.get(name);
|
|
197
|
+
}
|
|
198
|
+
create(name, options) {
|
|
199
|
+
if (name === 'zone-aware') {
|
|
200
|
+
return new ZoneAwareStrategy(options?.zone);
|
|
201
|
+
}
|
|
202
|
+
const strategy = this.get(name);
|
|
203
|
+
if (!strategy) {
|
|
204
|
+
throw new Error(`Load balancing strategy '${name}' not found`);
|
|
205
|
+
}
|
|
206
|
+
return strategy;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.LoadBalancerFactory = LoadBalancerFactory;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Registry
|
|
3
|
+
* Manages service registration and health checks
|
|
4
|
+
*/
|
|
5
|
+
import { ServiceInstance, ServiceRegistryConfig } from '../types';
|
|
6
|
+
import { RegistryBackend } from '../backends/registry-backend';
|
|
7
|
+
export declare class ServiceRegistry {
|
|
8
|
+
private config;
|
|
9
|
+
private backend;
|
|
10
|
+
private instance;
|
|
11
|
+
private heartbeatInterval;
|
|
12
|
+
private cleanupInterval;
|
|
13
|
+
constructor(config: ServiceRegistryConfig, backend?: RegistryBackend);
|
|
14
|
+
/**
|
|
15
|
+
* Register this service instance
|
|
16
|
+
*/
|
|
17
|
+
register(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Deregister this service instance
|
|
20
|
+
*/
|
|
21
|
+
deregister(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current service instance
|
|
24
|
+
*/
|
|
25
|
+
getInstance(): ServiceInstance | null;
|
|
26
|
+
/**
|
|
27
|
+
* Get the backend
|
|
28
|
+
*/
|
|
29
|
+
getBackend(): RegistryBackend;
|
|
30
|
+
/**
|
|
31
|
+
* Start heartbeat interval
|
|
32
|
+
*/
|
|
33
|
+
private startHeartbeat;
|
|
34
|
+
/**
|
|
35
|
+
* Start cleanup interval
|
|
36
|
+
*/
|
|
37
|
+
private startCleanup;
|
|
38
|
+
/**
|
|
39
|
+
* Perform health check
|
|
40
|
+
*/
|
|
41
|
+
private performHealthCheck;
|
|
42
|
+
/**
|
|
43
|
+
* Generate unique instance ID
|
|
44
|
+
*/
|
|
45
|
+
private generateInstanceId;
|
|
46
|
+
/**
|
|
47
|
+
* Get local IP address
|
|
48
|
+
*/
|
|
49
|
+
private getLocalIP;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=service-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-registry.d.ts","sourceRoot":"","sources":["../../src/registry/service-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAiB,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAM/D,qBAAa,eAAe;IAOxB,OAAO,CAAC,MAAM;IANhB,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,eAAe,CAA+B;gBAG5C,MAAM,EAAE,qBAAqB,EACrC,OAAO,CAAC,EAAE,eAAe;IAO3B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAgC/B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBjC;;OAEG;IACH,WAAW,IAAI,eAAe,GAAG,IAAI;IAIrC;;OAEG;IACH,UAAU,IAAI,eAAe;IAI7B;;OAEG;IACH,OAAO,CAAC,cAAc;IAYtB;;OAEG;IACH,OAAO,CAAC,YAAY;IAWpB;;OAEG;YACW,kBAAkB;IA0BhC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAI1B;;OAEG;IACH,OAAO,CAAC,UAAU;CAgBnB"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Service Registry
|
|
4
|
+
* Manages service registration and health checks
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ServiceRegistry = void 0;
|
|
11
|
+
const types_1 = require("../types");
|
|
12
|
+
const memory_backend_1 = require("../backends/memory-backend");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
const validation_1 = require("../utils/validation");
|
|
15
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
|
+
class ServiceRegistry {
|
|
17
|
+
constructor(config, backend) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.instance = null;
|
|
20
|
+
this.heartbeatInterval = null;
|
|
21
|
+
this.cleanupInterval = null;
|
|
22
|
+
(0, validation_1.validateServiceRegistryConfig)(config);
|
|
23
|
+
this.backend = backend || new memory_backend_1.MemoryRegistryBackend();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register this service instance
|
|
27
|
+
*/
|
|
28
|
+
async register() {
|
|
29
|
+
const host = this.config.host || this.getLocalIP();
|
|
30
|
+
const instanceId = this.generateInstanceId(this.config.name, host, this.config.port);
|
|
31
|
+
this.instance = {
|
|
32
|
+
id: instanceId,
|
|
33
|
+
name: this.config.name,
|
|
34
|
+
host,
|
|
35
|
+
port: this.config.port,
|
|
36
|
+
protocol: this.config.protocol || 'http',
|
|
37
|
+
metadata: this.config.metadata || {},
|
|
38
|
+
healthCheckPath: this.config.healthCheckPath || '/health',
|
|
39
|
+
healthCheckInterval: this.config.healthCheckInterval || 30000,
|
|
40
|
+
zone: this.config.zone,
|
|
41
|
+
tags: this.config.tags || [],
|
|
42
|
+
status: types_1.ServiceStatus.STARTING,
|
|
43
|
+
lastHeartbeat: new Date(),
|
|
44
|
+
registeredAt: new Date(),
|
|
45
|
+
};
|
|
46
|
+
await this.backend.register(this.instance);
|
|
47
|
+
// Start heartbeat
|
|
48
|
+
this.startHeartbeat();
|
|
49
|
+
// Start cleanup task
|
|
50
|
+
this.startCleanup();
|
|
51
|
+
// Perform initial health check
|
|
52
|
+
await this.performHealthCheck();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Deregister this service instance
|
|
56
|
+
*/
|
|
57
|
+
async deregister() {
|
|
58
|
+
if (this.heartbeatInterval) {
|
|
59
|
+
clearInterval(this.heartbeatInterval);
|
|
60
|
+
this.heartbeatInterval = null;
|
|
61
|
+
}
|
|
62
|
+
if (this.cleanupInterval) {
|
|
63
|
+
clearInterval(this.cleanupInterval);
|
|
64
|
+
this.cleanupInterval = null;
|
|
65
|
+
}
|
|
66
|
+
if (this.instance) {
|
|
67
|
+
await this.backend.deregister(this.instance.id);
|
|
68
|
+
this.instance = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the current service instance
|
|
73
|
+
*/
|
|
74
|
+
getInstance() {
|
|
75
|
+
return this.instance;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the backend
|
|
79
|
+
*/
|
|
80
|
+
getBackend() {
|
|
81
|
+
return this.backend;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Start heartbeat interval
|
|
85
|
+
*/
|
|
86
|
+
startHeartbeat() {
|
|
87
|
+
if (!this.instance)
|
|
88
|
+
return;
|
|
89
|
+
const interval = this.instance.healthCheckInterval || 30000;
|
|
90
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
91
|
+
if (this.instance) {
|
|
92
|
+
await this.performHealthCheck();
|
|
93
|
+
}
|
|
94
|
+
}, interval);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Start cleanup interval
|
|
98
|
+
*/
|
|
99
|
+
startCleanup() {
|
|
100
|
+
this.cleanupInterval = setInterval(async () => {
|
|
101
|
+
try {
|
|
102
|
+
await this.backend.cleanup();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
106
|
+
logger.error('Cleanup task failed', error);
|
|
107
|
+
}
|
|
108
|
+
}, 60000); // Run every minute
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Perform health check
|
|
112
|
+
*/
|
|
113
|
+
async performHealthCheck() {
|
|
114
|
+
if (!this.instance)
|
|
115
|
+
return;
|
|
116
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
117
|
+
try {
|
|
118
|
+
const url = `${this.instance.protocol}://${this.instance.host}:${this.instance.port}${this.instance.healthCheckPath}`;
|
|
119
|
+
const response = await axios_1.default.get(url, { timeout: 5000 });
|
|
120
|
+
if (response.status === 200) {
|
|
121
|
+
this.instance.status = types_1.ServiceStatus.UP;
|
|
122
|
+
await this.backend.heartbeat(this.instance.id);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.instance.status = types_1.ServiceStatus.DOWN;
|
|
126
|
+
await this.backend.updateStatus(this.instance.id, types_1.ServiceStatus.DOWN);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
this.instance.status = types_1.ServiceStatus.DOWN;
|
|
131
|
+
await this.backend.updateStatus(this.instance.id, types_1.ServiceStatus.DOWN);
|
|
132
|
+
logger.warn(`Health check failed for ${this.instance.name} (${this.instance.id}), marking as DOWN`, error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Generate unique instance ID
|
|
137
|
+
*/
|
|
138
|
+
generateInstanceId(name, host, port) {
|
|
139
|
+
return `${name}:${host}:${port}:${Date.now()}`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get local IP address
|
|
143
|
+
*/
|
|
144
|
+
getLocalIP() {
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
146
|
+
const { networkInterfaces } = require('os');
|
|
147
|
+
const nets = networkInterfaces();
|
|
148
|
+
for (const name of Object.keys(nets)) {
|
|
149
|
+
for (const net of nets[name]) {
|
|
150
|
+
// Skip internal and non-IPv4 addresses
|
|
151
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
152
|
+
return net.address;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return 'localhost';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.ServiceRegistry = ServiceRegistry;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Discovery Types
|
|
3
|
+
*/
|
|
4
|
+
export interface ServiceInstance {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
protocol?: 'http' | 'https' | 'grpc';
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
healthCheckPath?: string;
|
|
12
|
+
healthCheckInterval?: number;
|
|
13
|
+
zone?: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
status: ServiceStatus;
|
|
16
|
+
lastHeartbeat: Date;
|
|
17
|
+
registeredAt: Date;
|
|
18
|
+
}
|
|
19
|
+
export declare enum ServiceStatus {
|
|
20
|
+
UP = "UP",
|
|
21
|
+
DOWN = "DOWN",
|
|
22
|
+
STARTING = "STARTING",
|
|
23
|
+
OUT_OF_SERVICE = "OUT_OF_SERVICE",
|
|
24
|
+
UNKNOWN = "UNKNOWN"
|
|
25
|
+
}
|
|
26
|
+
export interface ServiceRegistryConfig {
|
|
27
|
+
name: string;
|
|
28
|
+
host?: string;
|
|
29
|
+
port: number;
|
|
30
|
+
protocol?: 'http' | 'https' | 'grpc';
|
|
31
|
+
healthCheckPath?: string;
|
|
32
|
+
healthCheckInterval?: number;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
zone?: string;
|
|
35
|
+
tags?: string[];
|
|
36
|
+
backend?: 'memory' | 'redis' | 'consul' | 'etcd' | 'kubernetes';
|
|
37
|
+
backendConfig?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export interface DiscoveryClientConfig {
|
|
40
|
+
backend?: 'memory' | 'redis' | 'consul' | 'etcd' | 'kubernetes';
|
|
41
|
+
backendConfig?: Record<string, unknown>;
|
|
42
|
+
cacheEnabled?: boolean;
|
|
43
|
+
cacheTTL?: number;
|
|
44
|
+
refreshInterval?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface LoadBalancerStrategy {
|
|
47
|
+
name: string;
|
|
48
|
+
choose(instances: ServiceInstance[]): ServiceInstance | null;
|
|
49
|
+
}
|
|
50
|
+
export interface HealthCheckResult {
|
|
51
|
+
status: ServiceStatus;
|
|
52
|
+
message?: string;
|
|
53
|
+
timestamp: Date;
|
|
54
|
+
}
|
|
55
|
+
export interface ServiceFilter {
|
|
56
|
+
zone?: string;
|
|
57
|
+
tags?: string[];
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
status?: ServiceStatus;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,IAAI,CAAC;IACpB,YAAY,EAAE,IAAI,CAAC;CACpB;AAED,oBAAY,aAAa;IACvB,EAAE,OAAO;IACT,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,cAAc,mBAAmB;IACjC,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,YAAY,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,YAAY,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,GAAG,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Service Discovery Types
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ServiceStatus = void 0;
|
|
7
|
+
var ServiceStatus;
|
|
8
|
+
(function (ServiceStatus) {
|
|
9
|
+
ServiceStatus["UP"] = "UP";
|
|
10
|
+
ServiceStatus["DOWN"] = "DOWN";
|
|
11
|
+
ServiceStatus["STARTING"] = "STARTING";
|
|
12
|
+
ServiceStatus["OUT_OF_SERVICE"] = "OUT_OF_SERVICE";
|
|
13
|
+
ServiceStatus["UNKNOWN"] = "UNKNOWN";
|
|
14
|
+
})(ServiceStatus || (exports.ServiceStatus = ServiceStatus = {}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filter utility for service instances
|
|
3
|
+
*/
|
|
4
|
+
import { ServiceInstance, ServiceFilter } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Apply a ServiceFilter to an array of ServiceInstances.
|
|
7
|
+
* Returns only instances matching all specified filter criteria.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyServiceFilter(instances: ServiceInstance[], filter?: ServiceFilter): ServiceInstance[];
|
|
10
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/utils/filter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,eAAe,EAAE,EAC5B,MAAM,CAAC,EAAE,aAAa,GACrB,eAAe,EAAE,CAgCnB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared filter utility for service instances
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.applyServiceFilter = applyServiceFilter;
|
|
7
|
+
/**
|
|
8
|
+
* Apply a ServiceFilter to an array of ServiceInstances.
|
|
9
|
+
* Returns only instances matching all specified filter criteria.
|
|
10
|
+
*/
|
|
11
|
+
function applyServiceFilter(instances, filter) {
|
|
12
|
+
if (!filter)
|
|
13
|
+
return instances;
|
|
14
|
+
return instances.filter((instance) => {
|
|
15
|
+
// Filter by zone
|
|
16
|
+
if (filter.zone && instance.zone !== filter.zone) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Filter by status
|
|
20
|
+
if (filter.status && instance.status !== filter.status) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// Filter by tags
|
|
24
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
25
|
+
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Filter by metadata
|
|
30
|
+
if (filter.metadata) {
|
|
31
|
+
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
32
|
+
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable logger for @hazeljs/discovery
|
|
3
|
+
*
|
|
4
|
+
* Consumers can supply their own logger via DiscoveryLogger.setLogger().
|
|
5
|
+
* The default logger writes to the console.
|
|
6
|
+
*/
|
|
7
|
+
export interface Logger {
|
|
8
|
+
debug(message: string, ...args: unknown[]): void;
|
|
9
|
+
info(message: string, ...args: unknown[]): void;
|
|
10
|
+
warn(message: string, ...args: unknown[]): void;
|
|
11
|
+
error(message: string, ...args: unknown[]): void;
|
|
12
|
+
}
|
|
13
|
+
export declare const DiscoveryLogger: {
|
|
14
|
+
/** Replace the default console logger with a custom implementation */
|
|
15
|
+
setLogger(logger: Logger): void;
|
|
16
|
+
/** Reset to the default console logger */
|
|
17
|
+
resetLogger(): void;
|
|
18
|
+
/** Get the current logger instance */
|
|
19
|
+
getLogger(): Logger;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD;AAeD,eAAO,MAAM,eAAe;IAC1B,sEAAsE;sBACpD,MAAM,GAAG,IAAI;IAI/B,0CAA0C;mBAC3B,IAAI;IAInB,sCAAsC;iBACzB,MAAM;CAGpB,CAAC"}
|