@lgriffin/esi.ts 8.0.0 → 9.0.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 (107) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +26 -26
  3. package/README.md +986 -967
  4. package/dist/config/jest/globalSetup.d.ts.map +1 -1
  5. package/dist/config/jest/globalTeardown.d.ts.map +1 -1
  6. package/dist/core/configureApiClient.d.ts.map +1 -1
  7. package/dist/core/pagination/CursorPaginationHandler.d.ts +9 -4
  8. package/dist/core/pagination/CursorPaginationHandler.d.ts.map +1 -1
  9. package/dist/core/pagination/PaginationHandler.d.ts +6 -5
  10. package/dist/core/pagination/PaginationHandler.d.ts.map +1 -1
  11. package/dist/core/requestPipeline/cachePolicy.d.ts +1 -0
  12. package/dist/core/requestPipeline/cachePolicy.d.ts.map +1 -1
  13. package/dist/core/requestPipeline/paginationOrchestration.d.ts.map +1 -1
  14. package/dist/core/requestPipeline/statusHandling.d.ts.map +1 -1
  15. package/dist/core/util/error.d.ts +2 -0
  16. package/dist/core/util/error.d.ts.map +1 -1
  17. package/dist/errors.d.ts +4 -0
  18. package/dist/errors.d.ts.map +1 -0
  19. package/dist/errors.js +196 -0
  20. package/dist/errors.js.map +1 -0
  21. package/dist/errors.mjs +146 -0
  22. package/dist/errors.mjs.map +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +400 -389
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +399 -389
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/schemas/index.js +2087 -0
  30. package/dist/schemas/index.js.map +1 -0
  31. package/dist/schemas/index.mjs +1914 -0
  32. package/dist/schemas/index.mjs.map +1 -0
  33. package/dist/testing/index.d.ts +2 -0
  34. package/dist/testing/index.d.ts.map +1 -0
  35. package/dist/testing/index.js +935 -0
  36. package/dist/testing/index.js.map +1 -0
  37. package/dist/testing/index.mjs +898 -0
  38. package/dist/testing/index.mjs.map +1 -0
  39. package/dist/types/generated/esi-spec.generated.d.ts +3 -3
  40. package/dist/types/generated/esi-spec.generated.d.ts.map +1 -1
  41. package/package.json +253 -235
  42. package/dist/schemas/generated/alliance.generated.d.ts +0 -15
  43. package/dist/schemas/generated/alliance.generated.d.ts.map +0 -1
  44. package/dist/schemas/generated/assets.generated.d.ts +0 -272
  45. package/dist/schemas/generated/assets.generated.d.ts.map +0 -1
  46. package/dist/schemas/generated/calendar.generated.d.ts +0 -41
  47. package/dist/schemas/generated/calendar.generated.d.ts.map +0 -1
  48. package/dist/schemas/generated/character.generated.d.ts +0 -670
  49. package/dist/schemas/generated/character.generated.d.ts.map +0 -1
  50. package/dist/schemas/generated/clones.generated.d.ts +0 -23
  51. package/dist/schemas/generated/clones.generated.d.ts.map +0 -1
  52. package/dist/schemas/generated/contacts.generated.d.ts +0 -50
  53. package/dist/schemas/generated/contacts.generated.d.ts.map +0 -1
  54. package/dist/schemas/generated/contracts.generated.d.ts +0 -162
  55. package/dist/schemas/generated/contracts.generated.d.ts.map +0 -1
  56. package/dist/schemas/generated/corporation-projects.generated.d.ts +0 -89
  57. package/dist/schemas/generated/corporation-projects.generated.d.ts.map +0 -1
  58. package/dist/schemas/generated/corporation.generated.d.ts +0 -1515
  59. package/dist/schemas/generated/corporation.generated.d.ts.map +0 -1
  60. package/dist/schemas/generated/dogma.generated.d.ts +0 -57
  61. package/dist/schemas/generated/dogma.generated.d.ts.map +0 -1
  62. package/dist/schemas/generated/faction-warfare.generated.d.ts +0 -155
  63. package/dist/schemas/generated/faction-warfare.generated.d.ts.map +0 -1
  64. package/dist/schemas/generated/fittings.generated.d.ts +0 -57
  65. package/dist/schemas/generated/fittings.generated.d.ts.map +0 -1
  66. package/dist/schemas/generated/fleets.generated.d.ts +0 -45
  67. package/dist/schemas/generated/fleets.generated.d.ts.map +0 -1
  68. package/dist/schemas/generated/freelance-jobs.generated.d.ts +0 -173
  69. package/dist/schemas/generated/freelance-jobs.generated.d.ts.map +0 -1
  70. package/dist/schemas/generated/incursions.generated.d.ts +0 -16
  71. package/dist/schemas/generated/incursions.generated.d.ts.map +0 -1
  72. package/dist/schemas/generated/index.d.ts +0 -33
  73. package/dist/schemas/generated/index.d.ts.map +0 -1
  74. package/dist/schemas/generated/industry.generated.d.ts +0 -117
  75. package/dist/schemas/generated/industry.generated.d.ts.map +0 -1
  76. package/dist/schemas/generated/insurance.generated.d.ts +0 -10
  77. package/dist/schemas/generated/insurance.generated.d.ts.map +0 -1
  78. package/dist/schemas/generated/killmails.generated.d.ts +0 -55
  79. package/dist/schemas/generated/killmails.generated.d.ts.map +0 -1
  80. package/dist/schemas/generated/location.generated.d.ts +0 -18
  81. package/dist/schemas/generated/location.generated.d.ts.map +0 -1
  82. package/dist/schemas/generated/loyalty.generated.d.ts +0 -18
  83. package/dist/schemas/generated/loyalty.generated.d.ts.map +0 -1
  84. package/dist/schemas/generated/mail.generated.d.ts +0 -68
  85. package/dist/schemas/generated/mail.generated.d.ts.map +0 -1
  86. package/dist/schemas/generated/market.generated.d.ts +0 -201
  87. package/dist/schemas/generated/market.generated.d.ts.map +0 -1
  88. package/dist/schemas/generated/meta.generated.d.ts +0 -26
  89. package/dist/schemas/generated/meta.generated.d.ts.map +0 -1
  90. package/dist/schemas/generated/planetary-interaction.generated.d.ts +0 -90
  91. package/dist/schemas/generated/planetary-interaction.generated.d.ts.map +0 -1
  92. package/dist/schemas/generated/routes.generated.d.ts +0 -5
  93. package/dist/schemas/generated/routes.generated.d.ts.map +0 -1
  94. package/dist/schemas/generated/search.generated.d.ts +0 -15
  95. package/dist/schemas/generated/search.generated.d.ts.map +0 -1
  96. package/dist/schemas/generated/skills.generated.d.ts +0 -32
  97. package/dist/schemas/generated/skills.generated.d.ts.map +0 -1
  98. package/dist/schemas/generated/sovereignty.generated.d.ts +0 -37
  99. package/dist/schemas/generated/sovereignty.generated.d.ts.map +0 -1
  100. package/dist/schemas/generated/status.generated.d.ts +0 -8
  101. package/dist/schemas/generated/status.generated.d.ts.map +0 -1
  102. package/dist/schemas/generated/universe.generated.d.ts +0 -394
  103. package/dist/schemas/generated/universe.generated.d.ts.map +0 -1
  104. package/dist/schemas/generated/wallet.generated.d.ts +0 -411
  105. package/dist/schemas/generated/wallet.generated.d.ts.map +0 -1
  106. package/dist/schemas/generated/wars.generated.d.ts +0 -31
  107. package/dist/schemas/generated/wars.generated.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -92,6 +92,7 @@ __export(index_exports, {
92
92
  fetchAllCursorPages: () => fetchAllCursorPages,
93
93
  fetchPages: () => fetchPages,
94
94
  getDefaultClient: () => getDefaultClient,
95
+ isCircuitOpen: () => isCircuitOpen,
95
96
  isEsiError: () => isEsiError,
96
97
  isForbidden: () => isForbidden,
97
98
  isNotFound: () => isNotFound,
@@ -308,6 +309,216 @@ var ApiClient = class {
308
309
  }
309
310
  };
310
311
 
312
+ // src/core/logger/logger.ts
313
+ var import_pino = __toESM(require("pino"));
314
+ var pinoLogger = (0, import_pino.default)({ level: process.env.ESI_LOG_LEVEL || "warn" });
315
+ var logger = {
316
+ info: (msg) => {
317
+ pinoLogger.info(msg);
318
+ },
319
+ warn: (msg) => {
320
+ pinoLogger.warn(msg);
321
+ },
322
+ error: (msg) => {
323
+ pinoLogger.error(msg);
324
+ },
325
+ debug: (msg) => {
326
+ pinoLogger.debug(msg);
327
+ }
328
+ };
329
+ var logger_default = logger;
330
+
331
+ // src/core/logger/loggerUtil.ts
332
+ var activeLogger = logger_default;
333
+ var setLogger = (customLogger) => {
334
+ activeLogger = customLogger;
335
+ };
336
+ var logInfo = (message) => {
337
+ activeLogger.info(message);
338
+ };
339
+ var logError = (message) => {
340
+ activeLogger.error(message);
341
+ };
342
+ var logWarn = (message) => {
343
+ activeLogger.warn(message);
344
+ };
345
+ var logDebug = (message) => {
346
+ activeLogger.debug(message);
347
+ };
348
+
349
+ // src/core/circuitBreaker/CircuitBreaker.ts
350
+ var CircuitBreaker = class {
351
+ circuits = /* @__PURE__ */ new Map();
352
+ failureThreshold;
353
+ resetTimeoutMs;
354
+ halfOpenMaxAttempts;
355
+ staleThresholdMs;
356
+ keyStrategy;
357
+ cleanupTimer = null;
358
+ constructor(config = {}) {
359
+ this.failureThreshold = config.failureThreshold ?? 5;
360
+ this.resetTimeoutMs = config.resetTimeoutMs ?? 3e4;
361
+ this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 1;
362
+ this.staleThresholdMs = config.staleThresholdMs ?? 36e5;
363
+ this.keyStrategy = config.keyStrategy ?? "resolved";
364
+ const cleanupIntervalMs = config.cleanupIntervalMs;
365
+ if (cleanupIntervalMs !== void 0 && cleanupIntervalMs > 0) {
366
+ this.startCleanupTimer(cleanupIntervalMs);
367
+ }
368
+ }
369
+ getKey(endpoint) {
370
+ return endpoint.split("?")[0];
371
+ }
372
+ getOrCreate(key) {
373
+ let record = this.circuits.get(key);
374
+ if (!record) {
375
+ record = {
376
+ state: "closed",
377
+ failures: 0,
378
+ lastFailureTime: 0,
379
+ halfOpenAttempts: 0
380
+ };
381
+ this.circuits.set(key, record);
382
+ }
383
+ return record;
384
+ }
385
+ checkCircuit(endpoint) {
386
+ const key = this.getKey(endpoint);
387
+ const record = this.getOrCreate(key);
388
+ if (record.state === "closed") return;
389
+ if (record.state === "open") {
390
+ const elapsed = Date.now() - record.lastFailureTime;
391
+ if (elapsed >= this.resetTimeoutMs) {
392
+ record.state = "half-open";
393
+ record.halfOpenAttempts = 0;
394
+ logInfo(`Circuit half-open for ${key}, allowing probe request`);
395
+ return;
396
+ }
397
+ const remainingMs = this.resetTimeoutMs - elapsed;
398
+ throw new CircuitOpenError(key, record.failures, remainingMs);
399
+ }
400
+ if (record.state === "half-open") {
401
+ if (record.halfOpenAttempts >= this.halfOpenMaxAttempts) {
402
+ throw new CircuitOpenError(key, record.failures, this.resetTimeoutMs);
403
+ }
404
+ record.halfOpenAttempts++;
405
+ }
406
+ }
407
+ recordSuccess(endpoint) {
408
+ const key = this.getKey(endpoint);
409
+ const record = this.circuits.get(key);
410
+ if (!record) return;
411
+ if (record.state === "half-open") {
412
+ logInfo(`Circuit closed for ${key} after successful probe`);
413
+ }
414
+ record.state = "closed";
415
+ record.failures = 0;
416
+ record.halfOpenAttempts = 0;
417
+ }
418
+ recordFailure(endpoint, statusCode) {
419
+ if (statusCode !== 0 && statusCode < 500 && statusCode !== 420 && statusCode !== 429) {
420
+ return;
421
+ }
422
+ const key = this.getKey(endpoint);
423
+ const record = this.getOrCreate(key);
424
+ record.failures++;
425
+ record.lastFailureTime = Date.now();
426
+ if (record.state === "half-open") {
427
+ record.state = "open";
428
+ logWarn(
429
+ `Circuit re-opened for ${key} after failed probe (${record.failures} failures)`
430
+ );
431
+ return;
432
+ }
433
+ if (record.failures >= this.failureThreshold) {
434
+ record.state = "open";
435
+ logWarn(
436
+ `Circuit opened for ${key} after ${record.failures} consecutive failures`
437
+ );
438
+ }
439
+ }
440
+ getState(endpoint) {
441
+ const key = this.getKey(endpoint);
442
+ const record = this.circuits.get(key);
443
+ if (!record) return "closed";
444
+ if (record.state === "open") {
445
+ const elapsed = Date.now() - record.lastFailureTime;
446
+ if (elapsed >= this.resetTimeoutMs) {
447
+ return "half-open";
448
+ }
449
+ }
450
+ return record.state;
451
+ }
452
+ getStats() {
453
+ const circuits = {};
454
+ let openCircuits = 0;
455
+ for (const [key, record] of this.circuits.entries()) {
456
+ const state = this.getState(key);
457
+ circuits[key] = { state, failures: record.failures };
458
+ if (state === "open") openCircuits++;
459
+ }
460
+ return {
461
+ totalCircuits: this.circuits.size,
462
+ openCircuits,
463
+ circuits
464
+ };
465
+ }
466
+ reset(endpoint) {
467
+ if (endpoint) {
468
+ this.circuits.delete(this.getKey(endpoint));
469
+ } else {
470
+ this.circuits.clear();
471
+ }
472
+ }
473
+ cleanup() {
474
+ const now = Date.now();
475
+ let cleaned = 0;
476
+ for (const [key, record] of this.circuits.entries()) {
477
+ if (record.state === "closed" && record.failures === 0 && record.lastFailureTime > 0 && now - record.lastFailureTime > this.staleThresholdMs) {
478
+ this.circuits.delete(key);
479
+ cleaned++;
480
+ }
481
+ }
482
+ if (cleaned > 0) {
483
+ logInfo(`Circuit breaker cleanup: removed ${cleaned} stale circuits`);
484
+ }
485
+ return cleaned;
486
+ }
487
+ getKeyStrategy() {
488
+ return this.keyStrategy;
489
+ }
490
+ destroy() {
491
+ if (this.cleanupTimer) {
492
+ clearInterval(this.cleanupTimer);
493
+ this.cleanupTimer = null;
494
+ }
495
+ this.circuits.clear();
496
+ }
497
+ shutdown() {
498
+ this.destroy();
499
+ }
500
+ startCleanupTimer(intervalMs) {
501
+ this.cleanupTimer = setInterval(() => {
502
+ this.cleanup();
503
+ }, intervalMs);
504
+ this.cleanupTimer.unref();
505
+ }
506
+ };
507
+ var CircuitOpenError = class extends Error {
508
+ endpoint;
509
+ failures;
510
+ retryAfterMs;
511
+ constructor(endpoint, failures, retryAfterMs) {
512
+ super(
513
+ `Circuit breaker open for ${endpoint} after ${failures} failures (retry after ${Math.ceil(retryAfterMs / 1e3)}s)`
514
+ );
515
+ this.name = "CircuitOpenError";
516
+ this.endpoint = endpoint;
517
+ this.failures = failures;
518
+ this.retryAfterMs = retryAfterMs;
519
+ }
520
+ };
521
+
311
522
  // src/core/util/error.ts
312
523
  var SENSITIVE_PARAMS = [
313
524
  "token",
@@ -402,6 +613,9 @@ function isTimeout(error) {
402
613
  function isRetryable(error) {
403
614
  return error instanceof EsiError && error.retryable;
404
615
  }
616
+ function isCircuitOpen(error) {
617
+ return error instanceof CircuitOpenError;
618
+ }
405
619
  var buildError = (message, type = "ERROR", url) => {
406
620
  const error = new Error(`[${type}] ${message}`);
407
621
  if (url) {
@@ -425,43 +639,6 @@ function isValidationError(error) {
425
639
  return error instanceof EsiValidationError;
426
640
  }
427
641
 
428
- // src/core/logger/logger.ts
429
- var import_pino = __toESM(require("pino"));
430
- var pinoLogger = (0, import_pino.default)({ level: process.env.ESI_LOG_LEVEL || "warn" });
431
- var logger = {
432
- info: (msg) => {
433
- pinoLogger.info(msg);
434
- },
435
- warn: (msg) => {
436
- pinoLogger.warn(msg);
437
- },
438
- error: (msg) => {
439
- pinoLogger.error(msg);
440
- },
441
- debug: (msg) => {
442
- pinoLogger.debug(msg);
443
- }
444
- };
445
- var logger_default = logger;
446
-
447
- // src/core/logger/loggerUtil.ts
448
- var activeLogger = logger_default;
449
- var setLogger = (customLogger) => {
450
- activeLogger = customLogger;
451
- };
452
- var logInfo = (message) => {
453
- activeLogger.info(message);
454
- };
455
- var logError = (message) => {
456
- activeLogger.error(message);
457
- };
458
- var logWarn = (message) => {
459
- activeLogger.warn(message);
460
- };
461
- var logDebug = (message) => {
462
- activeLogger.debug(message);
463
- };
464
-
465
642
  // src/core/constants.ts
466
643
  var PACKAGE_NAME = "esi.ts";
467
644
  var PACKAGE_VERSION = "7.4.0";
@@ -642,238 +819,67 @@ var esiCacheTtls = {
642
819
  "GET:universe/system_kills": 3600,
643
820
  "GET:wars": 3600,
644
821
  "GET:wars/{war_id}": 3600,
645
- "GET:wars/{war_id}/killmails": 3600
646
- };
647
-
648
- // src/core/requestPipeline/cachePolicy.ts
649
- function lookupSpecTtl(method, templatePath) {
650
- const normalized = templatePath.replace(/\/$/, "").replace(/\{(\w+)\}/g, (_, name) => `{${camelToSnake(name)}}`);
651
- const key = `${method}:${normalized}`;
652
- const seconds = esiCacheTtls[key];
653
- return typeof seconds === "number" ? seconds * 1e3 : void 0;
654
- }
655
- function trySpecAwareCacheHit(client, url, method, templatePath, resolveCache2) {
656
- if (method !== "GET" || !templatePath) return null;
657
- const specTtlMs = lookupSpecTtl(method, templatePath);
658
- if (!specTtlMs) return null;
659
- const cache = resolveCache2(client);
660
- if (!cache) return null;
661
- const entry = cache.get(url);
662
- if (!entry) return null;
663
- const age = Date.now() - entry.timestamp;
664
- if (age < specTtlMs) {
665
- logDebug(
666
- `Spec-aware cache hit for ${url} (age=${Math.round(age / 1e3)}s, ttl=${Math.round(specTtlMs / 1e3)}s)`
667
- );
668
- return {
669
- headers: entry.headers,
670
- body: entry.data,
671
- fromCache: true,
672
- cacheHitType: "spec-ttl"
673
- };
674
- }
675
- return null;
676
- }
677
- function tryStaleCacheResponse(client, url, parsed, resolveCache2) {
678
- const cache = resolveCache2(client);
679
- if (!cache) return null;
680
- const cachedEntry = cache.get(url);
681
- if (!cachedEntry) return null;
682
- return {
683
- headers: { ...cachedEntry.headers, ...parsed.raw },
684
- body: cachedEntry.data,
685
- fromCache: true,
686
- stale: true,
687
- cacheHitType: "stale-on-error"
688
- };
689
- }
690
- function cacheResponse(client, url, method, endpoint, parsed, data, useETag, resolveCache2, templatePath) {
691
- const cache = resolveCache2(client);
692
- if (useETag && method === "GET" && cache && parsed.etag) {
693
- const headerTtl = parseCacheControlTtl(parsed.raw);
694
- const specTtlMs = templatePath ? lookupSpecTtl(method, templatePath) : void 0;
695
- const ttl = specTtlMs ?? headerTtl;
696
- cache.set(url, parsed.etag, data, parsed.raw, ttl);
697
- const ttlInfo = ttl ? ` (ttl=${ttl}ms)` : "";
698
- logDebug(`Cached response for ${url} with ETag ${parsed.etag}${ttlInfo}`);
699
- }
700
- if (method !== "GET" && cache) {
701
- cache.deleteByPath(endpoint.split("?")[0]);
702
- }
703
- }
704
-
705
- // src/core/circuitBreaker/CircuitBreaker.ts
706
- var CircuitBreaker = class {
707
- circuits = /* @__PURE__ */ new Map();
708
- failureThreshold;
709
- resetTimeoutMs;
710
- halfOpenMaxAttempts;
711
- staleThresholdMs;
712
- keyStrategy;
713
- cleanupTimer = null;
714
- constructor(config = {}) {
715
- this.failureThreshold = config.failureThreshold ?? 5;
716
- this.resetTimeoutMs = config.resetTimeoutMs ?? 3e4;
717
- this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 1;
718
- this.staleThresholdMs = config.staleThresholdMs ?? 36e5;
719
- this.keyStrategy = config.keyStrategy ?? "resolved";
720
- const cleanupIntervalMs = config.cleanupIntervalMs;
721
- if (cleanupIntervalMs !== void 0 && cleanupIntervalMs > 0) {
722
- this.startCleanupTimer(cleanupIntervalMs);
723
- }
724
- }
725
- getKey(endpoint) {
726
- return endpoint.split("?")[0];
727
- }
728
- getOrCreate(key) {
729
- let record = this.circuits.get(key);
730
- if (!record) {
731
- record = {
732
- state: "closed",
733
- failures: 0,
734
- lastFailureTime: 0,
735
- halfOpenAttempts: 0
736
- };
737
- this.circuits.set(key, record);
738
- }
739
- return record;
740
- }
741
- checkCircuit(endpoint) {
742
- const key = this.getKey(endpoint);
743
- const record = this.getOrCreate(key);
744
- if (record.state === "closed") return;
745
- if (record.state === "open") {
746
- const elapsed = Date.now() - record.lastFailureTime;
747
- if (elapsed >= this.resetTimeoutMs) {
748
- record.state = "half-open";
749
- record.halfOpenAttempts = 0;
750
- logInfo(`Circuit half-open for ${key}, allowing probe request`);
751
- return;
752
- }
753
- const remainingMs = this.resetTimeoutMs - elapsed;
754
- throw new CircuitOpenError(key, record.failures, remainingMs);
755
- }
756
- if (record.state === "half-open") {
757
- if (record.halfOpenAttempts >= this.halfOpenMaxAttempts) {
758
- throw new CircuitOpenError(key, record.failures, this.resetTimeoutMs);
759
- }
760
- record.halfOpenAttempts++;
761
- }
762
- }
763
- recordSuccess(endpoint) {
764
- const key = this.getKey(endpoint);
765
- const record = this.circuits.get(key);
766
- if (!record) return;
767
- if (record.state === "half-open") {
768
- logInfo(`Circuit closed for ${key} after successful probe`);
769
- }
770
- record.state = "closed";
771
- record.failures = 0;
772
- record.halfOpenAttempts = 0;
773
- }
774
- recordFailure(endpoint, statusCode) {
775
- if (statusCode !== 0 && statusCode < 500 && statusCode !== 420 && statusCode !== 429) {
776
- return;
777
- }
778
- const key = this.getKey(endpoint);
779
- const record = this.getOrCreate(key);
780
- record.failures++;
781
- record.lastFailureTime = Date.now();
782
- if (record.state === "half-open") {
783
- record.state = "open";
784
- logWarn(
785
- `Circuit re-opened for ${key} after failed probe (${record.failures} failures)`
786
- );
787
- return;
788
- }
789
- if (record.failures >= this.failureThreshold) {
790
- record.state = "open";
791
- logWarn(
792
- `Circuit opened for ${key} after ${record.failures} consecutive failures`
793
- );
794
- }
795
- }
796
- getState(endpoint) {
797
- const key = this.getKey(endpoint);
798
- const record = this.circuits.get(key);
799
- if (!record) return "closed";
800
- if (record.state === "open") {
801
- const elapsed = Date.now() - record.lastFailureTime;
802
- if (elapsed >= this.resetTimeoutMs) {
803
- return "half-open";
804
- }
805
- }
806
- return record.state;
807
- }
808
- getStats() {
809
- const circuits = {};
810
- let openCircuits = 0;
811
- for (const [key, record] of this.circuits.entries()) {
812
- const state = this.getState(key);
813
- circuits[key] = { state, failures: record.failures };
814
- if (state === "open") openCircuits++;
815
- }
816
- return {
817
- totalCircuits: this.circuits.size,
818
- openCircuits,
819
- circuits
820
- };
821
- }
822
- reset(endpoint) {
823
- if (endpoint) {
824
- this.circuits.delete(this.getKey(endpoint));
825
- } else {
826
- this.circuits.clear();
827
- }
828
- }
829
- cleanup() {
830
- const now = Date.now();
831
- let cleaned = 0;
832
- for (const [key, record] of this.circuits.entries()) {
833
- if (record.state === "closed" && record.failures === 0 && record.lastFailureTime > 0 && now - record.lastFailureTime > this.staleThresholdMs) {
834
- this.circuits.delete(key);
835
- cleaned++;
836
- }
837
- }
838
- if (cleaned > 0) {
839
- logInfo(`Circuit breaker cleanup: removed ${cleaned} stale circuits`);
840
- }
841
- return cleaned;
842
- }
843
- getKeyStrategy() {
844
- return this.keyStrategy;
845
- }
846
- destroy() {
847
- if (this.cleanupTimer) {
848
- clearInterval(this.cleanupTimer);
849
- this.cleanupTimer = null;
850
- }
851
- this.circuits.clear();
852
- }
853
- shutdown() {
854
- this.destroy();
855
- }
856
- startCleanupTimer(intervalMs) {
857
- this.cleanupTimer = setInterval(() => {
858
- this.cleanup();
859
- }, intervalMs);
860
- this.cleanupTimer.unref();
861
- }
822
+ "GET:wars/{war_id}/killmails": 3600
862
823
  };
863
- var CircuitOpenError = class extends Error {
864
- endpoint;
865
- failures;
866
- retryAfterMs;
867
- constructor(endpoint, failures, retryAfterMs) {
868
- super(
869
- `Circuit breaker open for ${endpoint} after ${failures} failures (retry after ${Math.ceil(retryAfterMs / 1e3)}s)`
824
+
825
+ // src/core/requestPipeline/cachePolicy.ts
826
+ function lookupSpecTtl(method, templatePath) {
827
+ const normalized = templatePath.replace(/\/$/, "").replace(/\{(\w+)\}/g, (_, name) => `{${camelToSnake(name)}}`);
828
+ const key = `${method}:${normalized}`;
829
+ const seconds = esiCacheTtls[key];
830
+ return typeof seconds === "number" ? seconds * 1e3 : void 0;
831
+ }
832
+ function trySpecAwareCacheHit(client, url, method, templatePath, resolveCache2) {
833
+ if (method !== "GET" || !templatePath) return null;
834
+ const specTtlMs = lookupSpecTtl(method, templatePath);
835
+ if (!specTtlMs) return null;
836
+ const cache = resolveCache2(client);
837
+ if (!cache) return null;
838
+ const entry = cache.get(url);
839
+ if (!entry) return null;
840
+ const age = Date.now() - entry.timestamp;
841
+ if (age < specTtlMs) {
842
+ logDebug(
843
+ `Spec-aware cache hit for ${url} (age=${Math.round(age / 1e3)}s, ttl=${Math.round(specTtlMs / 1e3)}s)`
870
844
  );
871
- this.name = "CircuitOpenError";
872
- this.endpoint = endpoint;
873
- this.failures = failures;
874
- this.retryAfterMs = retryAfterMs;
845
+ return {
846
+ headers: entry.headers,
847
+ body: entry.data,
848
+ status: 200,
849
+ fromCache: true,
850
+ cacheHitType: "spec-ttl"
851
+ };
875
852
  }
876
- };
853
+ return null;
854
+ }
855
+ function tryStaleCacheResponse(client, url, parsed, resolveCache2) {
856
+ const cache = resolveCache2(client);
857
+ if (!cache) return null;
858
+ const cachedEntry = cache.get(url);
859
+ if (!cachedEntry) return null;
860
+ return {
861
+ headers: { ...cachedEntry.headers, ...parsed.raw },
862
+ body: cachedEntry.data,
863
+ status: 200,
864
+ fromCache: true,
865
+ stale: true,
866
+ cacheHitType: "stale-on-error"
867
+ };
868
+ }
869
+ function cacheResponse(client, url, method, endpoint, parsed, data, useETag, resolveCache2, templatePath) {
870
+ const cache = resolveCache2(client);
871
+ if (useETag && method === "GET" && cache && parsed.etag) {
872
+ const headerTtl = parseCacheControlTtl(parsed.raw);
873
+ const specTtlMs = templatePath ? lookupSpecTtl(method, templatePath) : void 0;
874
+ const ttl = specTtlMs ?? headerTtl;
875
+ cache.set(url, parsed.etag, data, parsed.raw, ttl);
876
+ const ttlInfo = ttl ? ` (ttl=${ttl}ms)` : "";
877
+ logDebug(`Cached response for ${url} with ETag ${parsed.etag}${ttlInfo}`);
878
+ }
879
+ if (method !== "GET" && cache) {
880
+ cache.deleteByPath(endpoint.split("?")[0]);
881
+ }
882
+ }
877
883
 
878
884
  // src/core/requestPipeline/statusHandling.ts
879
885
  var STATUS_MESSAGES = {
@@ -894,11 +900,11 @@ var STATUS_MESSAGES = {
894
900
  };
895
901
  function handleEarlyStatus(client, status, url, parsed, useETag, resolveCache2) {
896
902
  if (status === 201) {
897
- return { headers: parsed.raw, body: void 0 };
903
+ return { headers: parsed.raw, body: void 0, status: 201 };
898
904
  }
899
905
  if (status === 204) {
900
906
  logInfo(`No Content for endpoint: ${url}`);
901
- return { headers: parsed.raw, body: void 0 };
907
+ return { headers: parsed.raw, body: void 0, status: 204 };
902
908
  }
903
909
  if (status === 304) {
904
910
  const cache = resolveCache2(client);
@@ -909,6 +915,7 @@ function handleEarlyStatus(client, status, url, parsed, useETag, resolveCache2)
909
915
  return {
910
916
  headers: { ...cachedEntry.headers, ...parsed.raw },
911
917
  body: cachedEntry.data,
918
+ status: 304,
912
919
  fromCache: true,
913
920
  cacheHitType: "etag-304"
914
921
  };
@@ -934,7 +941,7 @@ function handleErrorResponse(client, response, url, parsed, useETag, resolveCach
934
941
  );
935
942
  if (staleResult) {
936
943
  logWarn(`${errorMessage} for ${url} \u2014 serving stale cache`);
937
- return staleResult;
944
+ return { ...staleResult, status: response.status };
938
945
  }
939
946
  }
940
947
  if (response.status === 420 || response.status === 429) {
@@ -959,18 +966,102 @@ function wrapError(error) {
959
966
  throw buildError(String(error), "ESIJS_ERROR");
960
967
  }
961
968
 
969
+ // src/core/util/retry.ts
970
+ function retryDelay(attempt, baseMs, maxMs) {
971
+ const exponential = baseMs * Math.pow(2, attempt);
972
+ const jitter = exponential * (0.75 + Math.random() * 0.5);
973
+ return Math.min(jitter, maxMs);
974
+ }
975
+
962
976
  // src/core/util/sleep.ts
963
977
  function sleep(ms) {
964
978
  return new Promise((resolve) => setTimeout(resolve, ms));
965
979
  }
966
980
 
981
+ // src/core/RetryStrategy.ts
982
+ var RetryStrategy = class {
983
+ maxRetries;
984
+ baseDelayMs;
985
+ maxDelayMs;
986
+ retryMutations;
987
+ constructor(config) {
988
+ this.maxRetries = config?.maxRetries ?? 0;
989
+ this.baseDelayMs = config?.baseDelayMs ?? 1e3;
990
+ this.maxDelayMs = config?.maxDelayMs ?? 3e4;
991
+ this.retryMutations = config?.retryMutations ?? false;
992
+ }
993
+ async execute(operation, context) {
994
+ const canRetryMethod = context.method === "GET" || this.retryMutations;
995
+ let lastError;
996
+ let refreshAttempted = false;
997
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
998
+ try {
999
+ return await operation();
1000
+ } catch (error) {
1001
+ if (error instanceof CircuitOpenError) {
1002
+ throw error;
1003
+ }
1004
+ if (!refreshAttempted && error instanceof EsiError && error.statusCode === 401 && context.requiresAuth && context.refreshToken) {
1005
+ refreshAttempted = true;
1006
+ logInfo("Received 401, attempting token refresh...");
1007
+ try {
1008
+ await context.refreshToken();
1009
+ } catch (refreshError) {
1010
+ if (refreshError instanceof EsiError || refreshError instanceof CircuitOpenError) {
1011
+ throw refreshError;
1012
+ }
1013
+ const msg = refreshError instanceof Error ? refreshError.message : String(refreshError);
1014
+ logError(`Token refresh failed: ${msg}`);
1015
+ throw buildError(
1016
+ `Token refresh failed: ${msg}`,
1017
+ "TOKEN_REFRESH_FAILED"
1018
+ );
1019
+ }
1020
+ logInfo("Token refreshed, retrying request");
1021
+ attempt--;
1022
+ continue;
1023
+ }
1024
+ if (error instanceof EsiError && error.retryable && canRetryMethod && attempt < this.maxRetries) {
1025
+ const delay = retryDelay(attempt, this.baseDelayMs, this.maxDelayMs);
1026
+ logWarn(
1027
+ `Request to ${context.endpoint} failed (${error.statusCode}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${this.maxRetries})`
1028
+ );
1029
+ await sleep(delay);
1030
+ lastError = error;
1031
+ continue;
1032
+ }
1033
+ throw error;
1034
+ }
1035
+ }
1036
+ throw lastError;
1037
+ }
1038
+ };
1039
+
1040
+ // src/core/requestPipeline/dependencies.ts
1041
+ function resolveCache(client) {
1042
+ return client.getCache();
1043
+ }
1044
+ function resolveRateLimiter(client) {
1045
+ const limiter = client.getRateLimiter();
1046
+ if (!limiter) {
1047
+ throw buildError(
1048
+ "No rate limiter configured on ApiClient. Set one via apiClient.setRateLimiter(new RateLimiter()).",
1049
+ "CONFIGURATION_ERROR"
1050
+ );
1051
+ }
1052
+ return limiter;
1053
+ }
1054
+ function resolveCircuitBreaker(client) {
1055
+ return client.getCircuitBreaker();
1056
+ }
1057
+ function resolveRetryStrategy(client) {
1058
+ return client.getRetryStrategy() ?? new RetryStrategy(client.getRetryConfig() ?? void 0);
1059
+ }
1060
+
967
1061
  // src/core/pagination/PaginationHandler.ts
968
1062
  var PaginationHandler = class {
969
1063
  static DEFAULT_OPTIONS = {
970
1064
  maxPages: 1e3,
971
- // Reasonable limit to prevent infinite loops
972
- maxRetries: 3,
973
- retryDelayMs: 1e3,
974
1065
  stopOnEmptyPage: true
975
1066
  };
976
1067
  /**
@@ -990,9 +1081,11 @@ var PaginationHandler = class {
990
1081
  try {
991
1082
  if (rateLimiter) await rateLimiter.checkRateLimit(templatePath, method);
992
1083
  const pageData = await this.fetchPageWithRetry(
1084
+ client,
993
1085
  endpoint,
1086
+ method,
1087
+ requiresAuth,
994
1088
  page,
995
- opts,
996
1089
  pageFetch
997
1090
  );
998
1091
  if (opts.stopOnEmptyPage && (!pageData || pageData.length === 0)) {
@@ -1016,36 +1109,35 @@ var PaginationHandler = class {
1016
1109
  return allData;
1017
1110
  }
1018
1111
  /**
1019
- * Fetch a single page with retry logic
1112
+ * Fetch a single page with retry logic.
1113
+ *
1114
+ * Delegates to the configured `IRetryStrategy` (exponential backoff
1115
+ * with jitter) instead of implementing a custom retry loop.
1020
1116
  */
1021
- static async fetchPageWithRetry(endpoint, page, options, pageFetch) {
1022
- let lastError = null;
1023
- for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
1024
- try {
1025
- return await this.fetchSinglePage(endpoint, page, pageFetch);
1026
- } catch (error) {
1027
- lastError = error;
1028
- logWarn(
1029
- `Attempt ${attempt}/${options.maxRetries} failed for page ${page}: ${error instanceof Error ? error.message : String(error)}`
1030
- );
1031
- if (attempt < options.maxRetries) {
1032
- await sleep(options.retryDelayMs * attempt);
1033
- }
1117
+ static async fetchPageWithRetry(client, endpoint, method, requiresAuth, page, pageFetch) {
1118
+ const retryStrategy = resolveRetryStrategy(client);
1119
+ const paginatedEndpoint = this.buildPaginatedEndpoint(endpoint, page);
1120
+ return retryStrategy.execute(
1121
+ () => {
1122
+ logInfo(`Fetching page ${page} via pipeline: ${paginatedEndpoint}`);
1123
+ return pageFetch(paginatedEndpoint);
1124
+ },
1125
+ {
1126
+ endpoint: paginatedEndpoint,
1127
+ method,
1128
+ requiresAuth,
1129
+ refreshToken: client.hasTokenProvider() ? () => client.refreshToken().then(() => {
1130
+ }) : void 0
1034
1131
  }
1035
- }
1036
- throw lastError || new Error(
1037
- `Failed to fetch page ${page} after ${options.maxRetries} attempts`
1038
1132
  );
1039
1133
  }
1040
1134
  /**
1041
- * Fetch a single page.
1135
+ * Build a paginated endpoint path.
1042
1136
  * Preserves any existing query params on the endpoint and appends page=N.
1043
1137
  */
1044
- static async fetchSinglePage(endpoint, page, pageFetch) {
1138
+ static buildPaginatedEndpoint(endpoint, page) {
1045
1139
  const separator = endpoint.includes("?") ? "&" : "?";
1046
- const paginatedEndpoint = `${endpoint}${separator}page=${page}`;
1047
- logInfo(`Fetching page ${page} via pipeline: ${paginatedEndpoint}`);
1048
- return pageFetch(paginatedEndpoint);
1140
+ return `${endpoint}${separator}page=${page}`;
1049
1141
  }
1050
1142
  };
1051
1143
 
@@ -1056,12 +1148,12 @@ function handleCursorPagination(parsed, data) {
1056
1148
  before: parsed.cursorBefore,
1057
1149
  after: parsed.cursorAfter
1058
1150
  };
1059
- return { headers: parsed.raw, body: data, cursors };
1151
+ return { headers: parsed.raw, body: data, status: 200, cursors };
1060
1152
  }
1061
1153
  async function handleOffsetPagination(client, endpoint, method, requiresAuth, parsed, data, body, url, useETag, pageFetch, resolveCache2, templatePath) {
1062
1154
  const totalPages = parsed.xPages;
1063
1155
  if (totalPages <= 1) {
1064
- return { headers: parsed.raw, body: data };
1156
+ return { headers: parsed.raw, body: data, status: 200 };
1065
1157
  }
1066
1158
  logInfo(
1067
1159
  `Found ${totalPages} pages, fetching additional pages with rate limiting...`
@@ -1078,8 +1170,7 @@ async function handleOffsetPagination(client, endpoint, method, requiresAuth, pa
1078
1170
  body,
1079
1171
  {
1080
1172
  maxPages: Math.min(totalPages, 1e3),
1081
- stopOnEmptyPage: true,
1082
- maxRetries: 3
1173
+ stopOnEmptyPage: true
1083
1174
  },
1084
1175
  pageFetch,
1085
1176
  templatePath
@@ -1095,7 +1186,7 @@ async function handleOffsetPagination(client, endpoint, method, requiresAuth, pa
1095
1186
  resolveCache2,
1096
1187
  templatePath
1097
1188
  );
1098
- return { headers: parsed.raw, body: allData };
1189
+ return { headers: parsed.raw, body: allData, status: 200 };
1099
1190
  } catch (paginationError) {
1100
1191
  const msg = paginationError instanceof Error ? paginationError.message : String(paginationError);
1101
1192
  logWarn(`Pagination failed for ${url}: ${msg}`);
@@ -1137,7 +1228,7 @@ async function applyResponseInterceptors(client, result, url, endpoint, method,
1137
1228
  url,
1138
1229
  endpoint,
1139
1230
  method,
1140
- status: 200,
1231
+ status: result.status ?? 200,
1141
1232
  headers: result.headers,
1142
1233
  body: result.body,
1143
1234
  durationMs: Date.now() - startTime,
@@ -1299,93 +1390,6 @@ async function fetchOnePage(client, endpoint, method, body, requiresAuth, useETa
1299
1390
  return { data, parsed, url };
1300
1391
  }
1301
1392
 
1302
- // src/core/util/retry.ts
1303
- function retryDelay(attempt, baseMs, maxMs) {
1304
- const exponential = baseMs * Math.pow(2, attempt);
1305
- const jitter = exponential * (0.75 + Math.random() * 0.5);
1306
- return Math.min(jitter, maxMs);
1307
- }
1308
-
1309
- // src/core/RetryStrategy.ts
1310
- var RetryStrategy = class {
1311
- maxRetries;
1312
- baseDelayMs;
1313
- maxDelayMs;
1314
- retryMutations;
1315
- constructor(config) {
1316
- this.maxRetries = config?.maxRetries ?? 0;
1317
- this.baseDelayMs = config?.baseDelayMs ?? 1e3;
1318
- this.maxDelayMs = config?.maxDelayMs ?? 3e4;
1319
- this.retryMutations = config?.retryMutations ?? false;
1320
- }
1321
- async execute(operation, context) {
1322
- const canRetryMethod = context.method === "GET" || this.retryMutations;
1323
- let lastError;
1324
- let refreshAttempted = false;
1325
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1326
- try {
1327
- return await operation();
1328
- } catch (error) {
1329
- if (error instanceof CircuitOpenError) {
1330
- throw error;
1331
- }
1332
- if (!refreshAttempted && error instanceof EsiError && error.statusCode === 401 && context.requiresAuth && context.refreshToken) {
1333
- refreshAttempted = true;
1334
- logInfo("Received 401, attempting token refresh...");
1335
- try {
1336
- await context.refreshToken();
1337
- } catch (refreshError) {
1338
- if (refreshError instanceof EsiError || refreshError instanceof CircuitOpenError) {
1339
- throw refreshError;
1340
- }
1341
- const msg = refreshError instanceof Error ? refreshError.message : String(refreshError);
1342
- logError(`Token refresh failed: ${msg}`);
1343
- throw buildError(
1344
- `Token refresh failed: ${msg}`,
1345
- "TOKEN_REFRESH_FAILED"
1346
- );
1347
- }
1348
- logInfo("Token refreshed, retrying request");
1349
- attempt--;
1350
- continue;
1351
- }
1352
- if (error instanceof EsiError && error.retryable && canRetryMethod && attempt < this.maxRetries) {
1353
- const delay = retryDelay(attempt, this.baseDelayMs, this.maxDelayMs);
1354
- logWarn(
1355
- `Request to ${context.endpoint} failed (${error.statusCode}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${this.maxRetries})`
1356
- );
1357
- await sleep(delay);
1358
- lastError = error;
1359
- continue;
1360
- }
1361
- throw error;
1362
- }
1363
- }
1364
- throw lastError;
1365
- }
1366
- };
1367
-
1368
- // src/core/requestPipeline/dependencies.ts
1369
- function resolveCache(client) {
1370
- return client.getCache();
1371
- }
1372
- function resolveRateLimiter(client) {
1373
- const limiter = client.getRateLimiter();
1374
- if (!limiter) {
1375
- throw buildError(
1376
- "No rate limiter configured on ApiClient. Set one via apiClient.setRateLimiter(new RateLimiter()).",
1377
- "CONFIGURATION_ERROR"
1378
- );
1379
- }
1380
- return limiter;
1381
- }
1382
- function resolveCircuitBreaker(client) {
1383
- return client.getCircuitBreaker();
1384
- }
1385
- function resolveRetryStrategy(client) {
1386
- return client.getRetryStrategy() ?? new RetryStrategy(client.getRetryConfig() ?? void 0);
1387
- }
1388
-
1389
1393
  // src/core/ApiRequestHandler.ts
1390
1394
  var executeRequest = async (client, endpoint, method, body, requiresAuth = false, useETag = true, requestTimeout, templatePath) => {
1391
1395
  const startTime = Date.now();
@@ -1422,7 +1426,7 @@ var executeRequest = async (client, endpoint, method, body, requiresAuth = false
1422
1426
  } catch {
1423
1427
  data2 = void 0;
1424
1428
  }
1425
- return finish({ headers: parsed.raw, body: data2 });
1429
+ return finish({ headers: parsed.raw, body: data2, status: 201 });
1426
1430
  }
1427
1431
  const earlyResult = handleEarlyStatus(
1428
1432
  client,
@@ -8988,6 +8992,12 @@ function configureApiClient(client, config) {
8988
8992
  client.setRetryConfig(config.retryConfig);
8989
8993
  } else if (config?.retryAttempts !== void 0) {
8990
8994
  client.setRetryConfig({ maxRetries: config.retryAttempts });
8995
+ } else {
8996
+ client.setRetryConfig({
8997
+ maxRetries: 3,
8998
+ baseDelayMs: 1e3,
8999
+ maxDelayMs: 3e4
9000
+ });
8991
9001
  }
8992
9002
  if (config?.retryStrategy) {
8993
9003
  client.setRetryStrategy(config.retryStrategy);
@@ -9912,6 +9922,7 @@ __export(schemas_exports, {
9912
9922
  fetchAllCursorPages,
9913
9923
  fetchPages,
9914
9924
  getDefaultClient,
9925
+ isCircuitOpen,
9915
9926
  isEsiError,
9916
9927
  isForbidden,
9917
9928
  isNotFound,