Docs
Migrating from @yoltra/core (TypeScript)
Yoltra for Python is a semantic port of @yoltra/core. The reduce / diff /
subscription model is identical (and cross-language conformance-tested), so your
mental model carries over directly. This guide maps the API and calls out the
intentional Python divergences.
#API mapping
| TypeScript | Python |
|---|---|
createStore({...}) | create_store("Name", reducer={...}, ...) |
store.emit(channel, type, payload) → Promise | store.emit(channel, type, payload) → EmitHandle |
await store.emit(...) | await store.emit(...) / await store.aemit(...) / handle.wait() |
store.getState() | store.get_state() |
store.connect({ reducer, property }, handler) | store.connect(reducer=..., property=..., handler=...) |
store.subscribe(fn) | store.subscribe(fn) |
store.onEvent(channel, type, handler, phase) | store.on_event(channel, type, handler, phase="committed") |
store.onEffect(channel, type, handler) | store.on_effect(channel, type, handler) |
store.registerReducer / registerMiddleware / registerEffect | store.register_reducer / register_middleware / register_effect |
store.instrument(observer) | store.instrument(observer) |
detectChangedProps(prev, next) | detect_changed_props(prev, next) |
Change { oldValue, newValue, path } | Change(old_value, new_value, path) |
When ({ keys | channel | channels | any }) | When (same shape; a TypedDict) |
Naming is snake_case throughout, and Change's fields are old_value /
new_value. Event keys are plain tuples — no as const needed:
when = {"keys": [("ui", "add"), ("ui", "rename")]}#Decorator sugar
The Python edition adds decorators over the imperative registration API:
@store.reducer(name="todos", state={"items": []}, when={"keys": [("ui", "add")]})
def todos(prev, event): ...
@store.effect(when={"keys": [("ui", "add")]})
async def persist(event, get_state, emit): ...
@store.middleware()
def guard(state, event, emit): ...#Intentional divergences
These are deliberate, and documented so nothing surprises you.
#Synchronous commit, EmitHandle for effects
In both editions the reduce phase is synchronous. In TypeScript emit returns a
Promise that resolves after effects. In Python emit returns an EmitHandle
that is both awaitable (await store.emit(...)) and blockable from sync code
(handle.wait()), so the same store works in scripts, notebooks, and async
servers. Choose where effects run with effects_mode ("auto" / "inline" /
"asyncio") — a Python-specific control with no TS equivalent.
#Richer, type-aware diff
The TypeScript diff special-cases only Date and RegExp. The Python diff adds
handling that has no TS analog (design §7.4):
set/frozenset,datetime/date,Decimal,tuple,bytes, dataclasses / Pydantic models (by field iteration),- a
type(old) is not type(new)guard, so1vs1.0andTruevs1are distinguished (Python==would collapse them), NaNequality.
These extensions are excluded from the cross-language conformance corpus (the corpus covers only the JSON-serializable shared subset); everything in that shared subset behaves identically in both languages.
#Middleware veto semantics
Python middleware vetoes only on an explicit False; returning None
(a missing return) or True lets the event through. This is friendlier than
the TS !ok check, which also vetoes on undefined.
#Paths are plain strings
There is no compile-time Path<T> / PathValue<T, P> — Python has no
equivalent type-system feature. Paths are str; a stale path is a tested runtime
concern (Django-__-lookup style). This is a v1 non-goal, not a limitation of
the model.
#Not ported: HMR
replaceReducers / replaceMiddleware / replaceEffects / hotReplace are a
JS-bundler concept with no Python analog and are not ported. Use runtime
register_* / the returned unsubscribe functions for dynamic changes.
#First-class coarse_only
The per-slice performance opt-out is first-class in Python:
@store.reducer(name="big", state={...}, when={...}, coarse_only=True)
def big(prev, event): ...coarse_only=True skips the per-slice structural diff and fires one whole-slice
signal plus coarse listeners — for slices where you don't need path-level
granularity.
#Immutability mechanism
TypeScript deep-freezes state in place (Object.freeze). Python has no in-place
freeze, so get_state() returns a recursive read-only proxy in development;
stored state stays raw. The behavior (mutations raise) matches; the mechanism
differs. Production is a pass-through in both.
#Same semantics, verified
The diff algorithm and the glob matcher are pinned by a shared JSON corpus run
through both cores (conformance/), so a (prev, next) pair or a
pattern/path match resolves the same way in TypeScript and Python.