morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
queueing_dispatcher.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <cstddef>
5#include <deque>
6#include <exception>
7#include <functional>
8#include <memory>
9#include <string>
10#include <utility>
11
12#include "../attributes.hpp"
13#include "../core/backend.hpp"
14#include "../core/completion.hpp"
15#include "interpreter.hpp"
16
17/// @file
18/// The client's offline queue for a document's mutations (spec 5 ยง5, `"offline": "queue"`): a `Dispatcher` that holds
19/// a call that cannot reach the server and settles it when its replay does.
20
21namespace morph::ui {
22
23/// @brief A `Dispatcher` that holds a queued call the server cannot be reached for, and replays it on reconnect.
24///
25/// A call made with `CallOptions::queueOffline` goes through the wrapped dispatcher. When it fails with
26/// `backend::DisconnectedError` it is held, and the completion the caller has stays pending: the mutation stays
27/// `pending` and its elements `stale`. Once anything is held, a later queued call joins the back of the queue without
28/// being tried, so the server sees the calls in the order they were made. `replay` re-sends the held calls, oldest
29/// first and one at a time, and settles each caller's completion with the replay's reply or failure; a replay that
30/// cannot reach the server either stops the replay and stays held for the next one. A call without `queueOffline`, and
31/// an evaluation, are never held: offline, they fail at once.
32///
33/// Single-threaded: every call, and `replay`, runs on the owner the replies are delivered on. Held calls keep their
34/// channel, and with it the model binding, until they settle.
35class QueueingDispatcher final : public Dispatcher {
36public:
37 /// @brief Queues around @p inner, delivering on @p owner.
38 /// @param inner The dispatcher calls go through. Borrowed: it must outlive this one and every channel it bound.
39 /// @param owner The executor replies are delivered on: the runtime's owner. Borrowed.
40 QueueingDispatcher(Dispatcher& inner MORPH_LIFETIMEBOUND, exec::IExecutor* owner MORPH_LIFETIMEBOUND)
41 : _inner{&inner}, _state{std::make_shared<State>(owner)} {}
42
43 /// @brief Binds through the wrapped dispatcher.
44 /// @param typeId The model's registered type id.
45 /// @param sharing Private, or shared.
46 /// @param instanceKey For a shared binding, the instance key; empty for the model's shared singleton.
47 /// @return A channel whose queued calls are held while the server cannot be reached.
48 [[nodiscard]] std::shared_ptr<ModelChannel> bind(std::string const& typeId, bridge::BindSharing sharing,
49 std::string const& instanceKey) override {
50 return std::make_shared<Channel>(_inner->bind(typeId, sharing, instanceKey), _state);
51 }
52
53 /// @brief Re-sends the held calls, oldest first, one at a time; each caller's completion settles with its replay.
54 /// A call that still cannot reach the server stops the replay and stays held, with every call after it.
55 /// Nothing happens while a replay is already running.
56 void replay() { State::next(_state); }
57
58 /// @brief How many calls are held.
59 /// @return The count.
60 [[nodiscard]] std::size_t held() const noexcept { return _state->held.size(); }
61
62 /// @brief Called with the held count whenever it changes, such as for a "waiting to send" indicator.
63 /// @param observer The observer; empty for none.
64 void setOnHeld(std::function<void(std::size_t)> observer) { _state->observer = std::move(observer); }
65
66private:
67 struct Held {
68 std::shared_ptr<ModelChannel> channel;
69 std::string actionId;
70 std::string bodyJson;
72 };
73
74 struct State {
75 explicit State(exec::IExecutor* executor) : owner{executor} {}
76
77 exec::IExecutor* owner;
78 std::deque<Held> held;
79 bool replaying = false;
80 std::function<void(std::size_t)> observer;
81
82 // Holds @p call at the back of the queue.
83 void hold(Held call) {
84 held.push_back(std::move(call));
85 changed();
86 }
87
88 void changed() const {
89 if (observer) {
90 observer(held.size());
91 }
92 }
93
94 // Sends the oldest held call; on its reply settles its caller and sends the next.
95 static void next(std::shared_ptr<State> const& state) {
96 if (state->replaying || state->held.empty()) {
97 return;
98 }
99 state->replaying = true;
100 Held const& front = state->held.front();
101 std::weak_ptr<State> const weak = state;
102 front.channel->execute(front.actionId, front.bodyJson, CallOptions{})
103 .then([weak](std::string const& reply) {
104 if (auto const held = weak.lock()) {
105 settle(held, [&reply](Held& call) { call.promise.resolve(reply); });
106 }
107 })
108 .onError([weak](std::exception_ptr const& error) {
109 auto const held = weak.lock();
110 if (!held) {
111 return;
112 }
113 if (isOffline(error)) {
114 // Still unreachable: it stays first, and the next replay starts with it.
115 held->replaying = false;
116 return;
117 }
118 settle(held, [&error](Held& call) { call.promise.reject(error); });
119 });
120 }
121
122 // Settles the oldest held call, drops it and replays the next.
123 static void settle(std::shared_ptr<State> const& state, std::function<void(Held&)> const& outcome) {
124 Held call = std::move(state->held.front());
125 state->held.pop_front();
126 state->replaying = false;
127 state->changed();
128 outcome(call);
129 next(state);
130 }
131 };
132
133 class Channel final : public ModelChannel {
134 public:
135 Channel(std::shared_ptr<ModelChannel> inner, std::shared_ptr<State> state)
136 : _inner{std::move(inner)}, _state{std::move(state)} {}
137
138 [[nodiscard]] async::Completion<std::string> execute(std::string const& actionId, std::string const& bodyJson,
139 CallOptions options) override {
140 if (!options.queueOffline || options.evaluate) {
141 return _inner->execute(actionId, bodyJson, options);
142 }
143 auto [held, promise] = async::Completion<std::string>::makeSettleable(_state->owner);
144 if (!_state->held.empty()) {
145 // Behind calls already waiting: sending it now would overtake them.
146 _state->hold(Held{
147 .channel = _inner, .actionId = actionId, .bodyJson = bodyJson, .promise = std::move(promise)});
148 return std::move(held);
149 }
150 auto const settle = std::make_shared<async::Completion<std::string>::Promise>(std::move(promise));
151 std::weak_ptr<State> const weak = _state;
152 _inner->execute(actionId, bodyJson, options)
153 .then([settle](std::string const& reply) { settle->resolve(reply); })
154 .onError([inner = _inner, weak, settle, actionId, bodyJson](std::exception_ptr const& error) {
155 auto const state = weak.lock();
156 if (state && isOffline(error)) {
157 state->hold(Held{.channel = inner,
158 .actionId = actionId,
159 .bodyJson = bodyJson,
160 .promise = std::move(*settle)});
161 return;
162 }
163 settle->reject(error);
164 });
165 return std::move(held);
166 }
167
168 private:
169 std::shared_ptr<ModelChannel> _inner;
170 std::shared_ptr<State> _state;
171 };
172
173 [[nodiscard]] static bool isOffline(std::exception_ptr const& error) {
174 try {
175 std::rethrow_exception(error);
176 } catch (backend::DisconnectedError const&) {
177 return true;
178 } catch (...) {
179 return false;
180 }
181 }
182
183 Dispatcher* _inner;
184 std::shared_ptr<State> _state;
185};
186
187} // namespace morph::ui
Producer-side handle paired with a Completion<T> by makeSettleable().
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
void setOnHeld(std::function< void(std::size_t)> observer)
Called with the held count whenever it changes, such as for a "waiting to send" indicator.
void replay()
Re-sends the held calls, oldest first, one at a time; each caller's completion settles with its repla...
std::shared_ptr< ModelChannel > bind(std::string const &typeId, bridge::BindSharing sharing, std::string const &instanceKey) override
Binds through the wrapped dispatcher.
std::size_t held() const noexcept
How many calls are held.
QueueingDispatcher(Dispatcher &inner, exec::IExecutor *owner)
Queues around inner, delivering on owner.
The UI document interpreter: Interpreter::mount turns a loaded Document into reactive state and a vie...
Abstract executor interface.
Definition executor.hpp:70