---
description:
globs:
alwaysApply: true
---
# RPL (Reactive Programming Library) Guide

## Coding Style Note

**Use `auto`:** In the actual codebase, variable types are almost always deduced using `auto` (or `const auto`, `const auto &`) rather than being written out explicitly. Examples in this guide may use explicit types for clarity, but prefer `auto` in practice.

```cpp
// Prefer this:
auto intProducer = rpl::single(123);
const auto &lifetime = existingLifetime;

// Instead of this:
rpl::producer<int> intProducer = rpl::single(123);
const rpl::lifetime &lifetime = existingLifetime;

// Sometimes needed if deduction is ambiguous or needs help:
auto user = std::make_shared<UserData>();
auto data = QByteArray::fromHex("...");
```

## Introduction

RPL is the reactive programming library used in this project, residing in the `rpl::` namespace. It allows handling asynchronous streams of data over time.

The core concept is the `rpl::producer`, which represents a stream of values that can be generated over a certain lifetime.

## Producers: `rpl::producer<Type, Error = no_error>`

The fundamental building block is `rpl::producer<Type, Error>`. It produces values of `Type` and can optionally signal an error of type `Error`. By default, `Error` is `rpl::no_error`, indicating that the producer does not explicitly handle error signaling through this mechanism.

```cpp
// A producer that emits integers.
auto intProducer = /* ... */; // Type: rpl::producer<int>

// A producer that emits strings and can potentially emit a CustomError.
auto stringProducerWithError = /* ... */; // Type: rpl::producer<QString, CustomError>
```

Producers are typically lazy; they don't start emitting values until someone subscribes to them.

## Lifetime Management: `rpl::lifetime`

Reactive pipelines have a limited duration, managed by `rpl::lifetime`. An `rpl::lifetime` object essentially holds a collection of cleanup callbacks. When the lifetime ends (either explicitly destroyed or goes out of scope), these callbacks are executed, tearing down the associated pipeline and freeing resources.

```cpp
rpl::lifetime myLifetime;
// ... later ...
// myLifetime is destroyed, cleanup happens.

// Or, pass a lifetime instance to manage a pipeline's duration.
rpl::lifetime &parentLifetime = /* ... get lifetime from context ... */;
```

## Starting a Pipeline: `rpl::start_...`

To consume values from a producer, you start a pipeline using one of the `rpl::start_...` methods. These methods subscribe to the producer and execute callbacks for the events they handle.

The most common method is `rpl::on_next`:

```cpp
auto counter = /* ... */; // Type: rpl::producer<int>
rpl::lifetime lifetime;

// Counter is consumed here, use std::move if it's an l-value.
std::move(
    counter
) | rpl::on_next([=]\(int nextValue) {
    // Process the next integer value emitted by the producer.
    qDebug() << "Received: " << nextValue;
}, lifetime); // Pass the lifetime to manage the subscription.
// Note: `counter` is now in a moved-from state and likely invalid.

// If you need to start the same producer multiple times, duplicate it:
// rpl::duplicate(counter) | rpl::on_next(...);

// If you DON'T pass a lifetime to a start_... method:
auto counter2 = /* ... */; // Type: rpl::producer<int>
rpl::lifetime subscriptionLifetime = std::move(
    counter2
) | rpl::on_next([=]\(int nextValue) { /* ... */ });
// The returned lifetime MUST be stored. If it's discarded immediately,
// the subscription stops instantly.
// `counter2` is also moved-from here.
```

Other variants allow handling errors (`_error`) and completion (`_done`):

```cpp
auto dataStream = /* ... */; // Type: rpl::producer<QString, Error>
rpl::lifetime lifetime;

// Assuming dataStream might be used again, we duplicate it for the first start.
// If it's the only use, std::move(dataStream) would be preferred.
rpl::duplicate(
    dataStream
) | rpl::on_error([=]\(Error &&error) {
    // Handle the error signaled by the producer.
    qDebug() << "Error: " << error.text();
}, lifetime);

// Using dataStream again, perhaps duplicated again or moved if last use.
rpl::duplicate(
    dataStream
) | rpl::on_done([=] {
    // Execute when the producer signals it's finished emitting values.
    qDebug() << "Stream finished.";
}, lifetime);

// Last use of dataStream, so we move it.
std::move(
    dataStream
) | rpl::on_next_error_done(
    [=]\(QString &&value) { /* handle next value */ },
    [=]\(Error &&error) { /* handle error */ },
    [=] { /* handle done */ },
    lifetime);
```

