docs(specs): archive T-Bank cache boundary change
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
## Context
|
||||
|
||||
Frontend использует один `QueryClient`, а `openapi-react-query` формирует ключи
|
||||
GET-запросов как кортежи с HTTP method, canonical API path и, при наличии,
|
||||
params. Все пользовательские финансовые выборки текущего T-Bank connection
|
||||
находятся в `/api/v1/brokerage-accounts` subtree. Сейчас successful PUT и DELETE
|
||||
connection обновляют status cache, но сбрасывают только список Brokerage
|
||||
Accounts; сохранённые и выполняющиеся query остальных выборок могут пережить
|
||||
смену connection.
|
||||
|
||||
Backend также владеет connection-dependent cache. Account Ledger хранит страницы
|
||||
по User, Brokerage Account и query без идентичности текущего connection; Income
|
||||
Summary и Income Analytics используют те же страницы. Portfolio очищается при
|
||||
PUT и DELETE, но старый in-flight request может завершиться после очистки и
|
||||
снова записать snapshot. Browser `AbortSignal` прекращает потребление HTTP
|
||||
response, но не является гарантией отмены уже запущенного backend T-Bank RPC.
|
||||
Поэтому один frontend reset не может обеспечить запрет на данные предыдущей
|
||||
границы.
|
||||
|
||||
Каждый successful PUT сохраняет заново зашифрованный token с новым AES-GCM
|
||||
nonce, включая замену token тем же plaintext value. Persisted encrypted payload
|
||||
тем самым уже является сменяемой идентичностью connection; DELETE удаляет её.
|
||||
Это позволяет изолировать server-side cache без нового поля БД, transport DTO
|
||||
или API version. Обычная ошибка frontend background refetch по-прежнему
|
||||
сохраняет Client Cached Snapshot, а logout или потеря auth session очищает весь
|
||||
`QueryClient` независимо от connection boundary.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- сделать successful create, replace и delete общей границей frontend и backend
|
||||
cache reuse для connection-dependent финансовых данных;
|
||||
- исключить чтение сохранённого либо поздно завершившегося результата
|
||||
предыдущего connection независимо от порядка in-flight completions;
|
||||
- оставить failed mutation в текущем generation и сохранить Client Cached
|
||||
Snapshot только для текущего generation;
|
||||
- сохранить status-first frontend commit, refetch активных consumers и
|
||||
независимую полную session cleanup при logout;
|
||||
- удержать cache isolation внутри существующих QueryClient, connection access и
|
||||
cache-owning backend layers без изменения публичного API и storage schema.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- менять observable Requirements, backend endpoints, transport DTO или OpenAPI
|
||||
contract;
|
||||
- добавлять persisted connection version, data migration, persisted financial
|
||||
cache или новый глобальный frontend state container;
|
||||
- привязывать к User connection generation общие reference caches инструментов,
|
||||
логотипов и корпоративных событий, которые не содержат account snapshot;
|
||||
- менять stale times, retry policies или UI-семантику обычного background
|
||||
refetch;
|
||||
- сужать logout cleanup до T-Bank data, менять auth boundary либо сериализовать
|
||||
конкурентные replacements между распределёнными backend instances.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Одна connection boundary координирует два cache plane
|
||||
|
||||
Successful mutation создаёт одну логическую границу с двумя владельцами.
|
||||
Frontend feature `tbank-connection` коммитит подтверждённый status и сбрасывает
|
||||
все financial query текущего `QueryClient`. Backend connection access сообщает
|
||||
cache owners opaque identity текущего persisted connection, а каждый
|
||||
connection-dependent cache разрешает reuse только внутри этой identity.
|
||||
|
||||
Обе защиты обязательны. Frontend reset немедленно удаляет данные из UI и
|
||||
останавливает reuse browser responses; backend isolation не позволяет новому
|
||||
frontend fetch получить server response, вычисленный до границы. Failed mutation
|
||||
не меняет persisted connection identity и не запускает frontend commit, поэтому
|
||||
данные текущей границы сохраняются.
|
||||
|
||||
### Feature-owned frontend commit использует native reset brokerage subtree
|
||||
|
||||
Модель `tbank-connection` остаётся единственным владельцем frontend commit после
|
||||
successful PUT или DELETE. PUT использует status из response, DELETE — известный
|
||||
disconnected status. Оба paths вызывают одну операцию, которая сначала записывает
|
||||
status в существующий status query, затем применяет `QueryClient.resetQueries` с
|
||||
feature-owned predicate ко всем generated GET keys, чей canonical path равен
|
||||
`/api/v1/brokerage-accounts` или начинается с
|
||||
`/api/v1/brokerage-accounts/`. Connection status и несвязанные query в эту family
|
||||
не входят.
|
||||
|
||||
Native reset отменяет browser requests через существующий `AbortSignal`,
|
||||
возвращает matching query в initial state без предыдущих данных и запускает
|
||||
refetch active observers. Inactive query получает данные при следующем
|
||||
потреблении. Refetch не удерживает mutation UX в pending: обязательный status и
|
||||
cache commit завершается синхронно, а получение нового response продолжается
|
||||
независимо.
|
||||
|
||||
`invalidateQueries` отклонён, потому что оставляет предыдущие данные читаемыми до
|
||||
successful refetch. `removeQueries` отклонён для active observers: reset
|
||||
напрямую переводит их в состояние без старых данных и использует штатный
|
||||
TanStack Query lifecycle.
|
||||
|
||||
### Backend connection access выдаёт opaque cache generation
|
||||
|
||||
Ответственный connection access layer вместе с расшифрованным token получает
|
||||
persisted encrypted payload и вычисляет из него фиксированный non-reversible
|
||||
digest средствами Node runtime. Digest используется только как internal cache
|
||||
generation: он не сохраняется отдельно, не возвращается через API и не попадает
|
||||
в logs. Новый random nonce при каждом успешном PUT создаёт новое generation даже
|
||||
для того же plaintext token; DELETE оставляет последующие requests без
|
||||
generation до нового успешного PUT.
|
||||
|
||||
Generation определяется из persisted record при обслуживании request, поэтому
|
||||
все backend instances, читающие одну запись, получают одну identity, а
|
||||
process-local invalidation не является условием корректности. `connectedAt`
|
||||
отклонён как единственный generation key: timestamp имеет пользовательскую
|
||||
семантику и конечную точность. Отдельный UUID column также подходит, но добавляет
|
||||
schema migration и lifecycle нового persisted field без необходимого
|
||||
compatibility gap.
|
||||
|
||||
### Connection-dependent backend cache принимает только своё generation
|
||||
|
||||
Account Ledger page key расширяется backend generation. Проверка connection и
|
||||
доступа к Brokerage Account остаётся до cache reuse; Income Summary и Income
|
||||
Analytics автоматически получают ту же изоляцию через общий collection path.
|
||||
Старая page, записанная после boundary поздним RPC, остаётся помеченной старым
|
||||
generation и недоступна новому request; существующие TTL и LRU ограничивают её
|
||||
жизнь и память.
|
||||
|
||||
Portfolio разрешает cache hit только после разрешения текущего persisted
|
||||
connection и только когда snapshot принадлежит его generation. Cached fallback
|
||||
при временной upstream error также ограничен тем же generation. Snapshot старого
|
||||
in-flight request может завершиться после PUT или DELETE, но его generation не
|
||||
совпадёт с новым connection, а при отсутствии connection cache вообще не
|
||||
проверяется. Existing explicit invalidation может оставаться best-effort memory
|
||||
eviction, но не служит correctness boundary.
|
||||
|
||||
Brokerage Accounts не хранит полный server-side account snapshot. Events каждый
|
||||
раз начинает с current connection и current account positions; его общие
|
||||
instrument/dividend reference caches остаются connection-independent, поскольку
|
||||
не содержат пользовательскую account selection или financial snapshot. Любой
|
||||
будущий backend cache готового response из brokerage subtree обязан при
|
||||
добавлении классифицироваться как connection-dependent либо получить конкретное
|
||||
обоснование независимости.
|
||||
|
||||
### Connection cleanup и auth cleanup остаются раздельными
|
||||
|
||||
Connection boundary выполняется только после successful PUT или DELETE. Failed
|
||||
mutation не меняет status, generation и financial cache. Обычный background
|
||||
refetch использует существующие query policies и может сохранить последний
|
||||
successful Client Cached Snapshot текущего generation. Logout и auth failure
|
||||
продолжают вызывать полный `QueryClient.clear()`, включая connection status,
|
||||
financial и любые другие session-scoped query.
|
||||
|
||||
Regression coverage проверяет observable boundary и race ordering: successful
|
||||
create/replace/delete сбрасывает весь parameterized brokerage subtree;
|
||||
отменённый frontend response не возвращается; cached и late backend results
|
||||
предыдущего generation не обслуживают новый request; DELETE не допускает cache
|
||||
hit без connection; failed mutation, current-generation background snapshot и
|
||||
logout сохраняют свои независимые outcomes.
|
||||
|
||||
## Delivery Map
|
||||
|
||||
| Area | Status | Architectural outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| frontend | `affected` | `tbank-connection` централизует successful status/cache commit и через native QueryClient lifecycle сбрасывает весь brokerage query subtree, сохраняя failure, background snapshot и logout semantics. |
|
||||
| backend | `affected` | Connection access выдаёт opaque generation текущей persisted connection; Ledger/Income и Portfolio допускают cache reuse и fallback только внутри неё, поэтому local invalidation и browser cancellation не являются correctness boundary. |
|
||||
| specs | `affected` | Canonical change фиксирует пересмотренный двухсторонний Design, зависимые Tasks и lifecycle artifacts без изменения утверждённого observable Requirement. |
|
||||
| docs | `affected` | После поставки as-built connection, Portfolio и Account Ledger cache descriptions отражают generation isolation, frontend reset и отличие от current-generation Client Cached Snapshot. |
|
||||
|
||||
### Contract Gate Decision
|
||||
|
||||
| Gate | Status | Reason |
|
||||
| --- | --- | --- |
|
||||
| runtime-openapi | `N/A` | Frontend и backend меняют только internal cache orchestration; endpoints, request/response schemas и generated OpenAPI contract не меняются ни на одной стороне. |
|
||||
|
||||
## Solution Assessment
|
||||
|
||||
Change существенно меняет caching и concurrency boundary, поэтому рассмотрены
|
||||
native и custom alternatives.
|
||||
|
||||
| Option | Fit | Maturity and security | Operating constraints | Migration cost |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| TanStack Query reset плюс backend generation из digest persisted encrypted payload | Закрывает browser cache, server cache и late in-flight responses; один persisted connection определяет generation во всех processes | Использует установленный TanStack Query lifecycle, Node crypto и существующее AES-GCM storage; digest не раскрывает token и не создаёт новый public surface | Portfolio выполняет lightweight persisted connection lookup до cache reuse; cache owners обязаны различать connection-dependent и shared reference data | Низкая: без schema/API migration, локальные изменения ответственных cache layers |
|
||||
| Только frontend `resetQueries` | Закрывает browser state, но новый fetch может получить старый backend response | Native и безопасен на frontend | Browser abort не отменяет уже выполняющийся backend RPC | Низкая, но нарушает Requirement |
|
||||
| Fan-out `clear()`/`invalidate()` из connection mutation во все backend caches | Удаляет уже сохранённые local entries | Простая process-local операция | Late request может повторно записать старые данные после clear; другой instance не получает invalidation | Низкая, но не является correctness solution |
|
||||
| Explicit persisted UUID generation column | Даёт ту же строгую generation isolation и ясную storage identity | Зрелый стандартный механизм, без token-derived coupling | Требует schema ownership, migration существующих records и rollback data contract | Средняя и непропорциональная при уже сменяемом encrypted payload |
|
||||
| Отключить connection-dependent backend caches | Строго исключает server reuse предыдущей границы | Наименьшая cache logic surface | Увеличивает T-Bank RPC load, latency и rate-limit pressure для Portfolio, Ledger и Income | Низкая code migration, высокая постоянная operating cost |
|
||||
|
||||
Выбран первый вариант. Он использует существующую persisted connection identity
|
||||
и native cache lifecycle, а custom logic ограничивает одной доменной операцией:
|
||||
сравнением opaque generation перед reuse. Это устраняет compatibility gap,
|
||||
который нельзя закрыть одним frontend reset или invalidation fan-out, без нового
|
||||
storage field и без отказа от полезного backend caching.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Generation зависит от свойства AES-GCM storage создавать новый encrypted
|
||||
payload при каждом successful PUT. Если token storage перестанет
|
||||
re-encrypt-иться, перед таким изменением потребуется явная generation identity;
|
||||
regression boundary должна сделать нарушение наблюдаемым.
|
||||
- Opaque digest является security-sensitive internal identifier: его нельзя
|
||||
возвращать, логировать или включать в errors, хотя он не позволяет восстановить
|
||||
token.
|
||||
- Persisted connection lookup перед Portfolio cache reuse добавляет локальную DB
|
||||
работу, но сохраняет экономию дорогих T-Bank Portfolio/Positions calls и
|
||||
обеспечивает DELETE/replace isolation между processes.
|
||||
- Old-generation Ledger pages остаются в bounded LRU до TTL, а поздний Portfolio
|
||||
completion может временно вытеснить current entry. Generation check сохраняет
|
||||
корректность, но редкая гонка способна вызвать дополнительный refetch.
|
||||
- Active frontend view после смены connection временно переходит в initial
|
||||
loading/error state. Это намеренный trade-off: snapshot предыдущей границы не
|
||||
показывается, пока current-generation response ещё не получен.
|
||||
- Если current-generation refetch завершается ошибкой сразу после successful
|
||||
mutation, новый status уже виден, а данные предыдущего generation не
|
||||
восстанавливаются. Recovery использует обычный error path без ложного
|
||||
snapshot.
|
||||
- Path-subtree classification включает будущие brokerage subresources на
|
||||
frontend, но новый backend response cache всё равно требует явного решения о
|
||||
generation ownership.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Сначала в backward-compatible backend delivery добавить opaque generation в
|
||||
connection access context и ограничить ею Portfolio, Ledger и производные
|
||||
Income cache paths; existing endpoints и responses оставить без изменений.
|
||||
2. Перевести frontend successful PUT и DELETE на единый status/cache commit с
|
||||
reset всего brokerage query subtree.
|
||||
3. Зафиксировать boundary-level regression coverage для cached и in-flight
|
||||
ordering на обеих сторонах, failed mutation, current-generation background
|
||||
snapshot и независимого logout cleanup.
|
||||
4. После подтверждённой реализации обновить as-built connection, Portfolio и
|
||||
Account Ledger cache documentation.
|
||||
|
||||
Backend поставляется первым, потому что он совместим с текущим frontend и сразу
|
||||
делает новые reads generation-safe. Frontend delivery затем включает немедленную
|
||||
UI boundary. Data migration, compatibility window и OpenAPI regeneration не
|
||||
требуются.
|
||||
|
||||
### Rollback Contract
|
||||
|
||||
| Field | Decision |
|
||||
| --- | --- |
|
||||
| Status | `required` |
|
||||
| Reason | Change укрепляет isolation пользовательских финансовых данных после replace/delete; rollback не должен возвращать cross-generation cache reuse или cache hit без current connection. |
|
||||
| Trigger | Generation resolution ошибочно блокирует current connection либо вызывает системную деградацию доступности cache-backed финансовых выборок после delivery. |
|
||||
| Actions | Сохранить frontend reset и current connection checks; временно отключить reuse затронутого backend cache или перейти на uncached current-connection reads, затем исправить generation logic. Не откатывать систему к cache keys без generation. |
|
||||
| Data safety | Persisted token и финансовые данные не мигрируют; затронуты только in-memory entries, которые можно безопасно отбросить. |
|
||||
| Verification | Current connection обслуживает Brokerage Accounts, Portfolio, Operations, Events и Income; create/replace/delete не используют previous-generation entries, а failed mutation сохраняет current-generation data. |
|
||||
Reference in New Issue
Block a user