Notification Gate
The runtime that enforces the notification config - deliver, batch, or defer, but never drop.
The notification gate turns NotificationConfig from guidance into enforcement. Your app publishes notification intents through the gate; the gate resolves the current level's config and decides whether each intent is delivered now, batched, or deferred.
The design rule is inherited from field evidence (a shipped scheduler destroyed reminders that came due while suppressed): the gate never silently drops a notification. Anything not deliverable now is deferred and released when energy rises, suppression lifts, or the gate is disposed.
Basic usage
import { createEnergyEngine, createNotificationGate } from '@kumbatio/energy-system'
const engine = createEnergyEngine({ initialLevel: 75 })
const gate = createNotificationGate(engine, {
onDeliver({ notifications, reason, channels, level }) {
if (channels.visual) showToast(notifications, reason)
if (channels.sound) playChime()
},
})
gate.publish({ priority: 'high', payload: { title: 'Build finished' } })
// returns 'delivered' | 'batched' | 'deferred'At level 75 this delivers immediately. At 50 it enters a 5-minute batch. At 0 everything is deferred - held, not lost.
How each publish is decided
Priorities are 'normal' | 'high' | 'critical' (default 'normal'). The decision, in order:
- Suppressed? (e.g. a focus session is running) →
deferred - Below the level's priority threshold? →
deferred - Level has a batch interval? →
batched, delivered together when the window closes - Otherwise →
deliveredimmediately
The pure decision function is exported so you can unit-test your notification policy without constructing a gate:
import { resolveNotificationOutcome, notificationStrategy } from '@kumbatio/energy-system'
const config = notificationStrategy.resolve(25)
resolveNotificationOutcome(config, 'critical', false) // 'batched'
resolveNotificationOutcome(config, 'normal', false) // 'deferred'Defer, not drop
Deferred intents are re-evaluated on every energy change. The moment the new level's config (or lifted suppression) admits them, they're released immediately - not re-batched, because they already waited once. Delivery reason tells you which path an intent took:
| Reason | Meaning |
|---|---|
'immediate' | Admitted at publish time |
'batch' | A batch window closed |
'released' | Previously deferred, now admitted - or flushed out on dispose |
dispose() delivers everything still held as a final 'released' delivery before going inert. An intent that entered the gate always exits through onDeliver.
Channels
Each delivery carries the channels the active config permits - { visual, sound, vibration } - resolved at delivery time, not publish time. Your onDeliver decides how to render within those constraints.
Gate API
gate.publish({ priority: 'critical', payload }) // PublishOutcome
gate.setSuppressed(true) // focus sessions call this for you
gate.isSuppressed()
gate.pendingCount() // { batched: number, deferred: number }
gate.flush() // deliver the open batch now + release eligible deferred
gate.dispose() // final 'released' delivery, then inertProp
Type
Pair the gate with a focus session controller: the controller suppresses the gate on session start and is guaranteed to release it on stop, end, or dispose.
