Appearance
AccountValueTrackingManager
Manages CRUD operations for user tracking records and orchestrates subscribe, update, reset, and unsubscribe flows.
Purpose
AccountValueTrackingManager owns the trackings table, which stores one row per subscribed user with their current account version, daily extremes, and processing cursor. It is the entry point for all lifecycle operations: subscribing a user, updating their coefficients when positions change, resetting their account for new challenges, and unsubscribing.
High-Level Design
Role Within the System
AccountValueTrackingManager is the CRUD and lifecycle layer for the tracker. It does not calculate extremes or emit callbacks — those responsibilities belong to ExtremesTrackingManager and CallbackManager respectively. This manager's job is to maintain the trackings table and coordinate the initial state setup that other managers depend on.
UserPaperTradePortfolio
│
│ subscribe / unsubscribe
│ updateCoefficients / resetAccount
▼
AccountValueTrackingManager
│
├─ Writes to `trackings` table (one row per user)
├─ Delegates coefficient storage to CoefficientManager
├─ Fetches initial price via PriceFetchManager (subscribe/reset only)
└─ Provides query methods consumed by parent DO's alarm pipeline:
• getTrackingsBatch() ──► batch price processing
• hasTrackingsNeedingCatchUp() ──► day boundary guard
• getTrackingsNeedingDailyCallback() ──► daily emission
• updateTrackingExtremes() ──► cursor advancement
• resetTrackingForNewDay() ──► midnight resetSubscription Lifecycle
Each lifecycle operation has a specific contract:
- subscribe: Creates a tracking row + initial coefficient snapshot. Fetches the price at
coefficientsValidFromto compute the starting account value for daily extremes. ReturnsPRICE_UNAVAILABLEif data isn't ready, letting the caller retry. - updateCoefficients: Appends a new coefficient snapshot via CoefficientManager. Returns
NOT_SUBSCRIBEDif no tracking exists,VERSION_MISMATCHif account version differs, orDUPLICATEif the seq already exists. Uses sequence numbers for ordering — if a gap is detected (e.g., received seq 5 but max is 3), returnsgapDetected: truewithexpectedSeqso the caller can reconcile. - reconcileCoefficients: Bulk-inserts missing snapshots to fill gaps. Skips duplicates. Also validates
NOT_SUBSCRIBEDandVERSION_MISMATCH. - resetAccount: When a tracking exists, atomically replaces it for the new account version — deletes old coefficient history, updates the tracking row, and recalculates initial extremes. When no tracking exists (e.g., first-time subscribe during a reset call), falls through to
subscribe()to create one. Idempotent: silently succeeds ifexisting.accountVersion >= newAccountVersion. - unsubscribe: Deletes tracking + all coefficient snapshots in a transaction.
Deterministic Initial Value
Both subscribe and resetAccount need to set the initial daily peak and bottom. They do this by computing the account value at the exact coefficientsValidFrom timestamp:
- If coefficients are empty (no open positions),
accountValue = constantTerm— price fetch is skipped entirely - Otherwise, fetch the price at that timestamp from PriceCollector via PriceFetchManager
- If price isn't available yet, return
PRICE_UNAVAILABLE— never use a stale or approximate value
This ensures the first data point for extremes tracking is always deterministic and correct.
Query Interface for the Alarm Pipeline
The parent DO's alarm pipeline relies on this manager for all tracking queries. These methods are designed for the specific access patterns of batch processing and boundary handling:
| Method | Consumer | Pattern |
|---|---|---|
getAllTrackings() | Hourly extremes emission | Returns all tracked users (no pagination) |
getTrackingsBatch(limit, afterUserId) | Batch price processing | Keyset pagination by userId, 100 per page |
getTracking(userId) | Single-user lookup | Used by updateCoefficients, getCurrentExtremes |
getTrackingCount() | Warmup/diagnostics | Simple COUNT(*) |
hasTrackingsNeedingCatchUp(currentDate, yesterdayEnd) | Day boundary guard | Returns true if any tracking has daily_date != currentDate AND last_processed_timestamp < yesterdayEnd |
getTrackingsNeedingDailyCallback(currentDate, yesterdayEnd, excludeUserIds, limit, afterUserId) | Daily emission | Filters by daily_date != currentDate AND last_processed_timestamp >= yesterdayEnd, excludes users with pending retries |
getAccountVersionsBatch(userIds) | Batch processing | Staleness check — returns Map<userId, accountVersion> |
updateTrackingExtremes(userId, ...) | Batch processing | Advances cursor + updates daily peak/bottom |
resetTrackingForNewDay(tracking, ...) | Midnight reset | Resets extremes to midnight value, clears coefficient history to latest |
Day Reset Atomicity
resetTrackingForNewDay is the most sensitive operation — it must atomically:
- Delete all old coefficient snapshots for the user
- Insert the latest snapshot as the new baseline
- Update the tracking row with new daily extremes (set to midnight account value) and advance
dailyDate
All three steps run inside transactionSync so a crash mid-reset cannot leave the tracking in a half-reset state.
Keyset Pagination for Correctness
Day boundary queries use WHERE user_id > cursor (keyset pagination) instead of LIMIT/OFFSET. This is necessary because resetTrackingForNewDay mutates the daily_date column — which changes whether a row matches the query filter. With OFFSET, this would cause rows to be skipped or double-processed. Keyset pagination is immune to this because it anchors on the immutable user_id.
Account Version Staleness Guard
When a user resets their account mid-batch (e.g., starts a new challenge), the accountVersion increments. During batch processing, the parent DO batch-loads current versions and skips any tracking whose version no longer matches. This prevents ghost updates — applying price data to a tracking that has already been replaced.
Edge Cases & Error Handling
subscribeandresetAccountreturnPRICE_UNAVAILABLEinstead of failing when price data is not ready- No-coefficient optimization: skips price fetch when
coefficientsis empty - BigNumber validation rejects NaN, Infinity, and malformed strings before any database writes
resetAccountis idempotent: silently succeeds ifexisting.accountVersion >= newAccountVersionresetTrackingForNewDaywraps coefficient reset + tracking update intransactionSyncfor atomicityunsubscribewraps coefficient delete + tracking delete intransactionSync- Day boundary queries use keyset pagination (
user_id > cursor) instead of OFFSET for correctness during concurrent modifications getTrackingsNeedingDailyCallbackfiltersexcludeUserIdsin JS to avoid CF Workers' 50-parameter SQL limit
See Also
- CoefficientManager -- snapshot storage delegated to
- ExtremesTrackingManager -- initial value computation
- PriceFetchManager -- price fetching for subscribe/reset
- CallbackManager -- delivers the extremes managed by trackings
- AccountValueAggregationTracker -- parent DO