Skip to main content

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

BucketPurposeSize LimitControlled By
Context bucketRecent signals attached to outgoing API requests (sentinel context)contextSignalTypeSize per signal typeServer config via ConfigManager
Reporter bucketUnsent signals waiting to be flushed to the backend100 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

MethodSignatureDescription
setBucketSize(size: number) => voidSets the context bucket's per-type capacity
add(signal: Signal) => voidAdds 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>) => voidRemoves 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