@lgriffin/esi.ts 10.2.0 → 11.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 +414 -32
- package/LICENSE +667 -19
- package/NOTICE +19 -0
- package/README.md +149 -902
- package/dist/EsiClient.d.mts +5 -36
- package/dist/EsiClient.d.ts +5 -36
- package/dist/EsiClient.d.ts.map +1 -1
- package/dist/EsiClientBuilder.d.mts +128 -1
- package/dist/EsiClientBuilder.d.ts +128 -1
- package/dist/EsiClientBuilder.d.ts.map +1 -1
- package/dist/adapters/PipelineTransport.d.ts +16 -0
- package/dist/adapters/PipelineTransport.d.ts.map +1 -0
- package/dist/auth/EsiTokenManager.d.mts +62 -21
- package/dist/auth/EsiTokenManager.d.ts +62 -21
- package/dist/auth/EsiTokenManager.d.ts.map +1 -1
- package/dist/auth/EveSsoClient.d.mts +9 -9
- package/dist/auth/EveSsoClient.d.ts +9 -9
- package/dist/auth/EveSsoClient.d.ts.map +1 -1
- package/dist/auth/errors.d.mts +2 -2
- package/dist/auth/errors.d.ts +2 -2
- package/dist/auth/errors.d.ts.map +1 -1
- package/dist/auth/jwt.d.mts +7 -7
- package/dist/auth/jwt.d.ts +7 -7
- package/dist/auth/jwt.d.ts.map +1 -1
- package/dist/auth/storage/FileTokenStorage.d.mts +1 -1
- package/dist/auth/storage/FileTokenStorage.d.ts +1 -1
- package/dist/auth/storage/FileTokenStorage.d.ts.map +1 -1
- package/dist/{chunk-BBSHKVPI.js → chunk-CM63VGGF.js} +1 -1
- package/dist/chunk-CM63VGGF.js.map +1 -0
- package/dist/chunk-DFRTIZK4.js +275 -0
- package/dist/chunk-DFRTIZK4.js.map +1 -0
- package/dist/{chunk-VZS32UQ3.mjs → chunk-HXS7ZVEL.mjs} +129 -28
- package/dist/chunk-HXS7ZVEL.mjs.map +1 -0
- package/dist/chunk-JNFQXEWZ.mjs +275 -0
- package/dist/chunk-JNFQXEWZ.mjs.map +1 -0
- package/dist/chunk-KIY3XA5Q.mjs +2980 -0
- package/dist/chunk-KIY3XA5Q.mjs.map +1 -0
- package/dist/{chunk-R7D7M2LQ.mjs → chunk-MGOE2YNH.mjs} +1 -1
- package/dist/chunk-MGOE2YNH.mjs.map +1 -0
- package/dist/{chunk-SAQBXXTM.js → chunk-MRKZ55XI.js} +130 -29
- package/dist/chunk-MRKZ55XI.js.map +1 -0
- package/dist/chunk-NKFIHGC6.js +2980 -0
- package/dist/chunk-NKFIHGC6.js.map +1 -0
- package/dist/{chunk-MSJHIJXA.js → chunk-S73PKM2Y.js} +20 -5
- package/dist/chunk-S73PKM2Y.js.map +1 -0
- package/dist/{chunk-3ZI5A37L.mjs → chunk-YUHGSPQ7.mjs} +19 -4
- package/dist/chunk-YUHGSPQ7.mjs.map +1 -0
- package/dist/client/identity.d.mts +20 -0
- package/dist/client/identity.d.ts +20 -0
- package/dist/client/identity.d.ts.map +1 -0
- package/dist/client/index.d.mts +11 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6607 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +6607 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/client/runtime.d.mts +67 -0
- package/dist/client/runtime.d.ts +67 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/clients/BaseEsiClient.d.ts.map +1 -1
- package/dist/clients/ClientRegistry.d.mts +45 -0
- package/dist/clients/ClientRegistry.d.ts +45 -0
- package/dist/clients/ClientRegistry.d.ts.map +1 -0
- package/dist/clients/CorporationsClient.d.mts +3 -3
- package/dist/clients/CorporationsClient.d.ts +3 -3
- package/dist/clients/CorporationsClient.d.ts.map +1 -1
- package/dist/clients/MailClient.d.mts +2 -2
- package/dist/clients/MailClient.d.ts +2 -2
- package/dist/clients/MailClient.d.ts.map +1 -1
- package/dist/clients/MetaClient.d.mts +3 -2
- package/dist/clients/MetaClient.d.ts +3 -2
- package/dist/clients/MetaClient.d.ts.map +1 -1
- package/dist/clients/MilitaryCampaignsClient.d.mts +14 -7
- package/dist/clients/MilitaryCampaignsClient.d.ts +14 -7
- package/dist/clients/MilitaryCampaignsClient.d.ts.map +1 -1
- package/dist/clients/RouteClient.d.mts +4 -4
- package/dist/clients/RouteClient.d.ts +4 -4
- package/dist/clients/RouteClient.d.ts.map +1 -1
- package/dist/clients/SkillsClient.d.mts +1 -1
- package/dist/clients/SkillsClient.d.ts +1 -1
- package/dist/clients/SkillsClient.d.ts.map +1 -1
- package/dist/clients/SkyhooksClient.d.mts +4 -3
- package/dist/clients/SkyhooksClient.d.ts +4 -3
- package/dist/clients/SkyhooksClient.d.ts.map +1 -1
- package/dist/core/ApiClient.d.mts +11 -0
- package/dist/core/ApiClient.d.ts +11 -0
- package/dist/core/ApiClient.d.ts.map +1 -1
- package/dist/core/ApiClientBuilder.d.ts.map +1 -1
- package/dist/core/ApiRequestHandler.d.ts +2 -1
- package/dist/core/ApiRequestHandler.d.ts.map +1 -1
- package/dist/core/BatchRequestHandler.d.mts +13 -2
- package/dist/core/BatchRequestHandler.d.ts +13 -2
- package/dist/core/BatchRequestHandler.d.ts.map +1 -1
- package/dist/core/EsiClientConfig.d.mts +60 -0
- package/dist/core/EsiClientConfig.d.ts +60 -0
- package/dist/core/EsiClientConfig.d.ts.map +1 -0
- package/dist/core/RetryStrategy.d.mts +3 -3
- package/dist/core/RetryStrategy.d.ts +3 -3
- package/dist/core/RetryStrategy.d.ts.map +1 -1
- package/dist/core/cache/ETagCacheManager.d.mts +9 -4
- package/dist/core/cache/ETagCacheManager.d.ts +9 -4
- package/dist/core/cache/ETagCacheManager.d.ts.map +1 -1
- package/dist/core/cache/cacheKey.d.ts +31 -6
- package/dist/core/cache/cacheKey.d.ts.map +1 -1
- package/dist/core/circuitBreaker/CircuitBreaker.d.mts +8 -12
- package/dist/core/circuitBreaker/CircuitBreaker.d.ts +8 -12
- package/dist/core/circuitBreaker/CircuitBreaker.d.ts.map +1 -1
- package/dist/core/clock.d.ts +8 -0
- package/dist/core/clock.d.ts.map +1 -0
- package/dist/core/configureApiClient.d.mts +1 -1
- package/dist/core/configureApiClient.d.ts +1 -1
- package/dist/core/configureApiClient.d.ts.map +1 -1
- package/dist/core/constants.d.ts +3 -3
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/endpoints/EndpointDefinition.d.mts +17 -0
- package/dist/core/endpoints/EndpointDefinition.d.ts +17 -0
- package/dist/core/endpoints/EndpointDefinition.d.ts.map +1 -1
- package/dist/core/endpoints/assetEndpoints.d.mts +12 -0
- package/dist/core/endpoints/assetEndpoints.d.ts +12 -0
- package/dist/core/endpoints/assetEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/characterEndpoints.d.mts +10 -0
- package/dist/core/endpoints/characterEndpoints.d.ts +10 -0
- package/dist/core/endpoints/characterEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/contactEndpoints.d.mts +1 -0
- package/dist/core/endpoints/contactEndpoints.d.ts +1 -0
- package/dist/core/endpoints/contactEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/contractEndpoints.d.mts +2 -0
- package/dist/core/endpoints/contractEndpoints.d.ts +2 -0
- package/dist/core/endpoints/contractEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/corporationEndpoints.d.mts +16 -3
- package/dist/core/endpoints/corporationEndpoints.d.ts +16 -3
- package/dist/core/endpoints/corporationEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/createClient.d.mts +2 -2
- package/dist/core/endpoints/createClient.d.ts +2 -2
- package/dist/core/endpoints/createClient.d.ts.map +1 -1
- package/dist/core/endpoints/esi-cache-ttls.generated.d.ts.map +1 -1
- package/dist/core/endpoints/esi-rate-limit-groups.generated.d.ts.map +1 -1
- package/dist/core/endpoints/esi-scopes.generated.d.ts.map +1 -1
- package/dist/core/endpoints/fittingEndpoints.d.mts +3 -0
- package/dist/core/endpoints/fittingEndpoints.d.ts +3 -0
- package/dist/core/endpoints/fittingEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/fleetEndpoints.d.mts +6 -0
- package/dist/core/endpoints/fleetEndpoints.d.ts +6 -0
- package/dist/core/endpoints/fleetEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/industryEndpoints.d.mts +2 -2
- package/dist/core/endpoints/industryEndpoints.d.ts +2 -2
- package/dist/core/endpoints/mailEndpoints.d.mts +2 -0
- package/dist/core/endpoints/mailEndpoints.d.ts +2 -0
- package/dist/core/endpoints/mailEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/mercenaryEndpoints.d.mts +1 -1
- package/dist/core/endpoints/mercenaryEndpoints.d.ts +1 -1
- package/dist/core/endpoints/metaEndpoints.d.mts +23 -8
- package/dist/core/endpoints/metaEndpoints.d.ts +23 -8
- package/dist/core/endpoints/metaEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/militaryCampaignEndpoints.d.mts +78 -49
- package/dist/core/endpoints/militaryCampaignEndpoints.d.ts +78 -49
- package/dist/core/endpoints/militaryCampaignEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/paragonHubEndpoints.d.mts +5 -5
- package/dist/core/endpoints/paragonHubEndpoints.d.ts +5 -5
- package/dist/core/endpoints/skyhookEndpoints.d.mts +10 -8
- package/dist/core/endpoints/skyhookEndpoints.d.ts +10 -8
- package/dist/core/endpoints/skyhookEndpoints.d.ts.map +1 -1
- package/dist/core/logger/DefaultLogger.d.mts +20 -1
- package/dist/core/logger/DefaultLogger.d.ts +20 -1
- package/dist/core/logger/DefaultLogger.d.ts.map +1 -1
- package/dist/core/logger/ILogger.d.mts +9 -0
- package/dist/core/logger/ILogger.d.ts +9 -0
- package/dist/core/logger/ILogger.d.ts.map +1 -1
- package/dist/core/logger/NoopLogger.d.ts.map +1 -1
- package/dist/core/logger/clientLog.d.ts.map +1 -1
- package/dist/core/logger/loggerUtil.d.ts.map +1 -1
- package/dist/core/logger/redactLog.d.ts +23 -0
- package/dist/core/logger/redactLog.d.ts.map +1 -0
- package/dist/core/logger/resolveLogger.d.ts +3 -3
- package/dist/core/logger/resolveLogger.d.ts.map +1 -1
- package/dist/core/pagination/AsyncPaginationIterator.d.ts.map +1 -1
- package/dist/core/pagination/CursorPaginationHandler.d.mts +2 -1
- package/dist/core/pagination/CursorPaginationHandler.d.ts +2 -1
- package/dist/core/pagination/CursorPaginationHandler.d.ts.map +1 -1
- package/dist/core/ports/CacheStore.d.mts +32 -0
- package/dist/core/ports/CacheStore.d.ts +32 -0
- package/dist/core/ports/CacheStore.d.ts.map +1 -0
- package/dist/core/ports/Clock.d.mts +13 -0
- package/dist/core/ports/Clock.d.ts +13 -0
- package/dist/core/ports/Clock.d.ts.map +1 -0
- package/dist/core/ports/HttpTransport.d.mts +7 -0
- package/dist/core/ports/HttpTransport.d.ts +7 -0
- package/dist/core/ports/HttpTransport.d.ts.map +1 -0
- package/dist/core/ports/Identity.d.mts +31 -0
- package/dist/core/ports/Identity.d.ts +31 -0
- package/dist/core/ports/Identity.d.ts.map +1 -0
- package/dist/core/ports/Logger.d.mts +17 -0
- package/dist/core/ports/Logger.d.ts +17 -0
- package/dist/core/ports/Logger.d.ts.map +1 -0
- package/dist/core/ports/OperationTransport.d.mts +42 -0
- package/dist/core/ports/OperationTransport.d.ts +42 -0
- package/dist/core/ports/OperationTransport.d.ts.map +1 -0
- package/dist/core/ports/TokenProvider.d.mts +7 -0
- package/dist/core/ports/TokenProvider.d.ts +7 -0
- package/dist/core/ports/TokenProvider.d.ts.map +1 -0
- package/dist/core/ports/index.d.mts +14 -0
- package/dist/core/ports/index.d.ts +14 -0
- package/dist/core/ports/index.d.ts.map +1 -0
- package/dist/core/rateLimiter/RateLimiter.d.mts +11 -4
- package/dist/core/rateLimiter/RateLimiter.d.ts +11 -4
- package/dist/core/rateLimiter/RateLimiter.d.ts.map +1 -1
- package/dist/core/requestPipeline/cachePolicy.d.ts +5 -3
- package/dist/core/requestPipeline/cachePolicy.d.ts.map +1 -1
- package/dist/core/requestPipeline/fetchExecution.d.ts +3 -1
- package/dist/core/requestPipeline/fetchExecution.d.ts.map +1 -1
- package/dist/core/requestPipeline/headers.d.ts +1 -1
- package/dist/core/requestPipeline/headers.d.ts.map +1 -1
- package/dist/core/requestPipeline/index.d.ts +3 -5
- package/dist/core/requestPipeline/index.d.ts.map +1 -1
- package/dist/core/requestPipeline/paginationOrchestration.d.ts +1 -1
- package/dist/core/requestPipeline/paginationOrchestration.d.ts.map +1 -1
- package/dist/core/requestPipeline/statusHandling.d.ts +3 -3
- package/dist/core/requestPipeline/statusHandling.d.ts.map +1 -1
- package/dist/core/util/callerIdentity.d.ts +10 -0
- package/dist/core/util/callerIdentity.d.ts.map +1 -0
- package/dist/core/util/concurrency.d.ts +2 -2
- package/dist/core/util/concurrency.d.ts.map +1 -1
- package/dist/core/util/error.d.mts +92 -3
- package/dist/core/util/error.d.ts +92 -3
- package/dist/core/util/error.d.ts.map +1 -1
- package/dist/core/util/retry.d.mts +4 -4
- package/dist/core/util/retry.d.ts +4 -4
- package/dist/core/util/retry.d.ts.map +1 -1
- package/dist/core/util/validation.d.ts +6 -0
- package/dist/core/util/validation.d.ts.map +1 -1
- package/dist/errors.d.mts +2 -3
- package/dist/errors.d.ts +2 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +29 -3
- package/dist/errors.js.map +1 -1
- package/dist/errors.mjs +28 -2
- package/dist/generated/operations.generated.d.mts +9379 -0
- package/dist/generated/operations.generated.d.ts +9379 -0
- package/dist/generated/operations.generated.d.ts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +699 -2865
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +359 -2525
- package/dist/index.mjs.map +1 -1
- package/dist/schemas/character.d.mts +5 -0
- package/dist/schemas/character.d.ts +5 -0
- package/dist/schemas/character.d.ts.map +1 -1
- package/dist/schemas/contacts.d.mts +2 -0
- package/dist/schemas/contacts.d.ts +2 -0
- package/dist/schemas/contacts.d.ts.map +1 -1
- package/dist/schemas/corporation.d.mts +24 -3
- package/dist/schemas/corporation.d.ts +24 -3
- package/dist/schemas/corporation.d.ts.map +1 -1
- package/dist/schemas/fittings.d.mts +4 -0
- package/dist/schemas/fittings.d.ts +4 -0
- package/dist/schemas/fittings.d.ts.map +1 -1
- package/dist/schemas/fleet.d.mts +8 -0
- package/dist/schemas/fleet.d.ts +8 -0
- package/dist/schemas/fleet.d.ts.map +1 -1
- package/dist/schemas/index.js +24 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/index.mjs +23 -1
- package/dist/schemas/industry.d.mts +2 -2
- package/dist/schemas/industry.d.ts +2 -2
- package/dist/schemas/mail.d.mts +4 -0
- package/dist/schemas/mail.d.ts +4 -0
- package/dist/schemas/mail.d.ts.map +1 -1
- package/dist/schemas/mercenary.d.mts +1 -1
- package/dist/schemas/mercenary.d.ts +1 -1
- package/dist/schemas/meta.d.mts +18 -9
- package/dist/schemas/meta.d.ts +18 -9
- package/dist/schemas/meta.d.ts.map +1 -1
- package/dist/schemas/military-campaigns.d.mts +54 -10
- package/dist/schemas/military-campaigns.d.ts +54 -10
- package/dist/schemas/military-campaigns.d.ts.map +1 -1
- package/dist/schemas/paragon-hub.d.mts +4 -4
- package/dist/schemas/paragon-hub.d.ts +4 -4
- package/dist/schemas/skyhooks.d.mts +18 -6
- package/dist/schemas/skyhooks.d.ts +18 -6
- package/dist/schemas/skyhooks.d.ts.map +1 -1
- package/dist/sde/clock.d.ts +11 -0
- package/dist/sde/clock.d.ts.map +1 -0
- package/dist/sde/domain/characters/schemas.d.ts +149 -0
- package/dist/sde/domain/characters/schemas.d.ts.map +1 -0
- package/dist/sde/domain/characters/types.d.mts +163 -0
- package/dist/sde/domain/characters/types.d.ts +163 -0
- package/dist/sde/domain/characters/types.d.ts.map +1 -0
- package/dist/sde/domain/content/schemas.d.ts +84 -0
- package/dist/sde/domain/content/schemas.d.ts.map +1 -0
- package/dist/sde/domain/content/types.d.mts +91 -0
- package/dist/sde/domain/content/types.d.ts +91 -0
- package/dist/sde/domain/content/types.d.ts.map +1 -0
- package/dist/sde/domain/corporations/schemas.d.ts +113 -0
- package/dist/sde/domain/corporations/schemas.d.ts.map +1 -0
- package/dist/sde/domain/corporations/types.d.mts +118 -0
- package/dist/sde/domain/corporations/types.d.ts +118 -0
- package/dist/sde/domain/corporations/types.d.ts.map +1 -0
- package/dist/sde/domain/dogma/schemas.d.ts +76 -0
- package/dist/sde/domain/dogma/schemas.d.ts.map +1 -0
- package/dist/sde/domain/dogma/types.d.mts +80 -0
- package/dist/sde/domain/dogma/types.d.ts +80 -0
- package/dist/sde/domain/dogma/types.d.ts.map +1 -0
- package/dist/sde/domain/industry/schemas.d.ts +204 -0
- package/dist/sde/domain/industry/schemas.d.ts.map +1 -0
- package/dist/sde/domain/industry/types.d.mts +88 -0
- package/dist/sde/domain/industry/types.d.ts +88 -0
- package/dist/sde/domain/industry/types.d.ts.map +1 -0
- package/dist/sde/domain/market/schemas.d.ts +24 -0
- package/dist/sde/domain/market/schemas.d.ts.map +1 -0
- package/dist/sde/domain/market/types.d.mts +25 -0
- package/dist/sde/domain/market/types.d.ts +25 -0
- package/dist/sde/domain/market/types.d.ts.map +1 -0
- package/dist/sde/domain/schemas.d.ts +15 -0
- package/dist/sde/domain/schemas.d.ts.map +1 -0
- package/dist/sde/domain/skins/schemas.d.ts +109 -0
- package/dist/sde/domain/skins/schemas.d.ts.map +1 -0
- package/dist/sde/domain/skins/types.d.mts +121 -0
- package/dist/sde/domain/skins/types.d.ts +121 -0
- package/dist/sde/domain/skins/types.d.ts.map +1 -0
- package/dist/sde/domain/types/schemas.d.ts +163 -0
- package/dist/sde/domain/types/schemas.d.ts.map +1 -0
- package/dist/sde/domain/types/types.d.mts +182 -0
- package/dist/sde/domain/types/types.d.ts +182 -0
- package/dist/sde/domain/types/types.d.ts.map +1 -0
- package/dist/sde/domain/types.d.mts +16 -0
- package/dist/sde/domain/types.d.ts +16 -0
- package/dist/sde/domain/types.d.ts.map +1 -0
- package/dist/sde/domain/ui/schemas.d.ts +42 -0
- package/dist/sde/domain/ui/schemas.d.ts.map +1 -0
- package/dist/sde/domain/ui/types.d.mts +45 -0
- package/dist/sde/domain/ui/types.d.ts +45 -0
- package/dist/sde/domain/ui/types.d.ts.map +1 -0
- package/dist/sde/domain/universe/schemas.d.ts +239 -0
- package/dist/sde/domain/universe/schemas.d.ts.map +1 -0
- package/dist/sde/domain/universe/types.d.mts +206 -0
- package/dist/sde/domain/universe/types.d.ts +206 -0
- package/dist/sde/domain/universe/types.d.ts.map +1 -0
- package/dist/sde/domain/version/schemas.d.ts +11 -0
- package/dist/sde/domain/version/schemas.d.ts.map +1 -0
- package/dist/sde/errors.d.mts +1 -1
- package/dist/sde/errors.d.ts +1 -1
- package/dist/sde/errors.d.ts.map +1 -1
- package/dist/sde/index.d.mts +7 -6
- package/dist/sde/index.d.ts +7 -6
- package/dist/sde/index.d.ts.map +1 -1
- package/dist/sde/index.js +31 -21
- package/dist/sde/index.js.map +1 -1
- package/dist/sde/index.mjs +26 -16
- package/dist/sde/index.mjs.map +1 -1
- package/dist/sde/ingestion/SdeDatabaseBuilder.d.ts +11 -0
- package/dist/sde/ingestion/SdeDatabaseBuilder.d.ts.map +1 -1
- package/dist/sde/ingestion/SdeDownloader.d.ts +2 -2
- package/dist/sde/ingestion/SdeDownloader.d.ts.map +1 -1
- package/dist/sde/memory.d.mts +5 -5
- package/dist/sde/memory.d.ts +5 -5
- package/dist/sde/memory.d.ts.map +1 -1
- package/dist/sde/memory.js +2 -2
- package/dist/sde/memory.mjs +1 -1
- package/dist/sde/{IStaticDataProvider.d.mts → ports/IStaticDataProvider.d.mts} +9 -2
- package/dist/sde/{IStaticDataProvider.d.ts → ports/IStaticDataProvider.d.ts} +9 -2
- package/dist/sde/ports/IStaticDataProvider.d.ts.map +1 -0
- package/dist/sde/{MemorySdeProvider.d.mts → providers/memory/MemorySdeProvider.d.mts} +3 -3
- package/dist/sde/{MemorySdeProvider.d.ts → providers/memory/MemorySdeProvider.d.ts} +3 -3
- package/dist/sde/providers/memory/MemorySdeProvider.d.ts.map +1 -0
- package/dist/sde/providers/order.d.ts +10 -0
- package/dist/sde/providers/order.d.ts.map +1 -0
- package/dist/sde/{SdeDataProvider.d.mts → providers/yaml/SdeDataProvider.d.mts} +14 -5
- package/dist/sde/{SdeDataProvider.d.ts → providers/yaml/SdeDataProvider.d.ts} +14 -5
- package/dist/sde/providers/yaml/SdeDataProvider.d.ts.map +1 -0
- package/dist/sde/{SdeTestDataFactory.d.mts → testing/SdeTestDataFactory.d.mts} +3 -3
- package/dist/sde/{SdeTestDataFactory.d.ts → testing/SdeTestDataFactory.d.ts} +3 -3
- package/dist/sde/testing/SdeTestDataFactory.d.ts.map +1 -0
- package/dist/sde/version.d.mts +1 -1
- package/dist/sde/version.d.ts +1 -1
- package/dist/sde/version.d.ts.map +1 -1
- package/dist/testing/TestDataFactory.d.mts +21 -3
- package/dist/testing/TestDataFactory.d.ts +21 -3
- package/dist/testing/TestDataFactory.d.ts.map +1 -1
- package/dist/testing/index.d.mts +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +157 -10
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/index.mjs +155 -8
- package/dist/testing/index.mjs.map +1 -1
- package/dist/testing/mockTransport.d.mts +87 -0
- package/dist/testing/mockTransport.d.ts +87 -0
- package/dist/testing/mockTransport.d.ts.map +1 -0
- package/dist/types/generated/esi-spec.generated.d.mts +230 -10
- package/dist/types/generated/esi-spec.generated.d.ts +230 -10
- package/dist/types/generated/esi-spec.generated.d.ts.map +1 -1
- package/dist/types/military-campaigns.d.mts +4 -1
- package/dist/types/military-campaigns.d.ts +4 -1
- package/dist/types/military-campaigns.d.ts.map +1 -1
- package/dist/types/skyhooks.d.mts +2 -1
- package/dist/types/skyhooks.d.ts +2 -1
- package/dist/types/skyhooks.d.ts.map +1 -1
- package/package.json +167 -131
- package/dist/chunk-3ZI5A37L.mjs.map +0 -1
- package/dist/chunk-BBSHKVPI.js.map +0 -1
- package/dist/chunk-D5L5HZVF.mjs +0 -417
- package/dist/chunk-D5L5HZVF.mjs.map +0 -1
- package/dist/chunk-MSJHIJXA.js.map +0 -1
- package/dist/chunk-R7D7M2LQ.mjs.map +0 -1
- package/dist/chunk-SAQBXXTM.js.map +0 -1
- package/dist/chunk-VZS32UQ3.mjs.map +0 -1
- package/dist/chunk-XIB2OXEZ.js +0 -417
- package/dist/chunk-XIB2OXEZ.js.map +0 -1
- package/dist/config/jest/globalSetup.d.ts +0 -2
- package/dist/config/jest/globalSetup.d.ts.map +0 -1
- package/dist/config/jest/globalTeardown.d.ts +0 -2
- package/dist/config/jest/globalTeardown.d.ts.map +0 -1
- package/dist/config/jest/jest.setup.d.ts +0 -3
- package/dist/config/jest/jest.setup.d.ts.map +0 -1
- package/dist/core/ClientRegistry.d.mts +0 -45
- package/dist/core/ClientRegistry.d.ts +0 -45
- package/dist/core/ClientRegistry.d.ts.map +0 -1
- package/dist/core/logger/logger.d.ts +0 -12
- package/dist/core/logger/logger.d.ts.map +0 -1
- package/dist/sde/IStaticDataProvider.d.ts.map +0 -1
- package/dist/sde/MemorySdeProvider.d.ts.map +0 -1
- package/dist/sde/SdeDataProvider.d.ts.map +0 -1
- package/dist/sde/SdeTestDataFactory.d.ts.map +0 -1
- package/dist/sde/schemas.d.ts +0 -1121
- package/dist/sde/schemas.d.ts.map +0 -1
- package/dist/sde/types.d.mts +0 -1039
- package/dist/sde/types.d.ts +0 -1039
- package/dist/sde/types.d.ts.map +0 -1
|
@@ -0,0 +1,2980 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircuitOpenError,
|
|
3
|
+
EsiConfigurationError,
|
|
4
|
+
EsiError,
|
|
5
|
+
EsiNetworkError,
|
|
6
|
+
EsiPaginationError,
|
|
7
|
+
EsiParseError,
|
|
8
|
+
EsiTokenRefreshError,
|
|
9
|
+
EsiValidationError,
|
|
10
|
+
SENSITIVE_QUERY_PATTERN,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
isSensitiveQueryParam,
|
|
13
|
+
toEsiError
|
|
14
|
+
} from "./chunk-JNFQXEWZ.mjs";
|
|
15
|
+
|
|
16
|
+
// src/core/middleware/Middleware.ts
|
|
17
|
+
var MiddlewareManager = class {
|
|
18
|
+
requestInterceptors = [];
|
|
19
|
+
responseInterceptors = [];
|
|
20
|
+
addRequestInterceptor(fn) {
|
|
21
|
+
this.requestInterceptors.push(fn);
|
|
22
|
+
return () => {
|
|
23
|
+
this.requestInterceptors = this.requestInterceptors.filter(
|
|
24
|
+
(i) => i !== fn
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
addResponseInterceptor(fn) {
|
|
29
|
+
this.responseInterceptors.push(fn);
|
|
30
|
+
return () => {
|
|
31
|
+
this.responseInterceptors = this.responseInterceptors.filter(
|
|
32
|
+
(i) => i !== fn
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async applyRequestInterceptors(context) {
|
|
37
|
+
let ctx = context;
|
|
38
|
+
for (const interceptor of this.requestInterceptors) {
|
|
39
|
+
ctx = await interceptor(ctx);
|
|
40
|
+
}
|
|
41
|
+
return ctx;
|
|
42
|
+
}
|
|
43
|
+
async applyResponseInterceptors(context) {
|
|
44
|
+
let ctx = context;
|
|
45
|
+
for (const interceptor of this.responseInterceptors) {
|
|
46
|
+
ctx = await interceptor(ctx);
|
|
47
|
+
}
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
50
|
+
hasInterceptors() {
|
|
51
|
+
return this.requestInterceptors.length > 0 || this.responseInterceptors.length > 0;
|
|
52
|
+
}
|
|
53
|
+
clear() {
|
|
54
|
+
this.requestInterceptors = [];
|
|
55
|
+
this.responseInterceptors = [];
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/core/util/validation.ts
|
|
60
|
+
var UNSAFE_PATH_CHARS = /[/\\?#@!$&'()*+,;=<>{}|^`]/;
|
|
61
|
+
function validatePathParam(paramName, value) {
|
|
62
|
+
const str = value === null || value === void 0 ? "" : `${value}`;
|
|
63
|
+
if (str === "") {
|
|
64
|
+
throw new EsiConfigurationError(
|
|
65
|
+
"VALIDATION_ERROR",
|
|
66
|
+
`Path parameter '${paramName}' must not be empty`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (UNSAFE_PATH_CHARS.test(str)) {
|
|
70
|
+
throw new EsiConfigurationError(
|
|
71
|
+
"VALIDATION_ERROR",
|
|
72
|
+
`Path parameter '${paramName}' contains invalid characters`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (str === "." || str === "..") {
|
|
76
|
+
throw new EsiConfigurationError(
|
|
77
|
+
"VALIDATION_ERROR",
|
|
78
|
+
`Path parameter '${paramName}' must not be a dot segment`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
82
|
+
throw new EsiConfigurationError(
|
|
83
|
+
"VALIDATION_ERROR",
|
|
84
|
+
`Path parameter '${paramName}' must be a finite number`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return str;
|
|
88
|
+
}
|
|
89
|
+
var ALLOWED_ESI_HOSTS = ["esi.evetech.net"];
|
|
90
|
+
function validateBaseUrl(url, allowCustomHost) {
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = new URL(url);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new EsiConfigurationError(
|
|
96
|
+
"VALIDATION_ERROR",
|
|
97
|
+
`Invalid base URL: ${url}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (parsed.protocol !== "https:") {
|
|
101
|
+
throw new EsiConfigurationError(
|
|
102
|
+
"VALIDATION_ERROR",
|
|
103
|
+
"Base URL must use HTTPS protocol"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!allowCustomHost && !ALLOWED_ESI_HOSTS.includes(parsed.hostname)) {
|
|
107
|
+
throw new EsiConfigurationError(
|
|
108
|
+
"VALIDATION_ERROR",
|
|
109
|
+
`Base URL host '${parsed.hostname}' is not in the allowlist. Allowed hosts: ${ALLOWED_ESI_HOSTS.join(", ")}. Set unsafeAllowCustomHost to bypass this check.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return url.replace(/\/$/, "");
|
|
113
|
+
}
|
|
114
|
+
function validateQueryParam(paramName, value) {
|
|
115
|
+
if (value === null || value === void 0) {
|
|
116
|
+
throw new EsiConfigurationError(
|
|
117
|
+
"VALIDATION_ERROR",
|
|
118
|
+
`Query parameter '${paramName}' must not be null or undefined`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const str = `${value}`;
|
|
122
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
123
|
+
throw new EsiConfigurationError(
|
|
124
|
+
"VALIDATION_ERROR",
|
|
125
|
+
`Query parameter '${paramName}' must be a finite number`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (str.length > 2e3) {
|
|
129
|
+
throw new EsiConfigurationError(
|
|
130
|
+
"VALIDATION_ERROR",
|
|
131
|
+
`Query parameter '${paramName}' exceeds maximum length`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return str;
|
|
135
|
+
}
|
|
136
|
+
var HEADER_VALUE = /^[\t\x20-\x7e]+$/;
|
|
137
|
+
function validateHeaderOption(option, value) {
|
|
138
|
+
if (!HEADER_VALUE.test(value)) {
|
|
139
|
+
throw new EsiConfigurationError(
|
|
140
|
+
"VALIDATION_ERROR",
|
|
141
|
+
`${option} must be printable ASCII with no line breaks or control characters`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/core/ApiClient.ts
|
|
148
|
+
var ApiClient = class {
|
|
149
|
+
constructor(clientId, link, accessToken) {
|
|
150
|
+
this.clientId = clientId;
|
|
151
|
+
this.link = link;
|
|
152
|
+
this.accessToken = accessToken;
|
|
153
|
+
this.link = this.link.replace(/\/$/, "");
|
|
154
|
+
}
|
|
155
|
+
clientId;
|
|
156
|
+
link;
|
|
157
|
+
accessToken;
|
|
158
|
+
datasource;
|
|
159
|
+
tokenProvider;
|
|
160
|
+
refreshInFlight;
|
|
161
|
+
middleware = new MiddlewareManager();
|
|
162
|
+
cache = null;
|
|
163
|
+
rateLimiter = null;
|
|
164
|
+
circuitBreaker = null;
|
|
165
|
+
timeout = 3e4;
|
|
166
|
+
deduplicator = null;
|
|
167
|
+
retryConfig = null;
|
|
168
|
+
retryStrategy = null;
|
|
169
|
+
validateResponse = true;
|
|
170
|
+
validateRequest = false;
|
|
171
|
+
language;
|
|
172
|
+
tenant;
|
|
173
|
+
userAgent;
|
|
174
|
+
compatibilityDate;
|
|
175
|
+
fetchFn = null;
|
|
176
|
+
logger = null;
|
|
177
|
+
getTimeout() {
|
|
178
|
+
return this.timeout;
|
|
179
|
+
}
|
|
180
|
+
setTimeout(timeout) {
|
|
181
|
+
this.timeout = timeout;
|
|
182
|
+
}
|
|
183
|
+
getCache() {
|
|
184
|
+
return this.cache;
|
|
185
|
+
}
|
|
186
|
+
setCache(cache) {
|
|
187
|
+
this.cache = cache;
|
|
188
|
+
}
|
|
189
|
+
getRateLimiter() {
|
|
190
|
+
return this.rateLimiter;
|
|
191
|
+
}
|
|
192
|
+
setRateLimiter(limiter) {
|
|
193
|
+
this.rateLimiter = limiter;
|
|
194
|
+
}
|
|
195
|
+
getCircuitBreaker() {
|
|
196
|
+
return this.circuitBreaker;
|
|
197
|
+
}
|
|
198
|
+
setCircuitBreaker(cb) {
|
|
199
|
+
this.circuitBreaker = cb;
|
|
200
|
+
}
|
|
201
|
+
getDeduplicator() {
|
|
202
|
+
return this.deduplicator;
|
|
203
|
+
}
|
|
204
|
+
setDeduplicator(dedup) {
|
|
205
|
+
this.deduplicator = dedup;
|
|
206
|
+
}
|
|
207
|
+
getRetryConfig() {
|
|
208
|
+
return this.retryConfig;
|
|
209
|
+
}
|
|
210
|
+
setRetryConfig(config) {
|
|
211
|
+
this.retryConfig = config;
|
|
212
|
+
}
|
|
213
|
+
getRetryStrategy() {
|
|
214
|
+
return this.retryStrategy;
|
|
215
|
+
}
|
|
216
|
+
setRetryStrategy(strategy) {
|
|
217
|
+
this.retryStrategy = strategy;
|
|
218
|
+
}
|
|
219
|
+
getValidateResponse() {
|
|
220
|
+
return this.validateResponse;
|
|
221
|
+
}
|
|
222
|
+
setValidateResponse(validate) {
|
|
223
|
+
this.validateResponse = validate;
|
|
224
|
+
}
|
|
225
|
+
getValidateRequest() {
|
|
226
|
+
return this.validateRequest;
|
|
227
|
+
}
|
|
228
|
+
setValidateRequest(validate) {
|
|
229
|
+
this.validateRequest = validate;
|
|
230
|
+
}
|
|
231
|
+
getFetch() {
|
|
232
|
+
return this.fetchFn ?? globalThis.fetch;
|
|
233
|
+
}
|
|
234
|
+
setFetch(fn) {
|
|
235
|
+
this.fetchFn = fn;
|
|
236
|
+
}
|
|
237
|
+
getMiddleware() {
|
|
238
|
+
return this.middleware;
|
|
239
|
+
}
|
|
240
|
+
addRequestInterceptor(fn) {
|
|
241
|
+
return this.middleware.addRequestInterceptor(fn);
|
|
242
|
+
}
|
|
243
|
+
addResponseInterceptor(fn) {
|
|
244
|
+
return this.middleware.addResponseInterceptor(fn);
|
|
245
|
+
}
|
|
246
|
+
getAuthorizationHeader() {
|
|
247
|
+
return this.accessToken ? `Bearer ${this.accessToken}` : void 0;
|
|
248
|
+
}
|
|
249
|
+
getLink() {
|
|
250
|
+
return this.link;
|
|
251
|
+
}
|
|
252
|
+
getClientId() {
|
|
253
|
+
return this.clientId;
|
|
254
|
+
}
|
|
255
|
+
getDatasource() {
|
|
256
|
+
return this.datasource;
|
|
257
|
+
}
|
|
258
|
+
setDatasource(datasource) {
|
|
259
|
+
this.datasource = datasource;
|
|
260
|
+
}
|
|
261
|
+
getLanguage() {
|
|
262
|
+
return this.language;
|
|
263
|
+
}
|
|
264
|
+
setLanguage(language) {
|
|
265
|
+
this.language = language;
|
|
266
|
+
}
|
|
267
|
+
/** The `X-Tenant` every request names, or undefined for ESI's default. */
|
|
268
|
+
getTenant() {
|
|
269
|
+
return this.tenant;
|
|
270
|
+
}
|
|
271
|
+
/** @throws VALIDATION_ERROR when the value is not printable ASCII. */
|
|
272
|
+
setTenant(tenant) {
|
|
273
|
+
this.tenant = tenant === void 0 ? void 0 : validateHeaderOption("tenant", tenant);
|
|
274
|
+
}
|
|
275
|
+
/** The application's own user agent, sent ahead of the library's. */
|
|
276
|
+
getUserAgent() {
|
|
277
|
+
return this.userAgent;
|
|
278
|
+
}
|
|
279
|
+
/** @throws VALIDATION_ERROR when the value is not printable ASCII. */
|
|
280
|
+
setUserAgent(userAgent) {
|
|
281
|
+
this.userAgent = userAgent === void 0 ? void 0 : validateHeaderOption("userAgent", userAgent);
|
|
282
|
+
}
|
|
283
|
+
getCompatibilityDate() {
|
|
284
|
+
return this.compatibilityDate;
|
|
285
|
+
}
|
|
286
|
+
setCompatibilityDate(date) {
|
|
287
|
+
this.compatibilityDate = date;
|
|
288
|
+
}
|
|
289
|
+
setAccessToken(token) {
|
|
290
|
+
this.accessToken = token;
|
|
291
|
+
}
|
|
292
|
+
setTokenProvider(provider) {
|
|
293
|
+
this.tokenProvider = provider;
|
|
294
|
+
}
|
|
295
|
+
getLogger() {
|
|
296
|
+
return this.logger;
|
|
297
|
+
}
|
|
298
|
+
setLogger(logger) {
|
|
299
|
+
this.logger = logger;
|
|
300
|
+
}
|
|
301
|
+
hasTokenProvider() {
|
|
302
|
+
return this.tokenProvider !== void 0;
|
|
303
|
+
}
|
|
304
|
+
toJSON() {
|
|
305
|
+
return {
|
|
306
|
+
link: this.link,
|
|
307
|
+
datasource: this.datasource,
|
|
308
|
+
timeout: this.timeout,
|
|
309
|
+
hasCache: this.cache !== null,
|
|
310
|
+
hasRateLimiter: this.rateLimiter !== null,
|
|
311
|
+
hasCircuitBreaker: this.circuitBreaker !== null,
|
|
312
|
+
hasDeduplicator: this.deduplicator !== null,
|
|
313
|
+
hasTokenProvider: this.tokenProvider !== void 0,
|
|
314
|
+
validateResponse: this.validateResponse,
|
|
315
|
+
validateRequest: this.validateRequest,
|
|
316
|
+
language: this.language,
|
|
317
|
+
compatibilityDate: this.compatibilityDate
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async refreshToken() {
|
|
321
|
+
if (!this.tokenProvider) {
|
|
322
|
+
throw new Error("No token provider configured");
|
|
323
|
+
}
|
|
324
|
+
if (this.refreshInFlight) {
|
|
325
|
+
return this.refreshInFlight;
|
|
326
|
+
}
|
|
327
|
+
this.refreshInFlight = this.tokenProvider().then(
|
|
328
|
+
(token) => {
|
|
329
|
+
this.accessToken = token;
|
|
330
|
+
this.refreshInFlight = void 0;
|
|
331
|
+
return token;
|
|
332
|
+
},
|
|
333
|
+
(err) => {
|
|
334
|
+
this.refreshInFlight = void 0;
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
return this.refreshInFlight;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/core/logger/DefaultLogger.ts
|
|
343
|
+
import pino from "pino";
|
|
344
|
+
function createDefaultLogger(level) {
|
|
345
|
+
return buildDefaultLogger(level);
|
|
346
|
+
}
|
|
347
|
+
function buildDefaultLogger(level) {
|
|
348
|
+
const resolved = level || process.env.ESI_LOG_LEVEL || "warn";
|
|
349
|
+
return adaptPino(pino({ level: resolved }));
|
|
350
|
+
}
|
|
351
|
+
function toPinoLogger(p) {
|
|
352
|
+
return adaptPino(p);
|
|
353
|
+
}
|
|
354
|
+
function adaptPino(p) {
|
|
355
|
+
const emit3 = (level) => (message, context) => context ? p[level](context, message) : p[level](message);
|
|
356
|
+
const logger = {
|
|
357
|
+
fatal: emit3("fatal"),
|
|
358
|
+
error: emit3("error"),
|
|
359
|
+
warn: emit3("warn"),
|
|
360
|
+
info: emit3("info"),
|
|
361
|
+
debug: emit3("debug"),
|
|
362
|
+
trace: emit3("trace")
|
|
363
|
+
};
|
|
364
|
+
const values = p.levels?.values;
|
|
365
|
+
if (values && typeof p.levelVal === "number") {
|
|
366
|
+
logger.isLevelEnabled = (level) => (values[level] ?? Infinity) >= (p.levelVal ?? -Infinity);
|
|
367
|
+
} else if (typeof p.isLevelEnabled === "function") {
|
|
368
|
+
logger.isLevelEnabled = (level) => p.isLevelEnabled?.(level) ?? true;
|
|
369
|
+
}
|
|
370
|
+
return logger;
|
|
371
|
+
}
|
|
372
|
+
var LEVELS = [
|
|
373
|
+
"fatal",
|
|
374
|
+
"error",
|
|
375
|
+
"warn",
|
|
376
|
+
"info",
|
|
377
|
+
"debug",
|
|
378
|
+
"trace"
|
|
379
|
+
];
|
|
380
|
+
function createLazyDefaultLogger() {
|
|
381
|
+
const lazy = {};
|
|
382
|
+
let built;
|
|
383
|
+
const materialise = () => {
|
|
384
|
+
if (built) return built;
|
|
385
|
+
const real = buildDefaultLogger();
|
|
386
|
+
built = real;
|
|
387
|
+
for (const level of LEVELS) lazy[level] = real[level];
|
|
388
|
+
lazy.isLevelEnabled = real.isLevelEnabled ?? (() => true);
|
|
389
|
+
return real;
|
|
390
|
+
};
|
|
391
|
+
for (const level of LEVELS) {
|
|
392
|
+
lazy[level] = (message, context) => materialise()[level](message, context);
|
|
393
|
+
}
|
|
394
|
+
lazy.isLevelEnabled = (level) => materialise().isLevelEnabled?.(level) ?? true;
|
|
395
|
+
return lazy;
|
|
396
|
+
}
|
|
397
|
+
var defaultLogger = createLazyDefaultLogger();
|
|
398
|
+
|
|
399
|
+
// src/core/logger/redactLog.ts
|
|
400
|
+
var QUERY_PAIR = /([?&])([^=&#?\s]+)=([^&#\s,;"'`<>()[\]{}]*)/g;
|
|
401
|
+
var REDACTED = "%5BREDACTED%5D";
|
|
402
|
+
function redactLogText(text) {
|
|
403
|
+
if (!text.includes("?") || !SENSITIVE_QUERY_PATTERN.test(text)) return text;
|
|
404
|
+
return text.replace(
|
|
405
|
+
QUERY_PAIR,
|
|
406
|
+
(pair, separator, name, value) => value !== "" && isSensitiveQueryParam(name) ? `${separator}${name}=${REDACTED}` : pair
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
function redactLogContext(context) {
|
|
410
|
+
if (!context) return void 0;
|
|
411
|
+
const keys = Object.keys(context);
|
|
412
|
+
if (keys.length === 0) return void 0;
|
|
413
|
+
for (const key of keys) {
|
|
414
|
+
if (mayCarrySecret(context[key])) return redactValues(context, keys);
|
|
415
|
+
}
|
|
416
|
+
return context;
|
|
417
|
+
}
|
|
418
|
+
function mayCarrySecret(value) {
|
|
419
|
+
return typeof value === "string" && value.includes("?") && SENSITIVE_QUERY_PATTERN.test(value);
|
|
420
|
+
}
|
|
421
|
+
function redactValues(context, keys) {
|
|
422
|
+
const redacted = {};
|
|
423
|
+
for (const key of keys) {
|
|
424
|
+
const value = context[key];
|
|
425
|
+
redacted[key] = mayCarrySecret(value) ? redactLogText(value) : value;
|
|
426
|
+
}
|
|
427
|
+
return redacted;
|
|
428
|
+
}
|
|
429
|
+
function redactingLogger(logger) {
|
|
430
|
+
const at = (level) => (message, context) => {
|
|
431
|
+
if (logger.isLevelEnabled?.(level) === false) return;
|
|
432
|
+
const safe = redactLogContext(context);
|
|
433
|
+
if (safe === void 0) logger[level](redactLogText(message));
|
|
434
|
+
else logger[level](redactLogText(message), safe);
|
|
435
|
+
};
|
|
436
|
+
return {
|
|
437
|
+
fatal: at("fatal"),
|
|
438
|
+
error: at("error"),
|
|
439
|
+
warn: at("warn"),
|
|
440
|
+
info: at("info"),
|
|
441
|
+
debug: at("debug"),
|
|
442
|
+
trace: at("trace")
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/core/logger/loggerUtil.ts
|
|
447
|
+
var activeLogger = defaultLogger;
|
|
448
|
+
var setLogger = (customLogger) => {
|
|
449
|
+
activeLogger = customLogger;
|
|
450
|
+
};
|
|
451
|
+
var getLogger = () => activeLogger;
|
|
452
|
+
var emit = (level) => (message, context) => {
|
|
453
|
+
if (activeLogger.isLevelEnabled?.(level) === false) return;
|
|
454
|
+
activeLogger[level](redactLogText(message), redactLogContext(context));
|
|
455
|
+
};
|
|
456
|
+
var logFatal = emit("fatal");
|
|
457
|
+
var logError = emit("error");
|
|
458
|
+
var logWarn = emit("warn");
|
|
459
|
+
var logInfo = emit("info");
|
|
460
|
+
var logDebug = emit("debug");
|
|
461
|
+
var logTrace = emit("trace");
|
|
462
|
+
|
|
463
|
+
// src/core/logger/resolveLogger.ts
|
|
464
|
+
function resolveLogger(client) {
|
|
465
|
+
return client?.getLogger?.() ?? getLogger();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/core/logger/clientLog.ts
|
|
469
|
+
var emit2 = (level) => (client, message, context) => {
|
|
470
|
+
const logger = resolveLogger(client);
|
|
471
|
+
if (logger.isLevelEnabled?.(level) === false) return;
|
|
472
|
+
logger[level](redactLogText(message), redactLogContext(context));
|
|
473
|
+
};
|
|
474
|
+
var logFatal2 = emit2("fatal");
|
|
475
|
+
var logError2 = emit2("error");
|
|
476
|
+
var logWarn2 = emit2("warn");
|
|
477
|
+
var logInfo2 = emit2("info");
|
|
478
|
+
var logDebug2 = emit2("debug");
|
|
479
|
+
var logTrace2 = emit2("trace");
|
|
480
|
+
|
|
481
|
+
// src/core/circuitBreaker/CircuitBreaker.ts
|
|
482
|
+
var CircuitBreaker = class {
|
|
483
|
+
circuits = /* @__PURE__ */ new Map();
|
|
484
|
+
failureThreshold;
|
|
485
|
+
resetTimeoutMs;
|
|
486
|
+
halfOpenMaxAttempts;
|
|
487
|
+
staleThresholdMs;
|
|
488
|
+
keyStrategy;
|
|
489
|
+
cleanupTimer = null;
|
|
490
|
+
client = null;
|
|
491
|
+
setClient(client) {
|
|
492
|
+
this.client = client;
|
|
493
|
+
}
|
|
494
|
+
constructor(config = {}) {
|
|
495
|
+
this.failureThreshold = config.failureThreshold ?? 5;
|
|
496
|
+
this.resetTimeoutMs = config.resetTimeoutMs ?? 3e4;
|
|
497
|
+
this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 1;
|
|
498
|
+
this.staleThresholdMs = config.staleThresholdMs ?? 36e5;
|
|
499
|
+
this.keyStrategy = config.keyStrategy ?? "resolved";
|
|
500
|
+
const cleanupIntervalMs = config.cleanupIntervalMs;
|
|
501
|
+
if (cleanupIntervalMs !== void 0 && cleanupIntervalMs > 0) {
|
|
502
|
+
this.startCleanupTimer(cleanupIntervalMs);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
getKey(endpoint) {
|
|
506
|
+
return endpoint.split("?")[0];
|
|
507
|
+
}
|
|
508
|
+
getOrCreate(key) {
|
|
509
|
+
let record = this.circuits.get(key);
|
|
510
|
+
if (!record) {
|
|
511
|
+
record = {
|
|
512
|
+
state: "closed",
|
|
513
|
+
failures: 0,
|
|
514
|
+
lastFailureTime: 0,
|
|
515
|
+
halfOpenAttempts: 0
|
|
516
|
+
};
|
|
517
|
+
this.circuits.set(key, record);
|
|
518
|
+
}
|
|
519
|
+
return record;
|
|
520
|
+
}
|
|
521
|
+
checkCircuit(endpoint) {
|
|
522
|
+
const key = this.getKey(endpoint);
|
|
523
|
+
const record = this.getOrCreate(key);
|
|
524
|
+
if (record.state === "closed") return;
|
|
525
|
+
if (record.state === "open") {
|
|
526
|
+
const elapsed = Date.now() - record.lastFailureTime;
|
|
527
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
528
|
+
record.state = "half-open";
|
|
529
|
+
record.halfOpenAttempts = 1;
|
|
530
|
+
logInfo2(
|
|
531
|
+
this.client,
|
|
532
|
+
`Circuit half-open for ${key}, allowing probe request`,
|
|
533
|
+
{ key }
|
|
534
|
+
);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const remainingMs = this.resetTimeoutMs - elapsed;
|
|
538
|
+
throw new CircuitOpenError(key, record.failures, remainingMs);
|
|
539
|
+
}
|
|
540
|
+
if (record.state === "half-open") {
|
|
541
|
+
if (record.halfOpenAttempts >= this.halfOpenMaxAttempts) {
|
|
542
|
+
throw new CircuitOpenError(key, record.failures, this.resetTimeoutMs);
|
|
543
|
+
}
|
|
544
|
+
record.halfOpenAttempts++;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
recordSuccess(endpoint) {
|
|
548
|
+
const key = this.getKey(endpoint);
|
|
549
|
+
const record = this.circuits.get(key);
|
|
550
|
+
if (!record) return;
|
|
551
|
+
if (record.state === "open") return;
|
|
552
|
+
if (record.state === "half-open") {
|
|
553
|
+
logInfo2(this.client, `Circuit closed for ${key} after successful probe`, {
|
|
554
|
+
key
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
record.state = "closed";
|
|
558
|
+
record.failures = 0;
|
|
559
|
+
record.halfOpenAttempts = 0;
|
|
560
|
+
}
|
|
561
|
+
recordFailure(endpoint, statusCode) {
|
|
562
|
+
if (statusCode !== 0 && statusCode < 500 && statusCode !== 420 && statusCode !== 429) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const key = this.getKey(endpoint);
|
|
566
|
+
const record = this.getOrCreate(key);
|
|
567
|
+
record.failures++;
|
|
568
|
+
record.lastFailureTime = Date.now();
|
|
569
|
+
if (record.state === "half-open") {
|
|
570
|
+
record.state = "open";
|
|
571
|
+
logWarn2(
|
|
572
|
+
this.client,
|
|
573
|
+
`Circuit re-opened for ${key} after failed probe (${record.failures} failures)`,
|
|
574
|
+
{ key, failures: record.failures }
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (record.failures >= this.failureThreshold) {
|
|
579
|
+
record.state = "open";
|
|
580
|
+
logWarn2(
|
|
581
|
+
this.client,
|
|
582
|
+
`Circuit opened for ${key} after ${record.failures} consecutive failures`,
|
|
583
|
+
{ key, failures: record.failures }
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
getState(endpoint) {
|
|
588
|
+
const key = this.getKey(endpoint);
|
|
589
|
+
const record = this.circuits.get(key);
|
|
590
|
+
if (!record) return "closed";
|
|
591
|
+
if (record.state === "open") {
|
|
592
|
+
const elapsed = Date.now() - record.lastFailureTime;
|
|
593
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
594
|
+
return "half-open";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return record.state;
|
|
598
|
+
}
|
|
599
|
+
getStats() {
|
|
600
|
+
const circuits = {};
|
|
601
|
+
let openCircuits = 0;
|
|
602
|
+
for (const [key, record] of this.circuits.entries()) {
|
|
603
|
+
const state = this.getState(key);
|
|
604
|
+
circuits[key] = { state, failures: record.failures };
|
|
605
|
+
if (state === "open") openCircuits++;
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
totalCircuits: this.circuits.size,
|
|
609
|
+
openCircuits,
|
|
610
|
+
circuits
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
reset(endpoint) {
|
|
614
|
+
if (endpoint) {
|
|
615
|
+
this.circuits.delete(this.getKey(endpoint));
|
|
616
|
+
} else {
|
|
617
|
+
this.circuits.clear();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
cleanup() {
|
|
621
|
+
const now = Date.now();
|
|
622
|
+
let cleaned = 0;
|
|
623
|
+
for (const [key, record] of this.circuits.entries()) {
|
|
624
|
+
if (record.state === "closed" && record.failures === 0 && record.lastFailureTime > 0 && now - record.lastFailureTime > this.staleThresholdMs) {
|
|
625
|
+
this.circuits.delete(key);
|
|
626
|
+
cleaned++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (cleaned > 0) {
|
|
630
|
+
logInfo2(
|
|
631
|
+
this.client,
|
|
632
|
+
`Circuit breaker cleanup: removed ${cleaned} stale circuits`,
|
|
633
|
+
{
|
|
634
|
+
cleaned
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return cleaned;
|
|
639
|
+
}
|
|
640
|
+
getKeyStrategy() {
|
|
641
|
+
return this.keyStrategy;
|
|
642
|
+
}
|
|
643
|
+
destroy() {
|
|
644
|
+
if (this.cleanupTimer) {
|
|
645
|
+
clearInterval(this.cleanupTimer);
|
|
646
|
+
this.cleanupTimer = null;
|
|
647
|
+
}
|
|
648
|
+
this.circuits.clear();
|
|
649
|
+
}
|
|
650
|
+
shutdown() {
|
|
651
|
+
this.destroy();
|
|
652
|
+
}
|
|
653
|
+
startCleanupTimer(intervalMs) {
|
|
654
|
+
this.cleanupTimer = setInterval(() => {
|
|
655
|
+
this.cleanup();
|
|
656
|
+
}, intervalMs);
|
|
657
|
+
this.cleanupTimer.unref();
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// src/core/util/retry.ts
|
|
662
|
+
function retryDelay(attempt, baseMs, maxMs) {
|
|
663
|
+
const exponential = baseMs === 0 ? 0 : baseMs * Math.pow(2, attempt);
|
|
664
|
+
const jitter = exponential * (0.75 + Math.random() * 0.5);
|
|
665
|
+
return Math.min(jitter, maxMs);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/core/util/sleep.ts
|
|
669
|
+
function sleep(ms) {
|
|
670
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/core/RetryStrategy.ts
|
|
674
|
+
var RetryStrategy = class {
|
|
675
|
+
maxRetries;
|
|
676
|
+
baseDelayMs;
|
|
677
|
+
maxDelayMs;
|
|
678
|
+
retryMutations;
|
|
679
|
+
constructor(config) {
|
|
680
|
+
this.maxRetries = config?.maxRetries ?? 0;
|
|
681
|
+
this.baseDelayMs = config?.baseDelayMs ?? 1e3;
|
|
682
|
+
this.maxDelayMs = config?.maxDelayMs ?? 3e4;
|
|
683
|
+
this.retryMutations = config?.retryMutations ?? false;
|
|
684
|
+
}
|
|
685
|
+
async execute(operation, context) {
|
|
686
|
+
const canRetryMethod = context.method === "GET" || this.retryMutations;
|
|
687
|
+
let lastError;
|
|
688
|
+
let refreshAttempted = false;
|
|
689
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
690
|
+
try {
|
|
691
|
+
return await operation();
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (error instanceof CircuitOpenError) {
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
if (!refreshAttempted && error instanceof EsiError && error.statusCode === 401 && context.requiresAuth && context.refreshToken) {
|
|
697
|
+
refreshAttempted = true;
|
|
698
|
+
logInfo2(context.client, "Received 401, attempting token refresh...", {
|
|
699
|
+
endpoint: context.endpoint
|
|
700
|
+
});
|
|
701
|
+
try {
|
|
702
|
+
await context.refreshToken();
|
|
703
|
+
} catch (refreshError) {
|
|
704
|
+
if (refreshError instanceof EsiError) {
|
|
705
|
+
throw refreshError;
|
|
706
|
+
}
|
|
707
|
+
const msg = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
|
708
|
+
logError2(context.client, `Token refresh failed: ${msg}`, {
|
|
709
|
+
endpoint: context.endpoint
|
|
710
|
+
});
|
|
711
|
+
throw new EsiTokenRefreshError(refreshError);
|
|
712
|
+
}
|
|
713
|
+
logInfo2(context.client, "Token refreshed, retrying request", {
|
|
714
|
+
endpoint: context.endpoint
|
|
715
|
+
});
|
|
716
|
+
attempt--;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (error instanceof EsiError && error.retryable && canRetryMethod && attempt < this.maxRetries) {
|
|
720
|
+
const delay = retryDelay(attempt, this.baseDelayMs, this.maxDelayMs);
|
|
721
|
+
logWarn2(
|
|
722
|
+
context.client,
|
|
723
|
+
`Request to ${context.endpoint} failed (${error.statusCode}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
|
724
|
+
{ method: context.method, statusCode: error.statusCode }
|
|
725
|
+
);
|
|
726
|
+
await sleep(delay);
|
|
727
|
+
lastError = error;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
throw lastError;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// src/core/util/callerIdentity.ts
|
|
738
|
+
import { createHash } from "crypto";
|
|
739
|
+
var CHARACTER_SUBJECT = /^CHARACTER:EVE:(\d+)$/;
|
|
740
|
+
function characterIdOfToken(token) {
|
|
741
|
+
const parts = token.split(".");
|
|
742
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
743
|
+
let claims;
|
|
744
|
+
try {
|
|
745
|
+
claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
746
|
+
} catch {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
if (typeof claims !== "object" || claims === null) return null;
|
|
750
|
+
const sub = claims.sub;
|
|
751
|
+
const match = typeof sub === "string" ? CHARACTER_SUBJECT.exec(sub) : null;
|
|
752
|
+
return match ? Number(match[1]) : null;
|
|
753
|
+
}
|
|
754
|
+
function hashedIdentity(authorizationHeader) {
|
|
755
|
+
return createHash("sha256").update(authorizationHeader).digest("hex").slice(0, 16);
|
|
756
|
+
}
|
|
757
|
+
function callerIdentity(authorizationHeader) {
|
|
758
|
+
const token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.slice("Bearer ".length) : authorizationHeader;
|
|
759
|
+
const characterId = characterIdOfToken(token);
|
|
760
|
+
if (characterId !== null) return `character:${characterId}`;
|
|
761
|
+
return hashedIdentity(authorizationHeader);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/core/cache/cacheKey.ts
|
|
765
|
+
var identities = /* @__PURE__ */ new WeakMap();
|
|
766
|
+
var accepted = /* @__PURE__ */ new WeakMap();
|
|
767
|
+
var ACCEPTED_LIMIT = 64;
|
|
768
|
+
function identityOf(client, header) {
|
|
769
|
+
const known = identities.get(client);
|
|
770
|
+
if (known && known.header === header) return known;
|
|
771
|
+
const identity = {
|
|
772
|
+
header,
|
|
773
|
+
claimed: callerIdentity(header),
|
|
774
|
+
hashed: hashedIdentity(header)
|
|
775
|
+
};
|
|
776
|
+
identities.set(client, identity);
|
|
777
|
+
return identity;
|
|
778
|
+
}
|
|
779
|
+
function markTokenAccepted(client, authorizationHeader) {
|
|
780
|
+
let set = accepted.get(client);
|
|
781
|
+
if (!set) {
|
|
782
|
+
set = /* @__PURE__ */ new Set();
|
|
783
|
+
accepted.set(client, set);
|
|
784
|
+
}
|
|
785
|
+
const hashed = hashedIdentity(authorizationHeader);
|
|
786
|
+
if (set.has(hashed)) return;
|
|
787
|
+
if (set.size >= ACCEPTED_LIMIT) {
|
|
788
|
+
set.delete(set.values().next().value);
|
|
789
|
+
}
|
|
790
|
+
set.add(hashed);
|
|
791
|
+
}
|
|
792
|
+
function isAccepted(client, identity) {
|
|
793
|
+
return accepted.get(client)?.has(identity.hashed) ?? false;
|
|
794
|
+
}
|
|
795
|
+
function scope(key, identity) {
|
|
796
|
+
return identity === null ? key : `${identity}:${key}`;
|
|
797
|
+
}
|
|
798
|
+
function trustedIdentity(client, requiresAuth, authorizationHeader) {
|
|
799
|
+
if (!requiresAuth) return null;
|
|
800
|
+
const header = authorizationHeader ?? client.getAuthorizationHeader();
|
|
801
|
+
if (!header) return null;
|
|
802
|
+
const identity = identityOf(client, header);
|
|
803
|
+
return isAccepted(client, identity) ? identity.claimed : identity.hashed;
|
|
804
|
+
}
|
|
805
|
+
function claimedIdentity(client, requiresAuth) {
|
|
806
|
+
if (!requiresAuth) return null;
|
|
807
|
+
const header = client.getAuthorizationHeader();
|
|
808
|
+
if (!header) return null;
|
|
809
|
+
return identityOf(client, header).claimed;
|
|
810
|
+
}
|
|
811
|
+
function buildCacheKey(url, client, requiresAuth = false, authorizationHeader) {
|
|
812
|
+
return scope(url, trustedIdentity(client, requiresAuth, authorizationHeader));
|
|
813
|
+
}
|
|
814
|
+
function buildConditionalCacheKey(url, client, requiresAuth = false) {
|
|
815
|
+
return scope(url, claimedIdentity(client, requiresAuth));
|
|
816
|
+
}
|
|
817
|
+
function buildDedupeKey(endpoint, client, requiresAuth = false) {
|
|
818
|
+
return scope(endpoint, trustedIdentity(client, requiresAuth));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/core/util/stringUtil.ts
|
|
822
|
+
function camelToSnake(s) {
|
|
823
|
+
return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/core/endpoints/esi-cache-ttls.generated.ts
|
|
827
|
+
var esiCacheTtls = {
|
|
828
|
+
"GET:alliances": 3600,
|
|
829
|
+
"GET:alliances/{alliance_id}": 3600,
|
|
830
|
+
"GET:alliances/{alliance_id}/contacts": 300,
|
|
831
|
+
"GET:alliances/{alliance_id}/contacts/labels": 300,
|
|
832
|
+
"GET:alliances/{alliance_id}/corporations": 3600,
|
|
833
|
+
"GET:characters/{character_id}": 86400,
|
|
834
|
+
"GET:characters/{character_id}/access-lists": 300,
|
|
835
|
+
"GET:characters/{character_id}/access-lists/{access_list_id}": 300,
|
|
836
|
+
"GET:characters/{character_id}/agents_research": 3600,
|
|
837
|
+
"GET:characters/{character_id}/assets": 3600,
|
|
838
|
+
"GET:characters/{character_id}/attributes": 120,
|
|
839
|
+
"GET:characters/{character_id}/blueprints": 3600,
|
|
840
|
+
"GET:characters/{character_id}/calendar": 5,
|
|
841
|
+
"GET:characters/{character_id}/calendar/{event_id}": 5,
|
|
842
|
+
"PUT:characters/{character_id}/calendar/{event_id}": 5,
|
|
843
|
+
"GET:characters/{character_id}/calendar/{event_id}/attendees": 600,
|
|
844
|
+
"GET:characters/{character_id}/clones": 120,
|
|
845
|
+
"GET:characters/{character_id}/contacts": 300,
|
|
846
|
+
"GET:characters/{character_id}/contacts/labels": 300,
|
|
847
|
+
"GET:characters/{character_id}/contracts": 300,
|
|
848
|
+
"GET:characters/{character_id}/contracts/{contract_id}/bids": 300,
|
|
849
|
+
"GET:characters/{character_id}/contracts/{contract_id}/items": 3600,
|
|
850
|
+
"GET:characters/{character_id}/corporationhistory": 86400,
|
|
851
|
+
"GET:characters/{character_id}/cosmetics/skinr": 3600,
|
|
852
|
+
"GET:characters/{character_id}/cosmetics/skinr/components": 3600,
|
|
853
|
+
"GET:characters/{character_id}/fatigue": 300,
|
|
854
|
+
"GET:characters/{character_id}/fittings": 300,
|
|
855
|
+
"GET:characters/{character_id}/fleet": 60,
|
|
856
|
+
"GET:characters/{character_id}/freelance-jobs": 60,
|
|
857
|
+
"GET:characters/{character_id}/freelance-jobs/{job_id}/participation": 60,
|
|
858
|
+
"GET:characters/{character_id}/implants": 120,
|
|
859
|
+
"GET:characters/{character_id}/industry/jobs": 300,
|
|
860
|
+
"GET:characters/{character_id}/killmails/recent": 300,
|
|
861
|
+
"GET:characters/{character_id}/location": 5,
|
|
862
|
+
"GET:characters/{character_id}/loyalty/points": 3600,
|
|
863
|
+
"GET:characters/{character_id}/mail": 30,
|
|
864
|
+
"GET:characters/{character_id}/mail/{mail_id}": 30,
|
|
865
|
+
"GET:characters/{character_id}/mail/labels": 30,
|
|
866
|
+
"GET:characters/{character_id}/mail/lists": 120,
|
|
867
|
+
"GET:characters/{character_id}/medals": 3600,
|
|
868
|
+
"GET:characters/{character_id}/mercenary-tactical-operations": 300,
|
|
869
|
+
"GET:characters/{character_id}/mercenary-tactical-operations/{operation_id}": 300,
|
|
870
|
+
"GET:characters/{character_id}/military-campaigns/objectives": 60,
|
|
871
|
+
"GET:characters/{character_id}/military-campaigns/objectives/{objective_id}": 60,
|
|
872
|
+
"GET:characters/{character_id}/mining": 600,
|
|
873
|
+
"GET:characters/{character_id}/notifications": 600,
|
|
874
|
+
"GET:characters/{character_id}/notifications/contacts": 600,
|
|
875
|
+
"GET:characters/{character_id}/online": 60,
|
|
876
|
+
"GET:characters/{character_id}/orders": 1200,
|
|
877
|
+
"GET:characters/{character_id}/orders/history": 3600,
|
|
878
|
+
"GET:characters/{character_id}/planets": 600,
|
|
879
|
+
"GET:characters/{character_id}/planets/{planet_id}": 600,
|
|
880
|
+
"GET:characters/{character_id}/roles": 3600,
|
|
881
|
+
"GET:characters/{character_id}/search": 3600,
|
|
882
|
+
"GET:characters/{character_id}/ship": 5,
|
|
883
|
+
"GET:characters/{character_id}/skillqueue": 60,
|
|
884
|
+
"GET:characters/{character_id}/skills": 60,
|
|
885
|
+
"GET:characters/{character_id}/standings": 3600,
|
|
886
|
+
"GET:characters/{character_id}/structures/mercenary-dens": 3600,
|
|
887
|
+
"GET:characters/{character_id}/structures/mercenary-dens/{mercenary_den_id}": 3600,
|
|
888
|
+
"GET:characters/{character_id}/titles": 3600,
|
|
889
|
+
"GET:characters/{character_id}/wallet": 120,
|
|
890
|
+
"GET:characters/{character_id}/wallet/journal": 3600,
|
|
891
|
+
"GET:characters/{character_id}/wallet/transactions": 3600,
|
|
892
|
+
"POST:characters/affiliation": 3600,
|
|
893
|
+
"GET:contracts/public/{region_id}": 1800,
|
|
894
|
+
"GET:contracts/public/bids/{contract_id}": 300,
|
|
895
|
+
"GET:contracts/public/items/{contract_id}": 3600,
|
|
896
|
+
"GET:corporation/{corporation_id}/mining/extractions": 1800,
|
|
897
|
+
"GET:corporation/{corporation_id}/mining/observers": 3600,
|
|
898
|
+
"GET:corporation/{corporation_id}/mining/observers/{observer_id}": 3600,
|
|
899
|
+
"GET:corporations/{corporation_id}": 3600,
|
|
900
|
+
"GET:corporations/{corporation_id}/alliancehistory": 3600,
|
|
901
|
+
"GET:corporations/{corporation_id}/assets": 3600,
|
|
902
|
+
"GET:corporations/{corporation_id}/blueprints": 3600,
|
|
903
|
+
"GET:corporations/{corporation_id}/contacts": 300,
|
|
904
|
+
"GET:corporations/{corporation_id}/contacts/labels": 300,
|
|
905
|
+
"GET:corporations/{corporation_id}/containers/logs": 600,
|
|
906
|
+
"GET:corporations/{corporation_id}/contracts": 300,
|
|
907
|
+
"GET:corporations/{corporation_id}/contracts/{contract_id}/bids": 3600,
|
|
908
|
+
"GET:corporations/{corporation_id}/contracts/{contract_id}/items": 3600,
|
|
909
|
+
"GET:corporations/{corporation_id}/customs_offices": 3600,
|
|
910
|
+
"GET:corporations/{corporation_id}/divisions": 3600,
|
|
911
|
+
"GET:corporations/{corporation_id}/facilities": 3600,
|
|
912
|
+
"GET:corporations/{corporation_id}/icons": 3600,
|
|
913
|
+
"GET:corporations/{corporation_id}/industry/jobs": 300,
|
|
914
|
+
"GET:corporations/{corporation_id}/killmails/recent": 300,
|
|
915
|
+
"GET:corporations/{corporation_id}/medals": 3600,
|
|
916
|
+
"GET:corporations/{corporation_id}/medals/issued": 3600,
|
|
917
|
+
"GET:corporations/{corporation_id}/members": 3600,
|
|
918
|
+
"GET:corporations/{corporation_id}/members/limit": 3600,
|
|
919
|
+
"GET:corporations/{corporation_id}/members/titles": 3600,
|
|
920
|
+
"GET:corporations/{corporation_id}/membertracking": 3600,
|
|
921
|
+
"GET:corporations/{corporation_id}/orders": 1200,
|
|
922
|
+
"GET:corporations/{corporation_id}/orders/history": 3600,
|
|
923
|
+
"GET:corporations/{corporation_id}/projects/{project_id}": 60,
|
|
924
|
+
"GET:corporations/{corporation_id}/projects/{project_id}/contribution/{character_id}": 60,
|
|
925
|
+
"GET:corporations/{corporation_id}/roles": 3600,
|
|
926
|
+
"GET:corporations/{corporation_id}/roles/history": 3600,
|
|
927
|
+
"GET:corporations/{corporation_id}/shareholders": 3600,
|
|
928
|
+
"GET:corporations/{corporation_id}/standings": 3600,
|
|
929
|
+
"GET:corporations/{corporation_id}/starbases": 3600,
|
|
930
|
+
"GET:corporations/{corporation_id}/starbases/{starbase_id}": 3600,
|
|
931
|
+
"GET:corporations/{corporation_id}/structures": 3600,
|
|
932
|
+
"GET:corporations/{corporation_id}/structures/skyhooks": 3600,
|
|
933
|
+
"GET:corporations/{corporation_id}/structures/skyhooks/{skyhook_id}": 3600,
|
|
934
|
+
"GET:corporations/{corporation_id}/structures/sovereignty-hubs": 3600,
|
|
935
|
+
"GET:corporations/{corporation_id}/structures/sovereignty-hubs/{sovereignty_hub_id}": 3600,
|
|
936
|
+
"GET:corporations/{corporation_id}/titles": 3600,
|
|
937
|
+
"GET:corporations/{corporation_id}/wallets": 300,
|
|
938
|
+
"GET:corporations/{corporation_id}/wallets/{division}/journal": 3600,
|
|
939
|
+
"GET:corporations/{corporation_id}/wallets/{division}/transactions": 3600,
|
|
940
|
+
"GET:cosmetics/skinr/{skinr_id}": 31536e3,
|
|
941
|
+
"GET:fleets/{fleet_id}": 5,
|
|
942
|
+
"GET:fleets/{fleet_id}/members": 5,
|
|
943
|
+
"GET:fleets/{fleet_id}/wings": 5,
|
|
944
|
+
"GET:freelance-jobs/{job_id}": 60,
|
|
945
|
+
"GET:fw/systems": 1800,
|
|
946
|
+
"GET:incursions": 300,
|
|
947
|
+
"GET:industry/facilities": 3600,
|
|
948
|
+
"GET:industry/systems": 3600,
|
|
949
|
+
"GET:insurance/prices": 3600,
|
|
950
|
+
"GET:killmails/{killmail_id}/{killmail_hash}": 2592e3,
|
|
951
|
+
"GET:markets/{region_id}/orders": 300,
|
|
952
|
+
"GET:markets/{region_id}/types": 600,
|
|
953
|
+
"GET:markets/prices": 3600,
|
|
954
|
+
"GET:markets/structures/{structure_id}": 300,
|
|
955
|
+
"GET:meta/changelog": 600,
|
|
956
|
+
"GET:meta/compatibility-dates": 600,
|
|
957
|
+
"GET:meta/name": 600,
|
|
958
|
+
"GET:military-campaigns": 60,
|
|
959
|
+
"GET:military-campaigns/{campaign_id}": 60,
|
|
960
|
+
"GET:military-campaigns/{campaign_id}/objectives": 60,
|
|
961
|
+
"GET:military-campaigns/{campaign_id}/objectives/{objective_id}": 60,
|
|
962
|
+
"GET:skyhooks/raidable": 300,
|
|
963
|
+
"GET:sovereignty/campaigns": 5,
|
|
964
|
+
"GET:sovereignty/systems": 300,
|
|
965
|
+
"GET:status": 30,
|
|
966
|
+
"GET:universe/schematics/{schematic_id}": 3600,
|
|
967
|
+
"GET:universe/structures": 3600,
|
|
968
|
+
"GET:universe/structures/{structure_id}": 3600,
|
|
969
|
+
"GET:universe/system_jumps": 3600,
|
|
970
|
+
"GET:universe/system_kills": 3600,
|
|
971
|
+
"GET:wars": 3600,
|
|
972
|
+
"GET:wars/{war_id}": 3600,
|
|
973
|
+
"GET:wars/{war_id}/killmails": 3600
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// src/core/constants.ts
|
|
977
|
+
var PACKAGE_NAME = "esi.ts";
|
|
978
|
+
var PACKAGE_VERSION = "11.0.0";
|
|
979
|
+
var USER_AGENT = `${PACKAGE_NAME}/${PACKAGE_VERSION}`;
|
|
980
|
+
var COMPATIBILITY_DATE = "2026-08-18";
|
|
981
|
+
|
|
982
|
+
// src/core/requestPipeline/headers.ts
|
|
983
|
+
var parseCacheControlTtl = (headers) => {
|
|
984
|
+
const cacheControl = headers["cache-control"] ?? headers["Cache-Control"];
|
|
985
|
+
if (!cacheControl) return void 0;
|
|
986
|
+
const match = /max-age=(\d+)/.exec(cacheControl);
|
|
987
|
+
return match ? parseInt(match[1], 10) * 1e3 : void 0;
|
|
988
|
+
};
|
|
989
|
+
var HEADER_VALUE2 = /^[\t\x20-\x7e]+$/;
|
|
990
|
+
function buildRequestHeaders(client, url, method, requiresAuth, useETag, body, resolveCache2, accept = "application/json") {
|
|
991
|
+
const userAgent = client.getUserAgent();
|
|
992
|
+
const headers = {
|
|
993
|
+
Accept: accept,
|
|
994
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
995
|
+
"User-Agent": userAgent ? `${userAgent} ${USER_AGENT}` : USER_AGENT,
|
|
996
|
+
"X-Compatibility-Date": client.getCompatibilityDate() ?? COMPATIBILITY_DATE
|
|
997
|
+
};
|
|
998
|
+
const clientId = client.getClientId();
|
|
999
|
+
if (userAgent) {
|
|
1000
|
+
headers["X-User-Agent"] = userAgent;
|
|
1001
|
+
} else if (HEADER_VALUE2.test(clientId)) {
|
|
1002
|
+
headers["X-User-Agent"] = clientId;
|
|
1003
|
+
}
|
|
1004
|
+
const tenant = client.getTenant();
|
|
1005
|
+
if (tenant) {
|
|
1006
|
+
headers["X-Tenant"] = tenant;
|
|
1007
|
+
}
|
|
1008
|
+
const language = client.getLanguage();
|
|
1009
|
+
if (language) {
|
|
1010
|
+
headers["Accept-Language"] = language;
|
|
1011
|
+
}
|
|
1012
|
+
if (requiresAuth) {
|
|
1013
|
+
const authHeader = client.getAuthorizationHeader();
|
|
1014
|
+
if (!authHeader) {
|
|
1015
|
+
throw new EsiConfigurationError(
|
|
1016
|
+
"NO_AUTH_TOKEN",
|
|
1017
|
+
[
|
|
1018
|
+
"Authorization header is required for this endpoint but no access token is configured",
|
|
1019
|
+
"Fix: set ESI_ACCESS_TOKEN in your environment (see .env.example), or call client.setAccessToken(token), or pass accessToken to the EsiClient constructor"
|
|
1020
|
+
].join(" \u2014 ")
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
headers["Authorization"] = authHeader;
|
|
1024
|
+
}
|
|
1025
|
+
const cache = resolveCache2(client);
|
|
1026
|
+
if (useETag && method === "GET" && cache) {
|
|
1027
|
+
const key = buildConditionalCacheKey(url, client, requiresAuth);
|
|
1028
|
+
const cachedETag = cache.getETag(key);
|
|
1029
|
+
if (cachedETag) {
|
|
1030
|
+
headers["If-None-Match"] = cachedETag;
|
|
1031
|
+
logDebug2(client, `Adding If-None-Match header: ${cachedETag}`, {
|
|
1032
|
+
etag: cachedETag
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (body) {
|
|
1037
|
+
headers["Content-Type"] = "application/json";
|
|
1038
|
+
}
|
|
1039
|
+
return headers;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/core/requestPipeline/cachePolicy.ts
|
|
1043
|
+
var STALE_RETENTION_MS = 60 * 60 * 1e3;
|
|
1044
|
+
function retentionTtl(freshnessTtlMs) {
|
|
1045
|
+
return freshnessTtlMs === void 0 ? void 0 : freshnessTtlMs + STALE_RETENTION_MS;
|
|
1046
|
+
}
|
|
1047
|
+
function lookupSpecTtl(method, templatePath) {
|
|
1048
|
+
const normalized = templatePath.replace(/\/$/, "").replace(/\{(\w+)\}/g, (_, name) => `{${camelToSnake(name)}}`);
|
|
1049
|
+
const key = `${method}:${normalized}`;
|
|
1050
|
+
const seconds = esiCacheTtls[key];
|
|
1051
|
+
return typeof seconds === "number" ? seconds * 1e3 : void 0;
|
|
1052
|
+
}
|
|
1053
|
+
function trySpecAwareCacheHit(client, url, method, templatePath, resolveCache2, requiresAuth = false) {
|
|
1054
|
+
if (method !== "GET" || !templatePath) return null;
|
|
1055
|
+
const specTtlMs = lookupSpecTtl(method, templatePath);
|
|
1056
|
+
if (!specTtlMs) return null;
|
|
1057
|
+
const cache = resolveCache2(client);
|
|
1058
|
+
if (!cache) return null;
|
|
1059
|
+
const key = buildCacheKey(url, client, requiresAuth);
|
|
1060
|
+
const entry = cache.get(key);
|
|
1061
|
+
if (!entry) return null;
|
|
1062
|
+
const age = Date.now() - entry.timestamp;
|
|
1063
|
+
if (age < specTtlMs) {
|
|
1064
|
+
logDebug2(
|
|
1065
|
+
client,
|
|
1066
|
+
`Spec-aware cache hit for ${url} (age=${Math.round(age / 1e3)}s, ttl=${Math.round(specTtlMs / 1e3)}s)`,
|
|
1067
|
+
{ method, templatePath }
|
|
1068
|
+
);
|
|
1069
|
+
return {
|
|
1070
|
+
headers: entry.headers,
|
|
1071
|
+
body: entry.data,
|
|
1072
|
+
status: 200,
|
|
1073
|
+
fromCache: true,
|
|
1074
|
+
cacheHitType: "spec-ttl"
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
function hasCachedEntry(client, url, resolveCache2, requiresAuth = false, authorization) {
|
|
1080
|
+
const cache = resolveCache2(client);
|
|
1081
|
+
return !!cache && cache.has(buildCacheKey(url, client, requiresAuth, authorization));
|
|
1082
|
+
}
|
|
1083
|
+
function tryStaleCacheResponse(client, url, parsed, resolveCache2, requiresAuth = false, authorization) {
|
|
1084
|
+
const cache = resolveCache2(client);
|
|
1085
|
+
if (!cache) return null;
|
|
1086
|
+
const key = buildCacheKey(url, client, requiresAuth, authorization);
|
|
1087
|
+
const cachedEntry = cache.get(key);
|
|
1088
|
+
if (!cachedEntry) return null;
|
|
1089
|
+
return {
|
|
1090
|
+
headers: { ...cachedEntry.headers, ...parsed.raw },
|
|
1091
|
+
body: cachedEntry.data,
|
|
1092
|
+
status: 200,
|
|
1093
|
+
fromCache: true,
|
|
1094
|
+
stale: true,
|
|
1095
|
+
cacheHitType: "stale-on-error"
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
var RECENT_WRITES = 64;
|
|
1099
|
+
var writeLogs = /* @__PURE__ */ new WeakMap();
|
|
1100
|
+
function writeLogFor(cache) {
|
|
1101
|
+
let log = writeLogs.get(cache);
|
|
1102
|
+
if (!log) {
|
|
1103
|
+
log = { generation: 0, recent: [] };
|
|
1104
|
+
writeLogs.set(cache, log);
|
|
1105
|
+
}
|
|
1106
|
+
return log;
|
|
1107
|
+
}
|
|
1108
|
+
function currentWriteGeneration(client, resolveCache2) {
|
|
1109
|
+
const cache = resolveCache2(client);
|
|
1110
|
+
return cache ? writeLogFor(cache).generation : 0;
|
|
1111
|
+
}
|
|
1112
|
+
function invalidatedSince(cache, generation, key) {
|
|
1113
|
+
const log = writeLogFor(cache);
|
|
1114
|
+
if (log.generation === generation) return false;
|
|
1115
|
+
if (log.generation - generation > log.recent.length) return true;
|
|
1116
|
+
return log.recent.some(
|
|
1117
|
+
(write) => write.generation > generation && key.includes(write.path)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
function cacheResponse(client, url, method, endpoint, parsed, data, useETag, resolveCache2, templatePath, requiresAuth = false, sentAtWriteGeneration, authorization) {
|
|
1121
|
+
const cache = resolveCache2(client);
|
|
1122
|
+
if (useETag && method === "GET" && cache && parsed.etag) {
|
|
1123
|
+
const key = buildCacheKey(url, client, requiresAuth, authorization);
|
|
1124
|
+
if (sentAtWriteGeneration !== void 0 && invalidatedSince(cache, sentAtWriteGeneration, key)) {
|
|
1125
|
+
logDebug2(
|
|
1126
|
+
client,
|
|
1127
|
+
`Not caching ${url}: a write invalidated it while the request was in flight`,
|
|
1128
|
+
{ method }
|
|
1129
|
+
);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const headerTtl = parseCacheControlTtl(parsed.raw);
|
|
1133
|
+
const specTtlMs = templatePath ? lookupSpecTtl(method, templatePath) : void 0;
|
|
1134
|
+
const freshnessTtl = specTtlMs ?? headerTtl;
|
|
1135
|
+
const ttl = retentionTtl(freshnessTtl);
|
|
1136
|
+
cache.set(key, parsed.etag, data, parsed.raw, ttl);
|
|
1137
|
+
const ttlInfo = freshnessTtl !== void 0 ? ` (ttl=${freshnessTtl}ms, kept for ${ttl}ms)` : "";
|
|
1138
|
+
logDebug2(
|
|
1139
|
+
client,
|
|
1140
|
+
`Cached response for ${url} with ETag ${parsed.etag}${ttlInfo}`,
|
|
1141
|
+
{
|
|
1142
|
+
method,
|
|
1143
|
+
etag: parsed.etag
|
|
1144
|
+
}
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
invalidateAfterWrite(client, method, endpoint, resolveCache2);
|
|
1148
|
+
}
|
|
1149
|
+
function evictRejectedResponse(client, endpoint, requiresAuth, resolveCache2) {
|
|
1150
|
+
const cache = resolveCache2(client);
|
|
1151
|
+
if (!cache) return;
|
|
1152
|
+
const url = `${client.getLink()}/${endpoint}`;
|
|
1153
|
+
if (cache.delete(buildCacheKey(url, client, requiresAuth))) {
|
|
1154
|
+
logDebug2(
|
|
1155
|
+
client,
|
|
1156
|
+
`Evicted cached response for ${url} after it failed validation`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function invalidateAfterWrite(client, method, endpoint, resolveCache2) {
|
|
1161
|
+
if (method === "GET") return;
|
|
1162
|
+
const path = endpoint.split("?")[0];
|
|
1163
|
+
client.getDeduplicator()?.detachByPath?.(path);
|
|
1164
|
+
const cache = resolveCache2(client);
|
|
1165
|
+
if (cache) {
|
|
1166
|
+
cache.deleteByPath(path);
|
|
1167
|
+
const log = writeLogFor(cache);
|
|
1168
|
+
log.generation += 1;
|
|
1169
|
+
log.recent.push({ generation: log.generation, path });
|
|
1170
|
+
if (log.recent.length > RECENT_WRITES) log.recent.shift();
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/core/requestPipeline/statusHandling.ts
|
|
1175
|
+
var STATUS_MESSAGES = {
|
|
1176
|
+
201: "Created",
|
|
1177
|
+
204: "No Content",
|
|
1178
|
+
304: "Not Modified",
|
|
1179
|
+
400: "Bad Request",
|
|
1180
|
+
401: "Unauthorized",
|
|
1181
|
+
403: "Forbidden",
|
|
1182
|
+
404: "Resource not found",
|
|
1183
|
+
420: "Error Limited",
|
|
1184
|
+
422: "Unprocessable Entity",
|
|
1185
|
+
429: "Too many requests",
|
|
1186
|
+
500: "Internal server error",
|
|
1187
|
+
502: "Bad Gateway",
|
|
1188
|
+
503: "Service Unavailable",
|
|
1189
|
+
504: "Gateway Timeout",
|
|
1190
|
+
520: "Internal server error, did the request terminate too soon?"
|
|
1191
|
+
};
|
|
1192
|
+
function statusMessage(response) {
|
|
1193
|
+
return STATUS_MESSAGES[response.status] || response.statusText || `HTTP ${response.status}`;
|
|
1194
|
+
}
|
|
1195
|
+
function handleEarlyStatus(client, status, url, parsed, useETag, resolveCache2, requiresAuth = false, authorization) {
|
|
1196
|
+
if (status === 201) {
|
|
1197
|
+
return { headers: parsed.raw, body: void 0, status: 201 };
|
|
1198
|
+
}
|
|
1199
|
+
if (status === 204) {
|
|
1200
|
+
logInfo2(client, `No Content for endpoint: ${url}`, { status });
|
|
1201
|
+
return { headers: parsed.raw, body: void 0, status: 204 };
|
|
1202
|
+
}
|
|
1203
|
+
if (status === 304) {
|
|
1204
|
+
const cache = resolveCache2(client);
|
|
1205
|
+
if (useETag && cache) {
|
|
1206
|
+
const key = buildCacheKey(url, client, requiresAuth, authorization);
|
|
1207
|
+
const cachedEntry = cache.get(key);
|
|
1208
|
+
if (cachedEntry) {
|
|
1209
|
+
logInfo2(client, `Cache hit (304) for endpoint: ${url}`, {
|
|
1210
|
+
status,
|
|
1211
|
+
cacheHitType: "etag-304"
|
|
1212
|
+
});
|
|
1213
|
+
const headers = {
|
|
1214
|
+
...cachedEntry.headers,
|
|
1215
|
+
...parsed.raw
|
|
1216
|
+
};
|
|
1217
|
+
if ("etag" in parsed.raw) headers["etag"] = cachedEntry.etag;
|
|
1218
|
+
cache.set(
|
|
1219
|
+
key,
|
|
1220
|
+
cachedEntry.etag,
|
|
1221
|
+
cachedEntry.data,
|
|
1222
|
+
headers,
|
|
1223
|
+
cachedEntry.ttl
|
|
1224
|
+
);
|
|
1225
|
+
return {
|
|
1226
|
+
headers,
|
|
1227
|
+
body: cachedEntry.data,
|
|
1228
|
+
status: 304,
|
|
1229
|
+
fromCache: true,
|
|
1230
|
+
cacheHitType: "etag-304"
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
throw new EsiError(
|
|
1235
|
+
304,
|
|
1236
|
+
"Not Modified \u2014 no cached data available",
|
|
1237
|
+
url,
|
|
1238
|
+
parsed.requestId ?? void 0
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
var MAX_REASON_LENGTH = 200;
|
|
1244
|
+
async function readEsiErrorReason(response) {
|
|
1245
|
+
try {
|
|
1246
|
+
const parsed = JSON.parse(await response.text());
|
|
1247
|
+
const reason = typeof parsed === "object" && parsed !== null ? parsed.error : void 0;
|
|
1248
|
+
if (typeof reason !== "string") return void 0;
|
|
1249
|
+
const trimmed = reason.trim();
|
|
1250
|
+
return trimmed ? trimmed.slice(0, MAX_REASON_LENGTH) : void 0;
|
|
1251
|
+
} catch {
|
|
1252
|
+
return void 0;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function handleErrorResponse(client, response, url, parsed, useETag, resolveCache2, requiresAuth = false, esiReason, authorization) {
|
|
1256
|
+
const text = statusMessage(response);
|
|
1257
|
+
const errorMessage = esiReason ? `${text}: ${esiReason}` : text;
|
|
1258
|
+
if (response.status >= 500 && useETag) {
|
|
1259
|
+
const staleResult = tryStaleCacheResponse(
|
|
1260
|
+
client,
|
|
1261
|
+
url,
|
|
1262
|
+
parsed,
|
|
1263
|
+
resolveCache2,
|
|
1264
|
+
requiresAuth,
|
|
1265
|
+
authorization
|
|
1266
|
+
);
|
|
1267
|
+
if (staleResult) {
|
|
1268
|
+
logWarn2(client, `${errorMessage} for ${url} \u2014 serving stale cache`, {
|
|
1269
|
+
status: response.status
|
|
1270
|
+
});
|
|
1271
|
+
return { ...staleResult, status: response.status };
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (response.status === 420 || response.status === 429) {
|
|
1275
|
+
logWarn2(client, `Rate limited (${response.status}) on ${url}`, {
|
|
1276
|
+
status: response.status
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
let message = errorMessage;
|
|
1280
|
+
if (response.status === 401) {
|
|
1281
|
+
message = `${errorMessage} \u2014 the access token was missing, expired, or lacks the required ESI scope. Fix: verify ESI_ACCESS_TOKEN, or configure onTokenRefresh for automatic refresh on 401`;
|
|
1282
|
+
} else if (response.status === 403) {
|
|
1283
|
+
message = `${errorMessage} \u2014 your access token does not have the OAuth scopes required for this endpoint. Check the scopes on your EVE SSO application`;
|
|
1284
|
+
}
|
|
1285
|
+
throw new EsiError(
|
|
1286
|
+
response.status,
|
|
1287
|
+
message,
|
|
1288
|
+
url,
|
|
1289
|
+
parsed.requestId ?? void 0
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
function wrapError(error, client) {
|
|
1293
|
+
if (error instanceof EsiError) {
|
|
1294
|
+
throw error;
|
|
1295
|
+
}
|
|
1296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1297
|
+
logError2(client, `Unexpected error: ${message}`);
|
|
1298
|
+
throw toEsiError(error);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/core/requestPipeline/dependencies.ts
|
|
1302
|
+
function resolveCache(client) {
|
|
1303
|
+
return client.getCache();
|
|
1304
|
+
}
|
|
1305
|
+
function resolveRateLimiter(client) {
|
|
1306
|
+
const limiter = client.getRateLimiter();
|
|
1307
|
+
if (!limiter) {
|
|
1308
|
+
throw new EsiConfigurationError(
|
|
1309
|
+
"CONFIGURATION_ERROR",
|
|
1310
|
+
"No rate limiter configured on ApiClient. Set one via apiClient.setRateLimiter(new RateLimiter())."
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
return limiter;
|
|
1314
|
+
}
|
|
1315
|
+
function resolveCircuitBreaker(client) {
|
|
1316
|
+
return client.getCircuitBreaker();
|
|
1317
|
+
}
|
|
1318
|
+
function resolveRetryStrategy(client) {
|
|
1319
|
+
return client.getRetryStrategy() ?? new RetryStrategy(client.getRetryConfig() ?? void 0);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/core/pagination/PaginationHandler.ts
|
|
1323
|
+
var PaginationHandler = class {
|
|
1324
|
+
static DEFAULT_OPTIONS = {
|
|
1325
|
+
maxPages: 1e3,
|
|
1326
|
+
stopOnEmptyPage: true
|
|
1327
|
+
};
|
|
1328
|
+
/**
|
|
1329
|
+
* Fetch remaining pages (2..totalPages) and combine with first page data.
|
|
1330
|
+
* The caller has already fetched page 1 and knows the total page count.
|
|
1331
|
+
*/
|
|
1332
|
+
static async fetchRemainingPages(client, endpoint, method, requiresAuth, firstPageData, totalPages, body, options, pageFetch, templatePath) {
|
|
1333
|
+
const opts = { ...this.DEFAULT_OPTIONS, ...options };
|
|
1334
|
+
const rateLimiter = client.getRateLimiter();
|
|
1335
|
+
const allData = [...firstPageData];
|
|
1336
|
+
const effectiveMaxPage = Math.min(totalPages, opts.maxPages);
|
|
1337
|
+
if (effectiveMaxPage <= 1) {
|
|
1338
|
+
return allData;
|
|
1339
|
+
}
|
|
1340
|
+
logInfo2(client, `Fetching pages 2-${effectiveMaxPage} for ${endpoint}...`, {
|
|
1341
|
+
method,
|
|
1342
|
+
totalPages
|
|
1343
|
+
});
|
|
1344
|
+
for (let page = 2; page <= effectiveMaxPage; page++) {
|
|
1345
|
+
try {
|
|
1346
|
+
if (rateLimiter) await rateLimiter.checkRateLimit(templatePath, method);
|
|
1347
|
+
const pageData = await this.fetchPageWithRetry(
|
|
1348
|
+
client,
|
|
1349
|
+
endpoint,
|
|
1350
|
+
method,
|
|
1351
|
+
requiresAuth,
|
|
1352
|
+
page,
|
|
1353
|
+
pageFetch
|
|
1354
|
+
);
|
|
1355
|
+
if (opts.stopOnEmptyPage && (!pageData || pageData.length === 0)) {
|
|
1356
|
+
logWarn2(client, `Page ${page} is empty. Stopping pagination.`, {
|
|
1357
|
+
page
|
|
1358
|
+
});
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
allData.push(...pageData);
|
|
1362
|
+
logInfo2(
|
|
1363
|
+
client,
|
|
1364
|
+
`Fetched page ${page}/${effectiveMaxPage} (${pageData.length} items)`,
|
|
1365
|
+
{ page, items: pageData.length }
|
|
1366
|
+
);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
logError2(
|
|
1369
|
+
client,
|
|
1370
|
+
`Failed to fetch page ${page}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1371
|
+
{ page }
|
|
1372
|
+
);
|
|
1373
|
+
throw error instanceof Error ? error : new Error(`Failed to fetch page ${page}: ${String(error)}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
logInfo2(
|
|
1377
|
+
client,
|
|
1378
|
+
`Pagination complete. Fetched ${allData.length} total items from up to ${effectiveMaxPage} pages.`,
|
|
1379
|
+
{ totalItems: allData.length }
|
|
1380
|
+
);
|
|
1381
|
+
return allData;
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Fetch a single page with retry logic.
|
|
1385
|
+
*
|
|
1386
|
+
* Delegates to the configured `IRetryStrategy` (exponential backoff
|
|
1387
|
+
* with jitter) instead of implementing a custom retry loop.
|
|
1388
|
+
*/
|
|
1389
|
+
static async fetchPageWithRetry(client, endpoint, method, requiresAuth, page, pageFetch) {
|
|
1390
|
+
const retryStrategy = resolveRetryStrategy(client);
|
|
1391
|
+
const paginatedEndpoint = this.buildPaginatedEndpoint(endpoint, page);
|
|
1392
|
+
return retryStrategy.execute(
|
|
1393
|
+
() => {
|
|
1394
|
+
logInfo2(
|
|
1395
|
+
client,
|
|
1396
|
+
`Fetching page ${page} via pipeline: ${paginatedEndpoint}`,
|
|
1397
|
+
{ page }
|
|
1398
|
+
);
|
|
1399
|
+
return pageFetch(paginatedEndpoint);
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
client,
|
|
1403
|
+
endpoint: paginatedEndpoint,
|
|
1404
|
+
method,
|
|
1405
|
+
requiresAuth,
|
|
1406
|
+
refreshToken: client.hasTokenProvider() ? () => client.refreshToken().then(() => {
|
|
1407
|
+
}) : void 0
|
|
1408
|
+
}
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Build a paginated endpoint path.
|
|
1413
|
+
* Preserves any existing query params on the endpoint and appends page=N.
|
|
1414
|
+
*/
|
|
1415
|
+
static buildPaginatedEndpoint(endpoint, page) {
|
|
1416
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
1417
|
+
return `${endpoint}${separator}page=${page}`;
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
// src/core/requestPipeline/paginationOrchestration.ts
|
|
1422
|
+
function handleCursorPagination(parsed, data) {
|
|
1423
|
+
if (!parsed.hasCursorPagination) return null;
|
|
1424
|
+
const cursors = {
|
|
1425
|
+
before: parsed.cursorBefore,
|
|
1426
|
+
after: parsed.cursorAfter
|
|
1427
|
+
};
|
|
1428
|
+
return { headers: parsed.raw, body: data, status: 200, cursors };
|
|
1429
|
+
}
|
|
1430
|
+
async function handleOffsetPagination(client, endpoint, method, requiresAuth, parsed, data, body, url, useETag, pageFetch, resolveCache2, templatePath, sentAtWriteGeneration, authorization) {
|
|
1431
|
+
const totalPages = parsed.xPages;
|
|
1432
|
+
if (totalPages <= 1) {
|
|
1433
|
+
return { headers: parsed.raw, body: data, status: 200 };
|
|
1434
|
+
}
|
|
1435
|
+
logInfo2(
|
|
1436
|
+
client,
|
|
1437
|
+
`Found ${totalPages} pages, fetching additional pages with rate limiting...`,
|
|
1438
|
+
{ endpoint, totalPages }
|
|
1439
|
+
);
|
|
1440
|
+
try {
|
|
1441
|
+
const firstPageData = Array.isArray(data) ? data : [data];
|
|
1442
|
+
const allData = await PaginationHandler.fetchRemainingPages(
|
|
1443
|
+
client,
|
|
1444
|
+
endpoint,
|
|
1445
|
+
method,
|
|
1446
|
+
requiresAuth,
|
|
1447
|
+
firstPageData,
|
|
1448
|
+
totalPages,
|
|
1449
|
+
body,
|
|
1450
|
+
{
|
|
1451
|
+
maxPages: Math.min(totalPages, 1e3),
|
|
1452
|
+
stopOnEmptyPage: true
|
|
1453
|
+
},
|
|
1454
|
+
pageFetch,
|
|
1455
|
+
templatePath
|
|
1456
|
+
);
|
|
1457
|
+
cacheResponse(
|
|
1458
|
+
client,
|
|
1459
|
+
url,
|
|
1460
|
+
method,
|
|
1461
|
+
endpoint,
|
|
1462
|
+
parsed,
|
|
1463
|
+
allData,
|
|
1464
|
+
useETag,
|
|
1465
|
+
resolveCache2,
|
|
1466
|
+
templatePath,
|
|
1467
|
+
requiresAuth,
|
|
1468
|
+
sentAtWriteGeneration,
|
|
1469
|
+
authorization
|
|
1470
|
+
);
|
|
1471
|
+
return { headers: parsed.raw, body: allData, status: 200 };
|
|
1472
|
+
} catch (paginationError) {
|
|
1473
|
+
const msg = paginationError instanceof Error ? paginationError.message : String(paginationError);
|
|
1474
|
+
logWarn2(client, `Pagination failed for ${url}: ${msg}`, {
|
|
1475
|
+
url,
|
|
1476
|
+
endpoint,
|
|
1477
|
+
error: msg
|
|
1478
|
+
});
|
|
1479
|
+
if (paginationError instanceof EsiError) {
|
|
1480
|
+
throw paginationError;
|
|
1481
|
+
}
|
|
1482
|
+
throw new EsiPaginationError(url, paginationError);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/core/requestPipeline/middlewareBridge.ts
|
|
1487
|
+
async function applyRequestMiddleware(client, url, endpoint, method, headers, body) {
|
|
1488
|
+
const middleware = client.getMiddleware();
|
|
1489
|
+
if (!middleware.hasInterceptors()) {
|
|
1490
|
+
return { url, headers, body };
|
|
1491
|
+
}
|
|
1492
|
+
const reqCtx = {
|
|
1493
|
+
url,
|
|
1494
|
+
endpoint,
|
|
1495
|
+
method,
|
|
1496
|
+
headers: { ...headers },
|
|
1497
|
+
body
|
|
1498
|
+
};
|
|
1499
|
+
const modified = await middleware.applyRequestInterceptors(reqCtx);
|
|
1500
|
+
return {
|
|
1501
|
+
url: modified.url,
|
|
1502
|
+
headers: modified.headers,
|
|
1503
|
+
body: modified.body
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function applyResponseInterceptors(client, result, url, endpoint, method, startTime) {
|
|
1507
|
+
const middleware = client.getMiddleware();
|
|
1508
|
+
if (!middleware.hasInterceptors()) return result;
|
|
1509
|
+
const responseCtx = {
|
|
1510
|
+
url,
|
|
1511
|
+
endpoint,
|
|
1512
|
+
method,
|
|
1513
|
+
status: result.status ?? 200,
|
|
1514
|
+
headers: result.headers,
|
|
1515
|
+
body: result.body,
|
|
1516
|
+
durationMs: Date.now() - startTime,
|
|
1517
|
+
fromCache: result.fromCache ?? false
|
|
1518
|
+
};
|
|
1519
|
+
const modified = await middleware.applyResponseInterceptors(responseCtx);
|
|
1520
|
+
return {
|
|
1521
|
+
...result,
|
|
1522
|
+
headers: modified.headers,
|
|
1523
|
+
body: modified.body
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/core/util/headersUtil.ts
|
|
1528
|
+
function parseWarning(value) {
|
|
1529
|
+
if (!value) return null;
|
|
1530
|
+
const match = /^(\d{3}) - "([^"]+)"$/.exec(value) ?? /^(\d{3}) - ([^ ].+[^ ])$/.exec(value);
|
|
1531
|
+
if (!match) return null;
|
|
1532
|
+
const [, code, message] = match;
|
|
1533
|
+
return { code: parseInt(code, 10), message };
|
|
1534
|
+
}
|
|
1535
|
+
function parseHeaders(fetchHeaders) {
|
|
1536
|
+
const raw = {};
|
|
1537
|
+
fetchHeaders.forEach((value, key) => {
|
|
1538
|
+
raw[key.toLowerCase()] = value;
|
|
1539
|
+
});
|
|
1540
|
+
const cursorBefore = raw["x-cursor-before"] ?? null;
|
|
1541
|
+
const cursorAfter = raw["x-cursor-after"] ?? null;
|
|
1542
|
+
return {
|
|
1543
|
+
raw,
|
|
1544
|
+
xPages: parseInt(raw["x-pages"] ?? "1", 10),
|
|
1545
|
+
etag: raw["etag"] ?? null,
|
|
1546
|
+
cacheControl: raw["cache-control"] ?? null,
|
|
1547
|
+
expires: raw["expires"] ?? null,
|
|
1548
|
+
lastModified: raw["last-modified"] ?? null,
|
|
1549
|
+
cursorBefore,
|
|
1550
|
+
cursorAfter,
|
|
1551
|
+
hasCursorPagination: "x-cursor-before" in raw || "x-cursor-after" in raw,
|
|
1552
|
+
warning: parseWarning(raw["warning"]),
|
|
1553
|
+
requestId: raw["x-esi-request-id"] ?? null,
|
|
1554
|
+
date: raw["date"] ?? null,
|
|
1555
|
+
contentLanguage: raw["content-language"] ?? null
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/core/requestPipeline/fetchExecution.ts
|
|
1560
|
+
function isAbortError(err) {
|
|
1561
|
+
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
1562
|
+
}
|
|
1563
|
+
function networkError(err, url) {
|
|
1564
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1565
|
+
return new EsiNetworkError(reason, url, err);
|
|
1566
|
+
}
|
|
1567
|
+
async function bufferBody(response) {
|
|
1568
|
+
if (typeof response.text !== "function") return;
|
|
1569
|
+
const text = await response.text();
|
|
1570
|
+
Object.defineProperties(response, {
|
|
1571
|
+
text: { value: () => Promise.resolve(text), configurable: true },
|
|
1572
|
+
json: {
|
|
1573
|
+
value: () => Promise.resolve(text).then((t) => JSON.parse(t)),
|
|
1574
|
+
configurable: true
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
async function executeSingleFetch(client, endpoint, method, body, requiresAuth, useETag, resolveCache2, resolveRateLimiter2, resolveCircuitBreaker2, requestTimeout, templatePath, accept) {
|
|
1579
|
+
const rawUrl = `${client.getLink()}/${endpoint}`;
|
|
1580
|
+
const builtHeaders = buildRequestHeaders(
|
|
1581
|
+
client,
|
|
1582
|
+
rawUrl,
|
|
1583
|
+
method,
|
|
1584
|
+
requiresAuth,
|
|
1585
|
+
useETag,
|
|
1586
|
+
body,
|
|
1587
|
+
resolveCache2,
|
|
1588
|
+
accept
|
|
1589
|
+
);
|
|
1590
|
+
const req = await applyRequestMiddleware(
|
|
1591
|
+
client,
|
|
1592
|
+
rawUrl,
|
|
1593
|
+
endpoint,
|
|
1594
|
+
method,
|
|
1595
|
+
builtHeaders,
|
|
1596
|
+
body
|
|
1597
|
+
);
|
|
1598
|
+
const options = {
|
|
1599
|
+
method,
|
|
1600
|
+
headers: req.headers,
|
|
1601
|
+
...req.body ? { body: JSON.stringify(req.body) } : {}
|
|
1602
|
+
};
|
|
1603
|
+
const url = req.url;
|
|
1604
|
+
logInfo2(client, `Hitting endpoint: ${url}`, { method, endpoint });
|
|
1605
|
+
const cb = resolveCircuitBreaker2(client);
|
|
1606
|
+
const cbKey = cb && templatePath && cb.getKeyStrategy?.() === "template" ? templatePath : endpoint;
|
|
1607
|
+
if (cb) cb.checkCircuit(cbKey);
|
|
1608
|
+
let cbRecorded = false;
|
|
1609
|
+
try {
|
|
1610
|
+
const rateLimiter = resolveRateLimiter2(client);
|
|
1611
|
+
await rateLimiter.checkRateLimit(templatePath, method, req.headers);
|
|
1612
|
+
const timeoutMs = requestTimeout ?? client.getTimeout();
|
|
1613
|
+
const controller = new AbortController();
|
|
1614
|
+
const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
1615
|
+
options.signal = controller.signal;
|
|
1616
|
+
let response;
|
|
1617
|
+
try {
|
|
1618
|
+
response = await client.getFetch()(url, options);
|
|
1619
|
+
await bufferBody(response);
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
clearTimeout(timer);
|
|
1622
|
+
if (cb) {
|
|
1623
|
+
cb.recordFailure(cbKey, 0);
|
|
1624
|
+
cbRecorded = true;
|
|
1625
|
+
}
|
|
1626
|
+
if (err instanceof EsiError) throw err;
|
|
1627
|
+
if (isAbortError(err)) {
|
|
1628
|
+
throw new TimeoutError(timeoutMs, url);
|
|
1629
|
+
}
|
|
1630
|
+
throw networkError(err, url);
|
|
1631
|
+
}
|
|
1632
|
+
clearTimeout(timer);
|
|
1633
|
+
const parsed = parseHeaders(response.headers);
|
|
1634
|
+
if (requiresAuth && (response.ok || response.status === 304)) {
|
|
1635
|
+
const sent = req.headers["Authorization"];
|
|
1636
|
+
if (sent) markTokenAccepted(client, sent);
|
|
1637
|
+
}
|
|
1638
|
+
rateLimiter.updateFromResponse(
|
|
1639
|
+
parsed.raw,
|
|
1640
|
+
response.status,
|
|
1641
|
+
templatePath,
|
|
1642
|
+
method,
|
|
1643
|
+
req.headers
|
|
1644
|
+
);
|
|
1645
|
+
if (cb) {
|
|
1646
|
+
if (response.status >= 500 || response.status === 420 || response.status === 429) {
|
|
1647
|
+
cb.recordFailure(cbKey, response.status);
|
|
1648
|
+
} else {
|
|
1649
|
+
cb.recordSuccess(cbKey);
|
|
1650
|
+
}
|
|
1651
|
+
cbRecorded = true;
|
|
1652
|
+
}
|
|
1653
|
+
if (parsed.warning) {
|
|
1654
|
+
logWarn2(
|
|
1655
|
+
client,
|
|
1656
|
+
`ESI Warning ${parsed.warning.code}: ${parsed.warning.message}`,
|
|
1657
|
+
{
|
|
1658
|
+
url,
|
|
1659
|
+
warningCode: parsed.warning.code
|
|
1660
|
+
}
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
response,
|
|
1665
|
+
parsed,
|
|
1666
|
+
url,
|
|
1667
|
+
authorization: req.headers["Authorization"]
|
|
1668
|
+
};
|
|
1669
|
+
} finally {
|
|
1670
|
+
if (cb && !cbRecorded) {
|
|
1671
|
+
cb.recordFailure(cbKey, 0);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async function parseJsonBody(client, response, _url) {
|
|
1676
|
+
try {
|
|
1677
|
+
return await response.json();
|
|
1678
|
+
} catch (jsonError) {
|
|
1679
|
+
const msg = jsonError instanceof Error ? jsonError.message : String(jsonError);
|
|
1680
|
+
logError2(client, `Failed to parse JSON response: ${msg}`, { url: _url });
|
|
1681
|
+
throw new EsiParseError(`Invalid JSON response: ${msg}`, _url, jsonError);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
async function fetchOnePage(client, endpoint, method, body, requiresAuth, useETag, resolveCache2, resolveRateLimiter2, resolveCircuitBreaker2, requestTimeout, templatePath) {
|
|
1685
|
+
const { response, parsed, url } = await executeSingleFetch(
|
|
1686
|
+
client,
|
|
1687
|
+
endpoint,
|
|
1688
|
+
method,
|
|
1689
|
+
body,
|
|
1690
|
+
requiresAuth,
|
|
1691
|
+
useETag,
|
|
1692
|
+
resolveCache2,
|
|
1693
|
+
resolveRateLimiter2,
|
|
1694
|
+
resolveCircuitBreaker2,
|
|
1695
|
+
requestTimeout,
|
|
1696
|
+
templatePath
|
|
1697
|
+
);
|
|
1698
|
+
if (!response.ok) {
|
|
1699
|
+
const text = statusMessage(response);
|
|
1700
|
+
const reason = await readEsiErrorReason(response);
|
|
1701
|
+
throw new EsiError(
|
|
1702
|
+
response.status,
|
|
1703
|
+
reason ? `${text}: ${reason}` : text,
|
|
1704
|
+
url,
|
|
1705
|
+
parsed.requestId ?? void 0
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
const data = await parseJsonBody(client, response, url);
|
|
1709
|
+
return { data, parsed, url };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/core/ApiRequestHandler.ts
|
|
1713
|
+
var executeRequest = async (client, endpoint, method, body, requiresAuth = false, useETag = true, requestTimeout, templatePath, textResponse, emptyWhenNoContent) => {
|
|
1714
|
+
const startTime = Date.now();
|
|
1715
|
+
const finish = (r) => {
|
|
1716
|
+
r.responseTimeMs = Date.now() - startTime;
|
|
1717
|
+
const rawUrl = `${client.getLink()}/${endpoint}`;
|
|
1718
|
+
return applyResponseInterceptors(
|
|
1719
|
+
client,
|
|
1720
|
+
r,
|
|
1721
|
+
rawUrl,
|
|
1722
|
+
endpoint,
|
|
1723
|
+
method,
|
|
1724
|
+
startTime
|
|
1725
|
+
);
|
|
1726
|
+
};
|
|
1727
|
+
try {
|
|
1728
|
+
const writeGeneration = currentWriteGeneration(client, resolveCache);
|
|
1729
|
+
const revalidating = useETag && method === "GET" && hasCachedEntry(
|
|
1730
|
+
client,
|
|
1731
|
+
`${client.getLink()}/${endpoint}`,
|
|
1732
|
+
resolveCache,
|
|
1733
|
+
requiresAuth
|
|
1734
|
+
);
|
|
1735
|
+
const { response, parsed, url, authorization } = await executeSingleFetch(
|
|
1736
|
+
client,
|
|
1737
|
+
endpoint,
|
|
1738
|
+
method,
|
|
1739
|
+
body,
|
|
1740
|
+
requiresAuth,
|
|
1741
|
+
useETag,
|
|
1742
|
+
resolveCache,
|
|
1743
|
+
resolveRateLimiter,
|
|
1744
|
+
resolveCircuitBreaker,
|
|
1745
|
+
requestTimeout,
|
|
1746
|
+
templatePath,
|
|
1747
|
+
textResponse?.accept
|
|
1748
|
+
);
|
|
1749
|
+
if (response.status === 201 || response.status === 204) {
|
|
1750
|
+
invalidateAfterWrite(client, method, endpoint, resolveCache);
|
|
1751
|
+
}
|
|
1752
|
+
if (response.status === 201) {
|
|
1753
|
+
let data2;
|
|
1754
|
+
try {
|
|
1755
|
+
data2 = await response.json();
|
|
1756
|
+
} catch {
|
|
1757
|
+
data2 = void 0;
|
|
1758
|
+
}
|
|
1759
|
+
return finish({ headers: parsed.raw, body: data2, status: 201 });
|
|
1760
|
+
}
|
|
1761
|
+
if (emptyWhenNoContent && response.status === 200 && response.headers.get("content-length") === "0") {
|
|
1762
|
+
logInfo2(client, `No Content for endpoint: ${url}`, { status: 200 });
|
|
1763
|
+
return finish({ headers: parsed.raw, body: void 0, status: 200 });
|
|
1764
|
+
}
|
|
1765
|
+
if (response.status === 304 && revalidating && !hasCachedEntry(client, url, resolveCache, requiresAuth, authorization)) {
|
|
1766
|
+
return await executeRequest(
|
|
1767
|
+
client,
|
|
1768
|
+
endpoint,
|
|
1769
|
+
method,
|
|
1770
|
+
body,
|
|
1771
|
+
requiresAuth,
|
|
1772
|
+
useETag,
|
|
1773
|
+
requestTimeout,
|
|
1774
|
+
templatePath,
|
|
1775
|
+
textResponse,
|
|
1776
|
+
emptyWhenNoContent
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
const earlyResult = handleEarlyStatus(
|
|
1780
|
+
client,
|
|
1781
|
+
response.status,
|
|
1782
|
+
url,
|
|
1783
|
+
parsed,
|
|
1784
|
+
useETag,
|
|
1785
|
+
resolveCache,
|
|
1786
|
+
requiresAuth,
|
|
1787
|
+
authorization
|
|
1788
|
+
);
|
|
1789
|
+
if (earlyResult) return finish(earlyResult);
|
|
1790
|
+
if (!response.ok) {
|
|
1791
|
+
const staleOrThrow = handleErrorResponse(
|
|
1792
|
+
client,
|
|
1793
|
+
response,
|
|
1794
|
+
url,
|
|
1795
|
+
parsed,
|
|
1796
|
+
useETag,
|
|
1797
|
+
resolveCache,
|
|
1798
|
+
requiresAuth,
|
|
1799
|
+
await readEsiErrorReason(response),
|
|
1800
|
+
authorization
|
|
1801
|
+
);
|
|
1802
|
+
return finish(staleOrThrow);
|
|
1803
|
+
}
|
|
1804
|
+
const data = textResponse ? await response.text() : await parseJsonBody(client, response, url);
|
|
1805
|
+
if (parsed.xPages <= 1 || parsed.hasCursorPagination) {
|
|
1806
|
+
cacheResponse(
|
|
1807
|
+
client,
|
|
1808
|
+
url,
|
|
1809
|
+
method,
|
|
1810
|
+
endpoint,
|
|
1811
|
+
parsed,
|
|
1812
|
+
data,
|
|
1813
|
+
useETag,
|
|
1814
|
+
resolveCache,
|
|
1815
|
+
templatePath,
|
|
1816
|
+
requiresAuth,
|
|
1817
|
+
writeGeneration,
|
|
1818
|
+
authorization
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
const cursorResult = handleCursorPagination(parsed, data);
|
|
1822
|
+
if (cursorResult) return finish(cursorResult);
|
|
1823
|
+
const pageFetch = async (paginatedEndpoint) => {
|
|
1824
|
+
const result = await fetchOnePage(
|
|
1825
|
+
client,
|
|
1826
|
+
paginatedEndpoint,
|
|
1827
|
+
method,
|
|
1828
|
+
body,
|
|
1829
|
+
requiresAuth,
|
|
1830
|
+
false,
|
|
1831
|
+
resolveCache,
|
|
1832
|
+
resolveRateLimiter,
|
|
1833
|
+
resolveCircuitBreaker,
|
|
1834
|
+
requestTimeout,
|
|
1835
|
+
templatePath
|
|
1836
|
+
);
|
|
1837
|
+
return Array.isArray(result.data) ? result.data : [result.data];
|
|
1838
|
+
};
|
|
1839
|
+
const paginatedResult = await handleOffsetPagination(
|
|
1840
|
+
client,
|
|
1841
|
+
endpoint,
|
|
1842
|
+
method,
|
|
1843
|
+
requiresAuth,
|
|
1844
|
+
parsed,
|
|
1845
|
+
data,
|
|
1846
|
+
body,
|
|
1847
|
+
url,
|
|
1848
|
+
useETag,
|
|
1849
|
+
pageFetch,
|
|
1850
|
+
resolveCache,
|
|
1851
|
+
templatePath,
|
|
1852
|
+
writeGeneration,
|
|
1853
|
+
authorization
|
|
1854
|
+
);
|
|
1855
|
+
return finish(paginatedResult);
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
wrapError(error, client);
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
var handleSinglePageRequest = async (client, endpoint, method, body, requiresAuth = false, templatePath, requestTimeout) => {
|
|
1861
|
+
const doExecute = () => fetchOnePage(
|
|
1862
|
+
client,
|
|
1863
|
+
endpoint,
|
|
1864
|
+
method,
|
|
1865
|
+
body,
|
|
1866
|
+
requiresAuth,
|
|
1867
|
+
false,
|
|
1868
|
+
resolveCache,
|
|
1869
|
+
resolveRateLimiter,
|
|
1870
|
+
resolveCircuitBreaker,
|
|
1871
|
+
requestTimeout,
|
|
1872
|
+
templatePath
|
|
1873
|
+
).then(({ data, parsed }) => ({
|
|
1874
|
+
headers: parsed.raw,
|
|
1875
|
+
body: data
|
|
1876
|
+
}));
|
|
1877
|
+
const retryStrategy = resolveRetryStrategy(client);
|
|
1878
|
+
return retryStrategy.execute(doExecute, {
|
|
1879
|
+
client,
|
|
1880
|
+
endpoint,
|
|
1881
|
+
method,
|
|
1882
|
+
requiresAuth,
|
|
1883
|
+
refreshToken: client.hasTokenProvider() ? () => client.refreshToken().then(() => {
|
|
1884
|
+
}) : void 0
|
|
1885
|
+
});
|
|
1886
|
+
};
|
|
1887
|
+
var handleRequest = async (client, endpoint, method, body, requiresAuth = false, useETag = true, templatePath, requestTimeout, textResponse, emptyWhenNoContent = false) => {
|
|
1888
|
+
const rawUrl = `${client.getLink()}/${endpoint}`;
|
|
1889
|
+
const startTime = Date.now();
|
|
1890
|
+
const specCacheHit = () => {
|
|
1891
|
+
const hit = trySpecAwareCacheHit(
|
|
1892
|
+
client,
|
|
1893
|
+
rawUrl,
|
|
1894
|
+
method,
|
|
1895
|
+
templatePath,
|
|
1896
|
+
resolveCache,
|
|
1897
|
+
requiresAuth
|
|
1898
|
+
);
|
|
1899
|
+
return hit ? applyResponseInterceptors(
|
|
1900
|
+
client,
|
|
1901
|
+
hit,
|
|
1902
|
+
rawUrl,
|
|
1903
|
+
endpoint,
|
|
1904
|
+
method,
|
|
1905
|
+
startTime
|
|
1906
|
+
) : null;
|
|
1907
|
+
};
|
|
1908
|
+
const specHit = specCacheHit();
|
|
1909
|
+
if (specHit) return specHit;
|
|
1910
|
+
const doExecute = () => executeRequest(
|
|
1911
|
+
client,
|
|
1912
|
+
endpoint,
|
|
1913
|
+
method,
|
|
1914
|
+
body,
|
|
1915
|
+
requiresAuth,
|
|
1916
|
+
useETag,
|
|
1917
|
+
requestTimeout,
|
|
1918
|
+
templatePath,
|
|
1919
|
+
textResponse,
|
|
1920
|
+
emptyWhenNoContent
|
|
1921
|
+
);
|
|
1922
|
+
const dedup = client.getDeduplicator();
|
|
1923
|
+
const canDedup = dedup && method === "GET" && !body;
|
|
1924
|
+
let attempted = false;
|
|
1925
|
+
const operation = () => {
|
|
1926
|
+
const retryHit = attempted ? specCacheHit() : null;
|
|
1927
|
+
attempted = true;
|
|
1928
|
+
if (retryHit) return retryHit;
|
|
1929
|
+
return canDedup ? dedup.dedupe(
|
|
1930
|
+
buildDedupeKey(endpoint, client, requiresAuth),
|
|
1931
|
+
doExecute
|
|
1932
|
+
) : doExecute();
|
|
1933
|
+
};
|
|
1934
|
+
const retryStrategy = resolveRetryStrategy(client);
|
|
1935
|
+
return retryStrategy.execute(operation, {
|
|
1936
|
+
client,
|
|
1937
|
+
endpoint,
|
|
1938
|
+
method,
|
|
1939
|
+
requiresAuth,
|
|
1940
|
+
refreshToken: client.hasTokenProvider() ? () => client.refreshToken().then(() => {
|
|
1941
|
+
}) : void 0
|
|
1942
|
+
});
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// src/core/pagination/AsyncPaginationIterator.ts
|
|
1946
|
+
function extractArray(body) {
|
|
1947
|
+
if (Array.isArray(body)) return body;
|
|
1948
|
+
if (body !== void 0 && body !== null) return [body];
|
|
1949
|
+
return [];
|
|
1950
|
+
}
|
|
1951
|
+
function validatePageBody(body, schema, url, validate) {
|
|
1952
|
+
if (!schema || !validate) return body;
|
|
1953
|
+
const result = schema.safeParse(body);
|
|
1954
|
+
if (!result.success) {
|
|
1955
|
+
throw new EsiValidationError(url, result.error);
|
|
1956
|
+
}
|
|
1957
|
+
return result.data;
|
|
1958
|
+
}
|
|
1959
|
+
async function fetchAllPages(client, endpoint, method, requiresAuth = false, body, templatePath, responseSchema, concurrency = 8) {
|
|
1960
|
+
const validate = client.getValidateResponse();
|
|
1961
|
+
const firstResponse = await handleSinglePageRequest(
|
|
1962
|
+
client,
|
|
1963
|
+
endpoint,
|
|
1964
|
+
method,
|
|
1965
|
+
body,
|
|
1966
|
+
requiresAuth,
|
|
1967
|
+
templatePath
|
|
1968
|
+
);
|
|
1969
|
+
const firstBody = validatePageBody(
|
|
1970
|
+
firstResponse.body,
|
|
1971
|
+
responseSchema,
|
|
1972
|
+
`${client.getLink()}/${endpoint}`,
|
|
1973
|
+
validate
|
|
1974
|
+
);
|
|
1975
|
+
const totalPages = parseInt(firstResponse.headers["x-pages"] || "1", 10);
|
|
1976
|
+
const result = extractArray(firstBody);
|
|
1977
|
+
const remaining = Array.from({ length: totalPages - 1 }, (_, i) => i + 2);
|
|
1978
|
+
for (let i = 0; i < remaining.length; i += concurrency) {
|
|
1979
|
+
const batch = remaining.slice(i, i + concurrency);
|
|
1980
|
+
const pages = await Promise.all(
|
|
1981
|
+
batch.map(async (page) => {
|
|
1982
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
1983
|
+
const pagedEndpoint = `${endpoint}${sep}page=${page}`;
|
|
1984
|
+
logInfo2(
|
|
1985
|
+
client,
|
|
1986
|
+
`Fetching page ${page}/${totalPages}: ${pagedEndpoint}`,
|
|
1987
|
+
{ page, totalPages }
|
|
1988
|
+
);
|
|
1989
|
+
const response = await handleSinglePageRequest(
|
|
1990
|
+
client,
|
|
1991
|
+
pagedEndpoint,
|
|
1992
|
+
method,
|
|
1993
|
+
body,
|
|
1994
|
+
requiresAuth,
|
|
1995
|
+
templatePath
|
|
1996
|
+
);
|
|
1997
|
+
return validatePageBody(
|
|
1998
|
+
response.body,
|
|
1999
|
+
responseSchema,
|
|
2000
|
+
`${client.getLink()}/${pagedEndpoint}`,
|
|
2001
|
+
validate
|
|
2002
|
+
);
|
|
2003
|
+
})
|
|
2004
|
+
);
|
|
2005
|
+
for (const pageBody of pages) {
|
|
2006
|
+
result.push(...extractArray(pageBody));
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return result;
|
|
2010
|
+
}
|
|
2011
|
+
async function* fetchPages(client, endpoint, method, requiresAuth = false, body, templatePath, responseSchema) {
|
|
2012
|
+
const validate = client.getValidateResponse();
|
|
2013
|
+
const firstResponse = await handleSinglePageRequest(
|
|
2014
|
+
client,
|
|
2015
|
+
endpoint,
|
|
2016
|
+
method,
|
|
2017
|
+
body,
|
|
2018
|
+
requiresAuth,
|
|
2019
|
+
templatePath
|
|
2020
|
+
);
|
|
2021
|
+
const firstBody = validatePageBody(
|
|
2022
|
+
firstResponse.body,
|
|
2023
|
+
responseSchema,
|
|
2024
|
+
`${client.getLink()}/${endpoint}`,
|
|
2025
|
+
validate
|
|
2026
|
+
);
|
|
2027
|
+
const totalPages = parseInt(firstResponse.headers["x-pages"] || "1", 10);
|
|
2028
|
+
yield { data: extractArray(firstBody), page: 1, totalPages };
|
|
2029
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
2030
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
2031
|
+
const pagedEndpoint = `${endpoint}${sep}page=${page}`;
|
|
2032
|
+
logInfo2(client, `Fetching page ${page}/${totalPages}: ${pagedEndpoint}`, {
|
|
2033
|
+
page,
|
|
2034
|
+
totalPages
|
|
2035
|
+
});
|
|
2036
|
+
const response = await handleSinglePageRequest(
|
|
2037
|
+
client,
|
|
2038
|
+
pagedEndpoint,
|
|
2039
|
+
method,
|
|
2040
|
+
body,
|
|
2041
|
+
requiresAuth,
|
|
2042
|
+
templatePath
|
|
2043
|
+
);
|
|
2044
|
+
const pageBody = validatePageBody(
|
|
2045
|
+
response.body,
|
|
2046
|
+
responseSchema,
|
|
2047
|
+
`${client.getLink()}/${pagedEndpoint}`,
|
|
2048
|
+
validate
|
|
2049
|
+
);
|
|
2050
|
+
const pageData = extractArray(pageBody);
|
|
2051
|
+
if (pageData.length === 0) break;
|
|
2052
|
+
yield { data: pageData, page, totalPages };
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/core/cache/ETagCacheManager.ts
|
|
2057
|
+
var ETagCacheManager = class {
|
|
2058
|
+
cache = /* @__PURE__ */ new Map();
|
|
2059
|
+
config;
|
|
2060
|
+
cleanupTimer;
|
|
2061
|
+
hits = 0;
|
|
2062
|
+
misses = 0;
|
|
2063
|
+
client = null;
|
|
2064
|
+
setClient(client) {
|
|
2065
|
+
this.client = client;
|
|
2066
|
+
}
|
|
2067
|
+
log(level, message, context) {
|
|
2068
|
+
const ctx = context && Object.keys(context).length > 0 ? context : void 0;
|
|
2069
|
+
const emit3 = level === "info" ? logInfo2 : logDebug2;
|
|
2070
|
+
emit3(this.client, message, ctx);
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* @param client - the client whose logger this cache logs to, from the
|
|
2074
|
+
* first line (the startup log) on. `setClient` can attach or change it
|
|
2075
|
+
* later; without one the cache logs to the global logger.
|
|
2076
|
+
*/
|
|
2077
|
+
constructor(config = {}, client) {
|
|
2078
|
+
this.client = client ?? null;
|
|
2079
|
+
this.config = {
|
|
2080
|
+
maxEntries: config.maxEntries ?? 1e3,
|
|
2081
|
+
defaultTtl: config.defaultTtl ?? 5 * 60 * 1e3,
|
|
2082
|
+
// 5 minutes default
|
|
2083
|
+
cleanupInterval: config.cleanupInterval ?? 60 * 1e3
|
|
2084
|
+
// 1 minute cleanup
|
|
2085
|
+
};
|
|
2086
|
+
this.startCleanupTimer();
|
|
2087
|
+
this.log(
|
|
2088
|
+
"info",
|
|
2089
|
+
`ETag cache manager initialized with ${this.config.maxEntries} max entries`,
|
|
2090
|
+
{ maxEntries: this.config.maxEntries }
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Get cached data if ETag matches
|
|
2095
|
+
*/
|
|
2096
|
+
get(url) {
|
|
2097
|
+
const entry = this.cache.get(url);
|
|
2098
|
+
if (!entry) {
|
|
2099
|
+
this.misses++;
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
if (this.isExpired(entry)) {
|
|
2103
|
+
this.cache.delete(url);
|
|
2104
|
+
this.misses++;
|
|
2105
|
+
this.log("debug", `Cache entry expired for ${url}`, { url });
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
this.hits++;
|
|
2109
|
+
this.log("debug", `Cache hit for ${url} with ETag ${entry.etag}`, { url });
|
|
2110
|
+
return entry;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Get ETag for a URL (for If-None-Match header)
|
|
2114
|
+
*/
|
|
2115
|
+
getETag(url) {
|
|
2116
|
+
const entry = this.get(url);
|
|
2117
|
+
return entry ? entry.etag : null;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Set cache entry
|
|
2121
|
+
*/
|
|
2122
|
+
set(url, etag, data, headers, customTtl) {
|
|
2123
|
+
if (!this.cache.has(url) && this.cache.size >= this.config.maxEntries) {
|
|
2124
|
+
this.evictOldest();
|
|
2125
|
+
}
|
|
2126
|
+
const entry = {
|
|
2127
|
+
etag,
|
|
2128
|
+
data,
|
|
2129
|
+
headers,
|
|
2130
|
+
timestamp: Date.now(),
|
|
2131
|
+
ttl: customTtl ?? this.config.defaultTtl
|
|
2132
|
+
};
|
|
2133
|
+
this.cache.set(url, entry);
|
|
2134
|
+
this.log("debug", `Cached response for ${url} with ETag ${etag}`, { url });
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Check if cached data exists and is valid
|
|
2138
|
+
*/
|
|
2139
|
+
has(url) {
|
|
2140
|
+
const entry = this.cache.get(url);
|
|
2141
|
+
return entry !== void 0 && !this.isExpired(entry);
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Clear specific cache entry
|
|
2145
|
+
*/
|
|
2146
|
+
delete(url) {
|
|
2147
|
+
return this.cache.delete(url);
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Delete all cache entries whose URL contains the given path segment
|
|
2151
|
+
*/
|
|
2152
|
+
deleteByPath(pathSegment) {
|
|
2153
|
+
let count = 0;
|
|
2154
|
+
for (const key of this.cache.keys()) {
|
|
2155
|
+
if (key.includes(pathSegment)) {
|
|
2156
|
+
this.cache.delete(key);
|
|
2157
|
+
count++;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (count > 0) {
|
|
2161
|
+
this.log(
|
|
2162
|
+
"debug",
|
|
2163
|
+
`Invalidated ${count} cache entries matching ${pathSegment}`,
|
|
2164
|
+
{
|
|
2165
|
+
count
|
|
2166
|
+
}
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
return count;
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Clear all cache entries
|
|
2173
|
+
*/
|
|
2174
|
+
clear() {
|
|
2175
|
+
this.cache.clear();
|
|
2176
|
+
this.hits = 0;
|
|
2177
|
+
this.misses = 0;
|
|
2178
|
+
this.log("info", "ETag cache cleared");
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Get cache statistics
|
|
2182
|
+
*/
|
|
2183
|
+
getStats() {
|
|
2184
|
+
let oldest = Infinity;
|
|
2185
|
+
let newest = -Infinity;
|
|
2186
|
+
for (const entry of this.cache.values()) {
|
|
2187
|
+
if (entry.timestamp < oldest) oldest = entry.timestamp;
|
|
2188
|
+
if (entry.timestamp > newest) newest = entry.timestamp;
|
|
2189
|
+
}
|
|
2190
|
+
const total = this.hits + this.misses;
|
|
2191
|
+
return {
|
|
2192
|
+
totalEntries: this.cache.size,
|
|
2193
|
+
maxEntries: this.config.maxEntries,
|
|
2194
|
+
hits: this.hits,
|
|
2195
|
+
misses: this.misses,
|
|
2196
|
+
hitRate: total > 0 ? this.hits / total : 0,
|
|
2197
|
+
oldestEntry: this.cache.size > 0 ? oldest : null,
|
|
2198
|
+
newestEntry: this.cache.size > 0 ? newest : null
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Update cache configuration
|
|
2203
|
+
*/
|
|
2204
|
+
updateConfig(newConfig) {
|
|
2205
|
+
this.config = {
|
|
2206
|
+
maxEntries: newConfig.maxEntries ?? this.config.maxEntries,
|
|
2207
|
+
defaultTtl: newConfig.defaultTtl ?? this.config.defaultTtl,
|
|
2208
|
+
cleanupInterval: newConfig.cleanupInterval ?? this.config.cleanupInterval
|
|
2209
|
+
};
|
|
2210
|
+
this.log("info", "ETag cache configuration updated", {
|
|
2211
|
+
maxEntries: this.config.maxEntries
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Cleanup expired entries
|
|
2216
|
+
*/
|
|
2217
|
+
cleanup() {
|
|
2218
|
+
const beforeSize = this.cache.size;
|
|
2219
|
+
for (const [url, entry] of this.cache.entries()) {
|
|
2220
|
+
if (this.isExpired(entry)) {
|
|
2221
|
+
this.cache.delete(url);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const cleanedCount = beforeSize - this.cache.size;
|
|
2225
|
+
if (cleanedCount > 0) {
|
|
2226
|
+
this.log("debug", `Cleaned up ${cleanedCount} expired cache entries`, {
|
|
2227
|
+
cleanedCount
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
return cleanedCount;
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Shutdown cache manager
|
|
2234
|
+
*/
|
|
2235
|
+
shutdown() {
|
|
2236
|
+
if (this.cleanupTimer) {
|
|
2237
|
+
clearInterval(this.cleanupTimer);
|
|
2238
|
+
}
|
|
2239
|
+
this.log("info", "ETag cache manager shut down");
|
|
2240
|
+
}
|
|
2241
|
+
isExpired(entry) {
|
|
2242
|
+
if (!entry.ttl) return false;
|
|
2243
|
+
return Date.now() - entry.timestamp > entry.ttl;
|
|
2244
|
+
}
|
|
2245
|
+
evictOldest() {
|
|
2246
|
+
let oldestKey = null;
|
|
2247
|
+
let oldestTime = Infinity;
|
|
2248
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
2249
|
+
if (entry.timestamp < oldestTime) {
|
|
2250
|
+
oldestTime = entry.timestamp;
|
|
2251
|
+
oldestKey = key;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
if (oldestKey) {
|
|
2255
|
+
this.cache.delete(oldestKey);
|
|
2256
|
+
this.log("debug", `Evicted oldest cache entry: ${oldestKey}`, {
|
|
2257
|
+
url: oldestKey
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
startCleanupTimer() {
|
|
2262
|
+
this.cleanupTimer = setInterval(() => {
|
|
2263
|
+
this.cleanup();
|
|
2264
|
+
}, this.config.cleanupInterval);
|
|
2265
|
+
this.cleanupTimer.unref();
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
// src/core/endpoints/esi-rate-limit-groups.generated.ts
|
|
2270
|
+
var esiRateLimitGroups = {
|
|
2271
|
+
"GET:alliances/{alliance_id}/contacts": { group: "alliance-social", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2272
|
+
"GET:alliances/{alliance_id}/contacts/labels": { group: "alliance-social", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2273
|
+
"GET:characters/{character_id}/access-lists": { group: "char-access", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2274
|
+
"GET:characters/{character_id}/access-lists/{access_list_id}": { group: "char-access", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2275
|
+
"GET:characters/{character_id}/agents_research": { group: "char-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2276
|
+
"GET:characters/{character_id}/assets": { group: "char-asset", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2277
|
+
"POST:characters/{character_id}/assets/locations": { group: "char-asset", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2278
|
+
"POST:characters/{character_id}/assets/names": { group: "char-asset", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2279
|
+
"GET:characters/{character_id}/attributes": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2280
|
+
"GET:characters/{character_id}/blueprints": { group: "char-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2281
|
+
"GET:characters/{character_id}/calendar": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2282
|
+
"GET:characters/{character_id}/calendar/{event_id}": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2283
|
+
"PUT:characters/{character_id}/calendar/{event_id}": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2284
|
+
"GET:characters/{character_id}/calendar/{event_id}/attendees": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2285
|
+
"GET:characters/{character_id}/clones": { group: "char-location", maxTokens: 1200, windowSizeMs: 9e5 },
|
|
2286
|
+
"GET:characters/{character_id}/contacts": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2287
|
+
"POST:characters/{character_id}/contacts": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2288
|
+
"PUT:characters/{character_id}/contacts": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2289
|
+
"DELETE:characters/{character_id}/contacts": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2290
|
+
"GET:characters/{character_id}/contacts/labels": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2291
|
+
"GET:characters/{character_id}/contracts": { group: "char-contract", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2292
|
+
"GET:characters/{character_id}/contracts/{contract_id}/bids": { group: "char-contract", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2293
|
+
"GET:characters/{character_id}/contracts/{contract_id}/items": { group: "char-contract", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2294
|
+
"GET:characters/{character_id}/cosmetics/skinr": { group: "char-skinr", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2295
|
+
"GET:characters/{character_id}/cosmetics/skinr/components": { group: "char-skinr", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2296
|
+
"POST:characters/{character_id}/cspa": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2297
|
+
"GET:characters/{character_id}/fatigue": { group: "char-location", maxTokens: 1200, windowSizeMs: 9e5 },
|
|
2298
|
+
"GET:characters/{character_id}/fittings": { group: "fitting", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2299
|
+
"POST:characters/{character_id}/fittings": { group: "fitting", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2300
|
+
"DELETE:characters/{character_id}/fittings/{fitting_id}": { group: "fitting", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2301
|
+
"GET:characters/{character_id}/fleet": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2302
|
+
"GET:characters/{character_id}/freelance-jobs": { group: "char-freelance-job", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2303
|
+
"GET:characters/{character_id}/freelance-jobs/{job_id}/participation": { group: "char-freelance-job", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2304
|
+
"GET:characters/{character_id}/fw/stats": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2305
|
+
"GET:characters/{character_id}/implants": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2306
|
+
"GET:characters/{character_id}/industry/jobs": { group: "char-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2307
|
+
"GET:characters/{character_id}/killmails/recent": { group: "char-killmail", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2308
|
+
"GET:characters/{character_id}/location": { group: "char-location", maxTokens: 1200, windowSizeMs: 9e5 },
|
|
2309
|
+
"GET:characters/{character_id}/loyalty/points": { group: "char-wallet", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2310
|
+
"GET:characters/{character_id}/mail": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2311
|
+
"POST:characters/{character_id}/mail": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2312
|
+
"GET:characters/{character_id}/mail/{mail_id}": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2313
|
+
"PUT:characters/{character_id}/mail/{mail_id}": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2314
|
+
"DELETE:characters/{character_id}/mail/{mail_id}": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2315
|
+
"GET:characters/{character_id}/mail/labels": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2316
|
+
"POST:characters/{character_id}/mail/labels": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2317
|
+
"DELETE:characters/{character_id}/mail/labels/{label_id}": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2318
|
+
"GET:characters/{character_id}/mail/lists": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2319
|
+
"GET:characters/{character_id}/medals": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2320
|
+
"GET:characters/{character_id}/mercenary-tactical-operations": { group: "char-activity", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2321
|
+
"GET:characters/{character_id}/mercenary-tactical-operations/{operation_id}": { group: "char-activity", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2322
|
+
"GET:characters/{character_id}/military-campaigns/objectives": { group: "char-military-campaign", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2323
|
+
"GET:characters/{character_id}/military-campaigns/objectives/{objective_id}": { group: "char-military-campaign", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2324
|
+
"GET:characters/{character_id}/mining": { group: "char-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2325
|
+
"GET:characters/{character_id}/notifications": { group: "char-notification", maxTokens: 15, windowSizeMs: 9e5 },
|
|
2326
|
+
"GET:characters/{character_id}/notifications/contacts": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2327
|
+
"GET:characters/{character_id}/online": { group: "char-location", maxTokens: 1200, windowSizeMs: 9e5 },
|
|
2328
|
+
"GET:characters/{character_id}/paragon-hub/skinr": { group: "char-paragon-hub", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2329
|
+
"GET:characters/{character_id}/planets": { group: "char-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2330
|
+
"GET:characters/{character_id}/planets/{planet_id}": { group: "char-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2331
|
+
"GET:characters/{character_id}/portrait": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2332
|
+
"GET:characters/{character_id}/roles": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2333
|
+
"GET:characters/{character_id}/ship": { group: "char-location", maxTokens: 1200, windowSizeMs: 9e5 },
|
|
2334
|
+
"GET:characters/{character_id}/skillqueue": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2335
|
+
"GET:characters/{character_id}/skills": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2336
|
+
"GET:characters/{character_id}/standings": { group: "char-social", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2337
|
+
"GET:characters/{character_id}/structures/mercenary-dens": { group: "char-structure", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2338
|
+
"GET:characters/{character_id}/structures/mercenary-dens/{mercenary_den_id}": { group: "char-structure", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2339
|
+
"GET:characters/{character_id}/titles": { group: "char-detail", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2340
|
+
"GET:characters/{character_id}/wallet": { group: "char-wallet", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2341
|
+
"GET:characters/{character_id}/wallet/journal": { group: "char-wallet", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2342
|
+
"GET:characters/{character_id}/wallet/transactions": { group: "char-wallet", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2343
|
+
"GET:corporation/{corporation_id}/mining/extractions": { group: "corp-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2344
|
+
"GET:corporation/{corporation_id}/mining/observers": { group: "corp-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2345
|
+
"GET:corporation/{corporation_id}/mining/observers/{observer_id}": { group: "corp-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2346
|
+
"GET:corporations/{corporation_id}/assets": { group: "corp-asset", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2347
|
+
"POST:corporations/{corporation_id}/assets/locations": { group: "corp-asset", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2348
|
+
"POST:corporations/{corporation_id}/assets/names": { group: "corp-asset", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2349
|
+
"GET:corporations/{corporation_id}/blueprints": { group: "corp-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2350
|
+
"GET:corporations/{corporation_id}/contacts": { group: "corp-social", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2351
|
+
"GET:corporations/{corporation_id}/contacts/labels": { group: "corp-social", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2352
|
+
"GET:corporations/{corporation_id}/containers/logs": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2353
|
+
"GET:corporations/{corporation_id}/contracts": { group: "corp-contract", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2354
|
+
"GET:corporations/{corporation_id}/contracts/{contract_id}/bids": { group: "corp-contract", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2355
|
+
"GET:corporations/{corporation_id}/contracts/{contract_id}/items": { group: "corp-contract", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2356
|
+
"GET:corporations/{corporation_id}/customs_offices": { group: "corp-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2357
|
+
"GET:corporations/{corporation_id}/divisions": { group: "corp-wallet", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2358
|
+
"GET:corporations/{corporation_id}/facilities": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2359
|
+
"GET:corporations/{corporation_id}/freelance-jobs": { group: "corp-freelance-job", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2360
|
+
"GET:corporations/{corporation_id}/freelance-jobs/{job_id}/participants": { group: "corp-freelance-job", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2361
|
+
"GET:corporations/{corporation_id}/fw/stats": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2362
|
+
"GET:corporations/{corporation_id}/industry/jobs": { group: "corp-industry", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2363
|
+
"GET:corporations/{corporation_id}/killmails/recent": { group: "corp-killmail", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2364
|
+
"GET:corporations/{corporation_id}/medals": { group: "corp-detail", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2365
|
+
"GET:corporations/{corporation_id}/medals/issued": { group: "corp-detail", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2366
|
+
"GET:corporations/{corporation_id}/members": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2367
|
+
"GET:corporations/{corporation_id}/members/limit": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2368
|
+
"GET:corporations/{corporation_id}/members/titles": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2369
|
+
"GET:corporations/{corporation_id}/membertracking": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2370
|
+
"GET:corporations/{corporation_id}/projects": { group: "corp-project", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2371
|
+
"GET:corporations/{corporation_id}/projects/{project_id}": { group: "corp-project", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2372
|
+
"GET:corporations/{corporation_id}/projects/{project_id}/contribution/{character_id}": { group: "corp-project", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2373
|
+
"GET:corporations/{corporation_id}/projects/{project_id}/contributors": { group: "corp-project", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2374
|
+
"GET:corporations/{corporation_id}/roles": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2375
|
+
"GET:corporations/{corporation_id}/roles/history": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2376
|
+
"GET:corporations/{corporation_id}/shareholders": { group: "corp-detail", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2377
|
+
"GET:corporations/{corporation_id}/standings": { group: "corp-member", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2378
|
+
"GET:corporations/{corporation_id}/starbases": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2379
|
+
"GET:corporations/{corporation_id}/starbases/{starbase_id}": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2380
|
+
"GET:corporations/{corporation_id}/structures": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2381
|
+
"GET:corporations/{corporation_id}/structures/skyhooks": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2382
|
+
"GET:corporations/{corporation_id}/structures/skyhooks/{skyhook_id}": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2383
|
+
"GET:corporations/{corporation_id}/structures/sovereignty-hubs": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2384
|
+
"GET:corporations/{corporation_id}/structures/sovereignty-hubs/{sovereignty_hub_id}": { group: "corp-structure", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2385
|
+
"GET:corporations/{corporation_id}/titles": { group: "corp-detail", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2386
|
+
"GET:corporations/{corporation_id}/wallets": { group: "corp-wallet", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2387
|
+
"GET:corporations/{corporation_id}/wallets/{division}/journal": { group: "corp-wallet", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2388
|
+
"GET:corporations/{corporation_id}/wallets/{division}/transactions": { group: "corp-wallet", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2389
|
+
"GET:cosmetics/skinr/{skinr_id}": { group: "skinr", maxTokens: 12e3, windowSizeMs: 9e5 },
|
|
2390
|
+
"GET:fleets/{fleet_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2391
|
+
"PUT:fleets/{fleet_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2392
|
+
"GET:fleets/{fleet_id}/members": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2393
|
+
"POST:fleets/{fleet_id}/members": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2394
|
+
"PUT:fleets/{fleet_id}/members/{member_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2395
|
+
"DELETE:fleets/{fleet_id}/members/{member_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2396
|
+
"PUT:fleets/{fleet_id}/squads/{squad_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2397
|
+
"DELETE:fleets/{fleet_id}/squads/{squad_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2398
|
+
"GET:fleets/{fleet_id}/wings": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2399
|
+
"POST:fleets/{fleet_id}/wings": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2400
|
+
"PUT:fleets/{fleet_id}/wings/{wing_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2401
|
+
"DELETE:fleets/{fleet_id}/wings/{wing_id}": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2402
|
+
"POST:fleets/{fleet_id}/wings/{wing_id}/squads": { group: "fleet", maxTokens: 1800, windowSizeMs: 9e5 },
|
|
2403
|
+
"GET:freelance-jobs": { group: "freelance-job", maxTokens: 12e3, windowSizeMs: 9e5 },
|
|
2404
|
+
"GET:freelance-jobs/{job_id}": { group: "freelance-job", maxTokens: 12e3, windowSizeMs: 9e5 },
|
|
2405
|
+
"GET:fw/leaderboards": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2406
|
+
"GET:fw/leaderboards/characters": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2407
|
+
"GET:fw/leaderboards/corporations": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2408
|
+
"GET:fw/stats": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2409
|
+
"GET:fw/systems": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2410
|
+
"GET:fw/wars": { group: "factional-warfare", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2411
|
+
"GET:incursions": { group: "incursion", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2412
|
+
"GET:industry/facilities": { group: "industry", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2413
|
+
"GET:industry/systems": { group: "industry", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2414
|
+
"GET:insurance/prices": { group: "insurance", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2415
|
+
"GET:killmails/{killmail_id}/{killmail_hash}": { group: "killmail", maxTokens: 3600, windowSizeMs: 9e5 },
|
|
2416
|
+
"GET:markets/{region_id}/orders": { group: "market-order", maxTokens: 12e3, windowSizeMs: 9e5 },
|
|
2417
|
+
"GET:meta/changelog": { group: "meta", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2418
|
+
"GET:meta/compatibility-dates": { group: "meta", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2419
|
+
"GET:meta/name": { group: "meta", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2420
|
+
"GET:meta/status": { group: "meta", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2421
|
+
"GET:military-campaigns": { group: "military-campaign", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2422
|
+
"GET:military-campaigns/{campaign_id}": { group: "military-campaign", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2423
|
+
"GET:military-campaigns/{campaign_id}/objectives": { group: "military-campaign", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2424
|
+
"GET:military-campaigns/{campaign_id}/objectives/{objective_id}": { group: "military-campaign", maxTokens: 300, windowSizeMs: 9e5 },
|
|
2425
|
+
"GET:paragon-hub/skinr": { group: "paragon-hub", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2426
|
+
"GET:paragon-hub/skinr/alliances/{alliance_id}": { group: "char-paragon-hub", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2427
|
+
"GET:paragon-hub/skinr/characters/{character_id}": { group: "char-paragon-hub", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2428
|
+
"GET:paragon-hub/skinr/corporations/{corporation_id}": { group: "char-paragon-hub", maxTokens: 150, windowSizeMs: 9e5 },
|
|
2429
|
+
"POST:route/{origin_system_id}/{destination_system_id}": { group: "routes", maxTokens: 3600, windowSizeMs: 9e5 },
|
|
2430
|
+
"GET:skyhooks/raidable": { group: "activity", maxTokens: 30, windowSizeMs: 9e5 },
|
|
2431
|
+
"GET:sovereignty/campaigns": { group: "sovereignty", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2432
|
+
"GET:sovereignty/systems": { group: "sovereignty", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2433
|
+
"GET:status": { group: "status", maxTokens: 600, windowSizeMs: 9e5 },
|
|
2434
|
+
"POST:ui/autopilot/waypoint": { group: "ui", maxTokens: 900, windowSizeMs: 9e5 },
|
|
2435
|
+
"POST:ui/openwindow/contract": { group: "ui", maxTokens: 900, windowSizeMs: 9e5 },
|
|
2436
|
+
"POST:ui/openwindow/information": { group: "ui", maxTokens: 900, windowSizeMs: 9e5 },
|
|
2437
|
+
"POST:ui/openwindow/marketdetails": { group: "ui", maxTokens: 900, windowSizeMs: 9e5 },
|
|
2438
|
+
"POST:ui/openwindow/newmail": { group: "ui", maxTokens: 900, windowSizeMs: 9e5 },
|
|
2439
|
+
"GET:wars": { group: "killmail", maxTokens: 3600, windowSizeMs: 9e5 },
|
|
2440
|
+
"GET:wars/{war_id}": { group: "killmail", maxTokens: 3600, windowSizeMs: 9e5 },
|
|
2441
|
+
"GET:wars/{war_id}/killmails": { group: "killmail", maxTokens: 3600, windowSizeMs: 9e5 }
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
// src/core/rateLimiter/RateLimiter.ts
|
|
2445
|
+
function getTokenCost(statusCode) {
|
|
2446
|
+
if (statusCode >= 200 && statusCode < 300) return 2;
|
|
2447
|
+
if (statusCode >= 300 && statusCode < 400) return 1;
|
|
2448
|
+
if (statusCode >= 400 && statusCode < 500) return 5;
|
|
2449
|
+
if (statusCode >= 500 && statusCode < 600) return 0;
|
|
2450
|
+
return 2;
|
|
2451
|
+
}
|
|
2452
|
+
function retryAfterDeadline(value, now) {
|
|
2453
|
+
const trimmed = value.trim();
|
|
2454
|
+
if (/^\d+$/.test(trimmed)) return now + parseInt(trimmed, 10) * 1e3;
|
|
2455
|
+
if (!/[A-Za-z]/.test(trimmed)) return void 0;
|
|
2456
|
+
const date = Date.parse(trimmed);
|
|
2457
|
+
return Number.isNaN(date) ? void 0 : date;
|
|
2458
|
+
}
|
|
2459
|
+
var DEFAULT_WINDOW_MS = 9e5;
|
|
2460
|
+
var DEFAULT_ERROR_LIMIT = {
|
|
2461
|
+
remain: 100,
|
|
2462
|
+
reset: 0,
|
|
2463
|
+
exhaustedUntil: 0
|
|
2464
|
+
};
|
|
2465
|
+
var RateLimiter = class _RateLimiter {
|
|
2466
|
+
minDelayMs;
|
|
2467
|
+
decelerationThreshold;
|
|
2468
|
+
userKeyExtractor;
|
|
2469
|
+
endpointOverrides;
|
|
2470
|
+
lastRequestTime = 0;
|
|
2471
|
+
minDelayChain = Promise.resolve();
|
|
2472
|
+
isTestMode = false;
|
|
2473
|
+
globalErrorLimit = { ...DEFAULT_ERROR_LIMIT };
|
|
2474
|
+
defaultBuckets = /* @__PURE__ */ new Map();
|
|
2475
|
+
userBuckets = null;
|
|
2476
|
+
fallbackBucket;
|
|
2477
|
+
lastCleanup = 0;
|
|
2478
|
+
static CLEANUP_INTERVAL_MS = 6e4;
|
|
2479
|
+
client = null;
|
|
2480
|
+
/**
|
|
2481
|
+
* Attach the client whose logger this limiter's warnings go to. Without
|
|
2482
|
+
* one they go to the global logger (`setLogger`), then the pino default.
|
|
2483
|
+
*/
|
|
2484
|
+
setClient(client) {
|
|
2485
|
+
this.client = client;
|
|
2486
|
+
}
|
|
2487
|
+
constructor(config) {
|
|
2488
|
+
this.minDelayMs = config?.minDelayMs ?? 50;
|
|
2489
|
+
this.decelerationThreshold = config?.decelerationThreshold ?? 0.2;
|
|
2490
|
+
this.userKeyExtractor = config?.userKeyExtractor;
|
|
2491
|
+
this.endpointOverrides = config?.endpointOverrides ?? {};
|
|
2492
|
+
if (this.userKeyExtractor) {
|
|
2493
|
+
this.userBuckets = /* @__PURE__ */ new Map();
|
|
2494
|
+
}
|
|
2495
|
+
this.fallbackBucket = this.createFallbackBucket();
|
|
2496
|
+
}
|
|
2497
|
+
createFallbackBucket() {
|
|
2498
|
+
return {
|
|
2499
|
+
group: "__fallback__",
|
|
2500
|
+
remaining: -1,
|
|
2501
|
+
limit: 0,
|
|
2502
|
+
used: 0,
|
|
2503
|
+
windowSizeMs: DEFAULT_WINDOW_MS,
|
|
2504
|
+
blockedUntil: 0,
|
|
2505
|
+
lastUpdated: 0
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
createBucketFromSpec(spec) {
|
|
2509
|
+
return {
|
|
2510
|
+
group: spec.group,
|
|
2511
|
+
remaining: spec.maxTokens,
|
|
2512
|
+
limit: spec.maxTokens,
|
|
2513
|
+
used: 0,
|
|
2514
|
+
windowSizeMs: spec.windowSizeMs,
|
|
2515
|
+
blockedUntil: 0,
|
|
2516
|
+
lastUpdated: 0
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
createBucketForGroup(groupName) {
|
|
2520
|
+
return {
|
|
2521
|
+
group: groupName,
|
|
2522
|
+
remaining: -1,
|
|
2523
|
+
limit: 0,
|
|
2524
|
+
used: 0,
|
|
2525
|
+
windowSizeMs: DEFAULT_WINDOW_MS,
|
|
2526
|
+
blockedUntil: 0,
|
|
2527
|
+
lastUpdated: 0
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
lookupGroupSpec(method, templatePath) {
|
|
2531
|
+
if (!templatePath || !method) return void 0;
|
|
2532
|
+
const normalized = templatePath.replace(/\/$/, "").replace(/\{(\w+)\}/g, (_, name) => `{${camelToSnake(name)}}`);
|
|
2533
|
+
const key = `${method}:${normalized}`;
|
|
2534
|
+
const override = this.endpointOverrides[key];
|
|
2535
|
+
if (override) {
|
|
2536
|
+
return {
|
|
2537
|
+
group: `endpoint:${key}`,
|
|
2538
|
+
maxTokens: override.maxTokens,
|
|
2539
|
+
windowSizeMs: override.windowSizeMs
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
return esiRateLimitGroups[key];
|
|
2543
|
+
}
|
|
2544
|
+
resolveUserSet(requestHeaders) {
|
|
2545
|
+
if (!this.userBuckets || !this.userKeyExtractor || !requestHeaders) {
|
|
2546
|
+
return null;
|
|
2547
|
+
}
|
|
2548
|
+
const userKey = this.userKeyExtractor(requestHeaders);
|
|
2549
|
+
let userSet = this.userBuckets.get(userKey);
|
|
2550
|
+
if (!userSet) {
|
|
2551
|
+
userSet = {
|
|
2552
|
+
groups: /* @__PURE__ */ new Map(),
|
|
2553
|
+
errorLimit: { ...DEFAULT_ERROR_LIMIT },
|
|
2554
|
+
lastAccessed: Date.now()
|
|
2555
|
+
};
|
|
2556
|
+
this.userBuckets.set(userKey, userSet);
|
|
2557
|
+
}
|
|
2558
|
+
userSet.lastAccessed = Date.now();
|
|
2559
|
+
return userSet;
|
|
2560
|
+
}
|
|
2561
|
+
resolveBucketMap(requestHeaders) {
|
|
2562
|
+
const userSet = this.resolveUserSet(requestHeaders);
|
|
2563
|
+
return userSet ? userSet.groups : this.defaultBuckets;
|
|
2564
|
+
}
|
|
2565
|
+
resolveErrorLimit(requestHeaders) {
|
|
2566
|
+
const userSet = this.resolveUserSet(requestHeaders);
|
|
2567
|
+
return userSet ? userSet.errorLimit : this.globalErrorLimit;
|
|
2568
|
+
}
|
|
2569
|
+
getBucket(templatePath, method, requestHeaders, groupNameOverride) {
|
|
2570
|
+
const bucketMap = this.resolveBucketMap(requestHeaders);
|
|
2571
|
+
const spec = this.lookupGroupSpec(method, templatePath);
|
|
2572
|
+
const groupName = groupNameOverride ?? spec?.group;
|
|
2573
|
+
if (!groupName) return this.fallbackBucket;
|
|
2574
|
+
let bucket = bucketMap.get(groupName);
|
|
2575
|
+
if (!bucket) {
|
|
2576
|
+
bucket = spec ? this.createBucketFromSpec(spec) : this.createBucketForGroup(groupName);
|
|
2577
|
+
bucketMap.set(groupName, bucket);
|
|
2578
|
+
}
|
|
2579
|
+
return bucket;
|
|
2580
|
+
}
|
|
2581
|
+
cleanupStaleUsers() {
|
|
2582
|
+
if (!this.userBuckets) return;
|
|
2583
|
+
const now = Date.now();
|
|
2584
|
+
if (now - this.lastCleanup < _RateLimiter.CLEANUP_INTERVAL_MS) return;
|
|
2585
|
+
this.lastCleanup = now;
|
|
2586
|
+
for (const [userKey, userSet] of this.userBuckets) {
|
|
2587
|
+
if (now - userSet.lastAccessed > DEFAULT_WINDOW_MS) {
|
|
2588
|
+
this.userBuckets.delete(userKey);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
updateFromResponse(headers, statusCode, templatePath, method, requestHeaders) {
|
|
2593
|
+
if (this.isTestMode) return;
|
|
2594
|
+
const now = Date.now();
|
|
2595
|
+
const responseGroup = headers["x-ratelimit-group"] || void 0;
|
|
2596
|
+
const bucket = this.getBucket(
|
|
2597
|
+
templatePath,
|
|
2598
|
+
method,
|
|
2599
|
+
requestHeaders,
|
|
2600
|
+
responseGroup
|
|
2601
|
+
);
|
|
2602
|
+
if ("x-ratelimit-remaining" in headers) {
|
|
2603
|
+
bucket.remaining = parseInt(headers["x-ratelimit-remaining"], 10);
|
|
2604
|
+
}
|
|
2605
|
+
if ("x-ratelimit-limit" in headers) {
|
|
2606
|
+
bucket.limit = parseInt(headers["x-ratelimit-limit"], 10);
|
|
2607
|
+
}
|
|
2608
|
+
if ("x-ratelimit-used" in headers) {
|
|
2609
|
+
bucket.used = parseInt(headers["x-ratelimit-used"], 10);
|
|
2610
|
+
}
|
|
2611
|
+
bucket.lastUpdated = now;
|
|
2612
|
+
if ("retry-after" in headers) {
|
|
2613
|
+
const until = retryAfterDeadline(headers["retry-after"], now);
|
|
2614
|
+
if (until !== void 0) bucket.blockedUntil = until;
|
|
2615
|
+
}
|
|
2616
|
+
if (statusCode === 420 || statusCode === 429) {
|
|
2617
|
+
if (bucket.blockedUntil <= now) {
|
|
2618
|
+
bucket.blockedUntil = now + 6e4;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
const errorLimit = this.resolveErrorLimit(requestHeaders);
|
|
2622
|
+
const reportedRemain = "x-esi-error-limit-remain" in headers ? parseInt(headers["x-esi-error-limit-remain"], 10) : void 0;
|
|
2623
|
+
if (reportedRemain !== void 0 && reportedRemain > 0 && errorLimit.remain <= 0 && now < errorLimit.exhaustedUntil) {
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
if (reportedRemain !== void 0) {
|
|
2627
|
+
errorLimit.remain = reportedRemain;
|
|
2628
|
+
}
|
|
2629
|
+
if ("x-esi-error-limit-reset" in headers) {
|
|
2630
|
+
errorLimit.reset = parseInt(headers["x-esi-error-limit-reset"], 10);
|
|
2631
|
+
}
|
|
2632
|
+
if (reportedRemain !== void 0 && reportedRemain <= 0) {
|
|
2633
|
+
errorLimit.exhaustedUntil = now + errorLimit.reset * 1e3;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
updateRateLimitInfo(headers) {
|
|
2637
|
+
this.updateFromResponse(headers, 200);
|
|
2638
|
+
}
|
|
2639
|
+
async checkRateLimit(templatePath, method, requestHeaders) {
|
|
2640
|
+
if (this.isTestMode) return;
|
|
2641
|
+
this.cleanupStaleUsers();
|
|
2642
|
+
let stillBlockedBucket;
|
|
2643
|
+
for (let i = 0; i < 10; i++) {
|
|
2644
|
+
const bucket2 = this.getBucket(templatePath, method, requestHeaders);
|
|
2645
|
+
const now = Date.now();
|
|
2646
|
+
if (bucket2.blockedUntil <= now) {
|
|
2647
|
+
stillBlockedBucket = void 0;
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
stillBlockedBucket = bucket2;
|
|
2651
|
+
const waitedUntil = bucket2.blockedUntil;
|
|
2652
|
+
const waitTime = waitedUntil - now;
|
|
2653
|
+
logWarn2(
|
|
2654
|
+
this.client,
|
|
2655
|
+
`[ESI Rate Limit] Group '${bucket2.group}' blocked for ${Math.ceil(waitTime / 1e3)}s (420/429 received)`,
|
|
2656
|
+
{ group: bucket2.group, waitMs: waitTime }
|
|
2657
|
+
);
|
|
2658
|
+
await sleep(waitTime);
|
|
2659
|
+
if (bucket2.blockedUntil <= waitedUntil) {
|
|
2660
|
+
bucket2.blockedUntil = 0;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
if (stillBlockedBucket && stillBlockedBucket.blockedUntil > Date.now()) {
|
|
2664
|
+
const waitTime = stillBlockedBucket.blockedUntil - Date.now();
|
|
2665
|
+
logWarn2(
|
|
2666
|
+
this.client,
|
|
2667
|
+
`[ESI Rate Limit] Group '${stillBlockedBucket.group}' still blocked after retry budget exhausted; aborting request instead of sending while blocked`,
|
|
2668
|
+
{ group: stillBlockedBucket.group }
|
|
2669
|
+
);
|
|
2670
|
+
throw new EsiError(
|
|
2671
|
+
429,
|
|
2672
|
+
`Rate limit group '${stillBlockedBucket.group}' still blocked for ${Math.ceil(waitTime / 1e3)}s`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
const errorLimit = this.resolveErrorLimit(requestHeaders);
|
|
2676
|
+
if (errorLimit.remain <= 10 && errorLimit.remain > 0) {
|
|
2677
|
+
const resetMs = errorLimit.reset * 1e3;
|
|
2678
|
+
const delay = Math.min(resetMs, 5e3);
|
|
2679
|
+
if (delay > 0) {
|
|
2680
|
+
logWarn2(
|
|
2681
|
+
this.client,
|
|
2682
|
+
`[ESI Rate Limit] Legacy error limit low (${errorLimit.remain}), waiting ${delay}ms`,
|
|
2683
|
+
{ errorLimitRemain: errorLimit.remain }
|
|
2684
|
+
);
|
|
2685
|
+
await sleep(delay);
|
|
2686
|
+
}
|
|
2687
|
+
} else if (errorLimit.remain <= 0 && errorLimit.reset > 0) {
|
|
2688
|
+
const waitTime = errorLimit.reset * 1e3;
|
|
2689
|
+
logWarn2(
|
|
2690
|
+
this.client,
|
|
2691
|
+
`[ESI Rate Limit] Legacy error limit exhausted, waiting ${Math.ceil(waitTime / 1e3)}s`,
|
|
2692
|
+
{ errorLimitReset: errorLimit.reset }
|
|
2693
|
+
);
|
|
2694
|
+
await sleep(waitTime);
|
|
2695
|
+
}
|
|
2696
|
+
const bucket = this.getBucket(templatePath, method, requestHeaders);
|
|
2697
|
+
if (bucket.remaining >= 0 && bucket.limit > 0) {
|
|
2698
|
+
const ratio = bucket.remaining / bucket.limit;
|
|
2699
|
+
if (bucket.remaining <= 0) {
|
|
2700
|
+
logWarn2(
|
|
2701
|
+
this.client,
|
|
2702
|
+
`[ESI Rate Limit] Group '${bucket.group}' token bucket empty, waiting 1s`,
|
|
2703
|
+
{ group: bucket.group }
|
|
2704
|
+
);
|
|
2705
|
+
await sleep(1e3);
|
|
2706
|
+
} else if (ratio <= this.decelerationThreshold) {
|
|
2707
|
+
const extraDelay = Math.ceil(
|
|
2708
|
+
(1 - ratio / this.decelerationThreshold) * 1e3
|
|
2709
|
+
);
|
|
2710
|
+
await sleep(extraDelay);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
await this.enforceMinDelay();
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Serializes min-delay waits so concurrent callers cannot all read the same
|
|
2717
|
+
* lastRequestTime and fire together after sleeping.
|
|
2718
|
+
*/
|
|
2719
|
+
async enforceMinDelay() {
|
|
2720
|
+
if (this.isTestMode) {
|
|
2721
|
+
this.lastRequestTime = Date.now();
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
const run = async () => {
|
|
2725
|
+
const now = Date.now();
|
|
2726
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
2727
|
+
if (timeSinceLastRequest < this.minDelayMs) {
|
|
2728
|
+
await sleep(this.minDelayMs - timeSinceLastRequest);
|
|
2729
|
+
}
|
|
2730
|
+
this.lastRequestTime = Date.now();
|
|
2731
|
+
};
|
|
2732
|
+
const scheduled = this.minDelayChain.then(run, run);
|
|
2733
|
+
this.minDelayChain = scheduled.then(
|
|
2734
|
+
() => void 0,
|
|
2735
|
+
() => void 0
|
|
2736
|
+
);
|
|
2737
|
+
await scheduled;
|
|
2738
|
+
}
|
|
2739
|
+
getStatus() {
|
|
2740
|
+
let worstBucket = this.fallbackBucket;
|
|
2741
|
+
let worstRatio = Infinity;
|
|
2742
|
+
const checkBucket = (bucket) => {
|
|
2743
|
+
if (bucket.limit > 0) {
|
|
2744
|
+
const ratio = bucket.remaining / bucket.limit;
|
|
2745
|
+
if (ratio < worstRatio) {
|
|
2746
|
+
worstRatio = ratio;
|
|
2747
|
+
worstBucket = bucket;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
for (const bucket of this.defaultBuckets.values()) {
|
|
2752
|
+
checkBucket(bucket);
|
|
2753
|
+
}
|
|
2754
|
+
if (this.userBuckets) {
|
|
2755
|
+
for (const userSet of this.userBuckets.values()) {
|
|
2756
|
+
for (const bucket of userSet.groups.values()) {
|
|
2757
|
+
checkBucket(bucket);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
remaining: worstBucket.remaining,
|
|
2763
|
+
limit: worstBucket.limit,
|
|
2764
|
+
used: worstBucket.used,
|
|
2765
|
+
group: worstBucket.group === "__fallback__" ? null : worstBucket.group,
|
|
2766
|
+
errorLimitRemain: this.globalErrorLimit.remain,
|
|
2767
|
+
errorLimitReset: this.globalErrorLimit.reset,
|
|
2768
|
+
retryAfter: worstBucket.blockedUntil > Date.now() ? Math.ceil((worstBucket.blockedUntil - Date.now()) / 1e3) : null,
|
|
2769
|
+
blockedUntil: worstBucket.blockedUntil
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
getGroupStatus(group) {
|
|
2773
|
+
const bucket = this.defaultBuckets.get(group);
|
|
2774
|
+
if (!bucket) return void 0;
|
|
2775
|
+
return {
|
|
2776
|
+
group: bucket.group,
|
|
2777
|
+
remaining: bucket.remaining,
|
|
2778
|
+
limit: bucket.limit,
|
|
2779
|
+
used: bucket.used,
|
|
2780
|
+
windowSizeMs: bucket.windowSizeMs,
|
|
2781
|
+
blockedUntil: bucket.blockedUntil
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
getAllGroupStatuses() {
|
|
2785
|
+
const result = /* @__PURE__ */ new Map();
|
|
2786
|
+
for (const [name, bucket] of this.defaultBuckets) {
|
|
2787
|
+
result.set(name, {
|
|
2788
|
+
group: bucket.group,
|
|
2789
|
+
remaining: bucket.remaining,
|
|
2790
|
+
limit: bucket.limit,
|
|
2791
|
+
used: bucket.used,
|
|
2792
|
+
windowSizeMs: bucket.windowSizeMs,
|
|
2793
|
+
blockedUntil: bucket.blockedUntil
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
return result;
|
|
2797
|
+
}
|
|
2798
|
+
static getTokenCost(statusCode) {
|
|
2799
|
+
return getTokenCost(statusCode);
|
|
2800
|
+
}
|
|
2801
|
+
isBlocked(group) {
|
|
2802
|
+
const now = Date.now();
|
|
2803
|
+
if (group) {
|
|
2804
|
+
const bucket = this.defaultBuckets.get(group);
|
|
2805
|
+
return bucket ? bucket.blockedUntil > now : false;
|
|
2806
|
+
}
|
|
2807
|
+
if (this.fallbackBucket.blockedUntil > now) return true;
|
|
2808
|
+
for (const bucket of this.defaultBuckets.values()) {
|
|
2809
|
+
if (bucket.blockedUntil > now) return true;
|
|
2810
|
+
}
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
setTestMode(enabled) {
|
|
2814
|
+
this.isTestMode = enabled;
|
|
2815
|
+
}
|
|
2816
|
+
reset() {
|
|
2817
|
+
this.defaultBuckets.clear();
|
|
2818
|
+
if (this.userBuckets) this.userBuckets.clear();
|
|
2819
|
+
this.fallbackBucket = this.createFallbackBucket();
|
|
2820
|
+
this.globalErrorLimit = { ...DEFAULT_ERROR_LIMIT };
|
|
2821
|
+
this.lastRequestTime = 0;
|
|
2822
|
+
this.minDelayChain = Promise.resolve();
|
|
2823
|
+
this.lastCleanup = 0;
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
|
|
2827
|
+
// src/core/RequestDeduplicator.ts
|
|
2828
|
+
var RequestDeduplicator = class {
|
|
2829
|
+
inflight = /* @__PURE__ */ new Map();
|
|
2830
|
+
client = null;
|
|
2831
|
+
setClient(client) {
|
|
2832
|
+
this.client = client;
|
|
2833
|
+
}
|
|
2834
|
+
async dedupe(key, execute) {
|
|
2835
|
+
const existing = this.inflight.get(key);
|
|
2836
|
+
if (existing) {
|
|
2837
|
+
if (this.client) {
|
|
2838
|
+
logDebug2(this.client, `[Dedup] Coalescing request: ${key}`);
|
|
2839
|
+
}
|
|
2840
|
+
return existing;
|
|
2841
|
+
}
|
|
2842
|
+
const promise = execute().finally(() => {
|
|
2843
|
+
this.inflight.delete(key);
|
|
2844
|
+
});
|
|
2845
|
+
this.inflight.set(key, promise);
|
|
2846
|
+
return promise;
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Drop the in-flight entries whose key contains `pathSegment`, so a call
|
|
2850
|
+
* made after a write cannot join a request sent before it.
|
|
2851
|
+
*
|
|
2852
|
+
* The requests themselves keep running and whoever already joined them still
|
|
2853
|
+
* gets their answer. Only the map entry goes, which is what new callers look
|
|
2854
|
+
* in. Matching is `includes`, the same rule ETagCacheManager.deleteByPath
|
|
2855
|
+
* uses, so one written path invalidates the cache and detaches the reads of
|
|
2856
|
+
* it by the same test.
|
|
2857
|
+
*/
|
|
2858
|
+
detachByPath(pathSegment) {
|
|
2859
|
+
let count = 0;
|
|
2860
|
+
for (const key of this.inflight.keys()) {
|
|
2861
|
+
if (key.includes(pathSegment)) {
|
|
2862
|
+
this.inflight.delete(key);
|
|
2863
|
+
count += 1;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
if (count > 0 && this.client) {
|
|
2867
|
+
logDebug2(
|
|
2868
|
+
this.client,
|
|
2869
|
+
`[Dedup] Detached ${count} in-flight request(s) matching ${pathSegment} after a write`
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
return count;
|
|
2873
|
+
}
|
|
2874
|
+
get pending() {
|
|
2875
|
+
return this.inflight.size;
|
|
2876
|
+
}
|
|
2877
|
+
clear() {
|
|
2878
|
+
this.inflight.clear();
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2882
|
+
// src/core/configureApiClient.ts
|
|
2883
|
+
function configureApiClient(client, config) {
|
|
2884
|
+
let deduplicator = null;
|
|
2885
|
+
if (config?.logger) {
|
|
2886
|
+
client.setLogger(config.logger);
|
|
2887
|
+
} else if (config?.logLevel) {
|
|
2888
|
+
client.setLogger(createDefaultLogger(config.logLevel));
|
|
2889
|
+
}
|
|
2890
|
+
if (config?.tenant !== void 0) client.setTenant(config.tenant);
|
|
2891
|
+
if (config?.userAgent !== void 0) client.setUserAgent(config.userAgent);
|
|
2892
|
+
const rateLimiter = new RateLimiter(config?.rateLimiterConfig);
|
|
2893
|
+
rateLimiter.setClient(client);
|
|
2894
|
+
client.setRateLimiter(rateLimiter);
|
|
2895
|
+
if (config?.enableRequestDeduplication !== false) {
|
|
2896
|
+
deduplicator = new RequestDeduplicator();
|
|
2897
|
+
deduplicator.setClient(client);
|
|
2898
|
+
client.setDeduplicator(deduplicator);
|
|
2899
|
+
}
|
|
2900
|
+
if (config?.enableETagCache !== false) {
|
|
2901
|
+
const cache = new ETagCacheManager(config?.etagCacheConfig, client);
|
|
2902
|
+
client.setCache(cache);
|
|
2903
|
+
}
|
|
2904
|
+
if (config?.enableCircuitBreaker) {
|
|
2905
|
+
const cb = new CircuitBreaker(config.circuitBreakerConfig);
|
|
2906
|
+
cb.setClient(client);
|
|
2907
|
+
client.setCircuitBreaker(cb);
|
|
2908
|
+
}
|
|
2909
|
+
if (config?.retryConfig) {
|
|
2910
|
+
client.setRetryConfig(config.retryConfig);
|
|
2911
|
+
} else if (config?.retryAttempts !== void 0) {
|
|
2912
|
+
client.setRetryConfig({ maxRetries: config.retryAttempts });
|
|
2913
|
+
} else {
|
|
2914
|
+
client.setRetryConfig({
|
|
2915
|
+
maxRetries: 3,
|
|
2916
|
+
baseDelayMs: 1e3,
|
|
2917
|
+
maxDelayMs: 3e4
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
if (config?.retryStrategy) {
|
|
2921
|
+
client.setRetryStrategy(config.retryStrategy);
|
|
2922
|
+
}
|
|
2923
|
+
if (config?.requestInterceptors) {
|
|
2924
|
+
for (const interceptor of config.requestInterceptors) {
|
|
2925
|
+
client.addRequestInterceptor(interceptor);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
if (config?.responseInterceptors) {
|
|
2929
|
+
for (const interceptor of config.responseInterceptors) {
|
|
2930
|
+
client.addResponseInterceptor(interceptor);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
if (config?.validateResponse !== void 0) {
|
|
2934
|
+
client.setValidateResponse(config.validateResponse);
|
|
2935
|
+
}
|
|
2936
|
+
if (config?.validateRequest !== void 0) {
|
|
2937
|
+
client.setValidateRequest(config.validateRequest);
|
|
2938
|
+
}
|
|
2939
|
+
if (config?.timeout !== void 0) {
|
|
2940
|
+
client.setTimeout(config.timeout);
|
|
2941
|
+
}
|
|
2942
|
+
return { deduplicator };
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
export {
|
|
2946
|
+
validatePathParam,
|
|
2947
|
+
validateBaseUrl,
|
|
2948
|
+
validateQueryParam,
|
|
2949
|
+
validateHeaderOption,
|
|
2950
|
+
ApiClient,
|
|
2951
|
+
characterIdOfToken,
|
|
2952
|
+
redactingLogger,
|
|
2953
|
+
createDefaultLogger,
|
|
2954
|
+
toPinoLogger,
|
|
2955
|
+
setLogger,
|
|
2956
|
+
getLogger,
|
|
2957
|
+
logFatal,
|
|
2958
|
+
logError,
|
|
2959
|
+
logWarn,
|
|
2960
|
+
logInfo,
|
|
2961
|
+
logDebug,
|
|
2962
|
+
logTrace,
|
|
2963
|
+
logWarn2,
|
|
2964
|
+
logInfo2,
|
|
2965
|
+
logDebug2,
|
|
2966
|
+
USER_AGENT,
|
|
2967
|
+
evictRejectedResponse,
|
|
2968
|
+
CircuitBreaker,
|
|
2969
|
+
RetryStrategy,
|
|
2970
|
+
resolveCache,
|
|
2971
|
+
parseWarning,
|
|
2972
|
+
handleRequest,
|
|
2973
|
+
fetchAllPages,
|
|
2974
|
+
fetchPages,
|
|
2975
|
+
ETagCacheManager,
|
|
2976
|
+
RateLimiter,
|
|
2977
|
+
RequestDeduplicator,
|
|
2978
|
+
configureApiClient
|
|
2979
|
+
};
|
|
2980
|
+
//# sourceMappingURL=chunk-KIY3XA5Q.mjs.map
|