@etohq/connector-engine 1.5.1-alpha.4
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/engine/clean-connector-engine.d.ts +81 -0
- package/dist/engine/clean-connector-engine.d.ts.map +1 -0
- package/dist/engine/clean-connector-engine.js +350 -0
- package/dist/engine/clean-connector-engine.js.map +1 -0
- package/dist/engine/connector-engine-impl.d.ts +73 -0
- package/dist/engine/connector-engine-impl.d.ts.map +1 -0
- package/dist/engine/connector-engine-impl.js +332 -0
- package/dist/engine/connector-engine-impl.js.map +1 -0
- package/dist/engine/connector-engine.d.ts +54 -0
- package/dist/engine/connector-engine.d.ts.map +1 -0
- package/dist/engine/connector-engine.js +694 -0
- package/dist/engine/connector-engine.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +10 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/routing-engine.d.ts +26 -0
- package/dist/engine/routing-engine.d.ts.map +1 -0
- package/dist/engine/routing-engine.js +329 -0
- package/dist/engine/routing-engine.js.map +1 -0
- package/dist/examples/booking-connector-example.d.ts +7 -0
- package/dist/examples/booking-connector-example.d.ts.map +1 -0
- package/dist/examples/booking-connector-example.js +221 -0
- package/dist/examples/booking-connector-example.js.map +1 -0
- package/dist/examples/dynamic-methods-example.d.ts +7 -0
- package/dist/examples/dynamic-methods-example.d.ts.map +1 -0
- package/dist/examples/dynamic-methods-example.js +163 -0
- package/dist/examples/dynamic-methods-example.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/types/base-plugin.d.ts +170 -0
- package/dist/types/base-plugin.d.ts.map +1 -0
- package/dist/types/base-plugin.js +68 -0
- package/dist/types/base-plugin.js.map +1 -0
- package/dist/types/connector-plugin.d.ts +22 -0
- package/dist/types/connector-plugin.d.ts.map +1 -0
- package/dist/types/connector-plugin.js +11 -0
- package/dist/types/connector-plugin.js.map +1 -0
- package/dist/types/engine.d.ts +223 -0
- package/dist/types/engine.d.ts.map +1 -0
- package/dist/types/engine.js +7 -0
- package/dist/types/engine.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/operation-groups.d.ts +78 -0
- package/dist/types/operation-groups.d.ts.map +1 -0
- package/dist/types/operation-groups.js +60 -0
- package/dist/types/operation-groups.js.map +1 -0
- package/dist/types/routing-config.d.ts +116 -0
- package/dist/types/routing-config.d.ts.map +1 -0
- package/dist/types/routing-config.js +6 -0
- package/dist/types/routing-config.js.map +1 -0
- package/dist/utils/create-connector-engine.d.ts +31 -0
- package/dist/utils/create-connector-engine.d.ts.map +1 -0
- package/dist/utils/create-connector-engine.js +30 -0
- package/dist/utils/create-connector-engine.js.map +1 -0
- package/examples/booking-example.ts +168 -0
- package/examples/booking-test.ts +231 -0
- package/hyperswitch-example.ts +263 -0
- package/jest.config.js +2 -0
- package/package.json +54 -0
- package/src/engine/clean-connector-engine.ts +726 -0
- package/src/engine/index.ts +13 -0
- package/src/engine/routing-engine.ts +394 -0
- package/src/index.ts +32 -0
- package/src/types/connector-plugin.ts +34 -0
- package/src/types/index.ts +5 -0
- package/src/types/routing-config.ts +196 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core Connector Engine
|
|
3
|
+
* @description Clean plugin-first connector engine
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { ConnectorEngine } from "./clean-connector-engine";
|
|
7
|
+
export type {
|
|
8
|
+
ConnectorRegistry,
|
|
9
|
+
OperationGroup,
|
|
10
|
+
OperationGroups,
|
|
11
|
+
EngineConfig,
|
|
12
|
+
GroupResult
|
|
13
|
+
} from "./clean-connector-engine";
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Hyperswitch-Inspired Routing Engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
RoutingConfig,
|
|
7
|
+
RoutingContext,
|
|
8
|
+
RoutingDecision,
|
|
9
|
+
ConnectorMetrics,
|
|
10
|
+
RoutingRule,
|
|
11
|
+
RoutingEngine as IRoutingEngine
|
|
12
|
+
} from "../types/routing-config";
|
|
13
|
+
|
|
14
|
+
export class RoutingEngine implements IRoutingEngine {
|
|
15
|
+
private metrics: Map<string, ConnectorMetrics> = new Map();
|
|
16
|
+
private circuitBreakers: Map<string, { state: string; failures: number; lastFailure?: Date }> = new Map();
|
|
17
|
+
|
|
18
|
+
async route(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
|
|
19
|
+
// 1. Check for routing override
|
|
20
|
+
if (context.forceConnector) {
|
|
21
|
+
return {
|
|
22
|
+
selected_connector: context.forceConnector,
|
|
23
|
+
algorithm_used: 'priority',
|
|
24
|
+
fallback_used: false,
|
|
25
|
+
decision_timestamp: new Date().toISOString(),
|
|
26
|
+
routing_profile_id: config.profile_id,
|
|
27
|
+
alternative_connectors: []
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Apply routing algorithm
|
|
32
|
+
switch (config.algorithm.type) {
|
|
33
|
+
case 'priority':
|
|
34
|
+
return await this.routeByPriority(context, config);
|
|
35
|
+
|
|
36
|
+
case 'volume_split':
|
|
37
|
+
return await this.routeByVolumeSplit(context, config);
|
|
38
|
+
|
|
39
|
+
case 'advanced':
|
|
40
|
+
return await this.routeByRules(context, config);
|
|
41
|
+
|
|
42
|
+
case 'performance':
|
|
43
|
+
return await this.routeByPerformance(context, config);
|
|
44
|
+
|
|
45
|
+
case 'cost_optimization':
|
|
46
|
+
return await this.routeByCost(context, config);
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unsupported routing algorithm: ${config.algorithm.type}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async routeByPriority(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
|
|
54
|
+
// Find applicable rules
|
|
55
|
+
const applicableRules = this.findApplicableRules(context, config.algorithm.data);
|
|
56
|
+
|
|
57
|
+
if (applicableRules.length === 0) {
|
|
58
|
+
return this.fallbackRouting(context, config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Sort connectors by priority
|
|
62
|
+
const sortedConnectors = applicableRules[0].connectors
|
|
63
|
+
.filter(c => this.isConnectorHealthy(c.connector))
|
|
64
|
+
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
65
|
+
|
|
66
|
+
if (sortedConnectors.length === 0) {
|
|
67
|
+
return this.fallbackRouting(context, config);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
selected_connector: sortedConnectors[0].connector,
|
|
72
|
+
merchant_connector_id: sortedConnectors[0].merchant_connector_id,
|
|
73
|
+
algorithm_used: 'priority',
|
|
74
|
+
rule_applied: applicableRules[0].name,
|
|
75
|
+
fallback_used: false,
|
|
76
|
+
decision_timestamp: new Date().toISOString(),
|
|
77
|
+
routing_profile_id: config.profile_id,
|
|
78
|
+
alternative_connectors: sortedConnectors.slice(1).map(c => c.connector)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async routeByVolumeSplit(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
|
|
83
|
+
const applicableRules = this.findApplicableRules(context, config.algorithm.data);
|
|
84
|
+
|
|
85
|
+
if (applicableRules.length === 0) {
|
|
86
|
+
return this.fallbackRouting(context, config);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Weighted random selection based on volume split
|
|
90
|
+
const connectors = applicableRules[0].connectors.filter(c => this.isConnectorHealthy(c.connector));
|
|
91
|
+
const totalWeight = connectors.reduce((sum, c) => sum + (c.weight || 1), 0);
|
|
92
|
+
const random = Math.random() * totalWeight;
|
|
93
|
+
|
|
94
|
+
let currentWeight = 0;
|
|
95
|
+
for (const connector of connectors) {
|
|
96
|
+
currentWeight += connector.weight || 1;
|
|
97
|
+
if (random <= currentWeight) {
|
|
98
|
+
return {
|
|
99
|
+
selected_connector: connector.connector,
|
|
100
|
+
merchant_connector_id: connector.merchant_connector_id,
|
|
101
|
+
algorithm_used: 'volume_split',
|
|
102
|
+
rule_applied: applicableRules[0].name,
|
|
103
|
+
fallback_used: false,
|
|
104
|
+
decision_timestamp: new Date().toISOString(),
|
|
105
|
+
routing_profile_id: config.profile_id,
|
|
106
|
+
alternative_connectors: connectors.filter(c => c.connector !== connector.connector).map(c => c.connector)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this.fallbackRouting(context, config);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async routeByRules(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
|
|
115
|
+
// Advanced rule-based routing with complex conditions
|
|
116
|
+
for (const rule of config.algorithm.data) {
|
|
117
|
+
if (this.evaluateRule(context, rule)) {
|
|
118
|
+
const healthyConnectors = rule.connectors.filter(c => this.isConnectorHealthy(c.connector));
|
|
119
|
+
|
|
120
|
+
if (healthyConnectors.length > 0) {
|
|
121
|
+
// Use priority within the rule
|
|
122
|
+
const selectedConnector = healthyConnectors.sort((a, b) => (b.priority || 0) - (a.priority || 0))[0];
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
selected_connector: selectedConnector.connector,
|
|
126
|
+
merchant_connector_id: selectedConnector.merchant_connector_id,
|
|
127
|
+
algorithm_used: 'advanced',
|
|
128
|
+
rule_applied: rule.name,
|
|
129
|
+
fallback_used: false,
|
|
130
|
+
decision_timestamp: new Date().toISOString(),
|
|
131
|
+
routing_profile_id: config.profile_id,
|
|
132
|
+
alternative_connectors: healthyConnectors.slice(1).map(c => c.connector)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.fallbackRouting(context, config);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async routeByPerformance(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
|
|
142
|
+
const applicableRules = this.findApplicableRules(context, config.algorithm.data);
|
|
143
|
+
|
|
144
|
+
if (applicableRules.length === 0) {
|
|
145
|
+
return this.fallbackRouting(context, config);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sort by success rate
|
|
149
|
+
const connectors = applicableRules[0].connectors
|
|
150
|
+
.filter(c => this.isConnectorHealthy(c.connector))
|
|
151
|
+
.sort((a, b) => {
|
|
152
|
+
const aMetrics = this.metrics.get(a.connector);
|
|
153
|
+
const bMetrics = this.metrics.get(b.connector);
|
|
154
|
+
return (bMetrics?.success_rate || 0) - (aMetrics?.success_rate || 0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (connectors.length === 0) {
|
|
158
|
+
return this.fallbackRouting(context, config);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
selected_connector: connectors[0].connector,
|
|
163
|
+
merchant_connector_id: connectors[0].merchant_connector_id,
|
|
164
|
+
algorithm_used: 'performance',
|
|
165
|
+
rule_applied: applicableRules[0].name,
|
|
166
|
+
fallback_used: false,
|
|
167
|
+
decision_timestamp: new Date().toISOString(),
|
|
168
|
+
routing_profile_id: config.profile_id,
|
|
169
|
+
alternative_connectors: connectors.slice(1).map(c => c.connector)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async routeByCost(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
|
|
174
|
+
// Cost-based routing (would need cost data from connectors)
|
|
175
|
+
return this.routeByPriority(context, config);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private findApplicableRules(context: RoutingContext, rules: RoutingRule[]): RoutingRule[] {
|
|
179
|
+
return rules.filter(rule => rule.enabled && this.evaluateRule(context, rule));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private evaluateRule(context: RoutingContext, rule: RoutingRule): boolean {
|
|
183
|
+
const conditions = rule.conditions;
|
|
184
|
+
|
|
185
|
+
// Resource type check
|
|
186
|
+
if (conditions.resourceType && context.resourceType && !conditions.resourceType.includes(context.resourceType)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Region check
|
|
191
|
+
if (conditions.region && context.region && !conditions.region.includes(context.region)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Resource size range check
|
|
196
|
+
if (conditions.resourceSize && context.resourceSize !== undefined) {
|
|
197
|
+
if (conditions.resourceSize.min && context.resourceSize < conditions.resourceSize.min) return false;
|
|
198
|
+
if (conditions.resourceSize.max && context.resourceSize > conditions.resourceSize.max) return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Country check
|
|
202
|
+
if (conditions.country && context.country && !conditions.country.includes(context.country)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Tenant check
|
|
207
|
+
if (conditions.tenantId && context.tenantId && !conditions.tenantId.includes(context.tenantId)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Priority check
|
|
212
|
+
if (conditions.priority && context.priority && !conditions.priority.includes(context.priority)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private isConnectorHealthy(connectorId: string): boolean {
|
|
220
|
+
const circuitBreaker = this.circuitBreakers.get(connectorId);
|
|
221
|
+
return !circuitBreaker || circuitBreaker.state !== 'open';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private fallbackRouting(context: RoutingContext, config: RoutingConfig): RoutingDecision {
|
|
225
|
+
if (!config.fallback_routing?.enabled || !config.fallback_routing.connectors.length) {
|
|
226
|
+
throw new Error('No healthy connectors available and fallback routing disabled');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const healthyFallbacks = config.fallback_routing.connectors.filter(c => this.isConnectorHealthy(c));
|
|
230
|
+
|
|
231
|
+
if (healthyFallbacks.length === 0) {
|
|
232
|
+
throw new Error('No healthy fallback connectors available');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
selected_connector: healthyFallbacks[0],
|
|
237
|
+
algorithm_used: 'priority',
|
|
238
|
+
fallback_used: true,
|
|
239
|
+
decision_timestamp: new Date().toISOString(),
|
|
240
|
+
routing_profile_id: config.profile_id,
|
|
241
|
+
alternative_connectors: healthyFallbacks.slice(1)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async updateMetrics(connectorId: string, success: boolean, responseTime: number, resourceSize?: number): Promise<void> {
|
|
246
|
+
// Try to get the existing metrics or create a new, fully-typed record
|
|
247
|
+
const existing = this.metrics.get(connectorId) || {
|
|
248
|
+
connector_name: connectorId,
|
|
249
|
+
success_rate: 0,
|
|
250
|
+
total_volume: 0,
|
|
251
|
+
successful_payments: 0,
|
|
252
|
+
failed_payments: 0,
|
|
253
|
+
avg_response_time_ms: 0,
|
|
254
|
+
p95_response_time_ms: 0,
|
|
255
|
+
total_amount_processed: 0,
|
|
256
|
+
avg_ticket_size: 0,
|
|
257
|
+
window_start: new Date().toISOString(),
|
|
258
|
+
window_end: new Date().toISOString(),
|
|
259
|
+
circuit_breaker_status: 'closed' as const,
|
|
260
|
+
consecutive_failures: 0,
|
|
261
|
+
// Required additional properties to fulfill ConnectorMetrics interface
|
|
262
|
+
total_requests: 0,
|
|
263
|
+
successful_requests: 0,
|
|
264
|
+
failed_requests: 0,
|
|
265
|
+
total_resources_processed: 0,
|
|
266
|
+
avg_resource_size: 0,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Incremental updates
|
|
270
|
+
existing.total_volume = (existing.total_volume || 0) + 1;
|
|
271
|
+
existing.total_requests = (existing.total_requests || 0) + 1;
|
|
272
|
+
existing.total_resources_processed = (existing.total_resources_processed || 0) + (resourceSize || 0);
|
|
273
|
+
|
|
274
|
+
existing.total_amount_processed = (existing.total_amount_processed || 0) + (resourceSize || 0);
|
|
275
|
+
|
|
276
|
+
if (success) {
|
|
277
|
+
existing.successful_payments = (existing.successful_payments || 0) + 1;
|
|
278
|
+
existing.successful_requests = (existing.successful_requests || 0) + 1;
|
|
279
|
+
} else {
|
|
280
|
+
existing.failed_payments = (existing.failed_payments || 0) + 1;
|
|
281
|
+
existing.failed_requests = (existing.failed_requests || 0) + 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
existing.success_rate =
|
|
285
|
+
(existing.successful_payments || 0) / (existing.total_volume || 1);
|
|
286
|
+
existing.avg_ticket_size =
|
|
287
|
+
(existing.total_amount_processed || 0) / (existing.total_volume || 1);
|
|
288
|
+
|
|
289
|
+
existing.avg_resource_size =
|
|
290
|
+
(existing.total_resources_processed || 0) / (existing.total_requests || 1);
|
|
291
|
+
|
|
292
|
+
// For response times, maintain a very basic moving average
|
|
293
|
+
if (existing.avg_response_time_ms && existing.total_volume > 1) {
|
|
294
|
+
existing.avg_response_time_ms =
|
|
295
|
+
((existing.avg_response_time_ms * (existing.total_volume - 1)) + responseTime) /
|
|
296
|
+
existing.total_volume;
|
|
297
|
+
} else {
|
|
298
|
+
existing.avg_response_time_ms = responseTime;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// For P95: This would need proper history tracking. Use the current value as a placeholder.
|
|
302
|
+
existing.p95_response_time_ms = existing.avg_response_time_ms;
|
|
303
|
+
|
|
304
|
+
// Optionally update window timestamps
|
|
305
|
+
existing.window_end = new Date().toISOString();
|
|
306
|
+
|
|
307
|
+
// The actual circuit breaker status and consecutive_failures will be updated in the updateCircuitBreaker call below
|
|
308
|
+
// but set them from existing state (if available)
|
|
309
|
+
const cb = this.circuitBreakers.get(connectorId);
|
|
310
|
+
if (cb) {
|
|
311
|
+
existing.circuit_breaker_status = cb.state as typeof existing.circuit_breaker_status;
|
|
312
|
+
existing.consecutive_failures = cb.failures || 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.metrics.set(connectorId, existing);
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
// Update circuit breaker after metrics
|
|
319
|
+
this.updateCircuitBreaker(connectorId, success);
|
|
320
|
+
|
|
321
|
+
// Set updated circuit breaker fields again after it might have changed
|
|
322
|
+
const cbUpdated = this.circuitBreakers.get(connectorId);
|
|
323
|
+
if (cbUpdated) {
|
|
324
|
+
existing.circuit_breaker_status = cbUpdated.state as typeof existing.circuit_breaker_status;
|
|
325
|
+
existing.consecutive_failures = cbUpdated.failures || 0;
|
|
326
|
+
this.metrics.set(connectorId, existing);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private updateCircuitBreaker(connectorId: string, success: boolean): void {
|
|
331
|
+
const cb = this.circuitBreakers.get(connectorId) || { state: 'closed', failures: 0 };
|
|
332
|
+
|
|
333
|
+
if (success) {
|
|
334
|
+
cb.failures = 0;
|
|
335
|
+
cb.state = 'closed';
|
|
336
|
+
} else {
|
|
337
|
+
cb.failures++;
|
|
338
|
+
cb.lastFailure = new Date();
|
|
339
|
+
|
|
340
|
+
if (cb.failures >= 5) { // Configurable threshold
|
|
341
|
+
cb.state = 'open';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
this.circuitBreakers.set(connectorId, cb);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async getConnectorHealth(connectorId: string): Promise<ConnectorMetrics> {
|
|
349
|
+
return this.metrics.get(connectorId) || {
|
|
350
|
+
connector_name: connectorId,
|
|
351
|
+
success_rate: 0,
|
|
352
|
+
total_volume: 0,
|
|
353
|
+
successful_payments: 0,
|
|
354
|
+
failed_payments: 0,
|
|
355
|
+
avg_response_time_ms: 0,
|
|
356
|
+
p95_response_time_ms: 0,
|
|
357
|
+
total_amount_processed: 0,
|
|
358
|
+
avg_ticket_size: 0,
|
|
359
|
+
window_start: new Date().toISOString(),
|
|
360
|
+
window_end: new Date().toISOString(),
|
|
361
|
+
circuit_breaker_status: 'closed' as const,
|
|
362
|
+
consecutive_failures: 0,
|
|
363
|
+
|
|
364
|
+
// Add missing required properties for ConnectorMetrics type
|
|
365
|
+
total_requests: 0,
|
|
366
|
+
successful_requests: 0,
|
|
367
|
+
failed_requests: 0,
|
|
368
|
+
total_resources_processed: 0,
|
|
369
|
+
avg_resource_size: 0,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async validateConfig(config: RoutingConfig): Promise<{ valid: boolean; errors: string[] }> {
|
|
375
|
+
const errors: string[] = [];
|
|
376
|
+
|
|
377
|
+
if (!config.profile_id) {
|
|
378
|
+
errors.push('Profile ID is required');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!config.algorithm?.type) {
|
|
382
|
+
errors.push('Routing algorithm type is required');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!config.algorithm?.data?.length) {
|
|
386
|
+
errors.push('At least one routing rule is required');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
valid: errors.length === 0,
|
|
391
|
+
errors
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ETO Framework Connector Engine
|
|
3
|
+
* @description Clean plugin-first connector engine with Hyperswitch-style routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ===== CORE ENGINE =====
|
|
7
|
+
export { ConnectorEngine } from "./engine/clean-connector-engine";
|
|
8
|
+
export type {
|
|
9
|
+
ConnectorRegistry,
|
|
10
|
+
OperationGroup,
|
|
11
|
+
OperationGroups,
|
|
12
|
+
EngineConfig,
|
|
13
|
+
GroupResult
|
|
14
|
+
} from "./engine/clean-connector-engine";
|
|
15
|
+
|
|
16
|
+
// ===== PLUGIN TYPES =====
|
|
17
|
+
export {
|
|
18
|
+
type ConnectorPlugin,
|
|
19
|
+
type OperationType,
|
|
20
|
+
AbstractConnectorPlugin
|
|
21
|
+
} from "./types/connector-plugin";
|
|
22
|
+
|
|
23
|
+
// ===== ROUTING TYPES =====
|
|
24
|
+
export type {
|
|
25
|
+
RoutingConfig,
|
|
26
|
+
RoutingContext,
|
|
27
|
+
RoutingDecision,
|
|
28
|
+
ConnectorMetrics,
|
|
29
|
+
RoutingRule,
|
|
30
|
+
ConnectorChoice,
|
|
31
|
+
RoutingAlgorithm
|
|
32
|
+
} from "./types/routing-config";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Clean Connector Plugin Design - First Principles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ===== OPERATION TYPE =====
|
|
6
|
+
export interface OperationType {
|
|
7
|
+
input: any;
|
|
8
|
+
output: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ===== CONNECTOR PLUGIN =====
|
|
12
|
+
export interface ConnectorPlugin<TOperations extends Record<string, OperationType> = Record<string, OperationType>> {
|
|
13
|
+
getName(): string;
|
|
14
|
+
getVersion(): string;
|
|
15
|
+
|
|
16
|
+
// Operations receive config at execution time
|
|
17
|
+
operations: {
|
|
18
|
+
[K in keyof TOperations]: (
|
|
19
|
+
input: TOperations[K]['input'],
|
|
20
|
+
config: any
|
|
21
|
+
) => Promise<TOperations[K]['output']>
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ===== ABSTRACT BASE =====
|
|
26
|
+
export abstract class AbstractConnectorPlugin<TOperations extends Record<string, OperationType> = Record<string, OperationType>>
|
|
27
|
+
implements ConnectorPlugin<TOperations> {
|
|
28
|
+
|
|
29
|
+
abstract getName(): string;
|
|
30
|
+
abstract getVersion(): string;
|
|
31
|
+
abstract operations: {
|
|
32
|
+
[K in keyof TOperations]: (input: TOperations[K]['input'], config: any) => Promise<TOperations[K]['output']>
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generic Routing Configuration (Not Payment-Specific)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ===== GENERIC ROUTING CONTEXT =====
|
|
6
|
+
export interface RoutingContext {
|
|
7
|
+
// Generic operation context
|
|
8
|
+
operation?: string;
|
|
9
|
+
domain?: string;
|
|
10
|
+
|
|
11
|
+
// Geographic context
|
|
12
|
+
region?: string;
|
|
13
|
+
country?: string;
|
|
14
|
+
|
|
15
|
+
// Business context
|
|
16
|
+
userId?: string;
|
|
17
|
+
tenantId?: string;
|
|
18
|
+
priority?: 'low' | 'normal' | 'high';
|
|
19
|
+
|
|
20
|
+
// Resource context
|
|
21
|
+
resourceType?: string;
|
|
22
|
+
resourceSize?: number;
|
|
23
|
+
|
|
24
|
+
// Custom metadata for any domain
|
|
25
|
+
metadata?: Record<string, any>;
|
|
26
|
+
|
|
27
|
+
// Routing overrides
|
|
28
|
+
forceConnector?: string;
|
|
29
|
+
skipRouting?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ===== GENERIC ROUTING RULE =====
|
|
33
|
+
export interface RoutingRule {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
|
|
37
|
+
// Generic conditions (not payment-specific)
|
|
38
|
+
conditions: {
|
|
39
|
+
// Resource conditions
|
|
40
|
+
resourceType?: string[];
|
|
41
|
+
resourceSize?: { min?: number; max?: number };
|
|
42
|
+
|
|
43
|
+
// Geographic conditions
|
|
44
|
+
region?: string[];
|
|
45
|
+
country?: string[];
|
|
46
|
+
|
|
47
|
+
// Business conditions
|
|
48
|
+
tenantId?: string[];
|
|
49
|
+
priority?: string[];
|
|
50
|
+
|
|
51
|
+
// Time-based conditions
|
|
52
|
+
timeOfDay?: { start?: string; end?: string };
|
|
53
|
+
dayOfWeek?: string[];
|
|
54
|
+
|
|
55
|
+
// Custom field conditions
|
|
56
|
+
customFields?: Record<string, any>;
|
|
57
|
+
|
|
58
|
+
// Metadata conditions
|
|
59
|
+
metadata?: Record<string, any>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Connector selection
|
|
63
|
+
connectors: ConnectorChoice[];
|
|
64
|
+
|
|
65
|
+
// Rule metadata
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
created_at: string;
|
|
68
|
+
modified_at: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ===== CONNECTOR CHOICE =====
|
|
72
|
+
export interface ConnectorChoice {
|
|
73
|
+
connector: string;
|
|
74
|
+
merchant_connector_id?: string;
|
|
75
|
+
|
|
76
|
+
// Selection criteria
|
|
77
|
+
priority?: number;
|
|
78
|
+
weight?: number;
|
|
79
|
+
|
|
80
|
+
// Resource constraints
|
|
81
|
+
maxConcurrency?: number;
|
|
82
|
+
maxResourceSize?: number;
|
|
83
|
+
|
|
84
|
+
// Geographic constraints
|
|
85
|
+
supportedRegions?: string[];
|
|
86
|
+
supportedCountries?: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ===== ROUTING ALGORITHM =====
|
|
90
|
+
export type RoutingAlgorithm =
|
|
91
|
+
| 'priority' // Priority-based routing
|
|
92
|
+
| 'volume_split' // Volume-based distribution
|
|
93
|
+
| 'advanced' // Rule-based routing
|
|
94
|
+
| 'cost_optimization' // Cost-aware routing
|
|
95
|
+
| 'performance' // Performance-based routing
|
|
96
|
+
| 'load_balancing' // Load balancing
|
|
97
|
+
| 'region_based' // Geographic routing
|
|
98
|
+
|
|
99
|
+
// ===== ROUTING CONFIGURATION =====
|
|
100
|
+
export interface RoutingConfig {
|
|
101
|
+
profile_id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
description?: string;
|
|
104
|
+
|
|
105
|
+
// Generic routing algorithm
|
|
106
|
+
algorithm: {
|
|
107
|
+
type: RoutingAlgorithm;
|
|
108
|
+
data: RoutingRule[];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Fallback configuration
|
|
112
|
+
fallback_routing?: {
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
connectors: string[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Circuit breaker
|
|
118
|
+
circuit_breaker?: {
|
|
119
|
+
failure_threshold: number;
|
|
120
|
+
recovery_time_seconds: number;
|
|
121
|
+
max_retries: number;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Metadata
|
|
125
|
+
created_at: string;
|
|
126
|
+
modified_at: string;
|
|
127
|
+
version: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ===== CONNECTOR METRICS =====
|
|
131
|
+
export interface ConnectorMetrics {
|
|
132
|
+
connector_name: string;
|
|
133
|
+
|
|
134
|
+
// Success metrics
|
|
135
|
+
success_rate: number;
|
|
136
|
+
total_requests: number;
|
|
137
|
+
successful_requests: number;
|
|
138
|
+
failed_requests: number;
|
|
139
|
+
|
|
140
|
+
// Volume metrics (for payment-specific routing)
|
|
141
|
+
total_volume?: number;
|
|
142
|
+
successful_payments?: number;
|
|
143
|
+
failed_payments?: number;
|
|
144
|
+
total_amount_processed?: number;
|
|
145
|
+
avg_ticket_size?: number;
|
|
146
|
+
|
|
147
|
+
// Performance metrics
|
|
148
|
+
avg_response_time_ms: number;
|
|
149
|
+
p95_response_time_ms: number;
|
|
150
|
+
|
|
151
|
+
// Resource metrics
|
|
152
|
+
total_resources_processed: number;
|
|
153
|
+
avg_resource_size: number;
|
|
154
|
+
|
|
155
|
+
// Time window
|
|
156
|
+
window_start: string;
|
|
157
|
+
window_end: string;
|
|
158
|
+
|
|
159
|
+
// Circuit breaker state
|
|
160
|
+
circuit_breaker_status: 'closed' | 'open' | 'half_open';
|
|
161
|
+
consecutive_failures: number;
|
|
162
|
+
last_failure_time?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ===== ROUTING DECISION =====
|
|
166
|
+
export interface RoutingDecision {
|
|
167
|
+
selected_connector: string;
|
|
168
|
+
merchant_connector_id?: string;
|
|
169
|
+
|
|
170
|
+
// Decision context
|
|
171
|
+
algorithm_used: RoutingAlgorithm;
|
|
172
|
+
rule_applied?: string;
|
|
173
|
+
fallback_used: boolean;
|
|
174
|
+
|
|
175
|
+
// Routing metadata
|
|
176
|
+
decision_timestamp: string;
|
|
177
|
+
routing_profile_id: string;
|
|
178
|
+
|
|
179
|
+
// Alternative connectors (for fallback)
|
|
180
|
+
alternative_connectors: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ===== ROUTING ENGINE INTERFACE =====
|
|
184
|
+
export interface RoutingEngine {
|
|
185
|
+
// Route request to best connector
|
|
186
|
+
route(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision>;
|
|
187
|
+
|
|
188
|
+
// Update connector metrics
|
|
189
|
+
updateMetrics(connectorId: string, success: boolean, responseTime: number, resourceSize?: number): Promise<void>;
|
|
190
|
+
|
|
191
|
+
// Get connector health
|
|
192
|
+
getConnectorHealth(connectorId: string): Promise<ConnectorMetrics>;
|
|
193
|
+
|
|
194
|
+
// Validate routing config
|
|
195
|
+
validateConfig(config: RoutingConfig): Promise<{ valid: boolean; errors: string[] }>;
|
|
196
|
+
}
|
package/tsconfig.json
ADDED