@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.mjs CHANGED
@@ -205,6 +205,216 @@ var ApiClient = class {
205
205
  }
206
206
  };
207
207
 
208
+ // src/core/logger/logger.ts
209
+ import pino from "pino";
210
+ var pinoLogger = pino({ level: process.env.ESI_LOG_LEVEL || "warn" });
211
+ var logger = {
212
+ info: (msg) => {
213
+ pinoLogger.info(msg);
214
+ },
215
+ warn: (msg) => {
216
+ pinoLogger.warn(msg);
217
+ },
218
+ error: (msg) => {
219
+ pinoLogger.error(msg);
220
+ },
221
+ debug: (msg) => {
222
+ pinoLogger.debug(msg);
223
+ }
224
+ };
225
+ var logger_default = logger;
226
+
227
+ // src/core/logger/loggerUtil.ts
228
+ var activeLogger = logger_default;
229
+ var setLogger = (customLogger) => {
230
+ activeLogger = customLogger;
231
+ };
232
+ var logInfo = (message) => {
233
+ activeLogger.info(message);
234
+ };
235
+ var logError = (message) => {
236
+ activeLogger.error(message);
237
+ };
238
+ var logWarn = (message) => {
239
+ activeLogger.warn(message);
240
+ };
241
+ var logDebug = (message) => {
242
+ activeLogger.debug(message);
243
+ };
244
+
245
+ // src/core/circuitBreaker/CircuitBreaker.ts
246
+ var CircuitBreaker = class {
247
+ circuits = /* @__PURE__ */ new Map();
248
+ failureThreshold;
249
+ resetTimeoutMs;
250
+ halfOpenMaxAttempts;
251
+ staleThresholdMs;
252
+ keyStrategy;
253
+ cleanupTimer = null;
254
+ constructor(config = {}) {
255
+ this.failureThreshold = config.failureThreshold ?? 5;
256
+ this.resetTimeoutMs = config.resetTimeoutMs ?? 3e4;
257
+ this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 1;
258
+ this.staleThresholdMs = config.staleThresholdMs ?? 36e5;
259
+ this.keyStrategy = config.keyStrategy ?? "resolved";
260
+ const cleanupIntervalMs = config.cleanupIntervalMs;
261
+ if (cleanupIntervalMs !== void 0 && cleanupIntervalMs > 0) {
262
+ this.startCleanupTimer(cleanupIntervalMs);
263
+ }
264
+ }
265
+ getKey(endpoint) {
266
+ return endpoint.split("?")[0];
267
+ }
268
+ getOrCreate(key) {
269
+ let record = this.circuits.get(key);
270
+ if (!record) {
271
+ record = {
272
+ state: "closed",
273
+ failures: 0,
274
+ lastFailureTime: 0,
275
+ halfOpenAttempts: 0
276
+ };
277
+ this.circuits.set(key, record);
278
+ }
279
+ return record;
280
+ }
281
+ checkCircuit(endpoint) {
282
+ const key = this.getKey(endpoint);
283
+ const record = this.getOrCreate(key);
284
+ if (record.state === "closed") return;
285
+ if (record.state === "open") {
286
+ const elapsed = Date.now() - record.lastFailureTime;
287
+ if (elapsed >= this.resetTimeoutMs) {
288
+ record.state = "half-open";
289
+ record.halfOpenAttempts = 0;
290
+ logInfo(`Circuit half-open for ${key}, allowing probe request`);
291
+ return;
292
+ }
293
+ const remainingMs = this.resetTimeoutMs - elapsed;
294
+ throw new CircuitOpenError(key, record.failures, remainingMs);
295
+ }
296
+ if (record.state === "half-open") {
297
+ if (record.halfOpenAttempts >= this.halfOpenMaxAttempts) {
298
+ throw new CircuitOpenError(key, record.failures, this.resetTimeoutMs);
299
+ }
300
+ record.halfOpenAttempts++;
301
+ }
302
+ }
303
+ recordSuccess(endpoint) {
304
+ const key = this.getKey(endpoint);
305
+ const record = this.circuits.get(key);
306
+ if (!record) return;
307
+ if (record.state === "half-open") {
308
+ logInfo(`Circuit closed for ${key} after successful probe`);
309
+ }
310
+ record.state = "closed";
311
+ record.failures = 0;
312
+ record.halfOpenAttempts = 0;
313
+ }
314
+ recordFailure(endpoint, statusCode) {
315
+ if (statusCode !== 0 && statusCode < 500 && statusCode !== 420 && statusCode !== 429) {
316
+ return;
317
+ }
318
+ const key = this.getKey(endpoint);
319
+ const record = this.getOrCreate(key);
320
+ record.failures++;
321
+ record.lastFailureTime = Date.now();
322
+ if (record.state === "half-open") {
323
+ record.state = "open";
324
+ logWarn(
325
+ `Circuit re-opened for ${key} after failed probe (${record.failures} failures)`
326
+ );
327
+ return;
328
+ }
329
+ if (record.failures >= this.failureThreshold) {
330
+ record.state = "open";
331
+ logWarn(
332
+ `Circuit opened for ${key} after ${record.failures} consecutive failures`
333
+ );
334
+ }
335
+ }
336
+ getState(endpoint) {
337
+ const key = this.getKey(endpoint);
338
+ const record = this.circuits.get(key);
339
+ if (!record) return "closed";
340
+ if (record.state === "open") {
341
+ const elapsed = Date.now() - record.lastFailureTime;
342
+ if (elapsed >= this.resetTimeoutMs) {
343
+ return "half-open";
344
+ }
345
+ }
346
+ return record.state;
347
+ }
348
+ getStats() {
349
+ const circuits = {};
350
+ let openCircuits = 0;
351
+ for (const [key, record] of this.circuits.entries()) {
352
+ const state = this.getState(key);
353
+ circuits[key] = { state, failures: record.failures };
354
+ if (state === "open") openCircuits++;
355
+ }
356
+ return {
357
+ totalCircuits: this.circuits.size,
358
+ openCircuits,
359
+ circuits
360
+ };
361
+ }
362
+ reset(endpoint) {
363
+ if (endpoint) {
364
+ this.circuits.delete(this.getKey(endpoint));
365
+ } else {
366
+ this.circuits.clear();
367
+ }
368
+ }
369
+ cleanup() {
370
+ const now = Date.now();
371
+ let cleaned = 0;
372
+ for (const [key, record] of this.circuits.entries()) {
373
+ if (record.state === "closed" && record.failures === 0 && record.lastFailureTime > 0 && now - record.lastFailureTime > this.staleThresholdMs) {
374
+ this.circuits.delete(key);
375
+ cleaned++;
376
+ }
377
+ }
378
+ if (cleaned > 0) {
379
+ logInfo(`Circuit breaker cleanup: removed ${cleaned} stale circuits`);
380
+ }
381
+ return cleaned;
382
+ }
383
+ getKeyStrategy() {
384
+ return this.keyStrategy;
385
+ }
386
+ destroy() {
387
+ if (this.cleanupTimer) {
388
+ clearInterval(this.cleanupTimer);
389
+ this.cleanupTimer = null;
390
+ }
391
+ this.circuits.clear();
392
+ }
393
+ shutdown() {
394
+ this.destroy();
395
+ }
396
+ startCleanupTimer(intervalMs) {
397
+ this.cleanupTimer = setInterval(() => {
398
+ this.cleanup();
399
+ }, intervalMs);
400
+ this.cleanupTimer.unref();
401
+ }
402
+ };
403
+ var CircuitOpenError = class extends Error {
404
+ endpoint;
405
+ failures;
406
+ retryAfterMs;
407
+ constructor(endpoint, failures, retryAfterMs) {
408
+ super(
409
+ `Circuit breaker open for ${endpoint} after ${failures} failures (retry after ${Math.ceil(retryAfterMs / 1e3)}s)`
410
+ );
411
+ this.name = "CircuitOpenError";
412
+ this.endpoint = endpoint;
413
+ this.failures = failures;
414
+ this.retryAfterMs = retryAfterMs;
415
+ }
416
+ };
417
+
208
418
  // src/core/util/error.ts
