morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
source.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <chrono>
5#include <cstdint>
6#include <expected>
7#include <functional>
8#include <map>
9#include <memory>
10#include <optional>
11#include <string>
12#include <string_view>
13#include <vector>
14
15#include "../core/backend.hpp"
16#include "../core/bridge.hpp"
17#include "../core/executor.hpp"
19#include "../session/session.hpp"
20#include "../ui/document.hpp"
21#include "../ui/explorer.hpp"
22#include "../ui/frontend.hpp"
23#include "../ui/interpreter.hpp"
25#include "../ui/service.hpp"
27#include "args.hpp"
28
29/// @file
30/// @brief Where the generic client's screens and models come from: a server (remote) or an application linked
31/// in-process (local). Spec 4 §3.
32///
33/// Specified in `docs/spec/client/client.md`.
34
35namespace morph::client {
36
37/// @brief What a local application is set up with.
38struct LocalSetup {
39 /// @brief The database path, from `--db`; empty for the application's default.
40 std::string db;
41 /// @brief The development principal, from `--user`.
42 std::string user;
43};
44
45/// @brief An application the client links and runs in-process.
46struct LocalApp {
47 /// @brief The name `--app` selects it by.
48 std::string name;
49 /// @brief Builds its bundle: the same function its server calls.
50 std::function<std::expected<std::shared_ptr<ui::ServedBundle const>, ui::Refusal>()> bundle;
51 /// @brief Run once before the first model call, with the client's options; empty for none.
52 std::function<void(LocalSetup const&)> setUp;
53 /// @brief The authorizer the application's own server checks requests with, for an in-process server to check
54 /// them the same way; empty for one that lets every request through. Only a test's simulated server calls
55 /// it.
56 std::function<std::shared_ptr<session::IAuthorizer>()> serverAuthorizer;
57};
58
59/// @brief The vocabularies the generic client speaks.
60/// @return Their names: `ui/1` and the expression vocabulary.
61[[nodiscard]] std::vector<std::string> clientVocabularies();
62
63/// @brief The `ui-hello` the generic client opens with, and sends again on each poll: its vocabularies, and the
64/// offer of server-sent events on the connection.
65/// @return The request.
67
68/// @brief An `AppSource` that also hands the shell what it mounts screens with, once it delivered a bundle.
69///
70/// Opened, it holds a bridge over its backend, owned by the context's executor, and the verified bundle with its
71/// catalog. Everything it hands out is valid from a successful `ready` until `close`.
72class Source : public ui::AppSource {
73public:
74 /// @brief Where the screens' model calls go.
75 /// @return The dispatcher over the source's bridge.
76 /// @throws std::logic_error before a bundle was delivered.
77 [[nodiscard]] ui::Dispatcher& dispatcher();
78
79 /// @brief The catalog facts the screens load with.
80 /// @return The facts of the catalog reply; empty before a bundle was delivered.
81 [[nodiscard]] ui::Catalog catalog() const;
82
83 /// @brief The screens the principal may open, in catalog order: each catalog entry that is available or public
84 /// and whose document the bundle holds.
85 /// @return Their ids.
86 [[nodiscard]] std::vector<std::string> screens() const;
87
88 /// @brief Whether the catalog marks @p screenId public: a screen served before sign-in, such as a login screen.
89 /// @param screenId The screen.
90 /// @return Whether it is listed and public.
91 [[nodiscard]] bool isPublic(std::string_view screenId) const;
92
93 /// @brief Opens the source: the transport, the bridge and the bundle, verified.
94 /// @param context The running frontend's context.
95 /// @param ready Called once, on the owner, unless `close` comes first.
96 void open(ui::AppContext& context, std::function<void(std::expected<ui::Bundle, ui::ConnectError>)> ready) final;
97
98 /// @brief Refused: a switch between backends is the hosting layer's (spec 6 §9).
99 /// @param backend Ignored.
100 /// @param done Called with the refusal.
101 void switchBackend(ui::Backend backend, std::function<void(std::expected<void, std::string>)> done) final;
102
103 /// @brief Drops a `ready` not yet delivered and releases the bridge, the transport and the bundle.
104 void close() final;
105
106 /// @brief Installs a sign-in (spec 6 §5): a session built from the current one with @p principal and @p token
107 /// replaced, carried by every later model call and UI-service request, then the catalog reloaded for it.
108 /// @param principal The principal the sign-in mutation answered.
109 /// @param token The session token it answered; empty when the source's server needs none, as in process.
110 /// @param done Called once, on the owner, once the catalog for the new session is current, with whether it
111 /// loaded; not called after `close`.
112 void signIn(std::string principal, std::string token, std::function<void(bool)> done);
113
114 /// @brief Ends the session: back to the principal before any sign-in, with no token, and the catalog reloaded
115 /// for it.
116 /// @param done As `signIn`'s.
117 void signOut(std::function<void(bool)> done);
118
119 /// @brief The lookup a `DocumentScreenHost` takes (`DocumentScreenHostOptions::documents`), over the loaded
120 /// bundle: each screen the source lists, loaded against the current catalog facts the first time it is
121 /// asked for after a load, so a shell of an application's own mounts with the same source.
122 /// @param onRefused Called with each document that does not load; may be empty.
123 /// @return The lookup, valid while the source lives; it answers null for a screen the source does not list or
124 /// whose document does not load.
125 [[nodiscard]] std::function<std::shared_ptr<ui::Document const>(std::string_view)> documents(
126 std::function<void(ui::Refusal const&)> onRefused = {});
127
128 /// @brief The bundle the catalog currently lists.
129 /// @return It; null before a bundle was delivered.
130 [[nodiscard]] ui::Bundle const* bundle() const noexcept { return _loaded ? &_loaded->bundle : nullptr; }
131
132 /// @brief The catalog for the current principal: its entries, what the principal may run, and the facts.
133 /// @return It; null before a bundle was delivered.
134 [[nodiscard]] ui::CatalogReply const* catalogReply() const noexcept {
135 return _loaded ? &_loaded->catalog : nullptr;
136 }
137
138 /// @brief The `schemas` document of every model the catalog names, by model type, or why it did not arrive: what
139 /// the explorer lists each model's actions from.
140 /// @return Them; empty before a bundle was delivered.
141 [[nodiscard]] ui::SchemasByModel const& schemasByModel() const noexcept { return _byModel; }
142
143 /// @brief The principal the source's session carries now.
144 /// @return It; empty for none.
145 [[nodiscard]] std::string const& principal() const noexcept { return _session.principal; }
146
147 /// @brief The file side channel (`files/1`) over the source's exchange, carrying its session: what a screen's
148 /// `upload` and `download` move bytes through.
149 /// @return It, or nothing before the source is open.
150 [[nodiscard]] std::optional<ui::FileTransfer> files() const;
151
152 /// @brief One step of development reload (spec 5 §12): sends `ui-hello`, and when the manifest digest changed,
153 /// fetches the changed bundle and the catalog for it. Nothing is sent while a poll is still in flight.
154 /// @param changed Called on the owner once the changed bundle is current, with the ids of the documents it
155 /// changed; not called for an unchanged digest, a failed poll, or after `close`.
156 void poll(std::function<void(std::vector<std::string> changed)> changed);
157
158 /// @brief Adds a listener called on the owner after each successful connect, the first included. On a reconnect
159 /// the source first sends `ui-hello` again, so the new connection negotiates events, then calls the
160 /// listeners.
161 /// @param listener Called on the owner; it must stay callable while the source is open.
162 void onConnected(std::function<void()> const& listener) final;
163
164 /// @brief Adds a listener for the events the server sends: what a shell hands to `Session::deliverEvent`.
165 /// @param listener Called on the owner, once per event, in the order they arrived.
166 void onEvent(std::function<void(wire::EventBody const&)> listener);
167
168 /// @brief What the session's `SessionOptions::watcher` sends the cache's watch changes through: `watcherOver` the
169 /// source's exchange, stamping the session current at each send. Empty for a source whose transport
170 /// carries no server events, such as an in-process one.
171 /// @return The watcher, or empty.
172 [[nodiscard]] ui::Watcher watcher();
173
174 /// @brief The principal before any sign-in, which `signOut` returns to.
175 /// @return It; empty for none.
176 [[nodiscard]] virtual std::string signedOutPrincipal() const { return {}; }
177
178protected:
179 /// @brief What a concrete source opens with: the bridge's backend and the exchange the UI service is reached by.
180 struct Transport {
181 /// @brief The backend; owned by the bridge from here on.
182 std::unique_ptr<backend::detail::IBackend> backend;
183 /// @brief The exchange; it may borrow the backend, which the source guards.
185 /// @brief Whether the server behind the transport sends events and handles `watch`: true for a server, false
186 /// in process.
187 bool serverEvents = false;
188 };
189
190 /// @brief Makes the transport on @p context.
191 /// @param context The running frontend's context.
192 /// @return The transport, or why there is none.
193 [[nodiscard]] virtual std::expected<Transport, ui::ConnectError> connect(ui::AppContext& context) = 0;
194
195 /// @brief How long `open` waits for the bundle before it reports the source unreachable; zero waits forever.
196 /// @return The bound.
197 [[nodiscard]] virtual std::chrono::milliseconds openTimeout() const { return std::chrono::milliseconds{0}; }
198
199 /// @brief Releases what `connect` made beside the backend; called by `close`, after the bridge is gone.
200 virtual void disconnect() {}
201
202 /// @brief Fetches the `{actionType: schema}` document of one model type, which a `form`, `collection` or
203 /// `wizard` node is built from. The default sends a `schemas` envelope over the source's exchange.
204 /// @param modelType The model type id.
205 /// @param done Called once with the document's JSON text, or why there is none.
206 virtual void fetchSchemas(std::string const& modelType,
207 std::function<void(std::expected<std::string, std::string>)> done);
208
209 /// @brief The session every request carries, for a source to fill before `open` sends anything.
210 /// @return It.
211 [[nodiscard]] session::Context& requestSession() noexcept { return _session; }
212
213private:
214 // The documents `documents()` loaded, by screen id, each with the text and the catalog load it was loaded for.
215 struct LoadedDocument {
216 std::string text;
217 std::uint64_t catalogLoad = 0;
218 std::shared_ptr<ui::Document const> document;
219 };
220 std::map<std::string, LoadedDocument, std::less<>> _documents;
221 // Counts the catalogs loaded, so a document loaded against an older one is loaded again.
222 std::uint64_t _catalogLoads = 0;
223 session::Context _session;
224 std::shared_ptr<bool> _alive;
225 std::unique_ptr<bridge::Bridge> _bridge;
226 std::unique_ptr<ui::BridgeDispatcher> _dispatcher;
227 // The offline queue in front of the dispatcher: what a screen's calls go through.
228 std::unique_ptr<ui::QueueingDispatcher> _queue;
229 // Retries the queue while it holds anything: the transports reconnect on their own and tell no one.
230 std::optional<reactive::TimerHandle> _replay;
231 ui::Exchange _exchange;
232 // Whether the open transport's server sends events and handles `watch`.
233 bool _serverEvents = false;
234 // Called after each connect, and with each event.
235 std::vector<std::function<void()>> _connectListeners;
236 std::vector<std::function<void(wire::EventBody const&)>> _eventListeners;
237 // A connect, on the owner: `ui-hello` again once the first bundle loaded, then the listeners.
238 void connected();
239 // Fetches the schemas of every model type the catalog names, then calls @p done; nothing after `close`.
240 void loadSchemas(std::shared_ptr<bool> const& alive, std::function<void()> done);
241 // The model types the loaded catalog names: its models, and the model of each action it has facts for.
242 [[nodiscard]] std::vector<std::string> modelTypes() const;
243 // Records each action's schema of one `{actionType: schema}` document; a document that does not parse adds none.
244 void recordSchemas(std::string const& document);
245 // The schema of each action, by action id, fetched for every model type the catalog names.
246 std::map<std::string, std::string, std::less<>> _schemas;
247 // Each model's schemas document as it arrived, or why it did not.
248 ui::SchemasByModel _byModel;
249 std::optional<ui::LoadedBundle> _loaded;
250 // The catalog the screens read through `catalog()`: replaced in place when a sign-in reloads it, so a session
251 // built over the first catalog sees the facts of the current one.
252 struct CurrentCatalog {
253 ui::Catalog facts;
254 std::map<std::string, std::string, std::less<>> schemas;
255 };
256 std::shared_ptr<CurrentCatalog> _current = std::make_shared<CurrentCatalog>();
257 // Verified files, kept across reloads: a reload fetches only what the new catalog adds.
258 std::shared_ptr<ui::IBundleStore> _store;
259 // Fetches the catalog and the bundle for the current session, then the schemas; `done` once they are current.
260 void reload(std::shared_ptr<bool> const& alive, std::function<void(std::expected<void, ui::ConnectError>)> done);
261 reactive::TimerHandle _timeout;
262 // Whether a development poll is in flight.
263 bool _polling = false;
264};
265
266/// @brief Fetches the screens from a server (`--server`), over the transport the frontend calls for.
267class RemoteSource final : public Source {
268public:
269 /// @brief A source for @p url.
270 /// @param url `ws://host:port`.
271 /// @param timeout How long to wait for the bundle before reporting the server unreachable.
272 explicit RemoteSource(std::string url, std::chrono::milliseconds timeout = std::chrono::seconds{10});
273
274protected:
275 [[nodiscard]] std::expected<Transport, ui::ConnectError> connect(ui::AppContext& context) override;
276 [[nodiscard]] std::chrono::milliseconds openTimeout() const override { return _timeout; }
277
278private:
279 std::string _url;
280 std::chrono::milliseconds _timeout;
281};
282
283/// @brief Runs a linked application in-process (`--app`): its models over a `LocalBackend`, its bundle answered by
284/// a `UiService` in the same process, and verified by the same client as a remote one.
285class LocalSource final : public Source {
286public:
287 /// @brief A source for @p app.
288 /// @param app The application.
289 /// @param setup Its set-up options.
291 ~LocalSource() override;
292 LocalSource(LocalSource const&) = delete;
293 LocalSource& operator=(LocalSource const&) = delete;
294 LocalSource(LocalSource&&) = delete;
295 LocalSource& operator=(LocalSource&&) = delete;
296
297 /// @brief The development user, from `--user`.
298 /// @return It.
299 [[nodiscard]] std::string signedOutPrincipal() const override { return _setup.user; }
300
301protected:
302 [[nodiscard]] std::expected<Transport, ui::ConnectError> connect(ui::AppContext& context) override;
303 void disconnect() override;
304 void fetchSchemas(std::string const& modelType,
305 std::function<void(std::expected<std::string, std::string>)> done) override;
306
307private:
308 LocalApp _app;
309 LocalSetup _setup;
310 bool _setUp = false;
311 std::unique_ptr<exec::ThreadPoolExecutor> _workers;
312 std::shared_ptr<ui::UiService> _service;
313};
314
315/// @brief The source the client's arguments name: remote for `--server`, local for `--app`.
316/// @param args The client's options.
317/// @param apps The applications linked in-process.
318/// @return The source, or why there is none: an `--app` that names no linked application.
319[[nodiscard]] std::expected<std::unique_ptr<Source>, std::string> makeSource(Args const& args,
320 std::vector<LocalApp> const& apps);
321
322} // namespace morph::client
The generic client's command line: which source it opens and how it starts. Spec 4 §3.
std::string signedOutPrincipal() const override
The development user, from --user.
Definition source.hpp:299
std::expected< Transport, ui::ConnectError > connect(ui::AppContext &context) override
Makes the transport on context.
LocalSource(LocalApp app, LocalSetup setup)
A source for app.
void fetchSchemas(std::string const &modelType, std::function< void(std::expected< std::string, std::string >)> done) override
Fetches the {actionType: schema} document of one model type, which a form, collection or wizard node ...
void disconnect() override
Releases what connect made beside the backend; called by close, after the bridge is gone.
std::expected< Transport, ui::ConnectError > connect(ui::AppContext &context) override
Makes the transport on context.
std::chrono::milliseconds openTimeout() const override
How long open waits for the bundle before it reports the source unreachable; zero waits forever.
Definition source.hpp:276
RemoteSource(std::string url, std::chrono::milliseconds timeout=std::chrono::seconds{10})
A source for url.
An AppSource that also hands the shell what it mounts screens with, once it delivered a bundle.
Definition source.hpp:72
virtual void fetchSchemas(std::string const &modelType, std::function< void(std::expected< std::string, std::string >)> done)
Fetches the {actionType: schema} document of one model type, which a form, collection or wizard node ...
bool isPublic(std::string_view screenId) const
Whether the catalog marks screenId public: a screen served before sign-in, such as a login screen.
std::optional< ui::FileTransfer > files() const
The file side channel (files/1) over the source's exchange, carrying its session: what a screen's upl...
ui::Catalog catalog() const
The catalog facts the screens load with.
std::function< std::shared_ptr< ui::Document const >(std::string_view)> documents(std::function< void(ui::Refusal const &)> onRefused={})
The lookup a DocumentScreenHost takes (DocumentScreenHostOptions::documents), over the loaded bundle:...
void onConnected(std::function< void()> const &listener) final
Adds a listener called on the owner after each successful connect, the first included....
std::vector< std::string > screens() const
The screens the principal may open, in catalog order: each catalog entry that is available or public ...
void switchBackend(ui::Backend backend, std::function< void(std::expected< void, std::string >)> done) final
Refused: a switch between backends is the hosting layer's (spec 6 §9).
virtual void disconnect()
Releases what connect made beside the backend; called by close, after the bridge is gone.
Definition source.hpp:200
virtual std::expected< Transport, ui::ConnectError > connect(ui::AppContext &context)=0
Makes the transport on context.
void poll(std::function< void(std::vector< std::string > changed)> changed)
One step of development reload (spec 5 §12): sends ui-hello, and when the manifest digest changed,...
void open(ui::AppContext &context, std::function< void(std::expected< ui::Bundle, ui::ConnectError >)> ready) final
Opens the source: the transport, the bridge and the bundle, verified.
virtual std::string signedOutPrincipal() const
The principal before any sign-in, which signOut returns to.
Definition source.hpp:176
ui::SchemasByModel const & schemasByModel() const noexcept
The schemas document of every model the catalog names, by model type, or why it did not arrive: what ...
Definition source.hpp:141
void onEvent(std::function< void(wire::EventBody const &)> listener)
Adds a listener for the events the server sends: what a shell hands to Session::deliverEvent.
void close() final
Drops a ready not yet delivered and releases the bridge, the transport and the bundle.
ui::Bundle const * bundle() const noexcept
The bundle the catalog currently lists.
Definition source.hpp:130
virtual std::chrono::milliseconds openTimeout() const
How long open waits for the bundle before it reports the source unreachable; zero waits forever.
Definition source.hpp:197
ui::CatalogReply const * catalogReply() const noexcept
The catalog for the current principal: its entries, what the principal may run, and the facts.
Definition source.hpp:134
void signOut(std::function< void(bool)> done)
Ends the session: back to the principal before any sign-in, with no token, and the catalog reloaded f...
std::string const & principal() const noexcept
The principal the source's session carries now.
Definition source.hpp:145
ui::Dispatcher & dispatcher()
Where the screens' model calls go.
session::Context & requestSession() noexcept
The session every request carries, for a source to fill before open sends anything.
Definition source.hpp:211
void signIn(std::string principal, std::string token, std::function< void(bool)> done)
Installs a sign-in (spec 6 §5): a session built from the current one with principal and token replace...
ui::Watcher watcher()
What the session's SessionOptions::watcher sends the cache's watch changes through: watcherOver the s...
What a frontend lends the source it opens and the screens it mounts.
Definition frontend.hpp:50
Where the screens and the models are: a server (remote) or the application linked in-process (local).
Definition frontend.hpp:137
The interpreter's view of the bridge: binds a model by its type id, privately or to a shared instance...
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
The app explorer: what a morph application is made of — its screens, the models they use with every a...
std::map< std::string, ModelSchemas, std::less<> > SchemasByModel
The schemas documents of the models, by model type.
Definition explorer.hpp:327
The UI document interpreter: Interpreter::mount turns a loaded Document into reactive state and a vie...
std::function< async::Completion< std::string >(std::string const &kind, std::string const &bodyJson)> Watcher
Sends a watch or unwatch request for a session's cache (spec 8 §7): the kind, and the WatchBody as JS...
morph::reactive::Scheduler: timers a frontend runs on the runtime's owner executor.
The server side of the UI service (spec 5 §12, §12b, §13): UiRegistry collects an application's scree...
The client side of the UI service (spec 5 §12, "Content addressing"): UiClient runs ui-hello,...
std::function< void(::morph::wire::Envelope request, std::function< void(ExchangeReply reply)> done)> Exchange
Sends one request and calls its callback once with the reply. The transport assigns the request's cal...
ui::HelloRequest clientHello()
The ui-hello the generic client opens with, and sends again on each poll: its vocabularies,...
std::vector< std::string > clientVocabularies()
The vocabularies the generic client speaks.
std::expected< std::unique_ptr< Source >, std::string > makeSource(Args const &args, std::vector< LocalApp > const &apps)
The source the client's arguments name: remote for --server, local for --app.
What the generic client reads from its command line.
Definition args.hpp:17
An application the client links and runs in-process.
Definition source.hpp:46
std::string name
The name --app selects it by.
Definition source.hpp:48
std::function< std::expected< std::shared_ptr< ui::ServedBundle const >, ui::Refusal >()> bundle
Builds its bundle: the same function its server calls.
Definition source.hpp:50
std::function< std::shared_ptr< session::IAuthorizer >()> serverAuthorizer
The authorizer the application's own server checks requests with, for an in-process server to check t...
Definition source.hpp:56
std::function< void(LocalSetup const &)> setUp
Run once before the first model call, with the client's options; empty for none.
Definition source.hpp:52
What a local application is set up with.
Definition source.hpp:38
std::string db
The database path, from --db; empty for the application's default.
Definition source.hpp:40
std::string user
The development principal, from --user.
Definition source.hpp:42
What a concrete source opens with: the bridge's backend and the exchange the UI service is reached by...
Definition source.hpp:180
ui::Exchange exchange
The exchange; it may borrow the backend, which the source guards.
Definition source.hpp:184
std::unique_ptr< backend::detail::IBackend > backend
The backend; owned by the bridge from here on.
Definition source.hpp:182
bool serverEvents
Whether the server behind the transport sends events and handles watch: true for a server,...
Definition source.hpp:187
Per-call session context carried through the bridge to the model.
Definition session.hpp:26
The application's opaque handle for one of its backends. Its name is what {"env": "backend"}...
Definition frontend.hpp:124
What an open source hands the frontend: the application's screens, verified.
Definition frontend.hpp:91
The body of a ui-catalog reply: the catalog filtered for the principal, and the facts a client's load...
Definition bundle.hpp:280
The facts load-time validation needs from the application's catalog. Local mode answers them from the...
Definition document.hpp:105
The body of a ui-hello request: what the client speaks and carries.
Definition bundle.hpp:164
Why a document, an expression or a value was refused: where, and the reason.
Definition value.hpp:38
The decoded body of an event envelope.
Definition wire.hpp:621
The frontend seam: main picks a frontend at runtime, and the frontend runs whatever an AppSource open...