Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/+saga-effects-expansion.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added Race (first-wins with loser cancellation), All (wait-all with fail-fast), Take (pause until action dispatched), and Debounce (cancel-and-restart timer) saga effects. Fixed Python 2 exception syntax bug in pipeline handler introspection. Added gateway test suite covering namespacing, routing, proxying, idle reaping, and error handling.
8 changes: 8 additions & 0 deletions src/milo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def __getattr__(name: str):
"Retry": "_types",
"Timeout": "_types",
"TryCall": "_types",
"Race": "_types",
"All": "_types",
"Take": "_types",
"Debounce": "_types",
"Cmd": "_types",
"Batch": "_types",
"Sequence": "_types",
Expand Down Expand Up @@ -154,6 +158,7 @@ def _Py_mod_gil() -> int: # noqa: N802
"CLI",
"DEFAULT_THEME",
"Action",
"All",
"App",
"AppError",
"AppStatus",
Expand All @@ -168,6 +173,7 @@ def _Py_mod_gil() -> int: # noqa: N802
"ConfigSpec",
"Context",
"CycleError",
"Debounce",
"Delay",
"Description",
"DevServer",
Expand Down Expand Up @@ -213,6 +219,7 @@ def _Py_mod_gil() -> int: # noqa: N802
"PromptDef",
"Put",
"Quit",
"Race",
"ReducerResult",
"RenderTarget",
"RequestLog",
Expand All @@ -225,6 +232,7 @@ def _Py_mod_gil() -> int: # noqa: N802
"SpecialKey",
"StateError",
"Store",
"Take",
"ThemeProxy",
"ThemeStyle",
"TickCmd",
Expand Down
72 changes: 72 additions & 0 deletions src/milo/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,78 @@ class TryCall:
kwargs: dict = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class Race:
"""Run multiple sagas concurrently, return the first result.

Losers are cancelled via their cancel events as soon as a winner
completes. If all racers fail, the first error is thrown into
the parent saga::

winner = yield Race(sagas=(fetch_primary(), fetch_fallback()))

Raises ``StateError`` if *sagas* is empty.
"""

sagas: tuple


@dataclass(frozen=True, slots=True)
class All:
"""Run multiple sagas concurrently, wait for all to complete.

Returns a tuple of results in the same order as the input sagas.
Fail-fast: if any saga raises, remaining sagas are cancelled and
the error is thrown into the parent::

a, b = yield All(sagas=(fetch_users(), fetch_roles()))

An empty tuple returns ``()`` immediately.
"""

sagas: tuple


@dataclass(frozen=True, slots=True)
class Take:
"""Pause the saga until a matching action is dispatched.

Waits for *future* actions only — actions dispatched before the
Take is yielded are not matched. Returns the full ``Action``
object so the saga can inspect both type and payload::

action = yield Take("USER_CONFIRMED")
name = action.payload["name"]

An optional *timeout* (in seconds) raises ``TimeoutError`` if the
action is not dispatched in time.
"""

action_type: str
timeout: float | None = None


@dataclass(frozen=True, slots=True)
class Debounce:
"""Delay-then-fork: start a timer, fork *saga* when it expires.

If the parent saga yields another ``Debounce`` before the timer
fires, the previous timer is cancelled and restarted. The parent
continues immediately (non-blocking)::

# In a keystroke handler saga:
while True:
key = yield Take("@@KEY")
yield Debounce(seconds=0.3, saga=search_saga)

The debounced saga runs independently; use ``Take`` if the parent
needs the result.
"""

seconds: float
saga: Callable


# ---------------------------------------------------------------------------
# Commands (lightweight alternative to sagas)
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading