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

TypeScriptPython
createStore({...})create_store("Name", reducer={...}, ...)
store.emit(channel, type, payload)Promisestore.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 / registerEffectstore.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:

python
when = {"keys": [("ui", "add"), ("ui", "rename")]}

#Decorator sugar

The Python edition adds decorators over the imperative registration API:

python
@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, so 1 vs 1.0 and True vs 1 are distinguished (Python == would collapse them),
  • NaN equality.

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:

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.

Edit this page on GitHub