nestjs-ddd-cli 2.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +26 -3
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Circuit Breaker & Resilience Patterns Generator
|
|
4
|
+
* Implements retry logic, timeout handling, and fallback strategies
|
|
5
|
+
*/
|
|
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
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.setupResiliencePatterns = setupResiliencePatterns;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupResiliencePatterns(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n🛡️ Setting up Resilience Patterns\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/resilience');
|
|
50
|
+
if (!fs.existsSync(sharedPath)) {
|
|
51
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Generate circuit breaker
|
|
54
|
+
fs.writeFileSync(path.join(sharedPath, 'circuit-breaker.ts'), generateCircuitBreaker());
|
|
55
|
+
console.log(chalk_1.default.green(` ✓ Created circuit breaker`));
|
|
56
|
+
// Generate retry strategy
|
|
57
|
+
fs.writeFileSync(path.join(sharedPath, 'retry.strategy.ts'), generateRetryStrategy());
|
|
58
|
+
console.log(chalk_1.default.green(` ✓ Created retry strategy`));
|
|
59
|
+
// Generate timeout handler
|
|
60
|
+
fs.writeFileSync(path.join(sharedPath, 'timeout.handler.ts'), generateTimeoutHandler());
|
|
61
|
+
console.log(chalk_1.default.green(` ✓ Created timeout handler`));
|
|
62
|
+
// Generate fallback decorator
|
|
63
|
+
fs.writeFileSync(path.join(sharedPath, 'fallback.decorator.ts'), generateFallbackDecorator());
|
|
64
|
+
console.log(chalk_1.default.green(` ✓ Created fallback decorator`));
|
|
65
|
+
// Generate bulkhead pattern
|
|
66
|
+
fs.writeFileSync(path.join(sharedPath, 'bulkhead.ts'), generateBulkhead());
|
|
67
|
+
console.log(chalk_1.default.green(` ✓ Created bulkhead pattern`));
|
|
68
|
+
// Generate resilience module
|
|
69
|
+
fs.writeFileSync(path.join(sharedPath, 'resilience.module.ts'), generateResilienceModule());
|
|
70
|
+
console.log(chalk_1.default.green(` ✓ Created resilience module`));
|
|
71
|
+
// Generate resilience decorators
|
|
72
|
+
fs.writeFileSync(path.join(sharedPath, 'resilience.decorators.ts'), generateResilienceDecorators());
|
|
73
|
+
console.log(chalk_1.default.green(` ✓ Created resilience decorators`));
|
|
74
|
+
console.log(chalk_1.default.bold.green('\n✅ Resilience patterns ready!\n'));
|
|
75
|
+
}
|
|
76
|
+
function generateCircuitBreaker() {
|
|
77
|
+
return `/**
|
|
78
|
+
* Circuit Breaker Implementation
|
|
79
|
+
* Prevents cascading failures in distributed systems
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
import { Logger } from '@nestjs/common';
|
|
83
|
+
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
84
|
+
|
|
85
|
+
export enum CircuitState {
|
|
86
|
+
CLOSED = 'CLOSED',
|
|
87
|
+
OPEN = 'OPEN',
|
|
88
|
+
HALF_OPEN = 'HALF_OPEN',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CircuitBreakerOptions {
|
|
92
|
+
name: string;
|
|
93
|
+
failureThreshold: number;
|
|
94
|
+
successThreshold: number;
|
|
95
|
+
timeout: number; // ms before trying again
|
|
96
|
+
resetTimeout?: number;
|
|
97
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
98
|
+
onSuccess?: () => void;
|
|
99
|
+
onFailure?: (error: Error) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class CircuitBreaker {
|
|
103
|
+
private readonly logger = new Logger(CircuitBreaker.name);
|
|
104
|
+
private state: CircuitState = CircuitState.CLOSED;
|
|
105
|
+
private failureCount = 0;
|
|
106
|
+
private successCount = 0;
|
|
107
|
+
private lastFailureTime: number = 0;
|
|
108
|
+
private nextAttempt: number = 0;
|
|
109
|
+
|
|
110
|
+
constructor(
|
|
111
|
+
private readonly options: CircuitBreakerOptions,
|
|
112
|
+
private readonly eventEmitter?: EventEmitter2,
|
|
113
|
+
) {}
|
|
114
|
+
|
|
115
|
+
get currentState(): CircuitState {
|
|
116
|
+
return this.state;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get isOpen(): boolean {
|
|
120
|
+
return this.state === CircuitState.OPEN;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get isClosed(): boolean {
|
|
124
|
+
return this.state === CircuitState.CLOSED;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get isHalfOpen(): boolean {
|
|
128
|
+
return this.state === CircuitState.HALF_OPEN;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
132
|
+
if (this.state === CircuitState.OPEN) {
|
|
133
|
+
if (Date.now() < this.nextAttempt) {
|
|
134
|
+
throw new CircuitOpenError(
|
|
135
|
+
\`Circuit \${this.options.name} is open. Retry after \${this.nextAttempt - Date.now()}ms\`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
this.transitionTo(CircuitState.HALF_OPEN);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const result = await fn();
|
|
143
|
+
this.onSuccess();
|
|
144
|
+
return result;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.onFailure(error as Error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private onSuccess(): void {
|
|
152
|
+
this.failureCount = 0;
|
|
153
|
+
|
|
154
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
155
|
+
this.successCount++;
|
|
156
|
+
if (this.successCount >= this.options.successThreshold) {
|
|
157
|
+
this.transitionTo(CircuitState.CLOSED);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.options.onSuccess?.();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private onFailure(error: Error): void {
|
|
165
|
+
this.failureCount++;
|
|
166
|
+
this.lastFailureTime = Date.now();
|
|
167
|
+
this.successCount = 0;
|
|
168
|
+
|
|
169
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
170
|
+
this.transitionTo(CircuitState.OPEN);
|
|
171
|
+
} else if (
|
|
172
|
+
this.state === CircuitState.CLOSED &&
|
|
173
|
+
this.failureCount >= this.options.failureThreshold
|
|
174
|
+
) {
|
|
175
|
+
this.transitionTo(CircuitState.OPEN);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.options.onFailure?.(error);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private transitionTo(newState: CircuitState): void {
|
|
182
|
+
const oldState = this.state;
|
|
183
|
+
this.state = newState;
|
|
184
|
+
|
|
185
|
+
if (newState === CircuitState.OPEN) {
|
|
186
|
+
this.nextAttempt = Date.now() + this.options.timeout;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (newState === CircuitState.CLOSED) {
|
|
190
|
+
this.failureCount = 0;
|
|
191
|
+
this.successCount = 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.logger.log(\`Circuit \${this.options.name}: \${oldState} -> \${newState}\`);
|
|
195
|
+
|
|
196
|
+
this.options.onStateChange?.(oldState, newState);
|
|
197
|
+
this.eventEmitter?.emit('circuit-breaker.state-change', {
|
|
198
|
+
name: this.options.name,
|
|
199
|
+
from: oldState,
|
|
200
|
+
to: newState,
|
|
201
|
+
timestamp: new Date(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
reset(): void {
|
|
206
|
+
this.transitionTo(CircuitState.CLOSED);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getStats(): CircuitBreakerStats {
|
|
210
|
+
return {
|
|
211
|
+
name: this.options.name,
|
|
212
|
+
state: this.state,
|
|
213
|
+
failureCount: this.failureCount,
|
|
214
|
+
successCount: this.successCount,
|
|
215
|
+
lastFailureTime: this.lastFailureTime ? new Date(this.lastFailureTime) : null,
|
|
216
|
+
nextAttempt: this.nextAttempt ? new Date(this.nextAttempt) : null,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface CircuitBreakerStats {
|
|
222
|
+
name: string;
|
|
223
|
+
state: CircuitState;
|
|
224
|
+
failureCount: number;
|
|
225
|
+
successCount: number;
|
|
226
|
+
lastFailureTime: Date | null;
|
|
227
|
+
nextAttempt: Date | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class CircuitOpenError extends Error {
|
|
231
|
+
constructor(message: string) {
|
|
232
|
+
super(message);
|
|
233
|
+
this.name = 'CircuitOpenError';
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Circuit Breaker Registry
|
|
239
|
+
*/
|
|
240
|
+
export class CircuitBreakerRegistry {
|
|
241
|
+
private readonly breakers = new Map<string, CircuitBreaker>();
|
|
242
|
+
|
|
243
|
+
constructor(private readonly eventEmitter?: EventEmitter2) {}
|
|
244
|
+
|
|
245
|
+
getOrCreate(options: CircuitBreakerOptions): CircuitBreaker {
|
|
246
|
+
let breaker = this.breakers.get(options.name);
|
|
247
|
+
if (!breaker) {
|
|
248
|
+
breaker = new CircuitBreaker(options, this.eventEmitter);
|
|
249
|
+
this.breakers.set(options.name, breaker);
|
|
250
|
+
}
|
|
251
|
+
return breaker;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
get(name: string): CircuitBreaker | undefined {
|
|
255
|
+
return this.breakers.get(name);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getAll(): CircuitBreaker[] {
|
|
259
|
+
return Array.from(this.breakers.values());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
getAllStats(): CircuitBreakerStats[] {
|
|
263
|
+
return this.getAll().map(b => b.getStats());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
resetAll(): void {
|
|
267
|
+
this.breakers.forEach(b => b.reset());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
function generateRetryStrategy() {
|
|
273
|
+
return `/**
|
|
274
|
+
* Retry Strategy Implementation
|
|
275
|
+
* Configurable retry logic with backoff strategies
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
import { Logger } from '@nestjs/common';
|
|
279
|
+
|
|
280
|
+
export interface RetryOptions {
|
|
281
|
+
maxAttempts: number;
|
|
282
|
+
delay: number; // base delay in ms
|
|
283
|
+
backoffMultiplier?: number;
|
|
284
|
+
maxDelay?: number;
|
|
285
|
+
retryOn?: (error: Error) => boolean;
|
|
286
|
+
onRetry?: (attempt: number, error: Error, delay: number) => void;
|
|
287
|
+
jitter?: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export type BackoffStrategy = 'fixed' | 'exponential' | 'linear' | 'fibonacci';
|
|
291
|
+
|
|
292
|
+
const DEFAULT_OPTIONS: RetryOptions = {
|
|
293
|
+
maxAttempts: 3,
|
|
294
|
+
delay: 1000,
|
|
295
|
+
backoffMultiplier: 2,
|
|
296
|
+
maxDelay: 30000,
|
|
297
|
+
jitter: true,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export class RetryStrategy {
|
|
301
|
+
private readonly logger = new Logger(RetryStrategy.name);
|
|
302
|
+
private readonly options: RetryOptions;
|
|
303
|
+
|
|
304
|
+
constructor(options: Partial<RetryOptions> = {}) {
|
|
305
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
309
|
+
let lastError: Error | null = null;
|
|
310
|
+
|
|
311
|
+
for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
|
|
312
|
+
try {
|
|
313
|
+
return await fn();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
lastError = error as Error;
|
|
316
|
+
|
|
317
|
+
if (attempt === this.options.maxAttempts) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.options.retryOn && !this.options.retryOn(lastError)) {
|
|
322
|
+
throw lastError;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const delay = this.calculateDelay(attempt);
|
|
326
|
+
this.logger.warn(
|
|
327
|
+
\`Attempt \${attempt} failed. Retrying in \${delay}ms...\`,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
this.options.onRetry?.(attempt, lastError, delay);
|
|
331
|
+
await this.sleep(delay);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new RetryExhaustedError(
|
|
336
|
+
\`All \${this.options.maxAttempts} retry attempts failed\`,
|
|
337
|
+
lastError!,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private calculateDelay(attempt: number): number {
|
|
342
|
+
let delay = this.options.delay * Math.pow(this.options.backoffMultiplier || 2, attempt - 1);
|
|
343
|
+
|
|
344
|
+
if (this.options.maxDelay) {
|
|
345
|
+
delay = Math.min(delay, this.options.maxDelay);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (this.options.jitter) {
|
|
349
|
+
delay = delay * (0.5 + Math.random());
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return Math.floor(delay);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private sleep(ms: number): Promise<void> {
|
|
356
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export class RetryExhaustedError extends Error {
|
|
361
|
+
constructor(message: string, public readonly lastError: Error) {
|
|
362
|
+
super(message);
|
|
363
|
+
this.name = 'RetryExhaustedError';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Retry with exponential backoff
|
|
369
|
+
*/
|
|
370
|
+
export function retryWithBackoff<T>(
|
|
371
|
+
fn: () => Promise<T>,
|
|
372
|
+
options?: Partial<RetryOptions>,
|
|
373
|
+
): Promise<T> {
|
|
374
|
+
return new RetryStrategy(options).execute(fn);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Retry decorator factory
|
|
379
|
+
*/
|
|
380
|
+
export function Retry(options?: Partial<RetryOptions>): MethodDecorator {
|
|
381
|
+
return function (
|
|
382
|
+
target: any,
|
|
383
|
+
propertyKey: string | symbol,
|
|
384
|
+
descriptor: PropertyDescriptor,
|
|
385
|
+
) {
|
|
386
|
+
const originalMethod = descriptor.value;
|
|
387
|
+
const strategy = new RetryStrategy(options);
|
|
388
|
+
|
|
389
|
+
descriptor.value = async function (...args: any[]) {
|
|
390
|
+
return strategy.execute(() => originalMethod.apply(this, args));
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return descriptor;
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Fibonacci backoff calculator
|
|
399
|
+
*/
|
|
400
|
+
export function fibonacciDelay(attempt: number, baseDelay: number): number {
|
|
401
|
+
const fib = [1, 1];
|
|
402
|
+
for (let i = 2; i <= attempt; i++) {
|
|
403
|
+
fib[i] = fib[i - 1] + fib[i - 2];
|
|
404
|
+
}
|
|
405
|
+
return fib[attempt] * baseDelay;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check if error is retryable
|
|
410
|
+
*/
|
|
411
|
+
export function isRetryableError(error: Error): boolean {
|
|
412
|
+
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE'];
|
|
413
|
+
const retryableMessages = ['timeout', 'network', 'connection', '503', '502', '429'];
|
|
414
|
+
|
|
415
|
+
if ((error as any).code && retryableCodes.includes((error as any).code)) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const message = error.message.toLowerCase();
|
|
420
|
+
return retryableMessages.some(m => message.includes(m));
|
|
421
|
+
}
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
function generateTimeoutHandler() {
|
|
425
|
+
return `/**
|
|
426
|
+
* Timeout Handler Implementation
|
|
427
|
+
* Prevents hanging operations with configurable timeouts
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
import { Logger } from '@nestjs/common';
|
|
431
|
+
|
|
432
|
+
export interface TimeoutOptions {
|
|
433
|
+
timeout: number; // ms
|
|
434
|
+
onTimeout?: () => void;
|
|
435
|
+
message?: string;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export class TimeoutHandler {
|
|
439
|
+
private readonly logger = new Logger(TimeoutHandler.name);
|
|
440
|
+
|
|
441
|
+
async execute<T>(fn: () => Promise<T>, options: TimeoutOptions): Promise<T> {
|
|
442
|
+
const { timeout, onTimeout, message } = options;
|
|
443
|
+
|
|
444
|
+
return new Promise<T>((resolve, reject) => {
|
|
445
|
+
const timer = setTimeout(() => {
|
|
446
|
+
onTimeout?.();
|
|
447
|
+
reject(new TimeoutError(message || \`Operation timed out after \${timeout}ms\`));
|
|
448
|
+
}, timeout);
|
|
449
|
+
|
|
450
|
+
fn()
|
|
451
|
+
.then(result => {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
resolve(result);
|
|
454
|
+
})
|
|
455
|
+
.catch(error => {
|
|
456
|
+
clearTimeout(timer);
|
|
457
|
+
reject(error);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export class TimeoutError extends Error {
|
|
464
|
+
constructor(message: string) {
|
|
465
|
+
super(message);
|
|
466
|
+
this.name = 'TimeoutError';
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Execute with timeout
|
|
472
|
+
*/
|
|
473
|
+
export function withTimeout<T>(
|
|
474
|
+
fn: () => Promise<T>,
|
|
475
|
+
timeout: number,
|
|
476
|
+
message?: string,
|
|
477
|
+
): Promise<T> {
|
|
478
|
+
return new TimeoutHandler().execute(fn, { timeout, message });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Timeout decorator factory
|
|
483
|
+
*/
|
|
484
|
+
export function Timeout(ms: number, message?: string): MethodDecorator {
|
|
485
|
+
return function (
|
|
486
|
+
target: any,
|
|
487
|
+
propertyKey: string | symbol,
|
|
488
|
+
descriptor: PropertyDescriptor,
|
|
489
|
+
) {
|
|
490
|
+
const originalMethod = descriptor.value;
|
|
491
|
+
const handler = new TimeoutHandler();
|
|
492
|
+
|
|
493
|
+
descriptor.value = async function (...args: any[]) {
|
|
494
|
+
return handler.execute(
|
|
495
|
+
() => originalMethod.apply(this, args),
|
|
496
|
+
{ timeout: ms, message: message || \`\${String(propertyKey)} timed out after \${ms}ms\` },
|
|
497
|
+
);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
return descriptor;
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Race multiple promises with timeout
|
|
506
|
+
*/
|
|
507
|
+
export async function raceWithTimeout<T>(
|
|
508
|
+
promises: Promise<T>[],
|
|
509
|
+
timeout: number,
|
|
510
|
+
): Promise<T> {
|
|
511
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
512
|
+
setTimeout(() => reject(new TimeoutError(\`Race timed out after \${timeout}ms\`)), timeout);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return Promise.race([...promises, timeoutPromise]);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Deadline-based timeout
|
|
520
|
+
*/
|
|
521
|
+
export class Deadline {
|
|
522
|
+
private readonly deadline: number;
|
|
523
|
+
|
|
524
|
+
constructor(timeout: number) {
|
|
525
|
+
this.deadline = Date.now() + timeout;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
get remaining(): number {
|
|
529
|
+
return Math.max(0, this.deadline - Date.now());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
get exceeded(): boolean {
|
|
533
|
+
return Date.now() > this.deadline;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
537
|
+
if (this.exceeded) {
|
|
538
|
+
throw new TimeoutError('Deadline already exceeded');
|
|
539
|
+
}
|
|
540
|
+
return withTimeout(fn, this.remaining);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
function generateFallbackDecorator() {
|
|
546
|
+
return `/**
|
|
547
|
+
* Fallback Pattern Implementation
|
|
548
|
+
* Provides graceful degradation when primary operations fail
|
|
549
|
+
*/
|
|
550
|
+
|
|
551
|
+
import { Logger } from '@nestjs/common';
|
|
552
|
+
|
|
553
|
+
export interface FallbackOptions<T> {
|
|
554
|
+
fallback: T | (() => T) | (() => Promise<T>);
|
|
555
|
+
onFallback?: (error: Error) => void;
|
|
556
|
+
shouldFallback?: (error: Error) => boolean;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export class FallbackHandler<T> {
|
|
560
|
+
private readonly logger = new Logger(FallbackHandler.name);
|
|
561
|
+
|
|
562
|
+
constructor(private readonly options: FallbackOptions<T>) {}
|
|
563
|
+
|
|
564
|
+
async execute(fn: () => Promise<T>): Promise<T> {
|
|
565
|
+
try {
|
|
566
|
+
return await fn();
|
|
567
|
+
} catch (error) {
|
|
568
|
+
if (this.options.shouldFallback && !this.options.shouldFallback(error as Error)) {
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
this.logger.warn(\`Operation failed, using fallback: \${(error as Error).message}\`);
|
|
573
|
+
this.options.onFallback?.(error as Error);
|
|
574
|
+
|
|
575
|
+
return this.resolveFallback();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private async resolveFallback(): Promise<T> {
|
|
580
|
+
const { fallback } = this.options;
|
|
581
|
+
|
|
582
|
+
if (typeof fallback === 'function') {
|
|
583
|
+
return (fallback as () => T | Promise<T>)();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return fallback;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Fallback decorator factory
|
|
592
|
+
*/
|
|
593
|
+
export function Fallback<T>(
|
|
594
|
+
fallbackValue: T | (() => T) | (() => Promise<T>),
|
|
595
|
+
options?: Omit<FallbackOptions<T>, 'fallback'>,
|
|
596
|
+
): MethodDecorator {
|
|
597
|
+
return function (
|
|
598
|
+
target: any,
|
|
599
|
+
propertyKey: string | symbol,
|
|
600
|
+
descriptor: PropertyDescriptor,
|
|
601
|
+
) {
|
|
602
|
+
const originalMethod = descriptor.value;
|
|
603
|
+
const handler = new FallbackHandler({ fallback: fallbackValue, ...options });
|
|
604
|
+
|
|
605
|
+
descriptor.value = async function (...args: any[]) {
|
|
606
|
+
return handler.execute(() => originalMethod.apply(this, args));
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
return descriptor;
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Execute with fallback
|
|
615
|
+
*/
|
|
616
|
+
export function withFallback<T>(
|
|
617
|
+
fn: () => Promise<T>,
|
|
618
|
+
fallback: T | (() => T) | (() => Promise<T>),
|
|
619
|
+
): Promise<T> {
|
|
620
|
+
return new FallbackHandler({ fallback }).execute(fn);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Cache-based fallback
|
|
625
|
+
*/
|
|
626
|
+
export class CachedFallback<T> {
|
|
627
|
+
private cache: T | null = null;
|
|
628
|
+
private cacheTime: number = 0;
|
|
629
|
+
|
|
630
|
+
constructor(private readonly ttl: number = 60000) {}
|
|
631
|
+
|
|
632
|
+
async execute(fn: () => Promise<T>): Promise<T> {
|
|
633
|
+
try {
|
|
634
|
+
const result = await fn();
|
|
635
|
+
this.cache = result;
|
|
636
|
+
this.cacheTime = Date.now();
|
|
637
|
+
return result;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (this.cache !== null && Date.now() - this.cacheTime < this.ttl) {
|
|
640
|
+
return this.cache;
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
invalidate(): void {
|
|
647
|
+
this.cache = null;
|
|
648
|
+
this.cacheTime = 0;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Multi-tier fallback
|
|
654
|
+
*/
|
|
655
|
+
export class FallbackChain<T> {
|
|
656
|
+
private readonly handlers: Array<() => Promise<T>> = [];
|
|
657
|
+
|
|
658
|
+
addHandler(handler: () => Promise<T>): this {
|
|
659
|
+
this.handlers.push(handler);
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async execute(): Promise<T> {
|
|
664
|
+
let lastError: Error | null = null;
|
|
665
|
+
|
|
666
|
+
for (const handler of this.handlers) {
|
|
667
|
+
try {
|
|
668
|
+
return await handler();
|
|
669
|
+
} catch (error) {
|
|
670
|
+
lastError = error as Error;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
throw new FallbackExhaustedError('All fallback handlers failed', lastError!);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export class FallbackExhaustedError extends Error {
|
|
679
|
+
constructor(message: string, public readonly lastError: Error) {
|
|
680
|
+
super(message);
|
|
681
|
+
this.name = 'FallbackExhaustedError';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
`;
|
|
685
|
+
}
|
|
686
|
+
function generateBulkhead() {
|
|
687
|
+
return `/**
|
|
688
|
+
* Bulkhead Pattern Implementation
|
|
689
|
+
* Isolates failures by limiting concurrent operations
|
|
690
|
+
*/
|
|
691
|
+
|
|
692
|
+
import { Logger } from '@nestjs/common';
|
|
693
|
+
|
|
694
|
+
export interface BulkheadOptions {
|
|
695
|
+
name: string;
|
|
696
|
+
maxConcurrent: number;
|
|
697
|
+
maxQueue?: number;
|
|
698
|
+
timeout?: number;
|
|
699
|
+
onReject?: () => void;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export class Bulkhead {
|
|
703
|
+
private readonly logger = new Logger(Bulkhead.name);
|
|
704
|
+
private activeCount = 0;
|
|
705
|
+
private readonly queue: Array<{
|
|
706
|
+
resolve: (value: any) => void;
|
|
707
|
+
reject: (error: Error) => void;
|
|
708
|
+
fn: () => Promise<any>;
|
|
709
|
+
timer?: NodeJS.Timeout;
|
|
710
|
+
}> = [];
|
|
711
|
+
|
|
712
|
+
constructor(private readonly options: BulkheadOptions) {}
|
|
713
|
+
|
|
714
|
+
get availableSlots(): number {
|
|
715
|
+
return this.options.maxConcurrent - this.activeCount;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
get queueSize(): number {
|
|
719
|
+
return this.queue.length;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
get isFull(): boolean {
|
|
723
|
+
return this.activeCount >= this.options.maxConcurrent;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
727
|
+
if (this.activeCount < this.options.maxConcurrent) {
|
|
728
|
+
return this.executeNow(fn);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (this.options.maxQueue && this.queue.length >= this.options.maxQueue) {
|
|
732
|
+
this.options.onReject?.();
|
|
733
|
+
throw new BulkheadRejectError(
|
|
734
|
+
\`Bulkhead \${this.options.name} is full (active: \${this.activeCount}, queued: \${this.queue.length})\`,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return this.enqueue(fn);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private async executeNow<T>(fn: () => Promise<T>): Promise<T> {
|
|
742
|
+
this.activeCount++;
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
return await fn();
|
|
746
|
+
} finally {
|
|
747
|
+
this.activeCount--;
|
|
748
|
+
this.processQueue();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private enqueue<T>(fn: () => Promise<T>): Promise<T> {
|
|
753
|
+
return new Promise((resolve, reject) => {
|
|
754
|
+
const item: any = { resolve, reject, fn };
|
|
755
|
+
|
|
756
|
+
if (this.options.timeout) {
|
|
757
|
+
item.timer = setTimeout(() => {
|
|
758
|
+
const index = this.queue.indexOf(item);
|
|
759
|
+
if (index !== -1) {
|
|
760
|
+
this.queue.splice(index, 1);
|
|
761
|
+
reject(new BulkheadTimeoutError(\`Bulkhead \${this.options.name} queue timeout\`));
|
|
762
|
+
}
|
|
763
|
+
}, this.options.timeout);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
this.queue.push(item);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private processQueue(): void {
|
|
771
|
+
if (this.queue.length === 0 || this.activeCount >= this.options.maxConcurrent) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const item = this.queue.shift()!;
|
|
776
|
+
if (item.timer) {
|
|
777
|
+
clearTimeout(item.timer);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this.executeNow(item.fn)
|
|
781
|
+
.then(item.resolve)
|
|
782
|
+
.catch(item.reject);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
getStats(): BulkheadStats {
|
|
786
|
+
return {
|
|
787
|
+
name: this.options.name,
|
|
788
|
+
activeCount: this.activeCount,
|
|
789
|
+
queueSize: this.queue.length,
|
|
790
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
791
|
+
maxQueue: this.options.maxQueue || 0,
|
|
792
|
+
availableSlots: this.availableSlots,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export interface BulkheadStats {
|
|
798
|
+
name: string;
|
|
799
|
+
activeCount: number;
|
|
800
|
+
queueSize: number;
|
|
801
|
+
maxConcurrent: number;
|
|
802
|
+
maxQueue: number;
|
|
803
|
+
availableSlots: number;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export class BulkheadRejectError extends Error {
|
|
807
|
+
constructor(message: string) {
|
|
808
|
+
super(message);
|
|
809
|
+
this.name = 'BulkheadRejectError';
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export class BulkheadTimeoutError extends Error {
|
|
814
|
+
constructor(message: string) {
|
|
815
|
+
super(message);
|
|
816
|
+
this.name = 'BulkheadTimeoutError';
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Bulkhead Registry
|
|
822
|
+
*/
|
|
823
|
+
export class BulkheadRegistry {
|
|
824
|
+
private readonly bulkheads = new Map<string, Bulkhead>();
|
|
825
|
+
|
|
826
|
+
getOrCreate(options: BulkheadOptions): Bulkhead {
|
|
827
|
+
let bulkhead = this.bulkheads.get(options.name);
|
|
828
|
+
if (!bulkhead) {
|
|
829
|
+
bulkhead = new Bulkhead(options);
|
|
830
|
+
this.bulkheads.set(options.name, bulkhead);
|
|
831
|
+
}
|
|
832
|
+
return bulkhead;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
get(name: string): Bulkhead | undefined {
|
|
836
|
+
return this.bulkheads.get(name);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
getAllStats(): BulkheadStats[] {
|
|
840
|
+
return Array.from(this.bulkheads.values()).map(b => b.getStats());
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Bulkhead decorator factory
|
|
846
|
+
*/
|
|
847
|
+
export function BulkheadLimit(options: BulkheadOptions): MethodDecorator {
|
|
848
|
+
const bulkhead = new Bulkhead(options);
|
|
849
|
+
|
|
850
|
+
return function (
|
|
851
|
+
target: any,
|
|
852
|
+
propertyKey: string | symbol,
|
|
853
|
+
descriptor: PropertyDescriptor,
|
|
854
|
+
) {
|
|
855
|
+
const originalMethod = descriptor.value;
|
|
856
|
+
|
|
857
|
+
descriptor.value = async function (...args: any[]) {
|
|
858
|
+
return bulkhead.execute(() => originalMethod.apply(this, args));
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
return descriptor;
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
`;
|
|
865
|
+
}
|
|
866
|
+
function generateResilienceModule() {
|
|
867
|
+
return `import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
868
|
+
import { CircuitBreakerRegistry } from './circuit-breaker';
|
|
869
|
+
import { BulkheadRegistry } from './bulkhead';
|
|
870
|
+
|
|
871
|
+
export interface ResilienceModuleOptions {
|
|
872
|
+
circuitBreaker?: {
|
|
873
|
+
defaultFailureThreshold?: number;
|
|
874
|
+
defaultSuccessThreshold?: number;
|
|
875
|
+
defaultTimeout?: number;
|
|
876
|
+
};
|
|
877
|
+
bulkhead?: {
|
|
878
|
+
defaultMaxConcurrent?: number;
|
|
879
|
+
defaultMaxQueue?: number;
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
@Global()
|
|
884
|
+
@Module({})
|
|
885
|
+
export class ResilienceModule {
|
|
886
|
+
static forRoot(options: ResilienceModuleOptions = {}): DynamicModule {
|
|
887
|
+
return {
|
|
888
|
+
module: ResilienceModule,
|
|
889
|
+
providers: [
|
|
890
|
+
{
|
|
891
|
+
provide: 'RESILIENCE_OPTIONS',
|
|
892
|
+
useValue: options,
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
provide: CircuitBreakerRegistry,
|
|
896
|
+
useFactory: () => new CircuitBreakerRegistry(),
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
provide: BulkheadRegistry,
|
|
900
|
+
useFactory: () => new BulkheadRegistry(),
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
exports: [CircuitBreakerRegistry, BulkheadRegistry],
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
`;
|
|
908
|
+
}
|
|
909
|
+
function generateResilienceDecorators() {
|
|
910
|
+
return `/**
|
|
911
|
+
* Combined Resilience Decorators
|
|
912
|
+
* Apply multiple resilience patterns at once
|
|
913
|
+
*/
|
|
914
|
+
|
|
915
|
+
import { CircuitBreaker, CircuitBreakerOptions } from './circuit-breaker';
|
|
916
|
+
import { RetryStrategy, RetryOptions } from './retry.strategy';
|
|
917
|
+
import { TimeoutHandler, TimeoutOptions } from './timeout.handler';
|
|
918
|
+
import { FallbackHandler, FallbackOptions } from './fallback.decorator';
|
|
919
|
+
import { Bulkhead, BulkheadOptions } from './bulkhead';
|
|
920
|
+
|
|
921
|
+
export interface ResilientOptions {
|
|
922
|
+
circuitBreaker?: Partial<CircuitBreakerOptions>;
|
|
923
|
+
retry?: Partial<RetryOptions>;
|
|
924
|
+
timeout?: number;
|
|
925
|
+
fallback?: any;
|
|
926
|
+
bulkhead?: Partial<BulkheadOptions>;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Combined resilience decorator
|
|
931
|
+
* Applies circuit breaker, retry, timeout, fallback, and bulkhead
|
|
932
|
+
*/
|
|
933
|
+
export function Resilient(options: ResilientOptions): MethodDecorator {
|
|
934
|
+
return function (
|
|
935
|
+
target: any,
|
|
936
|
+
propertyKey: string | symbol,
|
|
937
|
+
descriptor: PropertyDescriptor,
|
|
938
|
+
) {
|
|
939
|
+
const originalMethod = descriptor.value;
|
|
940
|
+
const methodName = String(propertyKey);
|
|
941
|
+
|
|
942
|
+
// Create handlers
|
|
943
|
+
const circuitBreaker = options.circuitBreaker
|
|
944
|
+
? new CircuitBreaker({
|
|
945
|
+
name: \`\${target.constructor.name}.\${methodName}\`,
|
|
946
|
+
failureThreshold: options.circuitBreaker.failureThreshold || 5,
|
|
947
|
+
successThreshold: options.circuitBreaker.successThreshold || 2,
|
|
948
|
+
timeout: options.circuitBreaker.timeout || 30000,
|
|
949
|
+
...options.circuitBreaker,
|
|
950
|
+
})
|
|
951
|
+
: null;
|
|
952
|
+
|
|
953
|
+
const retry = options.retry ? new RetryStrategy(options.retry) : null;
|
|
954
|
+
const timeout = options.timeout ? new TimeoutHandler() : null;
|
|
955
|
+
const fallback = options.fallback !== undefined
|
|
956
|
+
? new FallbackHandler({ fallback: options.fallback })
|
|
957
|
+
: null;
|
|
958
|
+
const bulkhead = options.bulkhead
|
|
959
|
+
? new Bulkhead({
|
|
960
|
+
name: \`\${target.constructor.name}.\${methodName}\`,
|
|
961
|
+
maxConcurrent: options.bulkhead.maxConcurrent || 10,
|
|
962
|
+
...options.bulkhead,
|
|
963
|
+
})
|
|
964
|
+
: null;
|
|
965
|
+
|
|
966
|
+
descriptor.value = async function (...args: any[]) {
|
|
967
|
+
let fn = () => originalMethod.apply(this, args);
|
|
968
|
+
|
|
969
|
+
// Apply patterns in order: bulkhead -> circuit breaker -> retry -> timeout -> fallback
|
|
970
|
+
if (bulkhead) {
|
|
971
|
+
const innerFn = fn;
|
|
972
|
+
fn = () => bulkhead.execute(innerFn);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (circuitBreaker) {
|
|
976
|
+
const innerFn = fn;
|
|
977
|
+
fn = () => circuitBreaker.execute(innerFn);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (retry) {
|
|
981
|
+
const innerFn = fn;
|
|
982
|
+
fn = () => retry.execute(innerFn);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (timeout) {
|
|
986
|
+
const innerFn = fn;
|
|
987
|
+
fn = () => timeout.execute(innerFn, { timeout: options.timeout! });
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (fallback) {
|
|
991
|
+
const innerFn = fn;
|
|
992
|
+
fn = () => fallback.execute(innerFn);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return fn();
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
return descriptor;
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Apply circuit breaker to a class method
|
|
1004
|
+
*/
|
|
1005
|
+
export function WithCircuitBreaker(options: Partial<CircuitBreakerOptions> = {}): MethodDecorator {
|
|
1006
|
+
return function (
|
|
1007
|
+
target: any,
|
|
1008
|
+
propertyKey: string | symbol,
|
|
1009
|
+
descriptor: PropertyDescriptor,
|
|
1010
|
+
) {
|
|
1011
|
+
const originalMethod = descriptor.value;
|
|
1012
|
+
const breaker = new CircuitBreaker({
|
|
1013
|
+
name: \`\${target.constructor.name}.\${String(propertyKey)}\`,
|
|
1014
|
+
failureThreshold: 5,
|
|
1015
|
+
successThreshold: 2,
|
|
1016
|
+
timeout: 30000,
|
|
1017
|
+
...options,
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
descriptor.value = async function (...args: any[]) {
|
|
1021
|
+
return breaker.execute(() => originalMethod.apply(this, args));
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
return descriptor;
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
`;
|
|
1028
|
+
}
|
|
1029
|
+
//# sourceMappingURL=resilience-patterns.js.map
|