mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-07 07:33:52 +02:00
216 lines
8.4 KiB
Text
216 lines
8.4 KiB
Text
---
|
|
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::start_with_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::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<int>
|
|
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<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::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<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 `start_with_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::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<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::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<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 `
|