@hazeljs/discovery 0.2.0-beta.16 → 0.2.0-beta.18

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 (42) hide show
  1. package/README.md +185 -19
  2. package/dist/__tests__/consul-backend.test.d.ts +6 -0
  3. package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
  4. package/dist/__tests__/consul-backend.test.js +300 -0
  5. package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
  6. package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
  7. package/dist/__tests__/kubernetes-backend.test.js +261 -0
  8. package/dist/__tests__/redis-backend.test.d.ts +6 -0
  9. package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
  10. package/dist/__tests__/redis-backend.test.js +280 -0
  11. package/dist/backends/consul-backend.d.ts +46 -7
  12. package/dist/backends/consul-backend.d.ts.map +1 -1
  13. package/dist/backends/consul-backend.js +23 -39
  14. package/dist/backends/kubernetes-backend.d.ts +44 -6
  15. package/dist/backends/kubernetes-backend.d.ts.map +1 -1
  16. package/dist/backends/kubernetes-backend.js +11 -32
  17. package/dist/backends/memory-backend.d.ts +0 -1
  18. package/dist/backends/memory-backend.d.ts.map +1 -1
  19. package/dist/backends/memory-backend.js +3 -32
  20. package/dist/backends/redis-backend.d.ts +11 -6
  21. package/dist/backends/redis-backend.d.ts.map +1 -1
  22. package/dist/backends/redis-backend.js +66 -46
  23. package/dist/client/discovery-client.d.ts +6 -4
  24. package/dist/client/discovery-client.d.ts.map +1 -1
  25. package/dist/client/discovery-client.js +30 -30
  26. package/dist/client/service-client.d.ts.map +1 -1
  27. package/dist/client/service-client.js +82 -17
  28. package/dist/index.d.ts +4 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +8 -1
  31. package/dist/registry/service-registry.d.ts.map +1 -1
  32. package/dist/registry/service-registry.js +13 -2
  33. package/dist/utils/filter.d.ts +10 -0
  34. package/dist/utils/filter.d.ts.map +1 -0
  35. package/dist/utils/filter.js +39 -0
  36. package/dist/utils/logger.d.ts +21 -0
  37. package/dist/utils/logger.d.ts.map +1 -0
  38. package/dist/utils/logger.js +34 -0
  39. package/dist/utils/validation.d.ts +36 -0
  40. package/dist/utils/validation.d.ts.map +1 -0
  41. package/dist/utils/validation.js +109 -0
  42. package/package.json +3 -3
@@ -7,10 +7,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.DiscoveryClient = void 0;
8
8
  const memory_backend_1 = require("../backends/memory-backend");
9
9
  const strategies_1 = require("../load-balancer/strategies");
