feat: add typed ESI transport extensions
This commit is contained in:
+77
-18
@@ -1,9 +1,11 @@
|
||||
# ESI operations
|
||||
|
||||
The server ESI adapter uses the fixed trusted base `https://esi.evetech.net` and pins
|
||||
`X-Compatibility-Date: 2026-08-18` (`ESI_COMPATIBILITY_DATE`). It exposes only the
|
||||
character asset GET and global market-price GET needed by the application; callers
|
||||
cannot provide an arbitrary ESI URL.
|
||||
`X-Compatibility-Date: 2026-08-18` (`ESI_COMPATIBILITY_DATE`). It exposes a fixed
|
||||
allowlist of typed safe reads: character assets (paged GET), character asset names
|
||||
and locations (bounded read-only POST), character wallet balance (unpaged GET), and
|
||||
global market prices (unpaged GET). Callers cannot provide an arbitrary ESI
|
||||
URL, host, method, or POST body.
|
||||
|
||||
## Asset synchronization
|
||||
|
||||
@@ -12,37 +14,94 @@ reads `X-Pages`, fetches all remaining pages, de-duplicates by `item_id` while
|
||||
checking for duplicates, and revalidates page one after the other pages. A changed
|
||||
page count, duplicate ID, malformed page, failed request, or changed first page
|
||||
fails the operation. The caller must not persist a returned partial result: the
|
||||
method returns only a complete `{ assets, syncedAt, expiresAt }` snapshot. Asset
|
||||
IDs are retained as decimal strings, and the response is parsed with
|
||||
`lossless-json` so an int64 cannot be rounded by JavaScript.
|
||||
method returns only a complete bounded-read snapshot with
|
||||
`consistency: "bounded-non-atomic"`. Asset IDs are retained as decimal strings,
|
||||
and the response is parsed with `lossless-json` so an int64 cannot be rounded by
|
||||
JavaScript.
|
||||
|
||||
This is a stability check, not a transaction. ESI's paginated endpoint does not
|
||||
promise a true multi-page snapshot while its cache expires between page requests.
|
||||
A caller should stage pages and replace the persisted Convex snapshot only after
|
||||
`getAssets` resolves. Convex persistence is intentionally outside this adapter.
|
||||
Only page one is conditionally revalidated: revalidating every page would multiply
|
||||
remote calls up to the bounded page cap and still could not make ESI's independently
|
||||
served pages atomic. The explicit `bounded-non-atomic` coverage state prevents this
|
||||
adapter from claiming transactional consistency. A caller should stage pages and
|
||||
replace the persisted Convex snapshot only after `getAssets` resolves. Convex
|
||||
persistence is intentionally outside this adapter.
|
||||
|
||||
The cache is private to an `EsiClient` and keyed by user/authorization context,
|
||||
character, and scopes. `authorizationKey` should therefore identify the linked
|
||||
application user and include the character/scopes when available:
|
||||
The cache is private to an `EsiClient` and keyed by a structured authorization
|
||||
context: application user/principal, character, sorted scopes, `grantRevision`,
|
||||
`authorizationEpoch`, and committed `credentialVersion`. Production callers pass
|
||||
the current counters:
|
||||
|
||||
```ts
|
||||
{ userId, characterId, scopes }
|
||||
{ userId, principal, characterId, scopes, grantRevision, authorizationEpoch, credentialVersion }
|
||||
```
|
||||
|
||||
A string key is supported for callers that have no scope object, but is treated as
|
||||
an isolated user key. Tokens are never used as cache keys or emitted in metrics.
|
||||
Tokens are never used as cache keys or emitted in metrics. Private reads reject
|
||||
legacy string keys and require all structured fields, including the matching
|
||||
character, required scope, `grantRevision`, `authorizationEpoch`, and
|
||||
`credentialVersion`. Changing any authority counter therefore cannot reuse a prior
|
||||
private body or snapshot.
|
||||
|
||||
Responses retain their raw text, ETag, Last-Modified, and cache metadata. Fresh
|
||||
responses use the server TTL; stale responses send `If-None-Match` and
|
||||
`If-Modified-Since`, and a 304 reuses the cached body. `Cache-Control`, `Expires`,
|
||||
`If-Modified-Since`, and a 304 reuses the cached body for both GET and declared
|
||||
read-only POST operations. Canonical helper bodies are sorted decimal int64 JSON
|
||||
literals, so ordering variants share one cache identity; operation/method/body
|
||||
cannot collide. Helper requests accept 1..1000 unique positive signed-int64 IDs
|
||||
and reject unsafe/duplicate/oversize input without Number coercion. Each operation
|
||||
also enforces a response `Content-Length`/stream byte cap and result-row cap before
|
||||
retaining a cache entry; asset pages and aggregate snapshots have separate bounds
|
||||
(1,000 pages, 5,000 rows/page, 50,000 rows/snapshot). `Cache-Control`, `Expires`,
|
||||
`Date`, and `Age` are honored. `no-store` responses retain neither body nor
|
||||
validators. Requests have one deadline covering response-body reads and bounded
|
||||
retries for transient network/5xx/420/429 responses. `Retry-After` (seconds or
|
||||
HTTP date) and the legacy ESI error-limit reset are respected. Exhausted new
|
||||
limiter buckets are cooled down per authorization context and group; the legacy
|
||||
error limiter is cooled down globally. A server delay that exceeds the remaining
|
||||
request budget returns a typed rate-limit error rather than retrying early. 401
|
||||
and 403 are never blindly retried.
|
||||
error limiter is cooled down globally. Query a private cooldown with
|
||||
`client.getCooldown(authorizationKey, characterId)` or the public limiter with
|
||||
`client.getPublicCooldown()`. A server delay that exceeds the remaining request
|
||||
budget returns a typed rate-limit error with `retryAt`, `retryAfterMs`, and
|
||||
scheduler-readable `cooldown` rather than retrying early. Response cancellation is
|
||||
abort-first and non-blocking, so cleanup cannot extend a deadline. Process-local
|
||||
retention is bounded by weighted response bytes and aggregate snapshot rows;
|
||||
`client.getCacheStats()` exposes current usage and both budgets. Eviction is
|
||||
oldest-insertion deterministic. 401 and 403 are never blindly retried. OAuth exchange/refresh remain in `EveSso` and never use
|
||||
these safe-read retries.
|
||||
|
||||
## Asset names and locations
|
||||
|
||||
`EsiClient.getAssetNames` and `getAssetLocations` call fixed character helper
|
||||
routes with the asset scope. Their outbound body is canonicalized and serialized
|
||||
as numeric JSON literals (including IDs above `2^53`). Typed observations include
|
||||
requested IDs, returned values, explicit `missing` IDs, `complete`, and
|
||||
retrieval/expiry provenance. A missing helper result is never silently represented
|
||||
as complete enrichment. Duplicate or unexpected IDs, malformed names, non-finite
|
||||
coordinates, and malformed arrays fail closed. Deterministic transport fixtures live
|
||||
under `tests/fixtures/esi/` so int64 names, finite positions, signed balances,
|
||||
and versioned route/scope metadata are parsed from raw JSON rather than rounded
|
||||
JavaScript literals. Asset helper metadata cites pinned OpenAPI evidence; wallet
|
||||
metadata cites the current official OpenAPI retrieval and explicitly records that
|
||||
it is not an immutable historical capture.
|
||||
|
||||
## Wallet balance
|
||||
|
||||
`EsiClient.getWalletBalance` reads the unpaged character wallet route with the
|
||||
wallet scope. The adapter returns only a transport-level observation with signed
|
||||
integer cents in `amount` (via T04's half-away-from-zero `decimalIskToCents`
|
||||
normalization) and `retrievedAt` / `expiresAt`; sub-cent source values are rounded
|
||||
by that approved policy, not rejected. Persistence, scheduling, and UI/service
|
||||
orchestration belong to later work. Negative and very large balances remain
|
||||
lossless, while malformed/non-finite numeric bodies are rejected. The tracked
|
||||
fixture metadata records the official OpenAPI URL, operation ID, 120-second
|
||||
TTL, `char-wallet` limiter, response SHA-256
|
||||
`9a9a0f4af77216300e960cfd2576c9baac085a8170022e40c928c28622d80fc6`, and the
|
||||
2026-09-19T19:30:12Z retrieval. This is a current retrieval using the pinned
|
||||
compatibility date, not an immutable historical capture from 2026-08-18. Official
|
||||
references: [ESI OpenAPI](https://esi.evetech.net/meta/openapi.json?compatibility_date=2026-08-18),
|
||||
[versioning](https://developers.eveonline.com/docs/services/esi/overview/),
|
||||
[caching](https://developers.eveonline.com/docs/services/esi/overview/#caching),
|
||||
and [rate limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/).
|
||||
|
||||
## Market estimates
|
||||
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
|
||||
**Planning baseline:** `fdcc486` (`Initial Commit`). The roadmap records an earlier 97-test gate; each prototype change reruns the current local suite. Existing `jobs`, `userPreferences` and `referenceSyncMetadata` tables are placeholders, not services.
|
||||
|
||||
**T06 evidence:** The centralized ESI client now has fixed typed asset, asset-name,
|
||||
asset-location, wallet-balance, and market-read strategies with bounded response
|
||||
bytes/rows/pages, bounded retries, abort-first cleanup, validators, canonical int64
|
||||
helper bodies, complete scoped authority-context cache identity, cooldown queries,
|
||||
and runtime schemas. Asset snapshots report bounded non-atomic coverage; page-one
|
||||
revalidation is not presented as a transaction. T07 durable scheduling/worker
|
||||
orchestration, T08 location/service orchestration, and T13 wallet persistence/UI/
|
||||
service orchestration remain unimplemented; production-readiness gates remain
|
||||
deferred. The narrow `beginSync` expected-authority fence is part of T06's atomic
|
||||
bridge contract: it prevents transport cache authority from being published under
|
||||
a different T05 grant/revision/credential fence; it is not T07 durable worker orchestration.
|
||||
|
||||
## Execution model
|
||||
|
||||
- One task per branch/worktree; branch suggestions below are labels, not created branches.
|
||||
|
||||
Reference in New Issue
Block a user