# fauxtp - Erlang/OTP primitives for Python

> Erlang/OTP primitives for Python's async ecosystem (via anyio)
> GitHub: https://github.com/fizzAI/fauxtp

## Installation

```bash
uv add fauxtp
```

## Quick Start

```python
from fauxtp import GenServer, call, cast
import anyio

class Counter(GenServer):
    async def init(self):
        return {"count": 0}

    async def handle_call(self, request, _from, state):
        match request:
            case "get":
                return state["count"], state
            case ("add", n):
                new_count = state["count"] + n
                return new_count, {"count": new_count}

    async def handle_cast(self, request, state):
        match request:
            case "reset":
                return {"count": 0}

async def main():
    async with anyio.create_task_group() as tg:
        pid = await Counter.start(task_group=tg)
        print(await call(pid, ("add", 5)))  # 5
        await cast(pid, "reset")
        print(await call(pid, "get"))       # 0

anyio.run(main)
```

---

# Core Concepts

## Actors

Actors are the fundamental unit of concurrency. Each actor is a lightweight process with its own private state and a mailbox for receiving messages.

**Key Principles:**
- **Isolation**: Actors do not share state. They communicate exclusively through message passing.
- **Mailbox**: Every actor has a mailbox that buffers incoming messages.
- **PID**: A Process Identifier used to address and send messages to an actor.

### Creating an Actor

```python
from fauxtp.actor.base import Actor
import anyio

class MyActor(Actor):
    async def run(self, state):
        msg = await self.receive()
        print(f"Received: {msg}")
        return state

async def main():
    async with anyio.create_task_group() as tg:
        pid = await MyActor.start(task_group=tg)
        await pid.send("Hello!")

anyio.run(main)
```

### Pattern Matching

Use Python's `match` statement for handling messages:

```python
async def run(self, state):
    match await self.receive():
        case ("ping", sender_pid):
            await sender_pid.send("pong")
        case "stop":
            self.stop()
    return state
```

## GenServer

`GenServer` (Generic Server) is a behavior module for implementing the server of a client-server relation. It abstracts away common patterns of state management and message handling.

**Benefits:**
- Standardized synchronous (`call`) and asynchronous (`cast`) communication
- Built-in state management
- Better integration with Supervisors

### Implementation

```python
from fauxtp import GenServer, call, cast

class Stack(GenServer):
    async def init(self):
        return []

    async def handle_call(self, request, _from, state):
        match request:
            case "pop":
                if not state:
                    return None, []
                val = state[0]
                return val, state[1:]
            case "peek":
                return state[0] if state else None, state

    async def handle_cast(self, request, state):
        match request:
            case ("push", item):
                return [item] + state
```

### Client API

```python
pid = await Stack.start(task_group=tg)
await cast(pid, ("push", 1))
val = await call(pid, "pop")  # 1
```

### Background Tasks

GenServer provides built-in support for long-running tasks:

```python
# Spawn a task
await self.spawn_task(some_async_function, arg1, arg2)

# Handle task completion
async def handle_task_end(self, child_pid, status, result, state):
    match status:
        case "success":
            print(f"Task {child_pid} succeeded with {result}")
        case "failure":
            print(f"Task {child_pid} failed with {result}")
    return state
```

## Supervisors

Supervisors monitor other actors (children) and restart them if they crash. This is the core of the "Let it crash" philosophy.

In fauxtp, the supervisor is intentionally minimal (see [`src/fauxtp/supervisor.py`](src/fauxtp/supervisor.py:1)):

- Children are declared up front via a list of `ChildSpec`.
- Children are restarted only when they exit with an `"error: ..."` reason.
- Supported strategies are `ONE_FOR_ONE` and `ONE_FOR_ALL`.

### Restart Strategies

- **ONE_FOR_ONE**: restart only the failed child
- **ONE_FOR_ALL**: cancel remaining children and restart the full set once all have exited

### Implementation

```python
import anyio

from fauxtp.registry import Registry
from fauxtp.supervisor import Supervisor, ChildSpec, RestartStrategy


async def main():
    async with anyio.create_task_group() as tg:
        registry = await Registry.start(task_group=tg)

        await Supervisor.start(
            children=[
                ChildSpec(actor=Worker, name="worker1"),
                ChildSpec(actor=Worker, name="worker2"),
            ],
            strategy=RestartStrategy.ONE_FOR_ONE,
            registry=registry,
            task_group=tg,
        )


anyio.run(main)
```

