--- 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 intProducer = rpl::single(123); const rpl::lifetime &lifetime = existingLifetime; // Sometimes needed if deduction is ambiguous or needs help: auto user = std::make_shared(); 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` The fundamental building block is `rpl::producer`. 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 // A producer that emits strings and can potentially emit a CustomError. auto stringProducerWithError = /* ... */; // Type: rpl::producer ``` 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::start_with_next`: ```cpp auto counter = /* ... */; // Type: rpl::producer rpl::lifetime lifetime; // Counter is consumed here, use std::move if it's an l-value. std::move( counter ) | rpl::start_with_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::start_with_next(...); // If you DON'T pass a lifetime to a start_... method: auto counter2 = /* ... */; // Type: rpl::producer rpl::lifetime subscriptionLifetime = std::move( counter2 ) | rpl::start_with_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 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::start_with_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::start_with_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::start_with_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 // 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 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 `start_with_next` can automatically unpack this tuple into separate lambda arguments. ```cpp auto countProducer = rpl::single(1); // Type: rpl::producer auto textProducer = rpl::single(u"hello"_q); // Type: rpl::producer 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::start_with_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::start_with_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 auto sourceB = /* ... */; // Type: rpl::producer // rpl::merge also duplicates internally. auto merged = rpl::merge(sourceA, sourceB); // Starting the merged producer consumes it. std::move( merged ) | rpl::start_with_next([=]\(QString &&value) { // Receives values from either sourceA or sourceB as they arrive. qDebug() << "Merged value: " << value; }, lifetime); ``` ## Key Concepts Summary * Use `rpl::producer` 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 `