10
+ const filter_1 = require("../utils/filter");
11
+ const logger_1 = require("../utils/logger");
12
+ const validation_1 = require("../utils/validation");
10
13
  class DiscoveryClient {
11
14
  constructor(config = {}, backend) {
12
15
  this.config = config;
13
16
  this.cache = new Map();
17
+ this.refreshIntervalHandle = null;
18
+ (0, validation_1.validateDiscoveryClientConfig)(config);
14
19
  this.backend = backend || new memory_backend_1.MemoryRegistryBackend();
15
20
  this.loadBalancerFactory = new strategies_1.LoadBalancerFactory();
16
21
  if (config.refreshInterval) {
@@ -28,7 +33,7 @@ class DiscoveryClient {
28
33
  const age = Date.now() - cached.timestamp;
29
34
  const ttl = this.config.cacheTTL || 30000;
30
35
  if (age < ttl) {
31
- return this.applyFilter(cached.instances, filter);
36
+ return (0, filter_1.applyServiceFilter)(cached.instances, filter);
32
37
  }
33
38
  }
34
39
  }
@@ -83,41 +88,36 @@ class DiscoveryClient {
83
88
  return this.loadBalancerFactory;
84
89
  }
85
90
  /**
86
- * Start cache refresh interval
91
+ * Close the discovery client and release all resources.
92
+ * Stops the cache refresh interval and clears the cache.
87
93
  */
88
- startRefreshInterval() {
89
- setInterval(async () => {
90
- const services = await this.getAllServices();
91
- for (const service of services) {
92
- const instances = await this.backend.getInstances(service);
93
- this.cache.set(service, {
94
- instances,
95
- timestamp: Date.now(),
96
- });
97
- }
98
- }, this.config.refreshInterval);
94
+ close() {
95
+ if (this.refreshIntervalHandle) {
96
+ clearInterval(this.refreshIntervalHandle);
97
+ this.refreshIntervalHandle = null;
98
+ }
99
+ this.cache.clear();
99
100
  }
100
101
  /**
101
- * Apply filter to instances
102
+ * Start cache refresh interval
102
103
  */
103
- applyFilter(instances, filter) {
104
- if (!filter)
105
- return instances;
106
- return instances.filter((instance) => {
107
- if (filter.zone && instance.zone !== filter.zone)
108
- return false;
109
- if (filter.status && instance.status !== filter.status)
110
- return false;
111
- if (filter.tags && !filter.tags.every((tag) => instance.tags?.includes(tag)))
112
- return false;
113
- if (filter.metadata) {
114
- for (const [key, value] of Object.entries(filter.metadata)) {
115
- if (instance.metadata?.[key] !== value)
116
- return false;
104
+ startRefreshInterval() {
105
+ const logger = logger_1.DiscoveryLogger.getLogger();
106
+ this.refreshIntervalHandle = setInterval(async () => {
107
+ try {
108
+ const services = await this.getAllServices();
109
+ for (const service of services) {
110
+ const instances = await this.backend.getInstances(service);
111
+ this.cache.set(service, {
112
+ instances,
113
+ timestamp: Date.now(),
114
+ });
117
115
  }
118
116
  }
119
- return true;
120
- });
117
+ catch (error) {
118
+ logger.error('Failed to refresh service cache', error);
119
+ }
120
+ }, this.config.refreshInterval);
121
121
  }
122
122
  }
123
123
  exports.DiscoveryClient = DiscoveryClient;
@@ -1 +1 @@
1
- {"version":3,"file":"service-client.d.ts","sourceRoot":"","sources":["../../src/client/service-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAc,EAAiB,kBAAkB,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEhF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,aAAa;IAMtB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,MAAM;IANhB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;gBAGjB,eAAe,EAAE,eAAe,EAChC,MAAM,EAAE,mBAAmB;IAUrC;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5F;;OAEG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;IACG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI/F;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;YACW,OAAO;IA2CrB;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd"}
1
+ {"version":3,"file":"service-client.d.ts","sourceRoot":"","sources":["../../src/client/service-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,OAAc,EAAiB,kBAAkB,EAAE,aAAa,EAAc,MAAM,OAAO,CAAC;AAE5F,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAqBD,qBAAa,aAAa;IAMtB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,MAAM;IANhB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;gBAGjB,eAAe,EAAE,eAAe,EAChC,MAAM,EAAE,mBAAmB;IAYrC;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5F;;OAEG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;IACG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI/F;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAI5B;;OAEG;YACW,OAAO;IAgErB;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd"}
@@ -3,18 +3,68 @@
3
3
  * Service Client
4
4
  * HTTP client with automatic service discovery and load balancing
5
5
  */
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
9
39
  Object.defineProperty(exports, "__esModule", { value: true });
10
40
  exports.ServiceClient = void 0;
11
- const axios_1 = __importDefault(require("axios"));
41
+ const strategies_1 = require("../load-balancer/strategies");
42
+ const logger_1 = require("../utils/logger");
43
+ const validation_1 = require("../utils/validation");
44
+ const axios_1 = __importStar(require("axios"));
45
+ /** HTTP status codes that are safe to retry */
46
+ const RETRYABLE_STATUS_CODES = new Set([502, 503, 504, 408, 429]);
47
+ /**
48
+ * Determine whether an error is transient and worth retrying.
49
+ * Client errors (4xx except 408/429) are NOT retried.
50
+ */
51
+ function isRetryableError(error) {
52
+ if (error instanceof axios_1.AxiosError) {
53
+ // Network / timeout errors are always retryable
54
+ if (!error.response)
55
+ return true;
56
+ return RETRYABLE_STATUS_CODES.has(error.response.status);
57
+ }
58
+ // Non-Axios errors (e.g. "no instances available") are retryable
59
+ return true;
60
+ }
12
61
  class ServiceClient {
13
62
  constructor(discoveryClient, config) {
14
63
  this.discoveryClient = discoveryClient;
15
64
  this.config = config;
16
- this.retries = config.retries || 3;
17
- this.retryDelay = config.retryDelay || 1000;
65
+ (0, validation_1.validateServiceClientConfig)(config);
66
+ this.retries = config.retries ?? 3;
67
+ this.retryDelay = config.retryDelay ?? 1000;
18
68
  this.axiosInstance = axios_1.default.create({
19
69
  timeout: config.timeout || 5000,
20
70
  });
@@ -53,14 +103,23 @@ class ServiceClient {
53
103
  * Generic request with service discovery
54
104
  */
55
105
  async request(config) {
106
+ const logger = logger_1.DiscoveryLogger.getLogger();
56
107
  let lastError = null;
57
108
  for (let attempt = 0; attempt < this.retries; attempt++) {
109
+ // Discover service instance
110
+ const instance = await this.discoveryClient.getInstance(this.config.serviceName, this.config.loadBalancingStrategy, this.config.filter);
111
+ if (!instance) {
112
+ throw new Error(`No instances available for service: ${this.config.serviceName}`);
113
+ }
114
+ // Track active connections for least-connections strategy
115
+ const lbStrategy = this.discoveryClient
116
+ .getLoadBalancerFactory()
117
+ .get(this.config.loadBalancingStrategy || 'round-robin');
118
+ const isLeastConn = lbStrategy instanceof strategies_1.LeastConnectionsStrategy;
119
+ if (isLeastConn) {
120
+ lbStrategy.incrementConnections(instance.id);
121
+ }
58
122
  try {
59
- // Discover service instance
60
- const instance = await this.discoveryClient.getInstance(this.config.serviceName, this.config.loadBalancingStrategy, this.config.filter);
61
- if (!instance) {
62
- throw new Error(`No instances available for service: ${this.config.serviceName}`);
63
- }
64
123
  // Build full URL
65
124
  const baseURL = `${instance.protocol}://${instance.host}:${instance.port}`;
66
125
  const fullConfig = {
@@ -68,20 +127,26 @@ class ServiceClient {
68
127
  baseURL,
69
128
  };
70
129
  // Make request
71
- return await this.axiosInstance.request(fullConfig);
130
+ const response = await this.axiosInstance.request(fullConfig);
131
+ return response;
72
132
  }
73
133
  catch (error) {
74
134
  lastError = error;
75
- // Log error only in development
76
- if (process.env.NODE_ENV === 'development') {
77
- // eslint-disable-next-line no-console
78
- console.error(`Request failed (attempt ${attempt + 1}/${this.retries}):`, error);
135
+ logger.warn(`Request to ${this.config.serviceName} failed (attempt ${attempt + 1}/${this.retries})`, error);
136
+ // Only retry on transient / network errors
137
+ if (!isRetryableError(error)) {
138
+ throw error;
79
139
  }
80
- // Wait before retry
140
+ // Wait before retry (skip delay on last attempt since we'll throw)
81
141
  if (attempt < this.retries - 1) {
82
142
  await this.sleep(this.retryDelay);
83
143
  }
84
144
  }
145
+ finally {
146
+ if (isLeastConn) {
147
+ lbStrategy.decrementConnections(instance.id);
148
+ }
149
+ }
85
150
  }
86
151
  throw lastError || new Error('Request failed after all retries');
87
152
  }
package/dist/index.d.ts CHANGED
@@ -10,9 +10,12 @@ export * from './load-balancer/strategies';
10
10
  export { RegistryBackend } from './backends/registry-backend';
11
11
  export { MemoryRegistryBackend } from './backends/memory-backend';
12
12
  export { RedisRegistryBackend, RedisBackendConfig } from './backends/redis-backend';
13
- export { ConsulRegistryBackend, ConsulBackendConfig } from './backends/consul-backend';
13
+ export { ConsulRegistryBackend, ConsulBackendConfig, ConsulClient, } from './backends/consul-backend';
14
14
  export { KubernetesRegistryBackend, KubernetesBackendConfig } from './backends/kubernetes-backend';
15
15
  export { ServiceRegistry as ServiceRegistryDecorator } from './decorators/service-registry.decorator';
16
16
  export { getServiceRegistryMetadata } from './decorators/service-registry.decorator';
17
17
  export * from './decorators/inject-service-client.decorator';
18
+ export { applyServiceFilter } from './utils/filter';
19
+ export { DiscoveryLogger, Logger } from './utils/logger';
20
+ export { ConfigValidationError } from './utils/validation';
18
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,SAAS,CAAC;AAGxB,cAAc,6BAA6B,CAAC;AAG5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AAGxC,cAAc,4BAA4B,CAAC;AAG3C,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGnG,OAAO,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yCAAyC,CAAC;AACtG,OAAO,EAAE,0BAA0B,EAAE,MAAM,yCAAyC,CAAC;AACrF,cAAc,8CAA8C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,SAAS,CAAC;AAGxB,cAAc,6BAA6B,CAAC;AAG5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AAGxC,cAAc,4BAA4B,CAAC;AAG3C,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,YAAY,GACb,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGnG,OAAO,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yCAAyC,CAAC;AACtG,OAAO,EAAE,0BAA0B,EAAE,MAAM,yCAAyC,CAAC;AACrF,cAAc,8CAA8C,CAAC;AAG7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
18
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.getServiceRegistryMetadata = exports.ServiceRegistryDecorator = exports.KubernetesRegistryBackend = exports.ConsulRegistryBackend = exports.RedisRegistryBackend = exports.MemoryRegistryBackend = void 0;
21
+ exports.ConfigValidationError = exports.DiscoveryLogger = exports.applyServiceFilter = exports.getServiceRegistryMetadata = exports.ServiceRegistryDecorator = exports.KubernetesRegistryBackend = exports.ConsulRegistryBackend = exports.RedisRegistryBackend = exports.MemoryRegistryBackend = void 0;
22
22
  // Types
23
23
  __exportStar(require("./types"), exports);
24
24
  // Registry
@@ -42,3 +42,10 @@ Object.defineProperty(exports, "ServiceRegistryDecorator", { enumerable: true, g
42
42
  var service_registry_decorator_2 = require("./decorators/service-registry.decorator");
43
43
  Object.defineProperty(exports, "getServiceRegistryMetadata", { enumerable: true, get: function () { return service_registry_decorator_2.getServiceRegistryMetadata; } });
44
44
  __exportStar(require("./decorators/inject-service-client.decorator"), exports);
45
+ // Utilities
46
+ var filter_1 = require("./utils/filter");
47
+ Object.defineProperty(exports, "applyServiceFilter", { enumerable: true, get: function () { return filter_1.applyServiceFilter; } });
48
+ var logger_1 = require("./utils/logger");
49
+ Object.defineProperty(exports, "DiscoveryLogger", { enumerable: true, get: function () { return logger_1.DiscoveryLogger; } });
50
+ var validation_1 = require("./utils/validation");
51
+ Object.defineProperty(exports, "ConfigValidationError", { enumerable: true, get: function () { return validation_1.ConfigValidationError; } });
@@ -1 +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;AAI/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;IAK3B;;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;IAMpB;;OAEG;YACW,kBAAkB;IAoBhC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAI1B;;OAEG;IACH,OAAO,CAAC,UAAU;CAgBnB"}
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"}
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.ServiceRegistry = void 0;
11
11
  const types_1 = require("../types");
12
12
  const memory_backend_1 = require("../backends/memory-backend");
13
+ const logger_1 = require("../utils/logger");
14
+ const validation_1 = require("../utils/validation");
13
15
  const axios_1 = __importDefault(require("axios"));
14
16
  class ServiceRegistry {
15
17
  constructor(config, backend) {
@@ -17,6 +19,7 @@ class ServiceRegistry {
17
19
  this.instance = null;
18
20
  this.heartbeatInterval = null;
19
21
  this.cleanupInterval = null;
22
+ (0, validation_1.validateServiceRegistryConfig)(config);
20
23
  this.backend = backend || new memory_backend_1.MemoryRegistryBackend();
21
24
  }
22
25
  /**
@@ -95,7 +98,13 @@ class ServiceRegistry {
95
98
  */
96
99
  startCleanup() {
97
100
  this.cleanupInterval = setInterval(async () => {
98
- await this.backend.cleanup();
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
+ }
99
108
  }, 60000); // Run every minute
100
109
  }
101
110
  /**
@@ -104,6 +113,7 @@ class ServiceRegistry {
104
113
  async performHealthCheck() {
105
114
  if (!this.instance)
106
115
  return;
116
+ const logger = logger_1.DiscoveryLogger.getLogger();
107
117
  try {
108
118
  const url = `${this.instance.protocol}://${this.instance.host}:${this.instance.port}${this.instance.healthCheckPath}`;
109
119
  const response = await axios_1.default.get(url, { timeout: 5000 });
@@ -116,9 +126,10 @@ class ServiceRegistry {
116
126
  await this.backend.updateStatus(this.instance.id, types_1.ServiceStatus.DOWN);
117
127
  }
118
128
  }
119
- catch {
129
+ catch (error) {
120
130
  this.instance.status = types_1.ServiceStatus.DOWN;
121
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);
122
133
  }
123
134
  }
124
135
  /**
@@ -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"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * Pluggable logger for @hazeljs/discovery
4
+ *
5
+ * Consumers can supply their own logger via DiscoveryLogger.setLogger().
6
+ * The default logger writes to the console.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.DiscoveryLogger = void 0;
10
+ const defaultLogger = {
11
+ // eslint-disable-next-line no-console
12
+ debug: (msg, ...args) => console.debug(`[discovery] ${msg}`, ...args),
13
+ // eslint-disable-next-line no-console
14
+ info: (msg, ...args) => console.info(`[discovery] ${msg}`, ...args),
15
+ // eslint-disable-next-line no-console
16
+ warn: (msg, ...args) => console.warn(`[discovery] ${msg}`, ...args),
17
+ // eslint-disable-next-line no-console
18
+ error: (msg, ...args) => console.error(`[discovery] ${msg}`, ...args),
19
+ };
20
+ let currentLogger = defaultLogger;
21
+ exports.DiscoveryLogger = {
22
+ /** Replace the default console logger with a custom implementation */
23
+ setLogger(logger) {
24
+ currentLogger = logger;
25
+ },
26
+ /** Reset to the default console logger */
27
+ resetLogger() {
28
+ currentLogger = defaultLogger;
29
+ },
30
+ /** Get the current logger instance */
31
+ getLogger() {
32
+ return currentLogger;
33
+ },
34
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Runtime validation utilities for discovery configuration objects
3
+ */
4
+ import { ServiceRegistryConfig, DiscoveryClientConfig } from '../types';
5
+ import { RedisBackendConfig } from '../backends/redis-backend';
6
+ import { ConsulBackendConfig } from '../backends/consul-backend';
7
+ import { KubernetesBackendConfig } from '../backends/kubernetes-backend';
8
+ import { ServiceClientConfig } from '../client/service-client';
9
+ export declare class ConfigValidationError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ /**
13
+ * Validate ServiceRegistryConfig at runtime
14
+ */
15
+ export declare function validateServiceRegistryConfig(config: ServiceRegistryConfig): void;
16
+ /**
17
+ * Validate DiscoveryClientConfig at runtime
18
+ */
19
+ export declare function validateDiscoveryClientConfig(config: DiscoveryClientConfig): void;
20
+ /**
21
+ * Validate ServiceClientConfig at runtime
22
+ */
23
+ export declare function validateServiceClientConfig(config: ServiceClientConfig): void;
24
+ /**
25
+ * Validate RedisBackendConfig at runtime
26
+ */
27
+ export declare function validateRedisBackendConfig(config: RedisBackendConfig): void;
28
+ /**
29
+ * Validate ConsulBackendConfig at runtime
30
+ */
31
+ export declare function validateConsulBackendConfig(config: ConsulBackendConfig): void;
32
+ /**
33
+ * Validate KubernetesBackendConfig at runtime
34
+ */
35
+ export declare function validateKubernetesBackendConfig(config: KubernetesBackendConfig): void;
36
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAgCjF;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAejF;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAkC7E;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAe3E;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAkB7E;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CASrF"}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Runtime validation utilities for discovery configuration objects
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigValidationError = void 0;
7
+ exports.validateServiceRegistryConfig = validateServiceRegistryConfig;
8
+ exports.validateDiscoveryClientConfig = validateDiscoveryClientConfig;
9
+ exports.validateServiceClientConfig = validateServiceClientConfig;
10
+ exports.validateRedisBackendConfig = validateRedisBackendConfig;
11
+ exports.validateConsulBackendConfig = validateConsulBackendConfig;
12
+ exports.validateKubernetesBackendConfig = validateKubernetesBackendConfig;
13
+ class ConfigValidationError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'ConfigValidationError';
17
+ }
18
+ }
19
+ exports.ConfigValidationError = ConfigValidationError;
20
+ /**
21
+ * Validate ServiceRegistryConfig at runtime
22
+ */
23
+ function validateServiceRegistryConfig(config) {
24
+ if (!config.name || typeof config.name !== 'string' || config.name.trim().length === 0) {
25
+ throw new ConfigValidationError('ServiceRegistryConfig: "name" is required and must be a non-empty string');
26
+ }
27
+ if (config.port == null ||
28
+ typeof config.port !== 'number' ||
29
+ config.port < 0 ||
30
+ config.port > 65535) {
31
+ throw new ConfigValidationError('ServiceRegistryConfig: "port" is required and must be a number between 0 and 65535');
32
+ }
33
+ if (config.healthCheckInterval != null &&
34
+ (typeof config.healthCheckInterval !== 'number' || config.healthCheckInterval <= 0)) {
35
+ throw new ConfigValidationError('ServiceRegistryConfig: "healthCheckInterval" must be a positive number (ms)');
36
+ }
37
+ if (config.protocol && !['http', 'https', 'grpc'].includes(config.protocol)) {
38
+ throw new ConfigValidationError('ServiceRegistryConfig: "protocol" must be one of "http", "https", "grpc"');
39
+ }
40
+ }
41
+ /**
42
+ * Validate DiscoveryClientConfig at runtime
43
+ */
44
+ function validateDiscoveryClientConfig(config) {
45
+ if (config.cacheTTL != null && (typeof config.cacheTTL !== 'number' || config.cacheTTL <= 0)) {
46
+ throw new ConfigValidationError('DiscoveryClientConfig: "cacheTTL" must be a positive number (ms)');
47
+ }
48
+ if (config.refreshInterval != null &&
49
+ (typeof config.refreshInterval !== 'number' || config.refreshInterval <= 0)) {
50
+ throw new ConfigValidationError('DiscoveryClientConfig: "refreshInterval" must be a positive number (ms)');
51
+ }
52
+ }
53
+ /**
54
+ * Validate ServiceClientConfig at runtime
55
+ */
56
+ function validateServiceClientConfig(config) {
57
+ if (!config.serviceName ||
58
+ typeof config.serviceName !== 'string' ||
59
+ config.serviceName.trim().length === 0) {
60
+ throw new ConfigValidationError('ServiceClientConfig: "serviceName" is required and must be a non-empty string');
61
+ }
62
+ if (config.timeout != null && (typeof config.timeout !== 'number' || config.timeout <= 0)) {
63
+ throw new ConfigValidationError('ServiceClientConfig: "timeout" must be a positive number (ms)');
64
+ }
65
+ if (config.retries != null &&
66
+ (typeof config.retries !== 'number' || config.retries < 0 || !Number.isInteger(config.retries))) {
67
+ throw new ConfigValidationError('ServiceClientConfig: "retries" must be a non-negative integer');
68
+ }
69
+ if (config.retryDelay != null &&
70
+ (typeof config.retryDelay !== 'number' || config.retryDelay < 0)) {
71
+ throw new ConfigValidationError('ServiceClientConfig: "retryDelay" must be a non-negative number (ms)');
72
+ }
73
+ }
74
+ /**
75
+ * Validate RedisBackendConfig at runtime
76
+ */
77
+ function validateRedisBackendConfig(config) {
78
+ if (config.ttl != null && (typeof config.ttl !== 'number' || config.ttl <= 0)) {
79
+ throw new ConfigValidationError('RedisBackendConfig: "ttl" must be a positive number (seconds)');
80
+ }
81
+ if (config.port != null &&
82
+ (typeof config.port !== 'number' || config.port < 0 || config.port > 65535)) {
83
+ throw new ConfigValidationError('RedisBackendConfig: "port" must be a number between 0 and 65535');
84
+ }
85
+ }
86
+ /**
87
+ * Validate ConsulBackendConfig at runtime
88
+ */
89
+ function validateConsulBackendConfig(config) {
90
+ if (config.ttl != null && typeof config.ttl === 'string') {
91
+ const match = config.ttl.match(/^(\d+)([smh])$/);
92
+ if (!match) {
93
+ throw new ConfigValidationError('ConsulBackendConfig: "ttl" must match format like "30s", "5m", or "1h"');
94
+ }
95
+ }
96
+ if (config.port != null &&
97
+ (typeof config.port !== 'number' || config.port < 0 || config.port > 65535)) {
98
+ throw new ConfigValidationError('ConsulBackendConfig: "port" must be a number between 0 and 65535');
99
+ }
100
+ }
101
+ /**
102
+ * Validate KubernetesBackendConfig at runtime
103
+ */
104
+ function validateKubernetesBackendConfig(config) {
105
+ if (config.namespace != null &&
106
+ (typeof config.namespace !== 'string' || config.namespace.trim().length === 0)) {
107
+ throw new ConfigValidationError('KubernetesBackendConfig: "namespace" must be a non-empty string');
108
+ }
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazeljs/discovery",
3
- "version": "0.2.0-beta.16",
3
+ "version": "0.2.0-beta.18",
4
4
  "description": "Service discovery and registry for HazelJS microservices - Eureka-inspired with multiple backend support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  "clean": "rm -rf dist"
19
19
  },
20
20
  "dependencies": {
21
- "@hazeljs/core": "^0.2.0-beta.16",
21
+ "@hazeljs/core": "^0.2.0-beta.18",
22
22
  "axios": "^1.6.0",
23
23
  "reflect-metadata": "^0.2.2"
24
24
  },
@@ -75,5 +75,5 @@
75
75
  "url": "https://github.com/hazeljs/hazel-js/issues"
76
76
  },
77
77
  "homepage": "https://hazeljs.com",
78
- "gitHead": "e022330edc869ed8dc8d9765aefd085438088297"
78
+ "gitHead": "22082c1277661421cd32b3c4371c2c9d0bdf0501"
79
79
  }