State, queries, and refresh
Find state declarations, fields, query shapes, and invalidation notifications.
state: declare queryable data
State answers what is true now. Pages can read once or subscribe. After binding, register through
plugin.bridge().state
with a callback that reads current data. See Keep the interface in sync with data.
class WebBridgeState (Protocol):
__call__(self, key: 'str', handler: 'WebStateHandler | None' = None, *, shape: 'WebStateShape' = 'item', fields: 'Mapping[str, WebStateFieldSchema] | None' = None, default_fields: 'tuple[str, ...]' = (), filterable: 'tuple[str, ...]' = (), sortable: 'tuple[str, ...]' = (), default_limit: 'int | None' = None, max_limit: 'int | None' = None, permissions: 'tuple[str, ...]' = ('nexus_web.state.plugin.read',), sensitivity: 'WebStateSensitivity' = 'safe_metadata') -> 'WebStateHandler | Callable[[WebStateHandler], WebStateHandler]'
invalidate(self, key: 'str') -> 'None'| Parameter | Meaning and limits |
|---|---|
| key, handler | A plugin.* state name and synchronous query function. Omitting handler returns a decorator; supplying it registers and returns that function. A client cannot register the same key twice. |
| shape | Defaults to item for one item; list declares a list. Determines whether pages read snapshot.item or snapshot.items. |
| fields | Although the signature defaults to None, a nonempty field schema is required. Do not omit it. Give each field an explicit type. |
| default_fields | Defaults to an empty tuple, using all declared fields when a query omits fields. Supplied defaults may reference only declared fields. |
| filterable, sortable | Defaults to empty tuples, rejecting corresponding query fields. Declarations allow inputs; handlers still perform filtering and sorting. |
| default_limit, max_limit | Both default to None. Supplied values must be positive integers, with default_limit no greater than max_limit. Requests exceeding max_limit fail rather than silently clamp. |
| permissions, sensitivity | Default permission is nexus_web.state.plugin.read and sensitivity is safe_metadata. Declare user_content for user data; do not mislabel data to bypass permissions. |
Field schemas and related types
WebStateFieldType = typing.Literal['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']fields maps names to schema descriptions, not data. Supported types are string, number, integer, boolean, object, array, and null. nullable permits null; enum is a tuple of allowed scalar values. Fields may declare permissions and sensitivity separately. Types do not replace business validation in callbacks.
fields = {
"id": {"type": "string"},
"name": {"type": "string"},
"selected": {"type": "boolean"},
"note": {"type": "string", "nullable": True, "sensitivity": "user_content"},
}View field, shape, and sensitivity definitions
class WebStateFieldSchema:
__init__(<signature-unavailable: ValueError>)
enum: ForwardRef('NotRequired[tuple[WebJSONScalar, ...]]', module='nexus_web.public')
nullable: ForwardRef('NotRequired[bool]', module='nexus_web.public')
permissions: ForwardRef('NotRequired[tuple[str, ...]]', module='nexus_web.public')
sensitivity: ForwardRef('NotRequired[WebStateSensitivity]', module='nexus_web.public')
type: ForwardRef('NotRequired[WebStateFieldType]', module='nexus_web.public')WebStateShape = typing.Literal['item', 'list']WebStateSensitivity = typing.Literal['safe_metadata', 'user_content']WebStateFieldSchema describes a dictionary, not a constructor you must call. ForwardRef/NotRequired indicate deferred types and optional fields. Use ordinary dictionaries as above.
Callback query and context
WebStateHandler = collections.abc.Callable[[nexus_web.public.WebStateQuery, nexus_web.public.WebStateContext], str | int | float | bool | None | list['WebJSONValue'] | dict[str, 'WebJSONValue']]class WebStateQuery (dataclass frozen slots):
__init__(fields: 'tuple[str, ...]', filter: 'Mapping[str, WebJSONValue]', sort: 'tuple[WebJSONValue, ...]', limit: 'int | None', raw: 'Mapping[str, WebJSONValue]') -> None
fields: tuple[str, ...]
filter: Mapping[str, WebJSONValue]
sort: tuple[WebJSONValue, ...]
limit: int | None
raw: Mapping[str, WebJSONValue]class WebStateContext (dataclass frozen slots):
__init__(origin: 'str', window_key: 'str | None' = None) -> None
origin: str
window_key: str | None = NoneThe callback signature is (query, context). query.fields is a normalized tuple, filter maps allowed filter fields to values, sort is an ordering tuple, limit is the result cap, and raw is the normalized query, not arbitrary extra request data. context provides origin and optional window_key; it is not bpy.context.
Custom state queries publicly support only fields, filter, sort, and limit, not offset, page, or cursor. Sort entries are declared field names or objects with field and direction (asc/desc). Unknown fields, disallowed filters/sorts, and excessive limits fail.
The callback performs actual filtering and sorting. The framework validates queries, projects fields, and limits lists; it does not apply filter or sort for the handler. Process ordering before returning the list. Premature truncation can produce incorrect results.
Return shapes: item and list
item may return an object or an object with an item field. list may return a list of objects or an object with items. Use [] for an empty list and None for no item. Return JSON-compatible data, not Blender objects or generators.
The framework builds snapshots, projects requested fields, and keeps the first limit entries, potentially marking truncated. String id fields form member IDs for state-source tracking. Declare and return stable IDs when actions operate on listed objects.
from .nexus_web import WebPlugin, WebStateQuery, WebStateContext, WebJSONValue
_ROWS = [
{"id": "draft", "name": "草稿", "selected": False},
{"id": "final", "name": "定稿", "selected": True},
]
def list_presets(query: WebStateQuery, context: WebStateContext) -> WebJSONValue:
rows = [dict(row) for row in _ROWS]
if "selected" in query.filter:
selected = query.filter["selected"]
if not isinstance(selected, bool):
raise ValueError("selected must be a boolean")
rows = [row for row in rows if row["selected"] == selected]
for order in reversed(query.sort):
if isinstance(order, str):
field, descending = order, False
elif isinstance(order, dict):
field = order["field"]
descending = order["direction"] == "desc"
else:
raise ValueError("Invalid sort entry")
if field != "name":
raise ValueError("Only name sorting is supported")
rows.sort(key=lambda row: row["name"], reverse=descending)
return rows
def setup_state(plugin: WebPlugin) -> None:
plugin.bridge().state(
"plugin.my_addon.presets", list_presets, shape="list",
fields={"id": {"type": "string"}, "name": {"type": "string"},
"selected": {"type": "boolean"}},
filterable=("selected",), sortable=("name",),
default_limit=20, max_limit=100,
)This example reads two add-on-owned demo records without changing Blender. Keep names, fields, and page calls consistent. Do not wrap callback results in snapshot; pages read snapshot from the framework result.
const webui = window.acquireNexusWebUI();
const { snapshot } = await webui.state.get("plugin.my_addon.presets", {
fields: ["id", "name", "selected"],
filter: { selected: true },
sort: [{ field: "name", direction: "asc" }],
limit: 20,
});
// Render snapshot.items in your existing view.invalidate: request a reread after changes
bridge.state.invalidate(key) Returns None. Change application data first, then invalidate its key. Actions can declare invalidates for automatic refresh after completion. Blender changes do not automatically invalidate custom add-on state; connect to your existing change notifications.
invalidate does not return a new snapshot or guarantee one notification per call. Unchanged projected content may not be pushed, and hidden pages may converge later. The framework owns snapshotRevision; do not increment it or substitute local timestamps.
Keep queries read-only. Do not create or modify scenes on reread. Mutually triggering callbacks and invalidations cause pointless refreshes; identify the real data-change entry point.
Errors and subscription cleanup
Missing fields and duplicate keys are registration errors. Invalid queries, permissions, and unknown state cause query failures. Callback exceptions or unsupported results fail page reads. Do not convert every error to an empty list, hiding the difference between no data and a failed read.
Call state.unsubscribe when the page no longer needs state. If leaving before asynchronous subscription finishes, release it after creation. See Subscription lifecycle. Public WebBridgeState has no per-key remove method; do not use internal removal APIs. Registrations are cleared when disposing the bridge or client.