Kumbatio
Concepts

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:

  1. Suppressed? (e.g. a focus session is running) → deferred
  2. Below the level's priority threshold?deferred
  3. Level has a batch interval?batched, delivered together when the window closes
  4. Otherwise → delivered immediately

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:

ReasonMeaning
'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 inert

Prop

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.