SignalStore & PersistentStorage
SignalStore is the in-memory signal buffer used by Sentinel. It maintains two separate buckets and transparently persists state to localStorage via PersistentStorage.
Dual-Bucket Design
| Bucket | Purpose | Size Limit | Controlled By |
|---|---|---|---|
| Context bucket | Recent signals attached to outgoing API requests (sentinel context) | contextSignalTypeSize per signal type | Server config via ConfigManager |
| Reporter bucket | Unsent signals waiting to be flushed to the backend | 100 per signal type (hardcoded) | REPORTER_BUCKET_LIMIT_BY_TYPE constant |
Both buckets are keyed by SignalType (Map<SignalType, Array<Signal>>). When a bucket exceeds its per-type limit, the oldest signal is evicted (FIFO).
Public API
| Method | Signature | Description |
|---|---|---|
setBucketSize | (size: number) => void | Sets the context bucket's per-type capacity |
add | (signal: Signal) => void | Adds a signal to both buckets and persists |
getContext | () => Array<Signal> | All context-bucket signals, sorted by timestamp |
getUnsent | () => Array<Signal> | All reporter-bucket signals, sorted by timestamp |
removeSentSignals | (ids: Array<string>) => void | Removes successfully sent signals from the reporter bucket and persists |
Persistence Layer — PersistentStorage
Signals are persisted to localStorage so they survive page reloads. The storage pipeline:
JSON.stringify(data)
→ TextEncoder.encode(…)
→ XOR with key derived from storage key
→ deflate (pako)
→ btoa (base64)
→ ExpirableLocalStorage.set(key, value, 24h TTL)
Reading reverses the pipeline. If decoding fails, the stored value is removed and an info log (sen.pst_load_failed) is emitted.
Write Debouncing
Writes are batched via setTimeout(…, 0) — multiple rapid add() calls within the same microtask tick produce only one localStorage write.
Storage Key
The key comes from @traveloka/app-meta (elsCmnSentinelSignals). The TTL is 24 hours.
Support Detection
PersistentStorage checks for TextEncoder, TextDecoder, and localStorage availability. If any are missing, persistence is silently disabled and sen.pst_unsupported is logged.
Hydration
On construction, SignalStore loads persisted data. Context signals are added without enforcing the bucket size limit (since contextBucketSize is not yet set). Reporter signals respect the hardcoded limit.
Data Flow
Collector emits Signal
└─► Sentinel.handleSignal()
├─► signalStore.add(signal)
│ ├─ context bucket (per-type ring buffer)
│ ├─ reporter bucket (per-type ring buffer)
│ └─ persist to localStorage
└─► reporter.flushIfNeeded()
└─ if threshold met → POST /sen/ss
└─ on success → signalStore.removeSentSignals(ids)
└─ persist updated state
Security Note
The XOR obfuscation is not cryptographic protection — it prevents casual inspection and accidental leakage but does not resist a determined attacker with access to the source code. Its primary purpose is tamper detection during deserialisation.
Last updated: 2026-02-24