DOCS/CACHE_ARCHITECTURE.MD
Cache Architecture
GlassFrame Protocol keeps all of its state in memory via one shared
primitive, ProtocolCache (src/core/Cache.js), rather than scattering
plain Maps through every layer. Every cache instance below is a
ProtocolCache, and every one of them can be put into debug mode so you can
watch it operate at runtime - see "Turning on cache logging" below.
Why a shared cache primitive
- One place to reason about TTL, eviction, and stats instead of five
slightly-different
Mappatterns spread across layers. - Optional debug logging is a constructor flag, not something bolted on per-module after the fact.
getStats()returns hits/misses/sets/evictions/size for any cache at runtime, which is useful when you're tuning thresholds inconfig.jsand want to know whether a value is actually being reused.countByPrefix(prefix)counts non-expired entries whose key starts with a given prefix, without listing them.frame.getGuildMetrics(guildId)uses this to count a single server's flagged members and open cases out of the shared threat-score and case caches, which are keyedguildId:userId.
Caches in use
| Cache | Owner | Key shape | TTL | Purpose |
|---|---|---|---|---|
threat-scores |
ThreatEngine |
guildId:userId |
none (decay-based; pruned once decayed near zero) | The running, decaying risk score every layer contributes signals to. |
open-cases |
PunishmentEngine |
guildId:userId |
punishment.caseCooldownMs (default 30s) |
Prevents re-punishing or re-logging the same member while an incident is still fresh. Expiry length is the debounce window. |
nuke-snapshots |
AntiNukeLayer |
guildId |
antiNuke.snapshotTtlMs (default 1h) |
A lightweight forensic snapshot of role/channel names and positions, captured the instant a threshold trips, kept only for reference. |
recent-webhooks |
AntiNukeLayer |
webhook ID | antiNuke.webhookAbuse.watchPeriodMs (default 5m) |
Maps a freshly created webhook back to whoever created it, so a message flood through that webhook (which carries no guild member of its own) can still be traced to an executor. |
groq-verdicts |
GroqClient |
hash of the first 500 characters of the message | aiModeration.cacheTtlMs (default 5m) |
Stops a burst of identical/near-identical messages from each costing a fresh Groq call. |
Two more Maps exist outside ProtocolCache because they're pure sliding
windows with no need for hit/miss stats: AntiRaidLayer.joinTimestamps /
AntiRaidLayer.joinProfiles (cluster correlation) and
AntiNukeLayer.actionLog / AntiNukeLayer.webhookMessageCounts. All four
are swept lazily - old timestamps are filtered out on the next write rather
than on a timer, since they're only ever read right after a write.
SmartLogger.recentLogs is a fixed-size array (last 30, newest first, not
a ProtocolCache) recording every log event across every guild - it's what
powers the owner-only !gf engine dashboard's Activity Log page. See
docs/ENGINE.md.
Bounded-concurrency queues (frame.actionQueue, frame.aiQueue) and the
internal metrics collector (frame.performance) are a separate concern from
caching - see docs/PERFORMANCE.md.
Turning on cache logging
Pass debug: true when constructing GlassFrame:
const frame = new GlassFrame(client, {
getLogChannel: (guild) => guild.channels.cache.find((c) => c.name === "security-logs"),
debug: true
});
(Equivalently, set cache: { debug: true } in config.js - the
constructor option takes priority if both are set.)
With debug mode on, every cache above prints a line like:
[GlassFrame:Cache:open-cases] SET 123456789012345678:987654321098765432
[GlassFrame:Cache:groq-verdicts] HIT h482910335
[GlassFrame:Cache:threat-scores] EXPIRE 123456789012345678:555555555555555555
SmartLogger has its own separate debug channel for the same flag
([GlassFrame:SmartLogger] merge ... / final edit ...), covering the
event-aggregation behavior described in docs/PROTOCOL_LAYERS.md.
This is meant for local debugging while you tune config.js - leave it off
in production, since it writes to console.log on every cache operation and
will get noisy fast on an active server.
Memory growth
threat-scores is the only cache with no hard TTL, since a member's score
needs to persist and decay rather than vanish at a fixed time.
ThreatEngine prunes it every 10 minutes, removing any entry whose decayed
score has fallen under 0.5 and hasn't been touched in at least four
half-lives - by then it's noise, not signal. Every other cache either has a
real TTL or is a self-trimming sliding window, so none of them grow
unbounded over a long-running process.