209
419
  var SENSITIVE_PARAMS = [
210
420
  "token",
@@ -299,6 +509,9 @@ function isTimeout(error) {
299
509
  function isRetryable(error) {
300
510
  return error instanceof EsiError && error.retryable;
301
511
  }
512
+ function isCircuitOpen(error) {
513
+ return error instanceof CircuitOpenError;
514
+ }
302
515
  var buildError = (message, type = "ERROR", url) => {
303
516
  const error = new Error(`[${type}] ${message}`);
304
517
  if (url) {
@@ -322,43 +535,6 @@ function isValidationError(error) {
322
535
  return error instanceof EsiValidationError;
323
536
  }
324
537
 
325
- // src/core/logger/logger.ts
326
- import pino from "pino";
327
- var pinoLogger = pino({ level: process.env.ESI_LOG_LEVEL || "warn" });
328
- var logger = {
329
- info: (msg) => {
330
- pinoLogger.info(msg);
331
- },
332
- warn: (msg) => {
333
- pinoLogger.warn(msg);
334
- },
335
- error: (msg) => {
336
- pinoLogger.error(msg);
337
- },
338
- debug: (msg) => {
339
- pinoLogger.debug(msg);
340
- }
341
- };
342
- var logger_default = logger;
343
-
344
- // src/core/logger/loggerUtil.ts
345
- var activeLogger = logger_default;
346
- var setLogger = (customLogger) => {
347
- activeLogger = customLogger;
348
- };
349
- var logInfo = (message) => {
350
- activeLogger.info(message);
351
- };
352
- var logError = (message) => {
353
- activeLogger.error(message);
354
- };
355
- var logWarn = (message) => {
356
- activeLogger.warn(message);
357
- };
358
- var logDebug = (message) => {
359
- activeLogger.debug(message);
360
- };
361
-
362
538
  // src/core/constants.ts
363
539
  var PACKAGE_NAME = "esi.ts";
364
540
  var PACKAGE_VERSION = "7.4.0";
@@ -539,238 +715,67 @@ var esiCacheTtls = {
539
715
  "GET:universe/system_kills": 3600,
540
716
  "GET:wars": 3600,
541
717
  "GET:wars/{war_id}": 3600,
542
- "GET:wars/{war_id}/killmails": 3600
543
- };
544
-
545
- // src/core/requestPipeline/cachePolicy.ts
546
- function lookupSpecTtl(method, templatePath) {
547
- const normalized = templatePath.replace(/\/$/, "").replace(/\{(\w+)\}/g, (_, name) => `{${camelToSnake(name)}}`);
548
- const key = `${method}:${normalized}`;
549
- const seconds = esiCacheTtls[key];
550
- return typeof seconds === "number" ? seconds * 1e3 : void 0;
551
- }
552
- function trySpecAwareCacheHit(client, url, method, templatePath, resolveCache2) {
553
- if (method !== "GET" || !templatePath) return null;
554
- const specTtlMs = lookupSpecTtl(method, templatePath);
555
- if (!specTtlMs) return null;
556
- const cache = resolveCache2(client);
557
- if (!cache) return null;
558
- const entry = cache.get(url);
559
- if (!entry) return null;
560
- const age = Date.now() - entry.timestamp;
561
- if (age < specTtlMs) {
562
- logDebug(
563
- `Spec-aware cache hit for ${url} (age=${Math.round(age / 1e3)}s, ttl=${Math.round(specTtlMs / 1e3)}s)`
564
- );
565
- return {
566
- headers: entry.headers,
567
- body: entry.data,
568
- fromCache: true,
569
- cacheHitType: "spec-ttl"
570
- };
571
- }
572
- return null;
573
- }
574
- function tryStaleCacheResponse(client, url, parsed, resolveCache2) {
575
- const cache = resolveCache2(client);
576
- if (!cache) return null;
577
- const cachedEntry = cache.get(url);
578
- if (!cachedEntry) return null;
579
- return {
580
- headers: { ...cachedEntry.headers, ...parsed.raw },
581
- body: cachedEntry.data,
582
- fromCache: true,
583
- stale: true,
584
- cacheHitType: "stale-on-error"
585
- };
586
- }
587
- function cacheResponse(client, url, method, endpoint, parsed, data, useETag, resolveCache2, templatePath) {
588
- const cache = resolveCache2(client);
589
- if (useETag && method === "GET" && cache && parsed.etag) {
590
- const headerTtl = parseCacheControlTtl(parsed.raw);
591
- const specTtlMs = templatePath ? lookupSpecTtl(method, templatePath) : void 0;
592
- const ttl = specTtlMs ?? headerTtl;
593
- cache.set(url, parsed.etag, data, parsed.raw, ttl);
594
- const ttlInfo = ttl ? ` (ttl=${ttl}ms)` : "";
595
- logDebug(`Cached response for ${url} with ETag ${parsed.etag}${ttlInfo}`);
596
- }
597
- if (method !== "GET" && cache) {
598
- cache.deleteByPath(endpoint.split("?")[0]);
599
- }
600
- }
601
-
602
- // src/core/circuitBreaker/CircuitBreaker.ts
603
- var CircuitBreaker = class {
604
- circuits = /* @__PURE__ */ new Map();
605
- failureThreshold;
606
- resetTimeoutMs;
607
- halfOpenMaxAttempts;
608
- staleThresholdMs;
609
- keyStrategy;
610
- cleanupTimer = null;
611
- constructor(config = {}) {
612
- this.failureThreshold = config.failureThreshold ?? 5;
613
- this.resetTimeoutMs = config.resetTimeoutMs ?? 3e4;
614
- this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 1;
615
- this.staleThresholdMs = config.staleThresholdMs ?? 36e5;
616
- this.keyStrategy = config.keyStrategy ?? "resolved";
617
- const cleanupIntervalMs = config.cleanupIntervalMs;
618
- if (cleanupIntervalMs !== void 0 && cleanupIntervalMs > 0) {
619
- this.startCleanupTimer(cleanupIntervalMs);
620
- }
621
- }
622
- getKey(endpoint) {
623
- return endpoint.split("?")[0];
624
- }
625
- getOrCreate(key) {
626
- let record = this.circuits.get(key);
627
- if (!record) {
628
- record = {
629
- state: "closed",
630
- failures: 0,
631
- lastFailureTime: 0,
632
- halfOpenAttempts: 0
633
- };
634
- this.circuits.set(key, record);
635
- }
636
- return record;
637
- }
638
- checkCircuit(endpoint) {
639
- const key = this.getKey(endpoint);
640
- const record = this.getOrCreate(key);
641
- if (record.state === "closed") return;
642
- if (record.state === "open") {
643
- const elapsed = Date.now() - record.lastFailureTime;
644
- if (elapsed >= this.resetTimeoutMs) {
645
- record.state = "half-open";
646
- record.halfOpenAttempts = 0;
647
- logInfo(`Circuit half-open for ${key}, allowing probe request`);
648
- return;
649
- }
650
- const remainingMs = this.resetTimeoutMs - elapsed;
651
- throw new CircuitOpenError(key, record.failures, remainingMs);
652
- }
653
- if (record.state === "half-open") {
654
- if (record.halfOpenAttempts >= this.halfOpenMaxAttempts) {
655
- throw new CircuitOpenError(key, record.failures, this.resetTimeoutMs);
656
- }
657
- record.halfOpenAttempts++;
658
- }
659
- }
660
- recordSuccess(endpoint) {
661
- const key = this.getKey(endpoint);
662
- const record = this.circuits.get(key);
663
- if (!record) return;
664
- if (record.state === "half-open") {
665
- logInfo(`Circuit closed for ${key} after successful probe`);
666
- }
667
- record.state = "closed";
668
- record.failures = 0;
669
- record.halfOpenAttempts = 0;
670
- }
671
- recordFailure(endpoint, statusCode) {
672
- if (statusCode !== 0 && statusCode < 500 && statusCode !== 420 && statusCode !== 429) {
673
- return;
674
- }
675
- const key = this.getKey(endpoint);
676
- const record = this.getOrCreate(key);
677
- record.failures++;
678
- record.lastFailureTime = Date.now();
679
- if (record.state === "half-open") {
680
- record.state = "open";
681
- logWarn(
682
- `Circuit re-opened for ${key} after failed probe (${record.failures} failures)`
683
- );
684
- return;
685
- }
686
- if (record.failures >= this.failureThreshold) {
687
- record.state = "open";
688
- logWarn(
689
- `Circuit opened for ${key} after ${record.failures} consecutive failures`
690
- );
691
- }
692
- }
693
- getState(endpoint) {
694
- const key = this.getKey(endpoint);
695
- const record = this.circuits.get(key);
696
- if (!record) return "closed";
697
- if (record.state === "open") {
698
- const elapsed = Date.now() - record.lastFailureTime;
699
- if (elapsed >= this.resetTimeoutMs) {
700
- return "half-open";
701
- }
702
- }
703
- return record.state;
704
- }
705
- getStats() {
706
- const circuits = {};
707
- let openCircuits = 0;
708
- for (const [key, record] of this.circuits.entries()) {
709
- const state = this.getState(key);
710
- circuits[key] = { state, failures: record.failures };
711
- if (state === "open") openCircuits++;
712
- }
713
- return {
714
- totalCircuits: this.circuits.size,
715
- openCircuits,
716
- circuits
717
- };
718
- }
719
- reset(endpoint) {
720
- if (endpoint) {
721
- this.circuits.delete(this.getKey(endpoint));
722
- } else {
723
- this.circuits.clear();
724
- }
725
- }
726
- cleanup() {
727
- const now = Date.now();
728
- let cleaned = 0;
729
- for (const [key, record] of this.circuits.entries()) {
730
- if (record.state === "closed" && record.failures === 0 && record.lastFailureTime > 0 && now - record.lastFailureTime > this.staleThresholdMs) {
731
- this.circuits.delete(key);
732
- cleaned++;
733
- }
734
- }
735
- if (cleaned > 0) {
736
- logInfo(`Circuit breaker cleanup: removed ${cleaned} stale circuits`);
737
- }
738
- return cleaned;
739
- }
740
- getKeyStrategy() {
741
- return this.keyStrategy;
742
- }
743
- destroy() {
744
- if (this.cleanupTimer) {
745
- clearInterval(this.cleanupTimer);
746
- this.cleanupTimer = null;
747
- }
748
- this.circuits.clear();
749
- }
750
- shutdown() {
751
- this.destroy();
752
- }
753
- startCleanupTimer(intervalMs) {
754
- this.cleanupTimer = setInterval(() => {
755
- this.cleanup();
756
- }, intervalMs);
757
- this.cleanupTimer.unref();
758
- }
718
+ "GET:wars/{war_id}/killmails": 3600
759
719
  };
760
- var CircuitOpenError = class extends Error {
761
- endpoint;
762
- failures;
763
- retryAfterMs;
764
- constructor(endpoint, failures, retryAfterMs) {
765
- super(
766
- `Circuit breaker open for ${endpoint} after ${failures} failures (retry after ${Math.ceil(retryAfterMs / 1e3)}s)`
720
+
721
+ // src/core/requestPipeline/cachePolicy.ts
722
+ function lookupSpecTtl(method, templatePath) {
723
+ const normalized = templatePath.replace(/\/$/, "").replace(/\{(\w+)\}/g, (_, name) => `{${camelToSnake(name)}}`);
724
+ const key = `${method}:${normalized}`;
725
+ const seconds = esiCacheTtls[key];
726
+ return typeof seconds === "number" ? seconds * 1e3 : void 0;
727
+ }
728
+ function trySpecAwareCacheHit(client, url, method, templatePath, resolveCache2) {
729
+ if (method !== "GET" || !templatePath) return null;
730
+ const specTtlMs = lookupSpecTtl(method, templatePath);
731
+ if (!specTtlMs) return null;
732
+ const cache = resolveCache2(client);
733
+ if (!cache) return null;
734
+ const entry = cache.get(url);
735
+ if (!entry) return null;
736
+ const age = Date.now() - entry.timestamp;
737
+ if (age < specTtlMs) {
738
+ logDebug(
739
+ `Spec-aware cache hit for ${url} (age=${Math.round(age / 1e3)}s, ttl=${Math.round(specTtlMs / 1e3)}s)`
767
740
  );
768
- this.name = "CircuitOpenError";
769
- this.endpoint = endpoint;
770
- this.failures = failures;
771
- this.retryAfterMs = retryAfterMs;
741
+ return {
742
+ headers: entry.headers,
743
+ body: entry.data,
744
+ status: 200,
745
+ fromCache: true,
746
+ cacheHitType: "spec-ttl"
747
+ };
772
748
  }
773
- };
749
+ return null;
750
+ }
751
+ function tryStaleCacheResponse(client, url, parsed, resolveCache2) {
752
+ const cache = resolveCache2(client);
753
+ if (!cache) return null;
754
+ const cachedEntry = cache.get(url);
755
+ if (!cachedEntry) return null;
756
+ return {
757
+ headers: { ...cachedEntry.headers, ...parsed.raw },
758
+ body: cachedEntry.data,
759
+ status: 200,
760
+ fromCache: true,
761
+ stale: true,
762
+ cacheHitType: "stale-on-error"
763
+ };
764
+ }
765
+ function cacheResponse(client, url, method, endpoint, parsed, data, useETag, resolveCache2, templatePath) {
766
+ const cache = resolveCache2(client);
767
+ if (useETag && method === "GET" && cache && parsed.etag) {
768
+ const headerTtl = parseCacheControlTtl(parsed.raw);
769
+ const specTtlMs = templatePath ? lookupSpecTtl(method, templatePath) : void 0;
770
+ const ttl = specTtlMs ?? headerTtl;
771
+ cache.set(url, parsed.etag, data, parsed.raw, ttl);
772
+ const ttlInfo = ttl ? ` (ttl=${ttl}ms)` : "";
773
+ logDebug(`Cached response for ${url} with ETag ${parsed.etag}${ttlInfo}`);
774
+ }
775
+ if (method !== "GET" && cache) {
776
+ cache.deleteByPath(endpoint.split("?")[0]);
777
+ }
778
+ }
774
779
 
775
780
  // src/core/requestPipeline/statusHandling.ts
776
781
  var STATUS_MESSAGES = {
@@ -791,11 +796,11 @@ var STATUS_MESSAGES = {
791
796
  };
792
797
  function handleEarlyStatus(client, status, url, parsed, useETag, resolveCache2) {
793
798
  if (status === 201) {
794
- return { headers: parsed.raw, body: void 0 };
799
+ return { headers: parsed.raw, body: void 0, status: 201 };
795
800
  }
796
801
  if (status === 204) {
797
802
  logInfo(`No Content for endpoint: ${url}`);
798
- return { headers: parsed.raw, body: void 0 };
803
+ return { headers: parsed.raw, body: void 0, status: 204 };
799
804
  }
800
805
  if (status === 304) {
801
806
  const cache = resolveCache2(client);
@@ -806,6 +811,7 @@ function handleEarlyStatus(client, status, url, parsed, useETag, resolveCache2)
806
811
  return {
807
812
  headers: { ...cachedEntry.headers, ...parsed.raw },
808
813
  body: cachedEntry.data,
814
+ status: 304,
809
815
  fromCache: true,
810
816
  cacheHitType: "etag-304"
811
817
  };
@@ -831,7 +837,7 @@ function handleErrorResponse(client, response, url, parsed, useETag, resolveCach
831
837
  );
832
838
  if (staleResult) {
833
839
  logWarn(`${errorMessage} for ${url} \u2014 serving stale cache`);
834
- return staleResult;
840
+ return { ...staleResult, status: response.status };
835
841
  }
836
842
  }
837
843
  if (response.status === 420 || response.status === 429) {
@@ -856,18 +862,102 @@ function wrapError(error) {
856
862
  throw buildError(String(error), "ESIJS_ERROR");
857
863
  }
858
864
 
865
+ // src/core/util/retry.ts
866
+ function retryDelay(attempt, baseMs, maxMs) {
867
+ const exponential = baseMs * Math.pow(2, attempt);
868
+ const jitter = exponential * (0.75 + Math.random() * 0.5);
869
+ return Math.min(jitter, maxMs);
870
+ }
871
+
859
872
  // src/core/util/sleep.ts
860
873
  function sleep(ms) {
861
874
  return new Promise((resolve) => setTimeout(resolve, ms));
862
875
  }
863
876
 
877
+ // src/core/RetryStrategy.ts
878
+ var RetryStrategy = class {
879
+ maxRetries;
880
+ baseDelayMs;
881
+ maxDelayMs;
882
+ retryMutations;
883
+ constructor(config) {
884
+ this.maxRetries = config?.maxRetries ?? 0;
885
+ this.baseDelayMs = config?.baseDelayMs ?? 1e3;
886
+ this.maxDelayMs = config?.maxDelayMs ?? 3e4;
887
+ this.retryMutations = config?.retryMutations ?? false;
888
+ }
889
+ async execute(operation, context) {
890
+ const canRetryMethod = context.method === "GET" || this.retryMutations;
891
+ let lastError;
892
+ let refreshAttempted = false;
893
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
894
+ try {
895
+ return await operation();
896
+ } catch (error) {
897
+ if (error instanceof CircuitOpenError) {
898
+ throw error;
899
+ }
900
+ if (!refreshAttempted && error instanceof EsiError && error.statusCode === 401 && context.requiresAuth && context.refreshToken) {
901
+ refreshAttempted = true;
902
+ logInfo("Received 401, attempting token refresh...");
903
+ try {
904
+ await context.refreshToken();
905
+ } catch (refreshError) {
906
+ if (refreshError instanceof EsiError || refreshError instanceof CircuitOpenError) {
907
+ throw refreshError;
908
+ }
909
+ const msg = refreshError instanceof Error ? refreshError.message : String(refreshError);
910
+ logError(`Token refresh failed: ${msg}`);
911
+ throw buildError(
912
+ `Token refresh failed: ${msg}`,
913
+ "TOKEN_REFRESH_FAILED"
914
+ );
915
+ }
916
+ logInfo("Token refreshed, retrying request");
917
+ attempt--;
918
+ continue;
919
+ }
920
+ if (error instanceof EsiError && error.retryable && canRetryMethod && attempt < this.maxRetries) {
921
+ const delay = retryDelay(attempt, this.baseDelayMs, this.maxDelayMs);
922
+ logWarn(
923
+ `Request to ${context.endpoint} failed (${error.statusCode}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${this.maxRetries})`
924
+ );
925
+ await sleep(delay);
926
+ lastError = error;
927
+ continue;
928
+ }
929
+ throw error;
930
+ }
931
+ }
932
+ throw lastError;
933
+ }
934
+ };
935
+
936
+ // src/core/requestPipeline/dependencies.ts
937
+ function resolveCache(client) {
938
+ return client.getCache();
939
+ }
940
+ function resolveRateLimiter(client) {
941
+ const limiter = client.getRateLimiter();
942
+ if (!limiter) {
943
+ throw buildError(
944
+ "No rate limiter configured on ApiClient. Set one via apiClient.setRateLimiter(new RateLimiter()).",
945
+ "CONFIGURATION_ERROR"
946
+ );
947
+ }
948
+ return limiter;
949
+ }
950
+ function resolveCircuitBreaker(client) {
951
+ return client.getCircuitBreaker();
952
+ }
953
+ function resolveRetryStrategy(client) {
954
+ return client.getRetryStrategy() ?? new RetryStrategy(client.getRetryConfig() ?? void 0);
955
+ }
956
+
864
957
  // src/core/pagination/PaginationHandler.ts
865
958
  var PaginationHandler = class {
866
959
  static DEFAULT_OPTIONS = {
867
960
  maxPages: 1e3,
868
- // Reasonable limit to prevent infinite loops
869
- maxRetries: 3,
870
- retryDelayMs: 1e3,
871
961
  stopOnEmptyPage: true
872
962
  };
873
963
  /**
@@ -887,9 +977,11 @@ var PaginationHandler = class {
887
977
  try {
888
978
  if (rateLimiter) await rateLimiter.checkRateLimit(templatePath, method);
889
979
  const pageData = await this.fetchPageWithRetry(
980
+ client,
890
981
  endpoint,
982
+ method,
983
+ requiresAuth,
891
984
  page,
892
- opts,
893
985
  pageFetch
894
986
  );
895
987
  if (opts.stopOnEmptyPage && (!pageData || pageData.length === 0)) {
@@ -913,36 +1005,35 @@ var PaginationHandler = class {
913
1005
  return allData;
914
1006
  }
915
1007
  /**
916
- * Fetch a single page with retry logic
1008
+ * Fetch a single page with retry logic.
1009
+ *
1010
+ * Delegates to the configured `IRetryStrategy` (exponential backoff
1011
+ * with jitter) instead of implementing a custom retry loop.
917
1012
  */
918
- static async fetchPageWithRetry(endpoint, page, options, pageFetch) {
919
- let lastError = null;
920
- for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
921
- try {
922
- return await this.fetchSinglePage(endpoint, page, pageFetch);
923
- } catch (error) {
924
- lastError = error;
925
- logWarn(
926
- `Attempt ${attempt}/${options.maxRetries} failed for page ${page}: ${error instanceof Error ? error.message : String(error)}`
927
- );
928
- if (attempt < options.maxRetries) {
929
- await sleep(options.retryDelayMs * attempt);
930
- }
1013
+ static async fetchPageWithRetry(client, endpoint, method, requiresAuth, page, pageFetch) {
1014
+ const retryStrategy = resolveRetryStrategy(client);
1015
+ const paginatedEndpoint = this.buildPaginatedEndpoint(endpoint, page);
1016
+ return retryStrategy.execute(
1017
+ () => {
1018
+ logInfo(`Fetching page ${page} via pipeline: ${paginatedEndpoint}`);
1019
+ return pageFetch(paginatedEndpoint);
1020
+ },
1021
+ {
1022
+ endpoint: paginatedEndpoint,
1023
+ method,
1024
+ requiresAuth,
1025
+ refreshToken: client.hasTokenProvider() ? () => client.refreshToken().then(() => {
1026
+ }) : void 0
931
1027
  }
932
- }
933
- throw lastError || new Error(
934
- `Failed to fetch page ${page} after ${options.maxRetries} attempts`
935
1028
  );
936
1029
  }
937
1030
  /**
938
- * Fetch a single page.
1031
+ * Build a paginated endpoint path.
939
1032
  * Preserves any existing query params on the endpoint and appends page=N.
940
1033
  */
941
- static async fetchSinglePage(endpoint, page, pageFetch) {
1034
+ static buildPaginatedEndpoint(endpoint, page) {
942
1035
  const separator = endpoint.includes("?") ? "&" : "?";
943
- const paginatedEndpoint = `${endpoint}${separator}page=${page}`;
944
- logInfo(`Fetching page ${page} via pipeline: ${paginatedEndpoint}`);
945
- return pageFetch(paginatedEndpoint);
1036
+ return `${endpoint}${separator}page=${page}`;
946
1037
  }
947
1038
  };
948
1039
 
@@ -953,12 +1044,12 @@ function handleCursorPagination(parsed, data) {
953
1044
  before: parsed.cursorBefore,
954
1045
  after: parsed.cursorAfter
955
1046
  };
956
- return { headers: parsed.raw, body: data, cursors };
1047
+ return { headers: parsed.raw, body: data, status: 200, cursors };
957
1048
  }
958
1049
  async function handleOffsetPagination(client, endpoint, method, requiresAuth, parsed, data, body, url, useETag, pageFetch, resolveCache2, templatePath) {
959
1050
  const totalPages = parsed.xPages;
960
1051
  if (totalPages <= 1) {
961
- return { headers: parsed.raw, body: data };
1052
+ return { headers: parsed.raw, body: data, status: 200 };
962
1053
  }
963
1054
  logInfo(
964
1055
  `Found ${totalPages} pages, fetching additional pages with rate limiting...`
@@ -975,8 +1066,7 @@ async function handleOffsetPagination(client, endpoint, method, requiresAuth, pa
975
1066
  body,
976
1067
  {
977
1068
  maxPages: Math.min(totalPages, 1e3),
978
- stopOnEmptyPage: true,
979
- maxRetries: 3
1069
+ stopOnEmptyPage: true
980
1070
  },
981
1071
  pageFetch,
982
1072
  templatePath
@@ -992,7 +1082,7 @@ async function handleOffsetPagination(client, endpoint, method, requiresAuth, pa
992
1082
  resolveCache2,
993
1083
  templatePath
994
1084
  );
995
- return { headers: parsed.raw, body: allData };
1085
+ return { headers: parsed.raw, body: allData, status: 200 };
996
1086
  } catch (paginationError) {
997
1087
  const msg = paginationError instanceof Error ? paginationError.message : String(paginationError);
998
1088
  logWarn(`Pagination failed for ${url}: ${msg}`);
@@ -1034,7 +1124,7 @@ async function applyResponseInterceptors(client, result, url, endpoint, method,
1034
1124
  url,
1035
1125
  endpoint,
1036
1126
  method,
1037
- status: 200,
1127
+ status: result.status ?? 200,
1038
1128
  headers: result.headers,
1039
1129
  body: result.body,
1040
1130
  durationMs: Date.now() - startTime,
@@ -1196,93 +1286,6 @@ async function fetchOnePage(client, endpoint, method, body, requiresAuth, useETa
1196
1286
  return { data, parsed, url };
1197
1287
  }
1198
1288
 
1199
- // src/core/util/retry.ts
1200
- function retryDelay(attempt, baseMs, maxMs) {
1201
- const exponential = baseMs * Math.pow(2, attempt);
1202
- const jitter = exponential * (0.75 + Math.random() * 0.5);
1203
- return Math.min(jitter, maxMs);
1204
- }
1205
-
1206
- // src/core/RetryStrategy.ts
1207
- var RetryStrategy = class {
1208
- maxRetries;
1209
- baseDelayMs;
1210
- maxDelayMs;
1211
- retryMutations;
1212
- constructor(config) {
1213
- this.maxRetries = config?.maxRetries ?? 0;
1214
- this.baseDelayMs = config?.baseDelayMs ?? 1e3;
1215
- this.maxDelayMs = config?.maxDelayMs ?? 3e4;
1216
- this.retryMutations = config?.retryMutations ?? false;
1217
- }
1218
- async execute(operation, context) {
1219
- const canRetryMethod = context.method === "GET" || this.retryMutations;
1220
- let lastError;
1221
- let refreshAttempted = false;
1222
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1223
- try {
1224
- return await operation();
1225
- } catch (error) {
1226
- if (error instanceof CircuitOpenError) {
1227
- throw error;
1228
- }
1229
- if (!refreshAttempted && error instanceof EsiError && error.statusCode === 401 && context.requiresAuth && context.refreshToken) {
1230
- refreshAttempted = true;
1231
- logInfo("Received 401, attempting token refresh...");
1232
- try {
1233
- await context.refreshToken();
1234
- } catch (refreshError) {
1235
- if (refreshError instanceof EsiError || refreshError instanceof CircuitOpenError) {
1236
- throw refreshError;
1237
- }
1238
- const msg = refreshError instanceof Error ? refreshError.message : String(refreshError);
1239
- logError(`Token refresh failed: ${msg}`);
1240
- throw buildError(
1241
- `Token refresh failed: ${msg}`,
1242
- "TOKEN_REFRESH_FAILED"
1243
- );
1244
- }
1245
- logInfo("Token refreshed, retrying request");
1246
- attempt--;
1247
- continue;
1248
- }
1249
- if (error instanceof EsiError && error.retryable && canRetryMethod && attempt < this.maxRetries) {
1250
- const delay = retryDelay(attempt, this.baseDelayMs, this.maxDelayMs);
1251
- logWarn(
1252
- `Request to ${context.endpoint} failed (${error.statusCode}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${this.maxRetries})`
1253
- );
1254
- await sleep(delay);
1255
- lastError = error;
1256
- continue;
1257
- }
1258
- throw error;
1259
- }
1260
- }
1261
- throw lastError;
1262
- }
1263
- };
1264
-
1265
- // src/core/requestPipeline/dependencies.ts
1266
- function resolveCache(client) {
1267
- return client.getCache();
1268
- }
1269
- function resolveRateLimiter(client) {
1270
- const limiter = client.getRateLimiter();
1271
- if (!limiter) {
1272
- throw buildError(
1273
- "No rate limiter configured on ApiClient. Set one via apiClient.setRateLimiter(new RateLimiter()).",
1274
- "CONFIGURATION_ERROR"
1275
- );
1276
- }
1277
- return limiter;
1278
- }
1279
- function resolveCircuitBreaker(client) {
1280
- return client.getCircuitBreaker();
1281
- }
1282
- function resolveRetryStrategy(client) {
1283
- return client.getRetryStrategy() ?? new RetryStrategy(client.getRetryConfig() ?? void 0);
1284
- }
1285
-
1286
1289
  // src/core/ApiRequestHandler.ts
1287
1290
  var executeRequest = async (client, endpoint, method, body, requiresAuth = false, useETag = true, requestTimeout, templatePath) => {
1288
1291
  const startTime = Date.now();
@@ -1319,7 +1322,7 @@ var executeRequest = async (client, endpoint, method, body, requiresAuth = false
1319
1322
  } catch {
1320
1323
  data2 = void 0;
1321
1324
  }
1322
- return finish({ headers: parsed.raw, body: data2 });
1325
+ return finish({ headers: parsed.raw, body: data2, status: 201 });
1323
1326
  }
1324
1327
  const earlyResult = handleEarlyStatus(
1325
1328
  client,
@@ -8885,6 +8888,12 @@ function configureApiClient(client, config) {
8885
8888
  client.setRetryConfig(config.retryConfig);
8886
8889
  } else if (config?.retryAttempts !== void 0) {
8887
8890
  client.setRetryConfig({ maxRetries: config.retryAttempts });
8891
+ } else {
8892
+ client.setRetryConfig({
8893
+ maxRetries: 3,
8894
+ baseDelayMs: 1e3,
8895
+ maxDelayMs: 3e4
8896
+ });
8888
8897
  }
8889
8898
  if (config?.retryStrategy) {
8890
8899
  client.setRetryStrategy(config.retryStrategy);
@@ -9808,6 +9817,7 @@ export {
9808
9817
  fetchAllCursorPages,
9809
9818
  fetchPages,
9810
9819
  getDefaultClient,
9820
+ isCircuitOpen,
9811
9821
  isEsiError,
9812
9822
  isForbidden,
9813
9823
  isNotFound,