Appearance
AlertStorageManager
CRUD operations for alerts with an in-memory cache indexed by symbol.
High-Level Design
AlertStorageManager is the persistence and caching layer for all alerts in a PriceAlert Durable Object. Its core responsibility is keeping two data stores — SQLite (durable) and an in-memory map (fast) — perfectly synchronized through a dual-write pattern.
Data Flow
┌──────────────────────────────────────────┐
│ AlertStorageManager │
registerAlert() │ │
─────────────────►│ 1. INSERT INTO alerts (...) │
│ 2. alertsBySymbol[symbol].push(alert) │
│ 3. alert_metrics.total_alerts += 1 │
│ │
deregisterAlert() │ │
─────────────────►│ 1. DELETE FROM alerts WHERE id = ? │
│ 2. splice alert from cache │
│ 3. prune empty symbol entries │
│ │
getAlertsBySymbol │ │
─────────────────►│ return alertsBySymbol (cache only) │
└──────────────────────────────────────────┘Read path: Always from the in-memory Map<SymbolIdentifiers, Alert[]> — zero database queries per evaluation tick.
Write path: Every mutation applies to both SQLite and cache in sequence. This dual-write is safe because the single-threaded Durable Object guarantees no concurrent writes.
Cache Lifecycle
DO starts → initialize() → SELECT * FROM alerts → build alertsBySymbol map
│
register/deregister mutations ◄─────────────────┘
keep SQLite + cache in sync │
│
DO evicts → cache lost → next alarm restarts → initialize() rebuilds from SQLiteThe cache is fully rebuilt from SQLite on every DO start. Between starts, all mutations maintain both stores. If the DO evicts, no data is lost — SQLite is the source of truth and the cache is a derived view.
Reverse Iteration for Safe Bulk Deletion
deregisterAllForUser() iterates each symbol's alert array in reverse when splicing, preventing the index-shifting bug that occurs with forward iteration and splice(). Empty symbol entries are pruned afterward to prevent unbounded map growth.
String Precision for Monetary Values
targetPrice and sizeUsd are stored as TEXT in SQLite rather than REAL. JavaScript numbers lose precision beyond 15 significant digits, and these values cross RPC boundaries as BigNumber strings (up to 18 decimal places). Storing as text preserves full precision through the round-trip.
Error Containment
All public methods catch exceptions and return { success: false } rather than throwing. This ensures the 1-second alarm loop is never interrupted by a storage failure — the evaluation continues on the next tick.
State & Storage
In-Memory State
| Property | Type | Lifecycle |
|---|---|---|
alertsBySymbol | Map<SymbolIdentifiers, Alert[]> | Populated on initialize(), mutated on register/deregister. |
SQLite Tables
alerts
| Column | Type | Purpose |
|---|---|---|
id | text (PK) | Unique alert identifier |
user_id | text | Owner user ID |
symbol | text | Trading pair (e.g., BTCUSD) |
type | text | LIMIT_ORDER, STOP_LOSS, TAKE_PROFIT, LIQUIDATION_WATCH |
direction | text | LONG or SHORT |
target_price | text | Target price as string for precision |
size_usd | text (nullable) | Order size in USD |
leverage | integer (nullable) | Leverage multiplier |
one_time | integer | 1 if alert should be deregistered after triggering |
created_at | integer | Unix timestamp of creation |
metadata | text (nullable) | Arbitrary JSON metadata |
account_id | integer | Account identifier |
Indexes: idx_symbol on symbol (cache rebuild, grouped queries), idx_user_id on user_id (bulk user deletion).
Interactions
Depends On
BaseDurableManager(base class)PriceAlert.state.storage.sql(SQLite access)
Depended By
- AlertEvaluationManager: reads
getAlertsBySymbol()every tick; callsderegisterAlert()for triggered/zombie alerts. - PriceAlert RPC methods: calls
registerAlert(),deregisterAlert(),deregisterAllForUser().
See Also
- AlertEvaluationManager -- primary consumer of alert data
- AlertCursorManager -- updates metrics on the same
alert_metricstable - PriceDataManager -- supplies prices that alerts are evaluated against
- Parent DO:
PriceAlert