---

# API Reference

## Actor (`fauxtp.actor.base.Actor`)

Base actor class. Subclass and implement `run()`.

### Methods to Override

**`async init(self, *args, **kwargs) -> Any`**
Initialize actor state. Returns initial state.

**`async run(self, state: Any) -> Any`**
Main actor loop body. Called repeatedly. Should await `receive()` and handle messages. Returns new state.

**`async terminate(self, reason: str, state: Any) -> None`**
Cleanup when actor stops.

### Core Methods

**`async receive(self, *patterns: tuple[Any, MaybeAwaitableCallable], timeout: float | None = None) -> Any`**
Receive from this actor's mailbox. Each pattern is a `(matcher, handler)` tuple.

**`classmethod async start(cls, *args, task_group: TaskGroup, **kwargs) -> PID`**
Start this actor inside the given AnyIO TaskGroup and return its PID.

**`classmethod async start_link(cls, *args, task_group: TaskGroup, on_exit: Callable[[PID, str], Awaitable[None]] | None = None, **kwargs) -> ActorHandle`**
Start this actor inside the given AnyIO TaskGroup. Returns an `ActorHandle`.

**`def stop(self, reason: str = "normal")`**
Manually exits the actor with a given reason.

**`def start_soon_child(self, fn: MaybeAwaitableCallable, *args, name: str | None = None)`**
Starts a task in the actor's child task group.

**`async spawn_child_actor(self, actor_cls: type[Actor], *args, on_exit: MaybeAwaitableCallable | None = None, **kwargs) -> ActorHandle`**
Spawns a child actor supervised by this actor.

### Properties

**`pid: PID`** - The PID of the running actor
**`children: TaskGroup`** - A TaskGroup owned by this actor, cancelled when the actor exits

## GenServer (`fauxtp.actor.genserver.GenServer`)

Generic Server implementation. Inherits from `Actor`.

### Methods to Override

**`async handle_call(self, request: R, from_ref: Ref, state: S) -> tuple[R, S]`**
Handle synchronous request. Returns `(reply, new_state)`.

**`async handle_cast(self, request: R, state: S) -> S`**
Handle asynchronous request. Returns `new_state`.

**`async handle_info(self, message: R, state: S) -> S`**
Handle other messages. Returns `new_state`.

**`async handle_task_end(self, child_pid: PID, status: Literal["success"] | Literal["failure"], result: R, state: S) -> S`**
Handle task completion or failure.

### Public Methods

**`async spawn_task(self, func: Callable, *args: Any, **kwargs: Any) -> PID | None`**
Spawn a new task managed by this GenServer. Returns the task's PID, or `None` if the task limit is reached.

**`set_max_tasks(self, limit: int | None) -> None`**
Set the maximum number of concurrent tasks.

## Supervisor (`fauxtp.supervisor.Supervisor`)

Minimal supervisor actor (see [`src/fauxtp/supervisor.py`](src/fauxtp/supervisor.py:1)).

Start it inside an AnyIO `TaskGroup` (same as any actor) and pass:

- `children: list[ChildSpec]`
- `strategy: RestartStrategy` (default: `ONE_FOR_ONE`)
- `registry: PID | None` (if `None`, the supervisor starts an internal registry)

The supervisor restarts children only when their exit reason starts with `"error:"`.

## ChildSpec (`fauxtp.supervisor.ChildSpec`)

Specification for a supervised child actor.

### Attributes

**`actor: type[Actor]`** - The actor class to start
**`name: str`** - Name used for bookkeeping and registry registration
**`args: tuple[Any, ...] | None`** - Positional args for the actor constructor

## Messaging (`fauxtp.messaging`)

**`async send(target: PID, message: Any) -> None`**
Send a message to an actor's mailbox.

**`async cast(target: PID, request: Any) -> None`**
Send request to a GenServer, don't wait for reply.

**`async call(target: PID, request: Any, timeout: float = 5.0) -> Any`**
Send request to a GenServer and wait for reply.

## Pattern Matching (`fauxtp.primitives.pattern`)

The pattern matching system is used by `receive()` to selectively process messages from an actor's mailbox.

### Matchers

