@emmvish/stable-request 1.7.2 → 1.8.0

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 (58) hide show
  1. package/README.md +354 -19
  2. package/dist/core/stable-api-gateway.d.ts +2 -2
  3. package/dist/core/stable-api-gateway.d.ts.map +1 -1
  4. package/dist/core/stable-api-gateway.js +58 -4
  5. package/dist/core/stable-api-gateway.js.map +1 -1
  6. package/dist/core/stable-request.d.ts +2 -2
  7. package/dist/core/stable-request.d.ts.map +1 -1
  8. package/dist/core/stable-request.js +35 -5
  9. package/dist/core/stable-request.js.map +1 -1
  10. package/dist/core/stable-workflow.d.ts.map +1 -1
  11. package/dist/core/stable-workflow.js +16 -4
  12. package/dist/core/stable-workflow.js.map +1 -1
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/types/index.d.ts +248 -16
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/utilities/cache-manager.d.ts +19 -0
  20. package/dist/utilities/cache-manager.d.ts.map +1 -1
  21. package/dist/utilities/cache-manager.js +40 -4
  22. package/dist/utilities/cache-manager.js.map +1 -1
  23. package/dist/utilities/circuit-breaker.d.ts +22 -0
  24. package/dist/utilities/circuit-breaker.d.ts.map +1 -1
  25. package/dist/utilities/circuit-breaker.js +65 -1
  26. package/dist/utilities/circuit-breaker.js.map +1 -1
  27. package/dist/utilities/concurrency-limiter.d.ts +26 -0
  28. package/dist/utilities/concurrency-limiter.d.ts.map +1 -1
  29. package/dist/utilities/concurrency-limiter.js +74 -4
  30. package/dist/utilities/concurrency-limiter.js.map +1 -1
  31. package/dist/utilities/execute-branch-workflow.d.ts.map +1 -1
  32. package/dist/utilities/execute-branch-workflow.js +18 -12
  33. package/dist/utilities/execute-branch-workflow.js.map +1 -1
  34. package/dist/utilities/execute-concurrently.d.ts.map +1 -1
  35. package/dist/utilities/execute-concurrently.js +5 -6
  36. package/dist/utilities/execute-concurrently.js.map +1 -1
  37. package/dist/utilities/execute-non-linear-workflow.d.ts.map +1 -1
  38. package/dist/utilities/execute-non-linear-workflow.js +2 -4
  39. package/dist/utilities/execute-non-linear-workflow.js.map +1 -1
  40. package/dist/utilities/execute-phase.d.ts.map +1 -1
  41. package/dist/utilities/execute-phase.js +11 -5
  42. package/dist/utilities/execute-phase.js.map +1 -1
  43. package/dist/utilities/execute-sequentially.d.ts.map +1 -1
  44. package/dist/utilities/execute-sequentially.js +5 -6
  45. package/dist/utilities/execute-sequentially.js.map +1 -1
  46. package/dist/utilities/index.d.ts +4 -3
  47. package/dist/utilities/index.d.ts.map +1 -1
  48. package/dist/utilities/index.js +4 -3
  49. package/dist/utilities/index.js.map +1 -1
  50. package/dist/utilities/metrics-aggregator.d.ts +51 -0
  51. package/dist/utilities/metrics-aggregator.d.ts.map +1 -0
  52. package/dist/utilities/metrics-aggregator.js +311 -0
  53. package/dist/utilities/metrics-aggregator.js.map +1 -0
  54. package/dist/utilities/rate-limiter.d.ts +19 -0
  55. package/dist/utilities/rate-limiter.d.ts.map +1 -1
  56. package/dist/utilities/rate-limiter.js +56 -3
  57. package/dist/utilities/rate-limiter.js.map +1 -1
  58. package/package.json +1 -1
