@hazeljs/gateway 0.2.0-beta.41

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 (72) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +255 -0
  3. package/dist/__tests__/canary-engine.test.d.ts +2 -0
  4. package/dist/__tests__/canary-engine.test.d.ts.map +1 -0
  5. package/dist/__tests__/canary-engine.test.js +133 -0
  6. package/dist/__tests__/decorators.test.d.ts +2 -0
  7. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  8. package/dist/__tests__/decorators.test.js +174 -0
  9. package/dist/__tests__/from-config.test.d.ts +2 -0
  10. package/dist/__tests__/from-config.test.d.ts.map +1 -0
  11. package/dist/__tests__/from-config.test.js +67 -0
  12. package/dist/__tests__/gateway-metrics.test.d.ts +2 -0
  13. package/dist/__tests__/gateway-metrics.test.d.ts.map +1 -0
  14. package/dist/__tests__/gateway-metrics.test.js +82 -0
  15. package/dist/__tests__/gateway-module.test.d.ts +2 -0
  16. package/dist/__tests__/gateway-module.test.d.ts.map +1 -0
  17. package/dist/__tests__/gateway-module.test.js +91 -0
  18. package/dist/__tests__/gateway.test.d.ts +2 -0
  19. package/dist/__tests__/gateway.test.d.ts.map +1 -0
  20. package/dist/__tests__/gateway.test.js +257 -0
  21. package/dist/__tests__/hazel-integration.test.d.ts +2 -0
  22. package/dist/__tests__/hazel-integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/hazel-integration.test.js +92 -0
  24. package/dist/__tests__/route-matcher.test.d.ts +2 -0
  25. package/dist/__tests__/route-matcher.test.d.ts.map +1 -0
  26. package/dist/__tests__/route-matcher.test.js +67 -0
  27. package/dist/__tests__/service-proxy.test.d.ts +2 -0
  28. package/dist/__tests__/service-proxy.test.d.ts.map +1 -0
  29. package/dist/__tests__/service-proxy.test.js +110 -0
  30. package/dist/__tests__/traffic-mirror.test.d.ts +2 -0
  31. package/dist/__tests__/traffic-mirror.test.d.ts.map +1 -0
  32. package/dist/__tests__/traffic-mirror.test.js +70 -0
  33. package/dist/__tests__/version-router.test.d.ts +2 -0
  34. package/dist/__tests__/version-router.test.d.ts.map +1 -0
  35. package/dist/__tests__/version-router.test.js +136 -0
  36. package/dist/canary/canary-engine.d.ts +107 -0
  37. package/dist/canary/canary-engine.d.ts.map +1 -0
  38. package/dist/canary/canary-engine.js +334 -0
  39. package/dist/decorators/index.d.ts +74 -0
  40. package/dist/decorators/index.d.ts.map +1 -0
  41. package/dist/decorators/index.js +170 -0
  42. package/dist/gateway.d.ts +67 -0
  43. package/dist/gateway.d.ts.map +1 -0
  44. package/dist/gateway.js +310 -0
  45. package/dist/gateway.module.d.ts +67 -0
  46. package/dist/gateway.module.d.ts.map +1 -0
  47. package/dist/gateway.module.js +82 -0
  48. package/dist/hazel-integration.d.ts +24 -0
  49. package/dist/hazel-integration.d.ts.map +1 -0
  50. package/dist/hazel-integration.js +70 -0
  51. package/dist/index.d.ts +20 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +62 -0
  54. package/dist/metrics/gateway-metrics.d.ts +64 -0
  55. package/dist/metrics/gateway-metrics.d.ts.map +1 -0
  56. package/dist/metrics/gateway-metrics.js +159 -0
  57. package/dist/middleware/traffic-mirror.d.ts +19 -0
  58. package/dist/middleware/traffic-mirror.d.ts.map +1 -0
  59. package/dist/middleware/traffic-mirror.js +60 -0
  60. package/dist/proxy/service-proxy.d.ts +68 -0
  61. package/dist/proxy/service-proxy.d.ts.map +1 -0
  62. package/dist/proxy/service-proxy.js +211 -0
  63. package/dist/routing/route-matcher.d.ts +31 -0
  64. package/dist/routing/route-matcher.d.ts.map +1 -0
  65. package/dist/routing/route-matcher.js +112 -0
  66. package/dist/routing/version-router.d.ts +36 -0
  67. package/dist/routing/version-router.d.ts.map +1 -0
  68. package/dist/routing/version-router.js +136 -0
  69. package/dist/types/index.d.ts +217 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +17 -0
  72. package/package.json +74 -0
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const version_router_1 = require("../routing/version-router");
4
+ function makeRequest(overrides = {}) {
5
+ return {
6
+ method: 'GET',
7
+ path: '/api/test',
8
+ headers: {},
9
+ ...overrides,
10
+ };
11
+ }
12
+ describe('VersionRouter', () => {
13
+ describe('header-based routing', () => {
14
+ it('should resolve version from header', () => {
15
+ const router = new version_router_1.VersionRouter({
16
+ header: 'X-API-Version',
17
+ routes: {
18
+ v1: { weight: 80 },
19
+ v2: { weight: 20 },
20
+ },
21
+ });
22
+ const result = router.resolve(makeRequest({ headers: { 'X-API-Version': 'v2' } }));
23
+ expect(result.version).toBe('v2');
24
+ expect(result.resolvedBy).toBe('header');
25
+ });
26
+ it('should resolve explicitly allowed version even with 0 weight', () => {
27
+ const router = new version_router_1.VersionRouter({
28
+ header: 'X-API-Version',
29
+ routes: {
30
+ v1: { weight: 100 },
31
+ v2: { weight: 0, allowExplicit: true },
32
+ },
33
+ });
34
+ const result = router.resolve(makeRequest({ headers: { 'X-API-Version': 'v2' } }));
35
+ expect(result.version).toBe('v2');
36
+ expect(result.resolvedBy).toBe('header');
37
+ });
38
+ it('should not resolve version with 0 weight and no allowExplicit', () => {
39
+ const router = new version_router_1.VersionRouter({
40
+ header: 'X-API-Version',
41
+ routes: {
42
+ v1: { weight: 100 },
43
+ v2: { weight: 0 },
44
+ },
45
+ });
46
+ // Header says v2 but it's not explicitly allowed
47
+ const result = router.resolve(makeRequest({ headers: { 'X-API-Version': 'v2' } }));
48
+ expect(result.version).toBe('v1');
49
+ expect(result.resolvedBy).toBe('weight');
50
+ });
51
+ });
52
+ describe('weighted routing', () => {
53
+ it('should fall back to weighted routing when no explicit version', () => {
54
+ const router = new version_router_1.VersionRouter({
55
+ routes: {
56
+ v1: { weight: 100 },
57
+ v2: { weight: 0 },
58
+ },
59
+ });
60
+ const result = router.resolve(makeRequest());
61
+ expect(result.version).toBe('v1');
62
+ expect(result.resolvedBy).toBe('weight');
63
+ });
64
+ it('should distribute based on weights', () => {
65
+ const router = new version_router_1.VersionRouter({
66
+ routes: {
67
+ v1: { weight: 50 },
68
+ v2: { weight: 50 },
69
+ },
70
+ });
71
+ const counts = { v1: 0, v2: 0 };
72
+ for (let i = 0; i < 1000; i++) {
73
+ const result = router.resolve(makeRequest());
74
+ counts[result.version]++;
75
+ }
76
+ // With 50/50 weight over 1000 runs, each should be roughly 500
77
+ expect(counts['v1']).toBeGreaterThan(350);
78
+ expect(counts['v2']).toBeGreaterThan(350);
79
+ });
80
+ });
81
+ describe('default version', () => {
82
+ it('should use defaultVersion when no other resolution works', () => {
83
+ const router = new version_router_1.VersionRouter({
84
+ defaultVersion: 'v1',
85
+ routes: {
86
+ v1: { weight: 0 },
87
+ v2: { weight: 0 },
88
+ },
89
+ });
90
+ const result = router.resolve(makeRequest());
91
+ expect(result.version).toBe('v1');
92
+ expect(result.resolvedBy).toBe('default');
93
+ });
94
+ });
95
+ describe('URI-based routing', () => {
96
+ it('should resolve version from URI path', () => {
97
+ const router = new version_router_1.VersionRouter({
98
+ strategy: 'uri',
99
+ routes: {
100
+ v1: { weight: 100 },
101
+ v2: { weight: 0, allowExplicit: true },
102
+ },
103
+ });
104
+ const result = router.resolve(makeRequest({ path: '/v2/api/test' }));
105
+ expect(result.version).toBe('v2');
106
+ expect(result.resolvedBy).toBe('uri');
107
+ });
108
+ });
109
+ describe('query-based routing', () => {
110
+ it('should resolve version from query parameter', () => {
111
+ const router = new version_router_1.VersionRouter({
112
+ strategy: 'query',
113
+ queryParam: 'version',
114
+ routes: {
115
+ v1: { weight: 100 },
116
+ v2: { weight: 0, allowExplicit: true },
117
+ },
118
+ });
119
+ const result = router.resolve(makeRequest({ query: { version: 'v2' } }));
120
+ expect(result.version).toBe('v2');
121
+ expect(result.resolvedBy).toBe('query');
122
+ });
123
+ });
124
+ describe('getVersions', () => {
125
+ it('should return all configured versions', () => {
126
+ const router = new version_router_1.VersionRouter({
127
+ routes: {
128
+ v1: { weight: 50 },
129
+ v2: { weight: 50 },
130
+ v3: { weight: 0 },
131
+ },
132
+ });
133
+ expect(router.getVersions()).toEqual(['v1', 'v2', 'v3']);
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Canary Deployment Engine
3
+ *
4
+ * The differentiator feature of @hazeljs/gateway.
5
+ *
6
+ * Tracks real-time error rates and latency per service version,
7
+ * automatically promotes canary traffic through configurable steps,
8
+ * and rolls back if error thresholds are breached.
9
+ *
10
+ * States:
11
+ * ACTIVE -> canary is receiving traffic at current weight
12
+ * PROMOTED -> canary promoted to 100%, rollout complete
13
+ * ROLLED_BACK -> canary failed, all traffic to stable
14
+ * PAUSED -> manual pause, no automatic transitions
15
+ */
16
+ import { EventEmitter } from 'events';
17
+ import { CanaryConfig, CanaryState, CanaryMetrics, ProxyRequest } from '../types';
18
+ export interface CanaryStatus {
19
+ state: CanaryState;
20
+ stableVersion: string;
21
+ canaryVersion: string;
22
+ currentStableWeight: number;
23
+ currentCanaryWeight: number;
24
+ currentStep: number;
25
+ totalSteps: number;
26
+ metrics: CanaryMetrics;
27
+ lastEvaluation?: Date;
28
+ lastTransition?: Date;
29
+ }
30
+ export declare class CanaryEngine extends EventEmitter {
31
+ private state;
32
+ private config;
33
+ private stableMetrics;
34
+ private canaryMetrics;
35
+ private currentStableWeight;
36
+ private currentCanaryWeight;
37
+ private currentStepIndex;
38
+ private evaluationTimer?;
39
+ private promotionTimer?;
40
+ private lastEvaluation?;
41
+ private lastTransition?;
42
+ private evaluationWindowMs;
43
+ private stepIntervalMs;
44
+ constructor(config: CanaryConfig);
45
+ /**
46
+ * Start the canary evaluation loop
47
+ */
48
+ start(): void;
49
+ /**
50
+ * Stop the canary evaluation loop
51
+ */
52
+ stop(): void;
53
+ /**
54
+ * Route a request: returns 'stable' or 'canary' based on current weights
55
+ */
56
+ selectVersion(_request: ProxyRequest): 'stable' | 'canary';
57
+ /**
58
+ * Record a successful request for a version
59
+ */
60
+ recordSuccess(target: 'stable' | 'canary', duration: number): void;
61
+ /**
62
+ * Record a failed request for a version
63
+ */
64
+ recordFailure(target: 'stable' | 'canary', duration: number, error?: string): void;
65
+ /**
66
+ * Get the current canary status
67
+ */
68
+ getStatus(): CanaryStatus;
69
+ /**
70
+ * Get current metrics for both versions
71
+ */
72
+ getMetrics(): CanaryMetrics;
73
+ /**
74
+ * Get the version string for a target
75
+ */
76
+ getVersion(target: 'stable' | 'canary'): string;
77
+ /**
78
+ * Manually promote to the next step
79
+ */
80
+ promote(): void;
81
+ /**
82
+ * Manually rollback the canary
83
+ */
84
+ rollback(): void;
85
+ /**
86
+ * Manually pause the canary
87
+ */
88
+ pause(): void;
89
+ /**
90
+ * Resume a paused canary
91
+ */
92
+ resume(): void;
93
+ private evaluate;
94
+ private makeDecision;
95
+ private evaluateByErrorRate;
96
+ private evaluateByLatency;
97
+ private schedulePromotion;
98
+ private doPromote;
99
+ private doRollback;
100
+ private findStepIndex;
101
+ }
102
+ /**
103
+ * Parse an interval string like '5m', '10s', '1h' to milliseconds.
104
+ * Also accepts raw numbers (treated as ms).
105
+ */
106
+ export declare function parseInterval(value: number | string): number;
107
+ //# sourceMappingURL=canary-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canary-engine.d.ts","sourceRoot":"","sources":["../../src/canary/canary-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAElG,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,cAAc,CAAC,EAAE,IAAI,CAAC;IACtB,cAAc,CAAC,EAAE,IAAI,CAAC;CACvB;AAED,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,aAAa,CAAmB;IACxC,OAAO,CAAC,aAAa,CAAmB;IACxC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAO;IAC9B,OAAO,CAAC,cAAc,CAAC,CAAO;IAC9B,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,EAAE,YAAY;IAiBhC;;OAEG;IACH,KAAK,IAAI,IAAI;IAeb;;OAEG;IACH,IAAI,IAAI,IAAI;IAWZ;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,YAAY,GAAG,QAAQ,GAAG,QAAQ;IAS1D;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQlE;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAQlF;;OAEG;IACH,SAAS,IAAI,YAAY;IAezB;;OAEG;IACH,UAAU,IAAI,aAAa;IAsB3B;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM;IAI/C;;OAEG;IACH,OAAO,IAAI,IAAI;IAIf;;OAEG;IACH,QAAQ,IAAI,IAAI;IAIhB;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,MAAM,IAAI,IAAI;IASd,OAAO,CAAC,QAAQ;IA+BhB,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,SAAS;IAkCjB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,aAAa;CAOtB;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAyB5D"}
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ /**
3
+ * Canary Deployment Engine
4
+ *
5
+ * The differentiator feature of @hazeljs/gateway.
6
+ *
7
+ * Tracks real-time error rates and latency per service version,
8
+ * automatically promotes canary traffic through configurable steps,
9
+ * and rolls back if error thresholds are breached.
10
+ *
11
+ * States:
12
+ * ACTIVE -> canary is receiving traffic at current weight
13
+ * PROMOTED -> canary promoted to 100%, rollout complete
14
+ * ROLLED_BACK -> canary failed, all traffic to stable
15
+ * PAUSED -> manual pause, no automatic transitions
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.CanaryEngine = void 0;
19
+ exports.parseInterval = parseInterval;
20
+ const events_1 = require("events");
21
+ const resilience_1 = require("@hazeljs/resilience");
22
+ const types_1 = require("../types");
23
+ class CanaryEngine extends events_1.EventEmitter {
24
+ constructor(config) {
25
+ super();
26
+ this.state = types_1.CanaryState.ACTIVE;
27
+ this.currentStepIndex = 0;
28
+ this.config = config;
29
+ this.currentStableWeight = config.stable.weight;
30
+ this.currentCanaryWeight = config.canary.weight;
31
+ this.evaluationWindowMs = parseInterval(config.promotion.evaluationWindow);
32
+ this.stepIntervalMs = parseInterval(config.promotion.stepInterval);
33
+ // Initialize metrics collectors with evaluation window
34
+ this.stableMetrics = new resilience_1.MetricsCollector(this.evaluationWindowMs);
35
+ this.canaryMetrics = new resilience_1.MetricsCollector(this.evaluationWindowMs);
36
+ // Find the initial step index based on current canary weight
37
+ this.currentStepIndex = this.findStepIndex(config.canary.weight);
38
+ }
39
+ /**
40
+ * Start the canary evaluation loop
41
+ */
42
+ start() {
43
+ if (this.state !== types_1.CanaryState.ACTIVE)
44
+ return;
45
+ // Schedule periodic evaluation
46
+ this.evaluationTimer = setInterval(() => {
47
+ this.evaluate();
48
+ }, this.evaluationWindowMs);
49
+ this.emit('canary:started', {
50
+ stable: this.config.stable.version,
51
+ canary: this.config.canary.version,
52
+ weight: this.currentCanaryWeight,
53
+ });
54
+ }
55
+ /**
56
+ * Stop the canary evaluation loop
57
+ */
58
+ stop() {
59
+ if (this.evaluationTimer) {
60
+ clearInterval(this.evaluationTimer);
61
+ this.evaluationTimer = undefined;
62
+ }
63
+ if (this.promotionTimer) {
64
+ clearTimeout(this.promotionTimer);
65
+ this.promotionTimer = undefined;
66
+ }
67
+ }
68
+ /**
69
+ * Route a request: returns 'stable' or 'canary' based on current weights
70
+ */
71
+ selectVersion(_request) {
72
+ if (this.state === types_1.CanaryState.ROLLED_BACK)
73
+ return 'stable';
74
+ if (this.state === types_1.CanaryState.PROMOTED)
75
+ return 'canary';
76
+ // Weighted random selection
77
+ const random = Math.random() * 100;
78
+ return random < this.currentCanaryWeight ? 'canary' : 'stable';
79
+ }
80
+ /**
81
+ * Record a successful request for a version
82
+ */
83
+ recordSuccess(target, duration) {
84
+ if (target === 'stable') {
85
+ this.stableMetrics.recordSuccess(duration);
86
+ }
87
+ else {
88
+ this.canaryMetrics.recordSuccess(duration);
89
+ }
90
+ }
91
+ /**
92
+ * Record a failed request for a version
93
+ */
94
+ recordFailure(target, duration, error) {
95
+ if (target === 'stable') {
96
+ this.stableMetrics.recordFailure(duration, error);
97
+ }
98
+ else {
99
+ this.canaryMetrics.recordFailure(duration, error);
100
+ }
101
+ }
102
+ /**
103
+ * Get the current canary status
104
+ */
105
+ getStatus() {
106
+ return {
107
+ state: this.state,
108
+ stableVersion: this.config.stable.version,
109
+ canaryVersion: this.config.canary.version,
110
+ currentStableWeight: this.currentStableWeight,
111
+ currentCanaryWeight: this.currentCanaryWeight,
112
+ currentStep: this.currentStepIndex,
113
+ totalSteps: this.config.promotion.steps.length,
114
+ metrics: this.getMetrics(),
115
+ lastEvaluation: this.lastEvaluation,
116
+ lastTransition: this.lastTransition,
117
+ };
118
+ }
119
+ /**
120
+ * Get current metrics for both versions
121
+ */
122
+ getMetrics() {
123
+ const stableSnapshot = this.stableMetrics.getSnapshot();
124
+ const canarySnapshot = this.canaryMetrics.getSnapshot();
125
+ return {
126
+ stable: {
127
+ totalRequests: stableSnapshot.totalCalls,
128
+ errorCount: stableSnapshot.failureCalls,
129
+ errorRate: stableSnapshot.failureRate,
130
+ averageLatency: stableSnapshot.averageResponseTime,
131
+ p99Latency: stableSnapshot.p99ResponseTime,
132
+ },
133
+ canary: {
134
+ totalRequests: canarySnapshot.totalCalls,
135
+ errorCount: canarySnapshot.failureCalls,
136
+ errorRate: canarySnapshot.failureRate,
137
+ averageLatency: canarySnapshot.averageResponseTime,
138
+ p99Latency: canarySnapshot.p99ResponseTime,
139
+ },
140
+ };
141
+ }
142
+ /**
143
+ * Get the version string for a target
144
+ */
145
+ getVersion(target) {
146
+ return target === 'stable' ? this.config.stable.version : this.config.canary.version;
147
+ }
148
+ /**
149
+ * Manually promote to the next step
150
+ */
151
+ promote() {
152
+ this.doPromote();
153
+ }
154
+ /**
155
+ * Manually rollback the canary
156
+ */
157
+ rollback() {
158
+ this.doRollback('manual');
159
+ }
160
+ /**
161
+ * Manually pause the canary
162
+ */
163
+ pause() {
164
+ this.state = types_1.CanaryState.PAUSED;
165
+ this.stop();
166
+ this.emit('canary:paused', this.getStatus());
167
+ }
168
+ /**
169
+ * Resume a paused canary
170
+ */
171
+ resume() {
172
+ if (this.state !== types_1.CanaryState.PAUSED)
173
+ return;
174
+ this.state = types_1.CanaryState.ACTIVE;
175
+ this.start();
176
+ this.emit('canary:resumed', this.getStatus());
177
+ }
178
+ // ─── Evaluation ───
179
+ evaluate() {
180
+ if (this.state !== types_1.CanaryState.ACTIVE)
181
+ return;
182
+ this.lastEvaluation = new Date();
183
+ const metrics = this.getMetrics();
184
+ // Check minimum requests threshold
185
+ const minRequests = this.config.promotion.minRequests ?? 10;
186
+ if (metrics.canary.totalRequests < minRequests) {
187
+ return; // Not enough data to evaluate
188
+ }
189
+ const decision = this.makeDecision(metrics);
190
+ switch (decision) {
191
+ case 'promote':
192
+ if (this.config.promotion.autoPromote) {
193
+ this.schedulePromotion();
194
+ }
195
+ break;
196
+ case 'rollback':
197
+ if (this.config.promotion.autoRollback) {
198
+ this.doRollback('auto');
199
+ }
200
+ break;
201
+ case 'hold':
202
+ // Do nothing, wait for next evaluation
203
+ break;
204
+ }
205
+ }
206
+ makeDecision(metrics) {
207
+ // Custom evaluator takes priority
208
+ if (this.config.promotion.customEvaluator) {
209
+ return this.config.promotion.customEvaluator(metrics);
210
+ }
211
+ const strategy = this.config.promotion.strategy;
212
+ if (strategy === 'error-rate') {
213
+ return this.evaluateByErrorRate(metrics);
214
+ }
215
+ if (strategy === 'latency') {
216
+ return this.evaluateByLatency(metrics);
217
+ }
218
+ return 'hold';
219
+ }
220
+ evaluateByErrorRate(metrics) {
221
+ const threshold = this.config.promotion.errorThreshold ?? 5;
222
+ // If canary error rate exceeds threshold, rollback
223
+ if (metrics.canary.errorRate > threshold) {
224
+ return 'rollback';
225
+ }
226
+ // If canary error rate is within threshold, promote
227
+ if (metrics.canary.errorRate <= threshold) {
228
+ return 'promote';
229
+ }
230
+ return 'hold';
231
+ }
232
+ evaluateByLatency(metrics) {
233
+ const threshold = this.config.promotion.latencyThreshold ?? 1000;
234
+ // If canary p99 latency exceeds threshold, rollback
235
+ if (metrics.canary.p99Latency > threshold) {
236
+ return 'rollback';
237
+ }
238
+ // If canary latency is acceptable, promote
239
+ return 'promote';
240
+ }
241
+ schedulePromotion() {
242
+ // If already at the last step, we're done
243
+ if (this.currentStepIndex >= this.config.promotion.steps.length - 1) {
244
+ this.doPromote();
245
+ return;
246
+ }
247
+ // If a promotion is already scheduled, don't double-schedule
248
+ if (this.promotionTimer)
249
+ return;
250
+ // Schedule the next step
251
+ this.promotionTimer = setTimeout(() => {
252
+ this.promotionTimer = undefined;
253
+ this.doPromote();
254
+ }, this.stepIntervalMs);
255
+ }
256
+ doPromote() {
257
+ if (this.state !== types_1.CanaryState.ACTIVE)
258
+ return;
259
+ this.currentStepIndex++;
260
+ this.lastTransition = new Date();
261
+ if (this.currentStepIndex >= this.config.promotion.steps.length) {
262
+ // Fully promoted
263
+ this.currentCanaryWeight = 100;
264
+ this.currentStableWeight = 0;
265
+ this.state = types_1.CanaryState.PROMOTED;
266
+ this.stop();
267
+ this.emit('canary:complete', {
268
+ version: this.config.canary.version,
269
+ metrics: this.getMetrics(),
270
+ });
271
+ return;
272
+ }
273
+ // Move to the next weight step
274
+ const newWeight = this.config.promotion.steps[this.currentStepIndex];
275
+ this.currentCanaryWeight = newWeight;
276
+ this.currentStableWeight = 100 - newWeight;
277
+ this.emit('canary:promote', {
278
+ step: this.currentStepIndex,
279
+ totalSteps: this.config.promotion.steps.length,
280
+ canaryWeight: this.currentCanaryWeight,
281
+ stableWeight: this.currentStableWeight,
282
+ metrics: this.getMetrics(),
283
+ });
284
+ }
285
+ doRollback(trigger) {
286
+ this.state = types_1.CanaryState.ROLLED_BACK;
287
+ this.currentCanaryWeight = 0;
288
+ this.currentStableWeight = 100;
289
+ this.lastTransition = new Date();
290
+ this.stop();
291
+ this.emit('canary:rollback', {
292
+ trigger,
293
+ canaryVersion: this.config.canary.version,
294
+ stableVersion: this.config.stable.version,
295
+ metrics: this.getMetrics(),
296
+ });
297
+ }
298
+ findStepIndex(weight) {
299
+ const steps = this.config.promotion.steps;
300
+ for (let i = 0; i < steps.length; i++) {
301
+ if (steps[i] >= weight)
302
+ return i;
303
+ }
304
+ return 0;
305
+ }
306
+ }
307
+ exports.CanaryEngine = CanaryEngine;
308
+ // ─── Utilities ───
309
+ /**
310
+ * Parse an interval string like '5m', '10s', '1h' to milliseconds.
311
+ * Also accepts raw numbers (treated as ms).
312
+ */
313
+ function parseInterval(value) {
314
+ if (typeof value === 'number')
315
+ return value;
316
+ const match = value.match(/^(\d+)(ms|s|m|h)$/);
317
+ if (!match) {
318
+ throw new Error(`Invalid interval format: "${value}". Use formats like "5m", "30s", "1h", or a number in ms.`);
319
+ }
320
+ const amount = parseInt(match[1], 10);
321
+ const unit = match[2];
322
+ switch (unit) {
323
+ case 'ms':
324
+ return amount;
325
+ case 's':
326
+ return amount * 1000;
327
+ case 'm':
328
+ return amount * 60 * 1000;
329
+ case 'h':
330
+ return amount * 60 * 60 * 1000;
331
+ default:
332
+ return amount;
333
+ }
334
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Gateway Decorators
3
+ * Declarative API for defining gateway routes and policies.
4
+ */
5
+ import 'reflect-metadata';
6
+ import { GatewayConfig, RouteConfig, ServiceRouteConfig, VersionRouteConfig, CanaryConfig, TrafficPolicyConfig } from '../types';
7
+ import { CircuitBreakerConfig, RateLimiterConfig } from '@hazeljs/resilience';
8
+ type Constructor = new (...args: unknown[]) => unknown;
9
+ /**
10
+ * @Gateway class decorator
11
+ * Marks a class as a gateway definition with global configuration.
12
+ */
13
+ export declare function Gateway(config: GatewayConfig): ClassDecorator;
14
+ /**
15
+ * @Route property/method decorator
16
+ * Defines a URL pattern this route handles.
17
+ */
18
+ export declare function Route(pathOrConfig: string | RouteConfig): PropertyDecorator;
19
+ /**
20
+ * @ServiceRoute property/method decorator
21
+ * Maps this route to a service in the discovery registry.
22
+ */
23
+ export declare function ServiceRoute(nameOrConfig: string | ServiceRouteConfig): PropertyDecorator;
24
+ /**
25
+ * @VersionRoute property/method decorator
26
+ * Enables version-based routing for this route.
27
+ */
28
+ export declare function VersionRoute(config: VersionRouteConfig): PropertyDecorator;
29
+ /**
30
+ * @Canary property/method decorator
31
+ * Enables canary deployment for this route.
32
+ */
33
+ export declare function Canary(config: CanaryConfig): PropertyDecorator;
34
+ /**
35
+ * @TrafficPolicy property/method decorator
36
+ * Defines traffic policies (mirroring, transformation, etc.)
37
+ */
38
+ export declare function TrafficPolicy(config: TrafficPolicyConfig): PropertyDecorator;
39
+ /**
40
+ * @CircuitBreaker property decorator (gateway-specific)
41
+ * Overrides circuit breaker configuration for this route.
42
+ */
43
+ export declare function GatewayCircuitBreaker(config: Partial<CircuitBreakerConfig>): PropertyDecorator;
44
+ /**
45
+ * @RateLimit property decorator (gateway-specific)
46
+ * Overrides rate limit configuration for this route.
47
+ */
48
+ export declare function GatewayRateLimit(config: Partial<RateLimiterConfig>): PropertyDecorator;
49
+ export declare function getGatewayConfig(target: Constructor): GatewayConfig | undefined;
50
+ export declare function getRouteConfig(target: object, propertyKey: string | symbol): RouteConfig | undefined;
51
+ export declare function getServiceRouteConfig(target: object, propertyKey: string | symbol): ServiceRouteConfig | undefined;
52
+ export declare function getVersionRouteConfig(target: object, propertyKey: string | symbol): VersionRouteConfig | undefined;
53
+ export declare function getCanaryConfig(target: object, propertyKey: string | symbol): CanaryConfig | undefined;
54
+ export declare function getTrafficPolicyConfig(target: object, propertyKey: string | symbol): TrafficPolicyConfig | undefined;
55
+ export declare function getCircuitBreakerConfig(target: object, propertyKey: string | symbol): Partial<CircuitBreakerConfig> | undefined;
56
+ export declare function getRateLimitConfig(target: object, propertyKey: string | symbol): Partial<RateLimiterConfig> | undefined;
57
+ /**
58
+ * Collect all route definitions from a gateway class instance
59
+ */
60
+ export declare function collectRouteDefinitions(gatewayClass: Constructor): {
61
+ config: GatewayConfig;
62
+ routes: Array<{
63
+ propertyKey: string;
64
+ route?: RouteConfig;
65
+ serviceRoute?: ServiceRouteConfig;
66
+ versionRoute?: VersionRouteConfig;
67
+ canary?: CanaryConfig;
68
+ trafficPolicy?: TrafficPolicyConfig;
69
+ circuitBreaker?: Partial<CircuitBreakerConfig>;
70
+ rateLimit?: Partial<RateLimiterConfig>;
71
+ }>;
72
+ };
73
+ export {};
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/decorators/index.ts"],"names":[],"mappings":"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"}