- **Literal Values**: Matches if the message is exactly equal to the value (e.g., `"ping"`, `123`)
- **Types**: Matches if the message is an instance of the type (e.g., `str`, `int`, `dict`). The value is extracted and passed to the handler.
- **Tuples**: Matches if the message is a tuple of the same length and each element matches its corresponding sub-pattern
- **`ANY`**: A special matcher that matches any value and extracts it
- **`IGNORE`** (or `_`): A special matcher that matches any value but does **not** extract it

### Examples

```python
from fauxtp.primitives.pattern import ANY, IGNORE

# Match a specific tag and extract the payload
# Message: ("data", 42)
pattern = ("data", ANY)
# Result: (42,) extracted

# Match a structure but ignore part of it
# Message: ("event", "user_login", "127.0.0.1")
pattern = ("event", IGNORE, ANY)
# Result: ("127.0.0.1",) extracted

# Match by type
# Message: {"key": "value"}
pattern = dict
# Result: ({"key": "value"},) extracted

# Nested matching
# Message: ("msg", ("sub", 1))
pattern = ("msg", ("sub", int))
# Result: (1,) extracted
```

## Registry (`fauxtp.registry.local`)

**`register(name: str, pid: PID) -> bool`**
Register a process globally by name. Returns `True` if successful.

**`unregister(name: str) -> bool`**
Unregister a process name globally.

**`whereis(name: str) -> Optional[PID]`**
Look up a process globally by name. Returns `PID` if found, `None` otherwise.

**`registered() -> list[str]`**
Get list of all registered process names.

## Task (`fauxtp.actor.task.Task`)

Generic actor wrapper that runs an asynchronous function.

**`classmethod async spawn(cls, func: MaybeAwaitableCallable, task_group: TaskGroup) -> TaskHandle`**
Spawns a task and returns a `TaskHandle`.

**`classmethod async spawn_and_notify(cls, func: MaybeAwaitableCallable, task_group: TaskGroup, parent_pid: PID, success_message_name: str = "$$success", failure_message_name: str = "$$failure") -> TaskHandle`**
Spawns a task and notifies the parent PID on completion.

---

# Common Patterns

## State Immutability

Always treat state as immutable. Return a new state from your handlers instead of modifying the existing state:

```python
# Good
async def handle_cast(self, request, state):
    match request:
        case ("push", item):
            return [item] + state  # New list

# Bad
async def handle_cast(self, request, state):
    match request:
        case ("push", item):
            state.append(item)  # Mutating state
            return state
```

## Supervision Trees

Supervisors can supervise other supervisors, allowing you to build hierarchical fault-tolerant systems.

Because `Supervisor` is just an `Actor`, you can supervise a subtree by using a `ChildSpec` whose `actor` is `Supervisor` and whose `args` construct the subtree supervisor:

```python
from fauxtp.supervisor import Supervisor, ChildSpec, RestartStrategy

app_children = [
    ChildSpec(actor=Worker, name="worker1"),
    ChildSpec(actor=Worker, name="worker2"),
]

app_spec = ChildSpec(
    actor=Supervisor,
    name="workers",
    args=(app_children, RestartStrategy.ONE_FOR_ONE, registry),
)

root_children = [
    app_spec,
    ChildSpec(actor=MonitorActor, name="monitor"),
]

# Start the root supervisor with ONE_FOR_ALL
await Supervisor.start(
    children=root_children,
    strategy=RestartStrategy.ONE_FOR_ALL,
    registry=registry,
    task_group=tg,
)
```

## Lifecycle Management

Actors are started within an AnyIO `TaskGroup`. When the `TaskGroup` exits, all actors started within it are cancelled:

```python
async def main():
    async with anyio.create_task_group() as tg:
        pid = await MyActor.start(task_group=tg)
        # Actor runs here
    # Actor is cancelled when exiting the context
```

You can also stop an actor manually:

```python
pid.stop("shutdown")
```

---

# Why fauxtp?

Async Python often devolves into a mess of unmanaged tasks and race conditions. OTP solved this decades ago with structured concurrency trees. fauxtp brings these battle-tested patterns to Python's async ecosystem.

**What's inside:**
- **Actors**: `send`, `receive` (with pattern matching)
- **GenServer**: `call`, `cast`, `info`
- **Supervisors**: `one_for_one`, `one_for_all`
- **Registry**: `register(name, pid)`, `whereis(name)`

**Note:** This is not the BEAM. It is not as fast. But it is a way to write concurrent Python that doesn't make you want to quit programming.
