@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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +26 -26
- package/README.md +986 -967
- package/dist/config/jest/globalSetup.d.ts.map +1 -1
- package/dist/config/jest/globalTeardown.d.ts.map +1 -1
- package/dist/core/configureApiClient.d.ts.map +1 -1
- package/dist/core/pagination/CursorPaginationHandler.d.ts +9 -4
- package/dist/core/pagination/CursorPaginationHandler.d.ts.map +1 -1
- package/dist/core/pagination/PaginationHandler.d.ts +6 -5
- package/dist/core/pagination/PaginationHandler.d.ts.map +1 -1
- package/dist/core/requestPipeline/cachePolicy.d.ts +1 -0
- package/dist/core/requestPipeline/cachePolicy.d.ts.map +1 -1
- package/dist/core/requestPipeline/paginationOrchestration.d.ts.map +1 -1
- package/dist/core/requestPipeline/statusHandling.d.ts.map +1 -1
- package/dist/core/util/error.d.ts +2 -0
- package/dist/core/util/error.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +196 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +146 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +400 -389
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +399 -389
- package/dist/index.mjs.map +1 -1
- package/dist/schemas/index.js +2087 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/index.mjs +1914 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +935 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +898 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/types/generated/esi-spec.generated.d.ts +3 -3
- package/dist/types/generated/esi-spec.generated.d.ts.map +1 -1
- package/package.json +253 -235
- package/dist/schemas/generated/alliance.generated.d.ts +0 -15
- package/dist/schemas/generated/alliance.generated.d.ts.map +0 -1
- package/dist/schemas/generated/assets.generated.d.ts +0 -272
- package/dist/schemas/generated/assets.generated.d.ts.map +0 -1
- package/dist/schemas/generated/calendar.generated.d.ts +0 -41
- package/dist/schemas/generated/calendar.generated.d.ts.map +0 -1
- package/dist/schemas/generated/character.generated.d.ts +0 -670
- package/dist/schemas/generated/character.generated.d.ts.map +0 -1
- package/dist/schemas/generated/clones.generated.d.ts +0 -23
- package/dist/schemas/generated/clones.generated.d.ts.map +0 -1
- package/dist/schemas/generated/contacts.generated.d.ts +0 -50
- package/dist/schemas/generated/contacts.generated.d.ts.map +0 -1
- package/dist/schemas/generated/contracts.generated.d.ts +0 -162
- package/dist/schemas/generated/contracts.generated.d.ts.map +0 -1
- package/dist/schemas/generated/corporation-projects.generated.d.ts +0 -89
- package/dist/schemas/generated/corporation-projects.generated.d.ts.map +0 -1
- package/dist/schemas/generated/corporation.generated.d.ts +0 -1515
- package/dist/schemas/generated/corporation.generated.d.ts.map +0 -1
- package/dist/schemas/generated/dogma.generated.d.ts +0 -57
- package/dist/schemas/generated/dogma.generated.d.ts.map +0 -1
- package/dist/schemas/generated/faction-warfare.generated.d.ts +0 -155
- package/dist/schemas/generated/faction-warfare.generated.d.ts.map +0 -1
- package/dist/schemas/generated/fittings.generated.d.ts +0 -57
- package/dist/schemas/generated/fittings.generated.d.ts.map +0 -1
- package/dist/schemas/generated/fleets.generated.d.ts +0 -45
- package/dist/schemas/generated/fleets.generated.d.ts.map +0 -1
- package/dist/schemas/generated/freelance-jobs.generated.d.ts +0 -173
- package/dist/schemas/generated/freelance-jobs.generated.d.ts.map +0 -1
- package/dist/schemas/generated/incursions.generated.d.ts +0 -16
- package/dist/schemas/generated/incursions.generated.d.ts.map +0 -1
- package/dist/schemas/generated/index.d.ts +0 -33
- package/dist/schemas/generated/index.d.ts.map +0 -1
- package/dist/schemas/generated/industry.generated.d.ts +0 -117
- package/dist/schemas/generated/industry.generated.d.ts.map +0 -1
- package/dist/schemas/generated/insurance.generated.d.ts +0 -10
- package/dist/schemas/generated/insurance.generated.d.ts.map +0 -1
- package/dist/schemas/generated/killmails.generated.d.ts +0 -55
- package/dist/schemas/generated/killmails.generated.d.ts.map +0 -1
- package/dist/schemas/generated/location.generated.d.ts +0 -18
- package/dist/schemas/generated/location.generated.d.ts.map +0 -1
- package/dist/schemas/generated/loyalty.generated.d.ts +0 -18
- package/dist/schemas/generated/loyalty.generated.d.ts.map +0 -1
- package/dist/schemas/generated/mail.generated.d.ts +0 -68
- package/dist/schemas/generated/mail.generated.d.ts.map +0 -1
- package/dist/schemas/generated/market.generated.d.ts +0 -201
- package/dist/schemas/generated/market.generated.d.ts.map +0 -1
- package/dist/schemas/generated/meta.generated.d.ts +0 -26
- package/dist/schemas/generated/meta.generated.d.ts.map +0 -1
- package/dist/schemas/generated/planetary-interaction.generated.d.ts +0 -90
- package/dist/schemas/generated/planetary-interaction.generated.d.ts.map +0 -1
- package/dist/schemas/generated/routes.generated.d.ts +0 -5
- package/dist/schemas/generated/routes.generated.d.ts.map +0 -1
- package/dist/schemas/generated/search.generated.d.ts +0 -15
- package/dist/schemas/generated/search.generated.d.ts.map +0 -1
- package/dist/schemas/generated/skills.generated.d.ts +0 -32
- package/dist/schemas/generated/skills.generated.d.ts.map +0 -1
- package/dist/schemas/generated/sovereignty.generated.d.ts +0 -37
- package/dist/schemas/generated/sovereignty.generated.d.ts.map +0 -1
- package/dist/schemas/generated/status.generated.d.ts +0 -8
- package/dist/schemas/generated/status.generated.d.ts.map +0 -1
- package/dist/schemas/generated/universe.generated.d.ts +0 -394
- package/dist/schemas/generated/universe.generated.d.ts.map +0 -1
- package/dist/schemas/generated/wallet.generated.d.ts +0 -411
- package/dist/schemas/generated/wallet.generated.d.ts.map +0 -1
- package/dist/schemas/generated/wars.generated.d.ts +0 -31
- 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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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,
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
*
|
|
1135
|
+
* Build a paginated endpoint path.
|
|
1042
1136
|
* Preserves any existing query params on the endpoint and appends page=N.
|
|
1043
1137
|
*/
|
|
1044
|
-
static
|
|
1138
|
+
static buildPaginatedEndpoint(endpoint, page) {
|
|
1045
1139
|
const separator = endpoint.includes("?") ? "&" : "?";
|
|
1046
|
-
|
|
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,
|