package/README.md CHANGED
@@ -16,6 +16,11 @@ A production-grade HTTP Workflow Execution Engine for Node.js that transforms un
16
16
  - [Circuit Breaker Pattern](#circuit-breaker-pattern)
17
17
  - [Response Caching](#response-caching)
18
18
  - [Rate Limiting and Concurrency Control](#rate-limiting-and-concurrency-control)
19
+ - [Metrics and Observability](#metrics-and-observability)
20
+ - [Request-Level Metrics](#request-level-metrics)
21
+ - [API Gateway Metrics](#api-gateway-metrics)
22
+ - [Workflow Metrics](#workflow-metrics)
23
+ - [MetricsAggregator Utility](#metricsaggregator-utility)
19
24
  - [Workflow Execution Patterns](#workflow-execution-patterns)
20
25
  - [Sequential and Concurrent Phases](#sequential-and-concurrent-phases)
21
26
  - [Mixed Execution Mode](#mixed-execution-mode)
@@ -260,9 +265,10 @@ await stableRequest({
260
265
  reqData: { hostname: 'unreliable-api.example.com', path: '/data' },
261
266
  attempts: 3,
262
267
  circuitBreaker: {
263
- failureThreshold: 5, // Open after 5 failures
264
- successThreshold: 2, // Close after 2 successes in half-open
265
- timeout: 60000, // Wait 60s before trying again (half-open)
268
+ failureThresholdPercentage: 50, // Open after 50% failures
269
+ minimumRequests: 10, // Minimum requests before evaluation
270
+ recoveryTimeoutMs: 60000, // Wait 60s before trying again (half-open)
271
+ successThresholdPercentage: 20, // Close after 20% successes in half-open
266
272
  trackIndividualAttempts: false // Track at request level (not attempt level)
267
273
  }
268
274
  });
@@ -278,9 +284,10 @@ await stableRequest({
278
284
  import { CircuitBreaker } from '@emmvish/stable-request';
279
285
 
280
286
  const sharedBreaker = new CircuitBreaker({
281
- failureThreshold: 10,
282
- successThreshold: 3,
283
- timeout: 120000
287
+ failureThresholdPercentage: 50, // 50% failure rate triggers open
288
+ minimumRequests: 10, // Minimum 10 requests before evaluation
289
+ recoveryTimeoutMs: 120000, // 120s timeout in open state
290
+ successThresholdPercentage: 50 // 50% success rate closes circuit
284
291
  });
285
292
 
286
293
  await stableWorkflow(phases, {
@@ -347,7 +354,7 @@ await stableWorkflow(phases, {
347
354
  // Rate limiting (token bucket algorithm)
348
355
  rateLimit: {
349
356
  maxRequests: 100, // 100 requests
350
- timeWindow: 60000 // per 60 seconds
357
+ windowMs: 60000 // per 60 seconds
351
358
  },
352
359
 
353
360
  // Concurrency limiting
@@ -363,7 +370,7 @@ const phases = [
363
370
  maxConcurrentRequests: 10, // Override workflow limit
364
371
  rateLimit: {
365
372
  maxRequests: 50,
366
- timeWindow: 10000
373
+ windowMs: 10000
367
374
  },
368
375
  requests: [...]
369
376
  }
@@ -374,15 +381,343 @@ const phases = [
374
381
  ```typescript
375
382
  import { RateLimiter } from '@emmvish/stable-request';
376
383
 
377
- const limiter = new RateLimiter({
378
- maxRequests: 1000,
379
- timeWindow: 3600000 // 1000 requests per hour
384
+ const limiter = new RateLimiter(1000, 3600000); // 1000 requests per hour
385
+
386
+ const state = await limiter.getState(); // Get current state
387
+ console.log(state);
388
+ // { availableTokens: 1000, queueLength: 0, maxRequests: 1000, windowMs: 3600000 }
389
+ ```
390
+
391
+ ## Metrics and Observability
392
+
393
+ `@emmvish/stable-request` provides comprehensive metrics at every level of execution, from individual requests to complete workflows. All metrics are automatically computed and included in results.
394
+
395
+ ### Request-Level Metrics
396
+
397
+ Every `stableRequest` call returns detailed metrics about the request execution:
398
+
399
+ ```typescript
400
+ import { stableRequest } from '@emmvish/stable-request';
401
+
402
+ const result = await stableRequest({
403
+ reqData: {
404
+ hostname: 'api.example.com',
405
+ path: '/users/123'
406
+ },
407
+ resReq: true,
408
+ attempts: 3,
409
+ wait: 1000,
410
+ logAllErrors: true
380
411
  });
381
412
 
382
- await limiter.acquire(); // Waits if limit exceeded
383
- // Make request
413
+ // Access request metrics
414
+ console.log('Request Result:', {
415
+ success: result.success, // true/false
416
+ data: result.data, // Response data
417
+ error: result.error, // Error message (if failed)
418
+ errorLogs: result.errorLogs, // All failed attempts
419
+ successfulAttempts: result.successfulAttempts, // All successful attempts
420
+ metrics: {
421
+ totalAttempts: result.metrics.totalAttempts, // 3
422
+ successfulAttempts: result.metrics.successfulAttempts, // 1
423
+ failedAttempts: result.metrics.failedAttempts, // 2
424
+ totalExecutionTime: result.metrics.totalExecutionTime, // ms
425
+ averageAttemptTime: result.metrics.averageAttemptTime, // ms
426
+ infrastructureMetrics: {
427
+ circuitBreaker: result.metrics.infrastructureMetrics?.circuitBreaker,
428
+ cache: result.metrics.infrastructureMetrics?.cache
429
+ }
430
+ }
431
+ });
432
+
433
+ // Error logs provide detailed attempt information
434
+ result.errorLogs?.forEach(log => {
435
+ console.log({
436
+ attempt: log.attempt, // "1/3"
437
+ timestamp: log.timestamp,
438
+ error: log.error,
439
+ statusCode: log.statusCode,
440
+ type: log.type, // "HTTP_ERROR" | "INVALID_CONTENT"
441
+ isRetryable: log.isRetryable,
442
+ executionTime: log.executionTime
443
+ });
444
+ });
445
+
446
+ // Successful attempts show what worked
447
+ result.successfulAttempts?.forEach(attempt => {
448
+ console.log({
449
+ attempt: attempt.attempt, // "3/3"
450
+ timestamp: attempt.timestamp,
451
+ executionTime: attempt.executionTime,
452
+ data: attempt.data,
453
+ statusCode: attempt.statusCode
454
+ });
455
+ });
384
456
  ```
385
457
 
458
+ **STABLE_REQUEST_RESULT Structure:**
459
+ - `success`: Boolean indicating if request succeeded
460
+ - `data`: Response data (if `resReq: true`)
461
+ - `error`: Error message (if request failed)
462
+ - `errorLogs`: Array of all failed attempt details
463
+ - `successfulAttempts`: Array of all successful attempt details
464
+ - `metrics`: Computed execution metrics and infrastructure statistics
465
+
466
+ ### API Gateway Metrics
467
+
468
+ `stableApiGateway` provides aggregated metrics for batch requests:
469
+
470
+ ```typescript
471
+ import { stableApiGateway } from '@emmvish/stable-request';
472
+
473
+ const requests = [
474
+ { id: 'user-1', groupId: 'users', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
475
+ { id: 'user-2', groupId: 'users', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
476
+ { id: 'order-1', groupId: 'orders', requestOptions: { reqData: { path: '/orders/1' }, resReq: true } },
477
+ { id: 'product-1', requestOptions: { reqData: { path: '/products/1' }, resReq: true } }
478
+ ];
479
+
480
+ const results = await stableApiGateway(requests, {
481
+ concurrentExecution: true,
482
+ commonRequestData: { hostname: 'api.example.com' },
483
+ commonAttempts: 3,
484
+ circuitBreaker: { failureThresholdPercentage: 50, minimumRequests: 5 },
485
+ rateLimit: { maxRequests: 100, windowMs: 60000 },
486
+ maxConcurrentRequests: 5
487
+ });
488
+
489
+ // Gateway-level metrics
490
+ console.log('Gateway Metrics:', {
491
+ totalRequests: results.metrics.totalRequests, // 4
492
+ successfulRequests: results.metrics.successfulRequests, // 3
493
+ failedRequests: results.metrics.failedRequests, // 1
494
+ successRate: results.metrics.successRate, // 75%
495
+ failureRate: results.metrics.failureRate // 25%
496
+ });
497
+
498
+ // Request group metrics
499
+ results.metrics.requestGroups?.forEach(group => {
500
+ console.log(`Group ${group.groupId}:`, {
501
+ totalRequests: group.totalRequests,
502
+ successfulRequests: group.successfulRequests,
503
+ failedRequests: group.failedRequests,
504
+ successRate: group.successRate, // %
505
+ failureRate: group.failureRate, // %
506
+ requestIds: group.requestIds // Array of request IDs
507
+ });
508
+ });
509
+
510
+ // Infrastructure metrics (when utilities are used)
511
+ if (results.metrics.infrastructureMetrics) {
512
+ const infra = results.metrics.infrastructureMetrics;
513
+
514
+ // Circuit Breaker metrics
515
+ if (infra.circuitBreaker) {
516
+ console.log('Circuit Breaker:', {
517
+ state: infra.circuitBreaker.state, // CLOSED | OPEN | HALF_OPEN
518
+ isHealthy: infra.circuitBreaker.isHealthy,
519
+ totalRequests: infra.circuitBreaker.totalRequests,
520
+ failurePercentage: infra.circuitBreaker.failurePercentage,
521
+ openCount: infra.circuitBreaker.openCount,
522
+ recoveryAttempts: infra.circuitBreaker.recoveryAttempts
523
+ });
524
+ }
525
+
526
+ // Cache metrics
527
+ if (infra.cache) {
528
+ console.log('Cache:', {
529
+ hitRate: infra.cache.hitRate, // %
530
+ currentSize: infra.cache.currentSize,
531
+ networkRequestsSaved: infra.cache.networkRequestsSaved,
532
+ cacheEfficiency: infra.cache.cacheEfficiency // %
533
+ });
534
+ }
535
+
536
+ // Rate Limiter metrics
537
+ if (infra.rateLimiter) {
538
+ console.log('Rate Limiter:', {
539
+ throttledRequests: infra.rateLimiter.throttledRequests,
540
+ throttleRate: infra.rateLimiter.throttleRate, // %
541
+ peakRequestRate: infra.rateLimiter.peakRequestRate
542
+ });
543
+ }
544
+
545
+ // Concurrency Limiter metrics
546
+ if (infra.concurrencyLimiter) {
547
+ console.log('Concurrency:', {
548
+ peakConcurrency: infra.concurrencyLimiter.peakConcurrency,
549
+ utilizationPercentage: infra.concurrencyLimiter.utilizationPercentage,
550
+ averageQueueWaitTime: infra.concurrencyLimiter.averageQueueWaitTime
551
+ });
552
+ }
553
+ }
554
+ ```
555
+
556
+ ### Workflow Metrics
557
+
558
+ `stableWorkflow` provides end-to-end metrics for complex orchestrations:
559
+
560
+ ```typescript
561
+ import { stableWorkflow, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
562
+
563
+ const phases = [
564
+ {
565
+ id: 'fetch-users',
566
+ requests: [/* ... */]
567
+ },
568
+ {
569
+ id: 'process-data',
570
+ concurrent: true,
571
+ requests: [/* ... */]
572
+ },
573
+ {
574
+ id: 'store-results',
575
+ requests: [/* ... */]
576
+ }
577
+ ];
578
+
579
+ const result = await stableWorkflow(phases, {
580
+ workflowId: 'data-processing-pipeline',
581
+ enableMixedExecution: true,
582
+ commonRequestData: { hostname: 'api.example.com' },
583
+ logPhaseResults: true
584
+ });
585
+
586
+ // Workflow-level metrics
587
+ console.log('Workflow Metrics:', {
588
+ workflowId: result.metrics.workflowId,
589
+ success: result.metrics.success,
590
+ executionTime: result.metrics.executionTime, // Total time in ms
591
+
592
+ // Phase statistics
593
+ totalPhases: result.metrics.totalPhases,
594
+ completedPhases: result.metrics.completedPhases,
595
+ skippedPhases: result.metrics.skippedPhases,
596
+ failedPhases: result.metrics.failedPhases,
597
+ phaseCompletionRate: result.metrics.phaseCompletionRate, // %
598
+ averagePhaseExecutionTime: result.metrics.averagePhaseExecutionTime, // ms
599
+
600
+ // Request statistics
601
+ totalRequests: result.metrics.totalRequests,
602
+ successfulRequests: result.metrics.successfulRequests,
603
+ failedRequests: result.metrics.failedRequests,
604
+ requestSuccessRate: result.metrics.requestSuccessRate, // %
605
+ requestFailureRate: result.metrics.requestFailureRate, // %
606
+
607
+ // Performance
608
+ throughput: result.metrics.throughput, // requests/second
609
+ totalPhaseReplays: result.metrics.totalPhaseReplays,
610
+ totalPhaseSkips: result.metrics.totalPhaseSkips,
611
+
612
+ // Branch metrics (if using branch execution)
613
+ totalBranches: result.metrics.totalBranches,
614
+ completedBranches: result.metrics.completedBranches,
615
+ failedBranches: result.metrics.failedBranches,
616
+ branchSuccessRate: result.metrics.branchSuccessRate // %
617
+ });
618
+
619
+ // Request group metrics aggregated across entire workflow
620
+ result.requestGroupMetrics?.forEach(group => {
621
+ console.log(`Request Group ${group.groupId}:`, {
622
+ totalRequests: group.totalRequests,
623
+ successRate: group.successRate, // %
624
+ requestIds: group.requestIds
625
+ });
626
+ });
627
+
628
+ // Per-phase metrics
629
+ result.phases.forEach(phase => {
630
+ console.log(`Phase ${phase.phaseId}:`, {
631
+ executionTime: phase.metrics?.executionTime,
632
+ totalRequests: phase.metrics?.totalRequests,
633
+ successfulRequests: phase.metrics?.successfulRequests,
634
+ requestSuccessRate: phase.metrics?.requestSuccessRate, // %
635
+ hasDecision: phase.metrics?.hasDecision,
636
+ decisionAction: phase.metrics?.decisionAction // CONTINUE | JUMP | REPLAY | etc.
637
+ });
638
+ });
639
+
640
+ // Branch metrics (for branched workflows)
641
+ result.branches?.forEach(branch => {
642
+ console.log(`Branch ${branch.branchId}:`, {
643
+ success: branch.metrics?.success,
644
+ executionTime: branch.metrics?.executionTime,
645
+ totalPhases: branch.metrics?.totalPhases,
646
+ completedPhases: branch.metrics?.completedPhases,
647
+ totalRequests: branch.metrics?.totalRequests,
648
+ requestSuccessRate: branch.metrics?.requestSuccessRate // %
649
+ });
650
+ });
651
+ ```
652
+
653
+ ### MetricsAggregator Utility
654
+
655
+ For custom metrics extraction and analysis:
656
+
657
+ ```typescript
658
+ import { MetricsAggregator } from '@emmvish/stable-request';
659
+
660
+ // Extract workflow metrics
661
+ const workflowMetrics = MetricsAggregator.extractWorkflowMetrics(workflowResult);
662
+
663
+ // Extract phase metrics
664
+ const phaseMetrics = MetricsAggregator.extractPhaseMetrics(phaseResult);
665
+
666
+ // Extract branch metrics
667
+ const branchMetrics = MetricsAggregator.extractBranchMetrics(branchResult);
668
+
669
+ // Extract request group metrics
670
+ const requestGroups = MetricsAggregator.extractRequestGroupMetrics(responses);
671
+
672
+ // Extract individual request metrics
673
+ const requestMetrics = MetricsAggregator.extractRequestMetrics(responses);
674
+
675
+ // Extract circuit breaker metrics
676
+ const cbMetrics = MetricsAggregator.extractCircuitBreakerMetrics(circuitBreaker);
677
+
678
+ // Extract cache metrics
679
+ const cacheMetrics = MetricsAggregator.extractCacheMetrics(cacheManager);
680
+
681
+ // Extract rate limiter metrics
682
+ const rateLimiterMetrics = MetricsAggregator.extractRateLimiterMetrics(rateLimiter);
683
+
684
+ // Extract concurrency limiter metrics
685
+ const concurrencyMetrics = MetricsAggregator.extractConcurrencyLimiterMetrics(limiter);
686
+
687
+ // Aggregate all system metrics
688
+ const systemMetrics = MetricsAggregator.aggregateSystemMetrics(
689
+ workflowResult,
690
+ circuitBreaker,
691
+ cacheManager,
692
+ rateLimiter,
693
+ concurrencyLimiter
694
+ );
695
+
696
+ console.log('Complete System View:', {
697
+ workflow: systemMetrics.workflow,
698
+ phases: systemMetrics.phases,
699
+ branches: systemMetrics.branches,
700
+ requestGroups: systemMetrics.requestGroups,
701
+ requests: systemMetrics.requests,
702
+ circuitBreaker: systemMetrics.circuitBreaker,
703
+ cache: systemMetrics.cache,
704
+ rateLimiter: systemMetrics.rateLimiter,
705
+ concurrencyLimiter: systemMetrics.concurrencyLimiter
706
+ });
707
+ ```
708
+
709
+ **Available Metrics Types:**
710
+ - `WorkflowMetrics`: Complete workflow statistics
711
+ - `BranchMetrics`: Branch execution metrics
712
+ - `PhaseMetrics`: Individual phase metrics
713
+ - `RequestGroupMetrics`: Grouped request statistics
714
+ - `RequestMetrics`: Individual request metrics
715
+ - `CircuitBreakerDashboardMetrics`: Circuit breaker state and performance
716
+ - `CacheDashboardMetrics`: Cache hit rates and efficiency
717
+ - `RateLimiterDashboardMetrics`: Throttling and rate limit statistics
718
+ - `ConcurrencyLimiterDashboardMetrics`: Concurrency and queue metrics
719
+ - `SystemMetrics`: Complete system-wide aggregation
720
+
386
721
  ## Workflow Execution Patterns
387
722
 
388
723
  ### Sequential and Concurrent Phases
@@ -659,7 +994,6 @@ const branches = [
659
994
  if (branchResult.failedRequests > 0) {
660
995
  return {
661
996
  action: PHASE_DECISION_ACTIONS.TERMINATE,
662
- terminateWorkflow: true, // Terminate entire workflow
663
997
  metadata: { reason: 'Critical branch failed' }
664
998
  };
665
999
  }
@@ -1074,7 +1408,7 @@ await stableWorkflow(phases, {
1074
1408
  },
1075
1409
 
1076
1410
  // Monitor non-linear execution decisions
1077
- handlePhaseDecision: async ({ workflowId, decision, phaseResult }) => {
1411
+ handlePhaseDecision: async ({ decision, phaseResult }) => {
1078
1412
  console.log(`Phase decision: ${decision.action}`);
1079
1413
  if (decision.targetPhaseId) {
1080
1414
  console.log(`Target: ${decision.targetPhaseId}`);
@@ -1285,12 +1619,13 @@ await stableWorkflow(pollingPhases, {
1285
1619
  ### Webhook Retry with Circuit Breaker
1286
1620
 
1287
1621
  ```typescript
1288
- import { CircuitBreaker, REQUEST_METHODS } from '@emmvish/stable-request';
1622
+ import { CircuitBreaker, REQUEST_METHODS, RETRY_STRATEGIES } from '@emmvish/stable-request';
1289
1623
 
1290
1624
  const webhookBreaker = new CircuitBreaker({
1291
- failureThreshold: 3,
1292
- successThreshold: 2,
1293
- timeout: 30000
1625
+ failureThresholdPercentage: 60, // 60% failure rate triggers open
1626
+ minimumRequests: 5, // Minimum 5 requests before evaluation
1627
+ recoveryTimeoutMs: 30000, // 30s timeout in open state
1628
+ successThresholdPercentage: 40 // 40% success rate closes circuit
1294
1629
  });
1295
1630
 
1296
1631
  async function sendWebhook(eventData: any) {
@@ -1,3 +1,3 @@
1
- import { API_GATEWAY_OPTIONS, API_GATEWAY_REQUEST } from '../types/index.js';
2
- export declare function stableApiGateway<RequestDataType = any, ResponseDataType = any>(requests?: API_GATEWAY_REQUEST<RequestDataType, ResponseDataType>[], options?: API_GATEWAY_OPTIONS<RequestDataType, ResponseDataType>): Promise<import("../types/index.js").API_GATEWAY_RESPONSE<ResponseDataType>[]>;
1
+ import { API_GATEWAY_OPTIONS, API_GATEWAY_REQUEST, API_GATEWAY_RESULT } from '../types/index.js';
2
+ export declare function stableApiGateway<RequestDataType = any, ResponseDataType = any>(requests?: API_GATEWAY_REQUEST<RequestDataType, ResponseDataType>[], options?: API_GATEWAY_OPTIONS<RequestDataType, ResponseDataType>): Promise<API_GATEWAY_RESULT<ResponseDataType>>;
3
3
  //# sourceMappingURL=stable-api-gateway.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stable-api-gateway.d.ts","sourceRoot":"","sources":["../../src/core/stable-api-gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,mBAAmB,EAGtB,MAAM,mBAAmB,CAAC;AAO3B,wBAAsB,gBAAgB,CAAC,eAAe,GAAG,GAAG,EAAE,gBAAgB,GAAG,GAAG,EAChF,QAAQ,GAAE,mBAAmB,CAAC,eAAe,EAAE,gBAAgB,CAAC,EAAO,EACvE,OAAO,GAAE,mBAAmB,CAAC,eAAe,EAAE,gBAAgB,CAAM,iFA8BvE"}
1
+ {"version":3,"file":"stable-api-gateway.d.ts","sourceRoot":"","sources":["../../src/core/stable-api-gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,mBAAmB,EAEnB,kBAAkB,EAOrB,MAAM,mBAAmB,CAAC;AAY3B,wBAAsB,gBAAgB,CAAC,eAAe,GAAG,GAAG,EAAE,gBAAgB,GAAG,GAAG,EAChF,QAAQ,GAAE,mBAAmB,CAAC,eAAe,EAAE,gBAAgB,CAAC,EAAO,EACvE,OAAO,GAAE,mBAAmB,CAAC,eAAe,EAAE,gBAAgB,CAAM,GACrE,OAAO,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC,CA6F/C"}
@@ -1,8 +1,20 @@
1
- import { executeConcurrently, executeSequentially, extractCommonRequestConfigOptions as extractCommonOptions } from '../utilities/index.js';
1
+ import { executeConcurrently, executeSequentially, extractCommonRequestConfigOptions as extractCommonOptions, MetricsAggregator } from '../utilities/index.js';
2
+ import { CircuitBreaker, getGlobalCircuitBreaker } from '../utilities/circuit-breaker.js';
3
+ import { getGlobalCacheManager } from '../utilities/cache-manager.js';
4
+ import { getGlobalRateLimiter } from '../utilities/rate-limiter.js';
5
+ import { getGlobalConcurrencyLimiter } from '../utilities/concurrency-limiter.js';
2
6
  export async function stableApiGateway(requests = [], options = {}) {
3
7
  const { concurrentExecution = true, stopOnFirstError = false, requestGroups = [], maxConcurrentRequests, rateLimit, circuitBreaker, } = options;
4
8
  if (!Array.isArray(requests) || requests.length === 0) {
5
- return [];
9
+ const emptyResult = [];
10
+ emptyResult.metrics = {
11
+ totalRequests: 0,
12
+ successfulRequests: 0,
13
+ failedRequests: 0,
14
+ successRate: 0,
15
+ failureRate: 0
16
+ };
17
+ return emptyResult;
6
18
  }
7
19
  const requestExecutionOptions = {
8
20
  stopOnFirstError,
@@ -13,11 +25,53 @@ export async function stableApiGateway(requests = [], options = {}) {
13
25
  ...(circuitBreaker !== undefined && { circuitBreaker }),
14
26
  ...extractCommonOptions(options)
15
27
  };
28
+ let responses;
16
29
  if (concurrentExecution) {
17
- return executeConcurrently(requests, requestExecutionOptions);
30
+ responses = await executeConcurrently(requests, requestExecutionOptions);
18
31
  }
19
32
  else {
20
- return executeSequentially(requests, requestExecutionOptions);
33
+ responses = await executeSequentially(requests, requestExecutionOptions);
21
34
  }
35
+ const successfulRequests = responses.filter(r => r.success).length;
36
+ const failedRequests = responses.filter(r => !r.success).length;
37
+ const totalRequests = responses.length;
38
+ const result = responses;
39
+ result.metrics = {
40
+ totalRequests,
41
+ successfulRequests,
42
+ failedRequests,
43
+ successRate: totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0,
44
+ failureRate: totalRequests > 0 ? (failedRequests / totalRequests) * 100 : 0,
45
+ requestGroups: MetricsAggregator.extractRequestGroupMetrics(responses)
46
+ };
47
+ const infrastructureMetrics = {};
48
+ if (circuitBreaker) {
49
+ const cb = circuitBreaker instanceof CircuitBreaker ? circuitBreaker : getGlobalCircuitBreaker();
50
+ if (cb) {
51
+ infrastructureMetrics.circuitBreaker = MetricsAggregator.extractCircuitBreakerMetrics(cb);
52
+ }
53
+ }
54
+ if (options.commonCache) {
55
+ const cache = getGlobalCacheManager();
56
+ if (cache) {
57
+ infrastructureMetrics.cache = MetricsAggregator.extractCacheMetrics(cache);
58
+ }
59
+ }
60
+ if (rateLimit) {
61
+ const rateLimiter = getGlobalRateLimiter();
62
+ if (rateLimiter) {
63
+ infrastructureMetrics.rateLimiter = MetricsAggregator.extractRateLimiterMetrics(rateLimiter);
64
+ }
65
+ }
66
+ if (maxConcurrentRequests) {
67
+ const concurrencyLimiter = getGlobalConcurrencyLimiter();
68
+ if (concurrencyLimiter) {
69
+ infrastructureMetrics.concurrencyLimiter = MetricsAggregator.extractConcurrencyLimiterMetrics(concurrencyLimiter);
70
+ }
71
+ }
72
+ if (Object.keys(infrastructureMetrics).length > 0) {
73
+ result.metrics.infrastructureMetrics = infrastructureMetrics;
74
+ }
75
+ return result;
22
76
  }
23
77
  //# sourceMappingURL=stable-api-gateway.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stable-api-gateway.js","sourceRoot":"","sources":["../../src/core/stable-api-gateway.ts"],"names":[],"mappings":"AAMA,OAAO,EACH,mBAAmB,EACnB,mBAAmB,EACnB,iCAAiC,IAAI,oBAAoB,EAC5D,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAClC,WAAqE,EAAE,EACvE,UAAkE,EAAE;IAEpE,MAAM,EACF,mBAAmB,GAAG,IAAI,EAC1B,gBAAgB,GAAG,KAAK,EACxB,aAAa,GAAG,EAAE,EAClB,qBAAqB,EACrB,SAAS,EACT,cAAc,GACjB,GAAG,OAAO,CAAC;IAEZ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,CAAC;IACd,CAAC;IAED,MAAM,uBAAuB,GAAgF;QACzG,gBAAgB;QAChB,aAAa;QACb,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,GAAG,CAAC,qBAAqB,KAAK,SAAS,IAAI,EAAE,qBAAqB,EAAE,CAAC;QACrE,GAAG,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC;QAC7C,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,EAAE,cAAc,EAAE,CAAC;QACvD,GAAG,oBAAoB,CAAoC,OAAO,CAAC;KACtE,CAAA;IAED,IAAI,mBAAmB,EAAE,CAAC;QACtB,OAAO,mBAAmB,CAAoC,QAAQ,EAAE,uBAAkG,CAAC,CAAC;IAChL,CAAC;SAAM,CAAC;QACJ,OAAO,mBAAmB,CAAoC,QAAQ,EAAE,uBAAkG,CAAC,CAAC;IAChL,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"stable-api-gateway.js","sourceRoot":"","sources":["../../src/core/stable-api-gateway.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { STABLE_REQUEST } from '../types/index.js';
2
- export declare function stableRequest<RequestDataType = any, ResponseDataType = any>(options: STABLE_REQUEST<RequestDataType, ResponseDataType>): Promise<ResponseDataType | boolean>;
1
+ import { STABLE_REQUEST, STABLE_REQUEST_RESULT } from '../types/index.js';
2
+ export declare function stableRequest<RequestDataType = any, ResponseDataType = any>(options: STABLE_REQUEST<RequestDataType, ResponseDataType>): Promise<STABLE_REQUEST_RESULT<ResponseDataType>>;
3
3
  //# sourceMappingURL=stable-request.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stable-request.d.ts","sourceRoot":"","sources":["../../src/core/stable-request.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAe3B,wBAAsB,aAAa,CAAC,eAAe,GAAG,GAAG,EAAE,gBAAgB,GAAG,GAAG,EAC/E,OAAO,EAAE,cAAc,CAAC,eAAe,EAAE,gBAAgB,CAAC,GACzD,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,CAmVrC"}
1
+ {"version":3,"file":"stable-request.d.ts","sourceRoot":"","sources":["../../src/core/stable-request.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,cAAc,EACd,qBAAqB,EAEtB,MAAM,mBAAmB,CAAC;AAkB3B,wBAAsB,aAAa,CAAC,eAAe,GAAG,GAAG,EAAE,gBAAgB,GAAG,GAAG,EAC/E,OAAO,EAAE,cAAc,CAAC,eAAe,EAAE,gBAAgB,CAAC,GACzD,OAAO,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,CAAC,CAqXlD"}
@@ -1,5 +1,5 @@
1
1
  import { RETRY_STRATEGIES, RESPONSE_ERRORS, CircuitBreakerState } from '../enums/index.js';
2
- import { CircuitBreaker, CircuitBreakerOpenError, executeWithPersistence, formatLogContext, generateAxiosRequestConfig, getNewDelayTime, delay, reqFn, safelyStringify, validateTrialModeProbabilities } from '../utilities/index.js';
2
+ import { CircuitBreaker, CircuitBreakerOpenError, executeWithPersistence, formatLogContext, generateAxiosRequestConfig, getNewDelayTime, getGlobalCacheManager, delay, reqFn, safelyStringify, validateTrialModeProbabilities, MetricsAggregator } from '../utilities/index.js';
3
3
  export async function stableRequest(options) {
4
4
  const { preExecution = {
5
5
  preExecutionHook: ({ inputParams, commonBuffer }) => { },
@@ -30,6 +30,33 @@ export async function stableRequest(options) {
30
30
  const { reqData: givenReqData, responseAnalyzer = ({ reqData, data, trialMode = { enabled: false } }) => true, resReq = false, attempts: givenAttempts = 1, performAllAttempts = false, wait = 1000, maxAllowedWait = 60000, retryStrategy = RETRY_STRATEGIES.FIXED, logAllErrors = false, handleErrors = ({ reqData, errorLog, maxSerializableChars = 1000, executionContext }) => console.error(`${formatLogContext(executionContext)}stable-request:\n`, 'Request data:\n', safelyStringify(reqData, maxSerializableChars), '\nError log:\n', safelyStringify(errorLog, maxSerializableChars)), logAllSuccessfulAttempts = false, handleSuccessfulAttemptData = ({ reqData, successfulAttemptData, maxSerializableChars = 1000, executionContext }) => console.info(`${formatLogContext(executionContext)}stable-request:\n`, 'Request data:\n', safelyStringify(reqData, maxSerializableChars), '\nSuccessful attempt:\n', safelyStringify(successfulAttemptData, maxSerializableChars)), maxSerializableChars = 1000, finalErrorAnalyzer = ({ reqData, error, trialMode = { enabled: false } }) => false, trialMode = { enabled: false }, hookParams = {}, cache, circuitBreaker, jitter = 0, statePersistence } = options;
31
31
  let attempts = givenAttempts;
32
32
  const reqData = generateAxiosRequestConfig(givenReqData);
33
+ const requestStartTime = Date.now();
34
+ const errorLogs = [];
35
+ const successfulAttemptsList = [];
36
+ let totalAttemptsMade = 0;
37
+ const buildResult = (success, data, error) => {
38
+ const totalExecutionTime = Date.now() - requestStartTime;
39
+ const successfulAttemptsCount = successfulAttemptsList.length;
40
+ const failedAttemptsCount = totalAttemptsMade - successfulAttemptsCount;
41
+ return {
42
+ success,
43
+ ...(data !== undefined && { data }),
44
+ ...(error && { error }),
45
+ ...(errorLogs.length > 0 && { errorLogs }),
46
+ ...(successfulAttemptsList.length > 0 && { successfulAttempts: successfulAttemptsList }),
47
+ metrics: {
48
+ totalAttempts: totalAttemptsMade,
49
+ successfulAttempts: successfulAttemptsCount,
50
+ failedAttempts: failedAttemptsCount,
51
+ totalExecutionTime,
52
+ averageAttemptTime: totalAttemptsMade > 0 ? totalExecutionTime / totalAttemptsMade : 0,
53
+ infrastructureMetrics: {
54
+ ...(circuitBreakerInstance && { circuitBreaker: MetricsAggregator.extractCircuitBreakerMetrics(circuitBreakerInstance) }),
55
+ ...(cache && getGlobalCacheManager() && { cache: MetricsAggregator.extractCacheMetrics(getGlobalCacheManager()) })
56
+ }
57
+ }
58
+ };
59
+ };
33
60
  let circuitBreakerInstance = null;
34
61
  if (circuitBreaker) {
35
62
  circuitBreakerInstance = circuitBreaker instanceof CircuitBreaker
@@ -51,6 +78,7 @@ export async function stableRequest(options) {
51
78
  do {
52
79
  attempts--;
53
80
  const currentAttempt = maxAttempts - attempts;
81
+ totalAttemptsMade = currentAttempt;
54
82
  if (circuitBreakerInstance) {
55
83
  const cbConfig = circuitBreakerInstance.getState().config;
56
84
  if (cbConfig.trackIndividualAttempts || currentAttempt === 1) {
@@ -66,7 +94,7 @@ export async function stableRequest(options) {
66
94
  if (trialMode.enabled) {
67
95
  console.info(`${formatLogContext(executionContext)}stable-request: Response served from cache:\n`, safelyStringify(res?.data, maxSerializableChars));
68
96
  }
69
- return resReq ? res?.data : true;
97
+ return buildResult(true, resReq ? res?.data : true);
70
98
  }
71
99
  }
72
100
  catch (attemptError) {
@@ -125,6 +153,7 @@ export async function stableRequest(options) {
125
153
  executionTime: res.executionTime,
126
154
  statusCode: res.statusCode
127
155
  };
156
+ errorLogs.push(errorLog);
128
157
  try {
129
158
  await executeWithPersistence(handleErrors, {
130
159
  reqData,
@@ -151,6 +180,7 @@ export async function stableRequest(options) {
151
180
  executionTime: res.executionTime,
152
181
  statusCode: res.statusCode
153
182
  };
183
+ successfulAttemptsList.push(successfulAttemptLog);
154
184
  try {
155
185
  await executeWithPersistence(handleSuccessfulAttemptData, {
156
186
  reqData,
@@ -182,14 +212,14 @@ export async function stableRequest(options) {
182
212
  if (trialMode.enabled) {
183
213
  console.info(`${formatLogContext(executionContext)}stable-request: Final response (performAllAttempts mode):\n`, safelyStringify(lastSuccessfulAttemptData, maxSerializableChars));
184
214
  }
185
- return resReq ? lastSuccessfulAttemptData : true;
215
+ return buildResult(true, resReq ? lastSuccessfulAttemptData : true);
186
216
  }
187
217
  else if (res.ok) {
188
218
  if (trialMode.enabled) {
189
219
  const finalResponse = res?.data ?? lastSuccessfulAttemptData;
190
220
  console.info(`${formatLogContext(executionContext)}stable-request: Final response:\n`, safelyStringify(finalResponse, maxSerializableChars));
191
221
  }
192
- return resReq ? res?.data ?? lastSuccessfulAttemptData : true;
222
+ return buildResult(true, resReq ? (res?.data ?? lastSuccessfulAttemptData) : true);
193
223
  }
194
224
  else {
195
225
  throw new Error(safelyStringify({
@@ -221,7 +251,7 @@ export async function stableRequest(options) {
221
251
  throw e;
222
252
  }
223
253
  else {
224
- return false;
254
+ return buildResult(false, undefined, e.message || 'Request failed');
225
255
  }
226
256
  }
227
257
  }