API: State Store¶
get_store()¶
Returns the singleton StateStore instance.
StateStore¶
Methods¶
dispatch(action_type, payload=None, source=None)¶
Dispatches an action through the middleware pipeline and into the matching reducer.
action_type(str): The action type (e.g.,"COUNTER_UPDATED")payload(dict, optional): Data for the actionsource(str, optional): ID of the dispatching view
Subscriber failures are caught and logged internally -- dispatch() does not raise from subscriber errors.
subscribe(subscriber_id, callback, action_filter=None, selector=None)¶
Registers a subscriber for state change notifications. Views auto-subscribe during __init__ and tear down through their own lifecycle hooks; direct calls from user code are rare.
subscriber_id(str): Unique ID for the subscribercallback(callable): Async function called on matching state changesaction_filter(set[str], optional): Only notify for these action typesselector(callable, optional): Function(state) -> valuethat extracts a slice. Subscriber is only notified when the selected value changes.
state¶
Public attribute holding the current state dict. Read-only by convention; mutate state through dispatch(), not by assignment.
Persistence The store has no direct persistence methods. Wire persistence through
PersistenceMiddleware, installed viasetup_middleware. The middleware'sinitialize(store)pass stashes aPersistenceManageronstore.persistence_managerfor runtime access (rehydrate is blocking; writes are debounced).
has_middleware(middleware_cls) -> bool¶
Returns True when an instance of the given middleware class is installed. Used by setup_middleware to gate duplicate installs; available to callers that need to branch on middleware presence.
from cascadeui.state.middleware import UndoMiddleware
if store.has_middleware(UndoMiddleware):
# undo/redo buttons will work
...
Install path
store._add_middlewareandstore._remove_middlewareare internal. User code installs middleware throughsetup_middleware, which gates duplicates viahas_middlewareand awaits each middleware'sinitialize(store)method in order.
batch()¶
Returns an async context manager that coalesces every dispatch inside the
block into a single subscriber notification at exit. Reducers run
immediately -- state is live throughout the block so later dispatches can
read earlier writes. At exit, one synthetic BATCH_COMPLETE action is
dispatched carrying the full action list.
async with store.batch():
await store.dispatch("ACTION_A", payload_a)
await store.dispatch("ACTION_B", payload_b)
Transitivity. Any helper routing through store.dispatch()
(update_session, dispatch_scoped, view-level dispatch, and the
internal _register_state) participates in the active batch. Nested
batch() blocks absorb into the outermost batch; no intermediate
BATCH_COMPLETE is emitted.
Exception semantics. If the block raises, queued actions for this batch are discarded before the exception propagates -- subscribers do not see the partial sequence. Reducers have already executed, so state reflects completed dispatches up to the raise point.
Profiling. Per-dispatch profiling samples are suppressed inside a
batch (individual notify_ms would be zero); the BATCH_COMPLETE
notification fires one sample for the whole batch.
See the State Management guide for typical scenarios.
on(event_name, callback)¶
Registers an event hook. event_name is a snake_case name (e.g., "view_created") that maps to the action type VIEW_CREATED.
off(event_name, callback)¶
Removes an event hook.
get_scoped(scope, *, user_id=None, guild_id=None)¶
Returns scoped state for the given scope type and ID. Reads from the live self.state.
get_scoped_from(state, scope, **identifiers) (staticmethod)¶
Reads a scoped slice from an explicit state dict rather than the live store. Intended for @computed selectors (which receive state as input) and custom reducers (which mutate deep-copied state). Using store.get_scoped() inside a reducer would bypass the deep-copied state -- get_scoped_from(state, ...) keeps the read aligned with what the reducer is mutating.
iter_scoped(scope, slot_name="scoped")¶
Iterates over every entry in the named scoped slot, yielding (identifier_key, data) pairs. Used by hub views that aggregate across many users or guilds (leaderboards, dashboards) without reaching into state["application"]["scoped"] directly.
set_scoped(scope, data, *, user_id=None, guild_id=None)¶
Sets scoped state for the given scope type and ID.
get_active_views() -> Mapping[str, Any]¶
Returns a read-only MappingProxyType over the internal active-view
registry (view_id -> view instance). The returned mapping is live,
not a snapshot -- subsequent register_view / unregister_view calls
on the store show through -- but mutation raises TypeError, so the
privacy boundary stays intact.
from cascadeui import get_store
store = get_store()
for view_id, view in store.get_active_views().items():
print(f"{view_id}: {type(view).__name__}")
Used by DevToolsCog to avoid reaching into store._active_views
directly. User code that needs to iterate or count live views can
consume the same accessor.
merge_scoped(state, scope, data, *, slot_name="scoped", subkey=None, **identifiers)¶
Reducer-side writer that merges data into the scope bucket and returns state. Completes the scoped family alongside get_scoped_from and iter_scoped. Used inside custom reducers to decode the canonical {"scope", "identifiers", "data"} payload emitted by view.dispatch_scoped_as(...).
Reducer, computed, view-registry, and participant plumbing are internal
_register_reducer,_register_computed,_register_view/_unregister_view/_get_active_views, and_register_participant/_unregister_participantare single-underscore internals. User code reaches the same behavior through public entry points:@cascade_reducerand@computeddecorators for registration, andStatefulView.send()/exit()/register_participant()for view lifecycle.
Properties¶
state(dict): The current state treeaction_history(list): Recent dispatched actionscomputed(dict-like): Access computed values by name (e.g.,store.computed["total_votes"])
@cascade_reducer(action_type)¶
Decorator that registers a reducer function for a custom action type.
@cascade_reducer("MY_ACTION")
async def my_reducer(action, state):
# State is already deep-copied by the decorator -- mutate directly
state["my_key"] = action["payload"]["value"]
return state
@computed(selector)¶
Decorator that registers a memoized derived value on the global store. The
decorated function's __name__ becomes the key in store.computed.
selector(callable):(state) -> valuethat picks the input slice- The decorated function receives the selector's output and returns the derived value
- Result is cached until the selector output changes
@computed(selector=lambda s: s.get("application", {}).get("votes", {}))
def vote_totals(votes):
return {lang: len(voters) for lang, voters in votes.items()}
# Access from any view:
totals = store.computed["vote_totals"]
ComputedValue¶
The object created by @computed. Rarely used directly.
Methods¶
get(state) -> Any¶
Returns the cached value, recomputing only if the selector output changed since the last call.
invalidate()¶
Forces recomputation on the next get() call, regardless of whether the
selector output changed.
setup_middleware(*middlewares, store=None)¶
Top-level async helper that installs middleware into the store's dispatch chain. Each middleware is installed once (guarded by store.has_middleware(type(mw))), then its async initialize(store) method is awaited if one is defined.
from cascadeui import setup_middleware
from cascadeui.persistence import SQLiteBackend
from cascadeui.state.middleware import (
LoggingMiddleware,
PersistenceMiddleware,
UndoMiddleware,
)
class MyBot(commands.Bot):
async def setup_hook(self):
await setup_middleware(
LoggingMiddleware(),
PersistenceMiddleware(backend=SQLiteBackend("data.db"), bot=self),
UndoMiddleware(),
)
Parameters
*middlewares-- middleware instances in the order they should appear in the dispatch chain.store-- optional explicit store. Defaults to the global singleton fromget_store().
Idempotency. initialize is always awaited, even when the middleware is already installed. Middlewares contract their initialize methods as idempotent -- subsequent calls return immediately -- so the always-await policy is safe.
ActionCreators¶
Static helper methods for building action payloads. Used internally; you can also call store.dispatch() or view.dispatch() directly with a type and payload dict.
For the full list of built-in action types, payload shapes, and reducer behavior, see Built-in Actions.
Type Aliases¶
StateData¶
Dict[str, Any] -- the canonical state-dict shape passed to reducers, selectors, and middleware. Exported from the package root for type hints on custom reducers and @computed selectors.
from cascadeui import StateData, cascade_reducer
@cascade_reducer("MY_ACTION")
async def my_reducer(action: dict, state: StateData) -> StateData:
state.setdefault("application", {})["counter"] = action["payload"]
return state
Additional type aliases (Action, ReducerFn, SubscriberFn, MiddlewareFn, SelectorFn, HookFn) live in cascadeui/state/types.py and can be imported directly from there when needed.