feat(mobile): API hardening — GraphQL cache isolation, idempotency keys, cancellation, OpenAPI contract check - #1016
Merged
llinsss merged 2 commits intoAug 28, 2026
Conversation
…ion, OpenAPI contract - DogStark#978 resetApolloStore() wipes the normalized GraphQL cache atomically on logout/account switch so cache-and-network can't leak a prior account's pets or records; wired into authService.logout(). - DogStark#976 Every mutating request (POST/PUT/PATCH/DELETE) carries a stable Idempotency-Key via the apiClient request interceptor; the offline queue persists the key and replays it verbatim so retries don't duplicate records, payments, appointments, or support requests. - DogStark#975 useAbortController hook: one AbortSignal per screen, aborted on unmount; renew() cancels the previous scope on rapid navigation/filter changes. isAbortError()/ignoreAbort() helpers for swallowing the expected rejection. - DogStark#977 scripts/validate-openapi-client.ts contract check wired into CI — scans src/services call sites against backend/docs/openapi.json and fails on NEW drift beyond a committed baseline. Focused tests added for each change.
|
@driftsorbit Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four narrowly-scoped mobile API-boundary hardening changes, each with focused tests. Based on the latest
main; synthetic data only.#978 — Prevent GraphQL cache leakage between accounts
InMemoryCacheis keyed by type + id, not by account, so acache-and-networkread could hand the next account a prior account's pets or medical records before the network response landed.resetApolloStore()(src/services/apolloClient.ts) drops all normalized state viaclearStore()— no active query is refetched with a missing/stale token during teardown.authService.logout()after tokens are cleared, via dynamic import so the Apollo/graphql-wsmodule graph stays out of auth unit tests and non-Expo environments.src/services/__tests__/apolloCacheIsolation.test.ts): reproduce the leak on a shared cache, verify the wipe on a warm cache, verify a safe no-op on an already-empty cache (offline logout), verify concurrent-reset coalescing.#976 — API idempotency keys for all mobile mutations
Network retries, 5xx retries, the 401-refresh replay, and offline-queue replay could each duplicate records, payments, appointments, and support requests.
apiClientrequest interceptor stamps a stableIdempotency-Key(RFC-4122 v4,crypto.randomUUIDwhen available) on everyPOST/PUT/PATCH/DELETEbefore it — or any retry of the same config — leaves the device. Reads are untouched; an explicit caller-supplied key is always preserved.offlineQueuepersists the key onQueuedMutationand replays it verbatim, so a mutation queued offline and retried days later still collapses server-side.generateIdempotencyKey,readIdempotencyKey,withIdempotencyKey,IDEMPOTENCY_HEADER.src/services/__tests__/apiClientIdempotency.test.ts): gap characterization, uniqueness/format, per-method behavior (mutations vs. reads), explicit-key preservation, retry-safety (re-stamping is a no-op), case-insensitive header match.#975 — Standardize API cancellation on screen unmount
Long requests were resolving into unmounted or stale screens and keeping the radio awake.
useAbortController()(src/hooks/useAbortController.ts): oneAbortControllerper mount, aborted exactly once on unmount;renew()aborts the previous scope and opens a new one for rapid navigation / filter changes so only the newest request can settle the screen. StrictMode-safe.isAbortError()/ignoreAbort()recognize the DOMException, axiosCanceledError(ERR_CANCELED), and NodeABORT_ERR, so call sites can swallow the expected post-cancel rejection.src/hooks/__tests__/useAbortController.test.ts): stale-screen hazard characterization, abort-on-unmount,renew()semantics, a 5-change rapid-filter burst leaving only the last scope alive,isAbortError/ignoreAbortclassification.#977 — Validate backend OpenAPI against the generated mobile client
Handwritten services can drift from backend routes/DTOs.
scripts/validate-openapi-client.tsscans literalapiClient/api/authClient.<method>('/path')call sites insrc/services, normalizes dynamic segments to the spec's{param}form, and diffs againstbackend/docs/openapi.json.scripts/openapi-client-baseline.jsonsnapshots the 51 currently-known-undocumented endpoints; documenting one (shrinking the baseline) is always allowed.--update-baselinere-snapshots;--strict(CI) exits non-zero on anything new.qualityjob in.github/workflows/ci.ymlasnpm run openapi:validate-client.scripts/__tests__/validate-openapi-client.test.ts): path normalizer, drift detector (undocumented path, undocumented method on a known path, happy path), and a live assertion that the critical mobile REST surface (auth, pets, medical records, appointments, medications) is documented.Testing
npx tsx scripts/validate-openapi-client.ts --strict→ passes (0 new drift).node_modulesis not vendored in this working copy, so the JS suites were not executed locally).closes #975
closes #976
closes #977
closes #978