Stores¶
Stores are read-only reactive mappings and sequences. Update them with a synchronous, scoped draft. Containers have entity identity: moving a row changes its position without changing which object its proxy represents.
from wybthon import create_store, flush, snapshot
state, write = create_store(
{
"user": {"name": "Ada"},
"items": [{"id": 1, "title": "Build a Python UI"}],
}
)
def edit(draft):
draft.user.name = "Grace"
draft["items"].append({"id": 2, "title": "Test it in the browser"})
write(edit)
flush()
assert state.user.name == "Grace"
Collection protocols¶
Store implements Mapping, and StoreList implements Sequence. Use indexing, iteration, len, membership, and ordinary mapping methods such as .get(), .items(), and .keys(). Attribute access is convenient for nonconflicting string keys. A key named items is state["items"]; state.items() is the mapping method. Missing entries raise ordinary Python exceptions: KeyError for indexing and AttributeError for attribute access. state.get(key, default=None) doesn't raise. It's tracked either way, so a computation reading state.get(row_id) reruns when that key appears.
Drafts implement MutableMapping and MutableSequence. Use assignment, deletion, update, append, extend, insert, pop, slices, reverse, and sort as appropriate. A draft and every nested draft expire when the callback returns. Escaped reads or writes raise DraftExpiredError.
A callback that raises leaves the published store unchanged. Return None after mutations. Returning replacement mapping or sequence data replaces the draft contents; don't accidentally return the result of pop or a tuple of mutations. Async setters are rejected. Await external work in an action, then perform a synchronous draft edit.
Versions and subscriptions¶
Successful edits stage versions without changing the revealed data. Before a flush, ordinary reads keep seeing the previous version, even for a key that has never been read. During held transitions the previous version remains visible until reveal. Actions see their own staged edits.
Property reads, membership, list length, mapping keys, and subtree versions have separate dependencies. Changing a sibling subtree doesn't rerun an effect that reads deep(state.user). Negative list indices also track length.
from wybthon import create_effect, deep
create_effect(lambda: state.user.name, print)
create_effect(lambda: deep(state.user), lambda user: print(user))
snapshot(value) returns detached plain data without tracking. Changing the snapshot can't change the store. deep(value) returns detached plain data and subscribes to that subtree. Neither exposes mutable backing data.
Identity and reconciliation¶
For(lambda: state["items"], row) can use store edit records directly. Default matching preserves entity identity. Use keyed=lambda item: item.id when replacement objects represent the same application entity; that callback receives item and index accessors.
from wybthon import reconcile
write(
reconcile(
{
"user": {"name": "Grace"},
"items": [
{"id": 2, "title": "Updated"},
{"id": 1, "title": "Build a Python UI"},
],
},
key="id",
)
)
key names the identifying field ("id" by default) or is a function computing the identity from an entity, such as key=lambda row: (row["kind"], row["id"]). Keyed reconciliation updates matching entities in place and matches duplicate keys in occurrence order. key=None replaces list entities positionally. Store drafts preserve entities when moving existing proxies. Initial cyclic input isn't supported.
Derived and optimistic stores¶
create_projection(fn, seed) and create_store(fn, seed) derive a read-only store from tracked reads. The callback may return data or mutate a draft argument. It can await work or yield values from an async generator. Draft changes publish atomically when that result is ready.
from wybthon import create_projection, is_pending, refresh
async def load_user():
return await fetch_user(user_id())
user = create_projection(load_user, {"name": ""})
# Read user.name inside Loading content.
busy = lambda: is_pending(lambda: user.name)
# In an async handler: await refresh(user)
Errors surface when the projection is read, so Errored handles them. Refresh is quiet and awaitable. Disposing the owner cancels the producer.
A projection also replaces a selection helper. Because it notifies only the keys that change, selecting a different row updates two rows, not every row:
from wybthon import For, create_projection, create_signal, td, tr
selected, set_selected = create_signal(None)
is_selected = create_projection(lambda: {} if selected() is None else {selected(): True})
For(
lambda: state["items"],
lambda item, i: tr(
td(lambda: item.title),
class_=lambda: "active" if is_selected.get(item.id) else None,
on_click=lambda: set_selected(item.id),
),
)
create_optimistic_store(source, seed) returns a store and an optimistic draft setter. Active edits replay over new authoritative data, and each action's edits disappear when its dependency group settles. Keep draft callbacks deterministic. See Runtime contracts for concurrent action, acknowledgment, and cancellation semantics.