## Transforming Producers

RPL provides functions to create new producers by transforming existing ones:

*   `rpl::map`: Transforms each value emitted by a producer.
    ```cpp
    auto ints = /* ... */; // Type: rpl::producer<int>
    // The pipe operator often handles the move implicitly for chained transformations.
    auto strings = std::move(
        ints // Explicit move is safer
    ) | rpl::map([](int value) {
        return QString::number(value * 2);
    }); // Emits strings like "0", "2", "4", ...
    ```

*   `rpl::filter`: Emits only the values from a producer that satisfy a condition.
    ```cpp
    auto ints = /* ... */; // Type: rpl::producer<int>
    auto evenInts = std::move(
        ints // Explicit move is safer
    ) | rpl::filter([](int value) {
        return (value % 2 == 0);
    }); // Emits only even numbers.
    ```

## Combining Producers

You can combine multiple producers into one:

*   `rpl::combine`: Combines the latest values from multiple producers whenever *any* of them emits a new value. Requires all producers to have emitted at least one value initially.
    While it produces a `std::tuple`, subsequent operators like `map`, `filter`, and `on_next` can automatically unpack this tuple into separate lambda arguments.
    ```cpp
    auto countProducer = rpl::single(1); // Type: rpl::producer<int>
    auto textProducer = rpl::single(u"hello"_q); // Type: rpl::producer<QString>
    rpl::lifetime lifetime;

    // rpl::combine takes producers by const-ref internally and duplicates,
    // so move/duplicate is usually not strictly needed here unless you
    // want to signal intent or manage the lifetime of p1/p2 explicitly.
    auto combined = rpl::combine(
        countProducer, // or rpl::duplicate(countProducer)
        textProducer   // or rpl::duplicate(textProducer)
    );

    // Starting the combined producer consumes it.
    // The lambda receives unpacked arguments, not the tuple itself.
    std::move(
        combined
    ) | rpl::on_next([=]\(int count, const QString &text) {
        // No need for std::get<0>(latest), etc.
        qDebug() << "Combined: Count=" << count << ", Text=" << text;
    }, lifetime);

    // This also works with map, filter, etc.
    std::move(
        combined
    ) | rpl::filter([=]\(int count, const QString &text) {
        return count > 0 && !text.isEmpty();
    }) | rpl::map([=]\(int count, const QString &text) {
        return text.repeated(count);
    }) | rpl::on_next([=]\(const QString &result) {
        qDebug() << "Mapped & Filtered: " << result;
    }, lifetime);
    ```

*   `rpl::merge`: Merges the output of multiple producers of the *same type* into a single producer. It emits a value whenever *any* of the source producers emits a value.
    ```cpp
    auto sourceA = /* ... */; // Type: rpl::producer<QString>
    auto sourceB = /* ... */; // Type: rpl::producer<QString>

    // rpl::merge also duplicates internally.
    auto merged = rpl::merge(sourceA, sourceB);

    // Starting the merged producer consumes it.
    std::move(
        merged
    ) | rpl::on_next([=]\(QString &&value) {
        // Receives values from either sourceA or sourceB as they arrive.
        qDebug() << "Merged value: " << value;
    }, lifetime);
    ```

## Key Concepts Summary

*   Use `rpl::producer<Type, Error>` to represent streams of values.
*   Manage subscription duration using `rpl::lifetime`.
*   Pass `rpl::lifetime` to `rpl::start_...` methods.
*   If `rpl::lifetime` is not passed, **store the returned lifetime** to keep the subscription active.
*   Use operators like `| rpl::map`, `| rpl::filter` to transform streams.
*   Use `rpl::combine` or `rpl::merge` to combine streams.
*   When starting a chain (`std::move(producer) | rpl::map(...)`), explicitly move the initial producer.
*   These functions typically duplicate their input producers internally.
*   Starting a pipeline consumes the producer; use `
