19#include "../attributes.hpp"
20#include "../core/completion.hpp"
22#include "../core/executor.hpp"
23#include "../core/logger.hpp"
24#include "../core/wire.hpp"
25#include "../session/session.hpp"
48 IBundleStore() =
default;
49 virtual ~IBundleStore() =
default;
50 IBundleStore(IBundleStore
const&) =
delete;
51 IBundleStore& operator=(IBundleStore
const&) =
delete;
52 IBundleStore(IBundleStore&&) =
delete;
53 IBundleStore& operator=(IBundleStore&&) =
delete;
58 [[nodiscard]]
virtual std::optional<std::string>
get(std::string_view hash)
const = 0;
64 virtual void put(std::string
const& hash, std::string bytes) = 0;
68 virtual void remove(std::string_view hash) = 0;
73 [[nodiscard]]
virtual std::optional<std::string>
lastDigest(std::string_view application)
const = 0;
79 virtual void setLastDigest(std::string
const& application, std::string digest) = 0;
88 [[nodiscard]] std::optional<std::string>
get(std::string_view hash)
const override {
89 std::scoped_lock
const lock{_mutex};
90 auto const found = _files.find(hash);
91 if (found == _files.end()) {
101 void put(std::string
const& hash, std::string bytes)
override {
102 std::scoped_lock
const lock{_mutex};
103 _files.insert_or_assign(hash, std::move(bytes));
108 void remove(std::string_view hash)
override {
109 std::scoped_lock
const lock{_mutex};
110 if (
auto const found = _files.find(hash); found != _files.end()) {
118 [[nodiscard]] std::optional<std::string>
lastDigest(std::string_view application)
const override {
119 std::scoped_lock
const lock{_mutex};
120 auto const found = _digests.find(application);
121 if (found == _digests.end()) {
124 return found->second;
131 void setLastDigest(std::string
const& application, std::string digest)
override {
132 std::scoped_lock
const lock{_mutex};
133 _digests.insert_or_assign(application, std::move(digest));
138 [[nodiscard]] std::size_t
size()
const {
139 std::scoped_lock
const lock{_mutex};
140 return _files.size();
144 mutable std::mutex _mutex;
145 std::map<std::string, std::string, std::less<>> _files;
146 std::map<std::string, std::string, std::less<>> _digests;
157 throw std::invalid_argument(
"VerifiedCache: the store is null");
164 [[nodiscard]] std::optional<std::string>
get(std::string
const& hash) {
165 auto bytes = _store->get(hash);
167 ::morph::log::logWarn(
"[ui-cache] dropped {}: the stored bytes do not hash to it", hash);
168 _store->remove(hash);
177 std::string
put(std::string bytes) {
179 _store->put(hash, std::move(bytes));
188 std::shared_ptr<IBundleStore> _store;
204 std::map<std::string, std::string, std::less<>>
files;
227 auto const answer = std::make_shared<std::function<void(
ExchangeReply reply)>>(std::move(done));
228 transport->request(std::move(request), *owner)
230 .onErrorDetached([answer](std::exception_ptr
const& failure) {
231 std::string message =
"the request was not delivered";
233 std::rethrow_exception(failure);
234 }
catch (std::exception
const& error) {
235 message = error.what();
238 (*answer)(std::unexpected(std::move(message)));
258 : _shared{std::make_shared<Shared>(std::move(exchange), std::move(client),
VerifiedCache{std::move(store)})} {
259 if (!_shared->exchange) {
260 throw std::invalid_argument(
"UiClient: the exchange is empty");
265 using Done = std::function<void(std::expected<LoadedBundle, ConnectError>)>;
272 auto const run = std::make_shared<Run>(_shared, std::move(session), std::move(done));
288 auto const answer = std::make_shared<PollDone>(std::move(done));
289 auto const run = std::make_shared<Run>(_shared, std::move(session),
290 [answer](std::expected<LoadedBundle, ConnectError> opened) {
292 (*answer)(std::unexpected(std::move(opened.error())));
295 (*answer)(std::optional<LoadedBundle>{std::move(*opened)});
297 run->knownDigest = std::move(knownDigest);
298 run->unchanged = [answer] { (*answer)(std::optional<LoadedBundle>{}); };
306 [[nodiscard]] std::expected<LoadedBundle, ConnectError>
openOffline(std::string_view application)
const {
307 auto const digest = _shared->cache.store().lastDigest(application);
309 return std::unexpected(
311 .message =
"no verified bundle of `" + std::string{application} +
"` is cached"});
313 auto const text = _shared->cache.get(*digest);
317 .message =
"the cached manifest of `" + std::string{application} +
"` is gone or does not verify"});
324 loaded.
manifest = std::move(*manifest);
326 if (!usable(_shared->client, file.
kind)) {
329 if (
auto bytes = _shared->cache.get(file.
hash)) {
330 loaded.
files.emplace(file.
hash, std::move(*bytes));
333 assemble(loaded, *digest);
341 : exchange{std::move(exchangeFn)}, client{std::move(clientInfo)}, cache{std::move(verified)} {}
354 : shared{std::move(sharedState)}, session{std::move(sessionContext)}, done{std::move(doneFn)} {}
356 std::shared_ptr<Shared> shared;
357 ::morph::session::Context session;
360 std::string knownDigest;
361 std::function<void()> unchanged;
364 std::size_t pending = 0;
365 std::optional<ConnectError> failure;
375 [[nodiscard]]
static bool usable(HelloRequest
const& client, std::string_view kind) {
376 if (kind == kComponentFile) {
377 return client.components;
384 static void assemble(LoadedBundle& loaded, std::string
const& digest) {
385 loaded.bundle.applicationId = loaded.manifest.application;
386 loaded.bundle.manifestDigest = digest.substr(
kHashPrefix.size());
387 for (ManifestFile
const& file : loaded.manifest.files) {
388 if (file.kind != kScreenFile && file.kind != kAppFile && file.kind != kThemeFile) {
391 if (
auto const found = loaded.files.find(file.hash); found != loaded.files.end()) {
392 auto& into = file.kind ==
kThemeFile ? loaded.bundle.themes : loaded.bundle.documents;
393 into.insert_or_assign(file.id, found->second);
398 for (
auto const& [fileId, text] : loaded.bundle.themes) {
399 static_cast<void>(fileId);
407 for (
auto const& [name, iconId] : index->icons) {
408 auto const file = std::ranges::find_if(loaded.manifest.files, [&iconId](ManifestFile
const& listed) {
409 return listed.kind == kIconFile && listed.id == iconId;
411 if (file == loaded.manifest.files.end()) {
414 if (
auto const found = loaded.files.find(file->hash); found != loaded.files.end()) {
415 loaded.bundle.icons.insert_or_assign(name, found->second);
422 [[nodiscard]] static ::morph::wire::Envelope requestOf(std::shared_ptr<Run>
const& run, std::string_view kind) {
423 ::morph::wire::Envelope
request;
424 request.kind = std::string{kind};
430 static void sendHello(std::shared_ptr<Run>
const& run) {
431 ::morph::wire::Envelope
request = requestOf(run, kHelloKind);
432 request.body = detail::writeMessage(
run->shared->client);
433 run->shared->exchange(std::move(request), [run](ExchangeReply reply) { onHello(run, std::move(reply)); });
437 static void sendCatalog(std::shared_ptr<Run>
const& run) {
438 run->shared->exchange(requestOf(run, kCatalogKind),
439 [run](ExchangeReply reply) { onCatalog(run, std::move(reply)); });
443 static void fetch(std::shared_ptr<Run>
const& run, std::string
const& hash,
444 std::function<
void(ExchangeReply)> then) {
445 ::morph::wire::Envelope
request = requestOf(run, kFetchKind);
446 request.body = detail::writeMessage(FetchRequest{.hash = hash});
447 run->shared->exchange(std::move(request), std::move(then));
450 static void fail(std::shared_ptr<Run>
const& run, ConnectError error) {
451 ::morph::log::logWarn(
"[ui-fetch] {}", error.message);
452 run->done(std::unexpected(std::move(error)));
455 static void fail(std::shared_ptr<Run>
const& run,
ConnectError::Kind kind, std::string message) {
456 fail(run, ConnectError{.kind = kind, .message = std::move(message)});
461 [[nodiscard]]
static std::expected<std::string, ConnectError> bodyOf(ExchangeReply reply) {
465 if (reply->kind !=
"ok") {
466 return std::unexpected(
469 return std::move(reply->body);
473 [[nodiscard]]
static std::expected<std::string, ConnectError> verified(ExchangeReply reply,
474 Expected
const& expected) {
475 auto body = bodyOf(std::move(reply));
477 body.error().message = expected.what +
": " + body.error().message;
480 if (body->size() > kMaxFileBytes) {
481 return std::unexpected(ConnectError{
483 .message = expected.what +
" is larger than " + std::to_string(kMaxFileBytes) +
" bytes"});
485 if (std::string
const actual =
contentHash(*body); actual != expected.hash) {
487 .message = expected.what +
" does not match its hash (expected " +
488 expected.hash +
", got " + actual +
")"});
493 static void onHello(std::shared_ptr<Run>
const& run, ExchangeReply reply) {
494 auto body = bodyOf(std::move(reply));
496 ConnectError error = std::move(body.error());
497 if (error.message ==
"unknown envelope kind: " + std::string{kHelloKind}) {
499 error.message =
"the server serves no UI";
500 }
else if (error.message.starts_with(
"missing vocabulary: ")) {
503 fail(run, std::move(error));
511 HelloRequest
const& client =
run->shared->client;
512 for (std::string
const& vocabulary : hello->vocabularies) {
513 if (std::ranges::find(client.vocabularies, vocabulary) == client.vocabularies.end()) {
515 "the application needs the vocabulary " + vocabulary +
", which this client does not speak");
519 if (
run->unchanged && hello->digest ==
run->knownDigest) {
520 ::morph::log::logDebug(
"[ui-fetch] polling `{}`: digest {}, known {}, manifest unchanged",
521 hello->application, hello->digest,
run->knownDigest);
525 ::morph::log::logDebug(
"[ui-fetch] {} `{}`: digest {}, known {}, manifest {}",
526 run->unchanged ?
"polling" :
"opening", hello->application, hello->digest,
527 run->knownDigest.empty() ? std::string{
"<none>"} :
run->knownDigest,
528 hello->digest ==
run->knownDigest ?
"unchanged" :
"changed");
529 run->loaded.hello = std::move(*hello);
530 std::string
const& digest =
run->loaded.hello.digest;
531 if (
auto cached =
run->shared->cache.get(digest)) {
532 onManifest(run, std::move(*cached));
535 fetch(run, digest, [run](ExchangeReply fetched) {
537 verified(std::move(fetched), Expected{.hash =
run->loaded.hello.digest, .what =
"the manifest"});
539 fail(run, std::move(
text.error()));
542 onManifest(run, std::move(*text));
546 static void onManifest(std::shared_ptr<Run>
const& run, std::string text) {
552 static_cast<void>(
run->shared->cache.put(std::move(text)));
553 run->loaded.manifest = std::move(*manifest);
557 static void onCatalog(std::shared_ptr<Run>
const& run, ExchangeReply reply) {
558 auto body = bodyOf(std::move(reply));
560 fail(run, std::move(body.error()));
568 run->loaded.catalog = std::move(*catalog);
569 std::vector<Expected> missing;
570 for (ManifestFile
const& file :
run->loaded.manifest.files) {
571 if (!wanted(
run->shared->client,
run->loaded.catalog, file)) {
574 if (
auto cached =
run->shared->cache.get(file.hash)) {
575 run->loaded.files.insert_or_assign(file.hash, std::move(*cached));
577 Expected expected{.hash = file.hash, .what =
"the " + file.kind +
" `" + file.id +
"`"};
578 ::morph::log::logDebug(
"[ui-fetch] fetching {} ({})", expected.hash, expected.what);
579 missing.push_back(std::move(expected));
584 run->pending = missing.size() + 1;
585 for (Expected& expected : missing) {
586 std::string
const hash = expected.hash;
587 fetch(run, hash, [run, expected = std::move(expected)](ExchangeReply fetched) {
588 auto bytes = verified(std::move(fetched), expected);
590 std::scoped_lock
const lock{
run->mutex};
593 run->failure = std::move(bytes.error());
596 static_cast<void>(
run->shared->cache.put(*bytes));
597 run->loaded.files.insert_or_assign(expected.hash, std::move(*bytes));
608 [[nodiscard]]
static bool wanted(HelloRequest
const& client, CatalogReply
const& catalog,
609 ManifestFile
const& file) {
610 if (!usable(client, file.kind)) {
613 if (file.kind != kScreenFile) {
616 return std::ranges::any_of(catalog.entries, [&file](CatalogEntry
const& entry) {
617 return entry.id == file.id && (entry.available || entry.isPublic);
622 static void settle(std::shared_ptr<Run>
const& run) {
624 std::scoped_lock
const lock{
run->mutex};
625 if (--
run->pending != 0) {
631 fail(run, std::move(*
run->failure));
634 assemble(
run->loaded,
run->loaded.hello.digest);
635 run->shared->cache.store().setLastDigest(
run->loaded.hello.application,
run->loaded.hello.digest);
636 run->done(std::move(
run->loaded));
639 std::shared_ptr<Shared> _shared;
651 return std::unexpected(std::move(reply.error()));
653 if (reply->kind !=
"ok") {
654 return std::unexpected(std::move(reply->message));
658 return std::unexpected(std::move(evaluated.error()));
660 Json::object_t errors;
661 for (
auto const& [field, message] : evaluated->errors) {
662 errors.insert_or_assign(field,
Json(message));
664 auto& members = evaluated->body.get<Json::object_t>();
665 members.insert_or_assign(
"errors",
Json(std::move(errors)));
666 members.insert_or_assign(
"ready",
Json(evaluated->ready));
667 auto text = glz::write_json(evaluated->body);
669 return std::unexpected(
"the ui-evaluate reply's body cannot be written");
671 return std::move(*
text);
680[[nodiscard]]
inline std::function<::morph::async::Completion<std::string>(std::string
const&, std::string
const&,
683 return [exchange = std::move(exchange), session = std::move(session), cbExec](
684 std::string
const& typeId, std::string
const& actionId, std::string
const& bodyJson) {
686 auto settle = std::make_shared<decltype(promise)>(std::move(promise));
689 request.session = session;
690 request.modelType = typeId;
691 request.actionType = actionId;
692 request.body = bodyJson;
696 settle->resolve(std::move(*value));
698 settle->reject(std::make_exception_ptr(std::runtime_error{value.error()}));
701 return std::move(completion);
716[[nodiscard]]
inline std::function<::morph::async::Completion<std::string>(std::string
const&, std::string
const&)>
719 return [exchange = std::move(exchange), currentSession = std::move(currentSession), cbExec](
720 std::string
const& kind, std::string
const& bodyJson) {
722 auto settle = std::make_shared<decltype(promise)>(std::move(promise));
726 request.body = bodyJson;
729 settle->reject(std::make_exception_ptr(std::runtime_error{reply.error()}));
730 }
else if (reply->kind !=
"ok") {
731 settle->reject(std::make_exception_ptr(std::runtime_error{reply->message}));
733 settle->resolve(std::move(reply->body));
736 return std::move(completion);
762 : _shared{std::make_shared<Shared>(Shared{.exchange = std::move(exchange), .session = std::move(session)})} {
763 if (!_shared->exchange) {
764 throw std::invalid_argument(
"FileTransfer: the exchange is empty");
775 void upload(
FileInfo info, std::string bytes, std::string model, std::string action,
776 std::function<
void(
double)>
progress,
777 std::function<
void(std::expected<std::string, std::string>)> done)
const {
778 auto const run = std::make_shared<Upload>(Upload{.shared = _shared,
779 .bytes = std::move(bytes),
782 .done = std::move(done)});
785 begin.name = std::move(info.name);
786 begin.type = std::move(info.type);
787 begin.size = run->bytes.size();
788 begin.model = std::move(model);
789 begin.action = std::move(action);
790 send(*_shared, begin, [run](std::expected<FileReply, std::string> reply) {
791 if (!reply || !reply->upload) {
792 run->fail(reply ? std::string{
"the ui-file begin reply has no upload id"} : std::move(reply.error()));
795 run->upload = std::move(*reply->upload);
805 std::function<
void(std::expected<DownloadedFile, std::string>)> done)
const {
806 auto const run = std::make_shared<Download>(Download{.shared = _shared,
807 .ref = std::move(ref),
810 .done = std::move(done)});
821 std::shared_ptr<Shared> shared;
824 std::function<void(
double)> progress;
825 std::function<void(std::expected<std::string, std::string>)> done;
827 void fail(std::string reason)
const {
829 done(std::unexpected(std::move(reason)));
835 std::shared_ptr<Shared> shared;
838 std::function<void(
double)> progress;
839 std::function<void(std::expected<DownloadedFile, std::string>)> done;
843 static void send(Shared
const& shared, FileRequest
const& request,
844 std::function<
void(std::expected<FileReply, std::string>)> then) {
845 ::morph::wire::Envelope envelope;
847 envelope.
session = shared.session;
848 envelope.
body = detail::writeMessage(request);
849 shared.exchange(std::move(envelope), [then = std::move(then)](ExchangeReply reply) {
851 then(std::unexpected(std::move(reply.error())));
854 if (reply->kind !=
"ok") {
855 then(std::unexpected(std::move(reply->message)));
863 static void sendChunk(std::shared_ptr<Upload>
const& run, std::size_t offset) {
864 if (offset >=
run->bytes.size()) {
867 end.upload =
run->upload;
868 send(*
run->shared, end, [run](std::expected<FileReply, std::string> reply) {
869 if (!reply || !reply->ref) {
870 run->fail(reply ? std::string{
"the ui-file end reply has no reference"}
871 : std::move(reply.error()));
875 run->done(std::move(*reply->ref));
882 chunk.upload =
run->upload;
883 chunk.offset = offset;
884 std::string_view
const all{
run->bytes};
885 chunk.data = ::morph::session::detail::base64UrlEncode(
all.substr(offset, kMaxFileChunkBytes));
886 send(*
run->shared, chunk, [run](std::expected<FileReply, std::string> reply) {
887 if (!reply || !reply->received) {
888 run->fail(reply ? std::string{
"the ui-file chunk reply has no count"} : std::move(reply.error()));
892 run->progress(static_cast<double>(*reply->received) / static_cast<double>(run->bytes.size()));
894 sendChunk(run, *reply->received);
899 static void readChunk(std::shared_ptr<Download>
const& run) {
903 read.offset =
run->file.bytes.size();
904 send(*
run->shared, read, [run](std::expected<FileReply, std::string> reply) {
905 std::optional<std::string> bytes;
906 if (reply && reply->data && reply->total) {
907 bytes = ::morph::session::detail::base64UrlDecode(*reply->data);
911 run->done(std::unexpected(reply ? std::string{
"the ui-file read reply is malformed"}
912 : std::move(reply.error())));
916 run->file.bytes += *bytes;
917 run->file.name = reply->name.value_or(
"");
918 run->file.type = reply->type.value_or(
"");
919 std::size_t
const total = *reply->total;
920 if (
run->progress && total > 0) {
921 run->progress(static_cast<double>(run->file.bytes.size()) / static_cast<double>(total));
923 if (
run->file.bytes.size() >= total || bytes->empty()) {
925 run->done(std::move(run->file));
933 std::shared_ptr<Shared> _shared;
Node text(TextOptions const &options)
A text node.
Command all(Commands const &steps)
all: steps started together.
Command run(MutationHandle< Action > const &mutation, Body< Action > const &body={}, RunOptions const &options={})
run: runs a mutation.
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
constexpr std::string_view kScreenFile
The file kinds of a manifest.
std::expected< FileReply, std::string > parseFileReply(std::string_view text)
Reads a ui-file reply body.
constexpr std::string_view kFileKind
The envelope kind of the file side channel (files/1, spec 6 §18): uploads and downloads in chunks.
constexpr std::string_view kIconFile
An icon of a theme's icon set: SVG text.
std::expected< EvaluateReply, std::string > parseEvaluateReply(std::string_view text)
Reads a ui-evaluate reply body.
std::expected< CatalogReply, std::string > parseCatalogReply(std::string_view text)
Reads a ui-catalog reply body.
constexpr std::string_view kHashPrefix
The prefix of a content hash.
std::expected< HelloReply, std::string > parseHelloReply(std::string_view text)
Reads a ui-hello reply body.
std::expected< Manifest, std::string > parseManifest(std::string_view text)
Reads a manifest a server sent.
constexpr std::string_view kEvaluateKind
The envelope kind that evaluates an action body without running it (spec 5 §7).
constexpr std::string_view kAppFile
The app document (spec 6 §5).
constexpr std::string_view kMessagesFile
A message catalogue.
std::string contentHash(std::string_view bytes)
The content hash of bytes: sha256: and the SHA-256 digest in 64 lower-case hexadecimal digits.
constexpr std::string_view kThemeFile
A theme (ui-theme/1, spec 6 §6).
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
What a wire transport offers beside IBackend: one request, one reply, matched by callId.
void download(std::string ref, std::function< void(double)> progress, std::function< void(std::expected< DownloadedFile, std::string >)> done) const
Downloads a file by its reference.
void upload(FileInfo info, std::string bytes, std::string model, std::string action, std::function< void(double)> progress, std::function< void(std::expected< std::string, std::string >)> done) const
Uploads a file, authorised as the mutation that will reference it.
FileTransfer(Exchange exchange, ::morph::session::Context session)
A transfer over exchange, its requests carrying session.
Where a client keeps verified files between runs, by content hash, and the digest of each application...
virtual std::optional< std::string > get(std::string_view hash) const =0
Reads a file.
virtual void setLastDigest(std::string const &application, std::string digest)=0
Records an application's last verified bundle.
virtual void remove(std::string_view hash)=0
Drops a file.
virtual std::optional< std::string > lastDigest(std::string_view application) const =0
The digest of an application's last verified bundle.
virtual void put(std::string const &hash, std::string bytes)=0
Keeps a file.
An IBundleStore in memory: it lasts as long as the process. Safe to call from any thread.
void put(std::string const &hash, std::string bytes) override
Keeps a file.
void setLastDigest(std::string const &application, std::string digest) override
Records an application's last verified bundle.
std::optional< std::string > get(std::string_view hash) const override
Reads a file.
std::optional< std::string > lastDigest(std::string_view application) const override
The digest of an application's last verified bundle.
void remove(std::string_view hash) override
Drops a file.
std::size_t size() const
How many files the store holds.
std::function< void(std::expected< std::optional< LoadedBundle >, ConnectError >)> PollDone
What poll delivers: the changed bundle, nothing when the digest is the one already held,...
std::function< void(std::expected< LoadedBundle, ConnectError >)> Done
What open delivers.
std::expected< LoadedBundle, ConnectError > openOffline(std::string_view application) const
Rebuilds the last bundle this store verified for application, without a connection....
void open(::morph::session::Context session, Done done) const
Opens the bundle. done is called once, from whichever thread the exchange calls back on; a synchronou...
void poll(::morph::session::Context session, std::string knownDigest, PollDone done) const
One step of a development client's reload (spec 5 §12): sends ui-hello, and when the manifest digest ...
UiClient(Exchange exchange, HelloRequest client, std::shared_ptr< IBundleStore > store)
A client.
A content-addressed cache that hands out only bytes that hash to the hash they are asked for.
std::string put(std::string bytes)
Keeps a file under its content hash.
VerifiedCache(std::shared_ptr< IBundleStore > store)
A cache over store.
IBundleStore & store() noexcept
The store.
std::optional< std::string > get(std::string const &hash)
Reads a file, verified. A stored file that does not hash to hash is dropped and reads as a miss.
IEnvelopeTransport: a transport that sends a request envelope of a kind its backend verbs do not buil...
The file side channel's store (files/1, spec 6 §18): uploads in chunks, and files a principal may rea...
std::function<::morph::async::Completion< std::string >(std::string const &, std::string const &)> watcherOver(Exchange exchange, std::function<::morph::session::Context()> currentSession, ::morph::exec::IExecutor *cbExec)
A watcher over the server (spec 8 §7): each watch or unwatch is one request of that kind through exch...
Exchange exchangeOver(::morph::backend::IEnvelopeTransport &backend, ::morph::exec::IExecutor &cbExec)
An Exchange over a transport: each request is one IEnvelopeTransport::request, and its reply,...
std::function<::morph::async::Completion< std::string >(std::string const &, std::string const &, std::string const &)> evaluatorOver(Exchange exchange, ::morph::session::Context session, ::morph::exec::IExecutor *cbExec)
An evaluator over the UI service: each evaluation is one ui-evaluate request through exchange,...
std::expected< std::string, std::string > evaluationValue(ExchangeReply reply)
The value an evaluate query takes from a ui-evaluate reply: the evaluated body's members,...
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...
std::expected<::morph::wire::Envelope, std::string > ExchangeReply
One reply, or why the transport could not deliver it.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Abstract executor interface.
Per-call session context carried through the bridge to the model.
What an open source hands the frontend: the application's screens, verified.
The body of a ui-catalog reply: the catalog filtered for the principal, and the facts a client's load...
Why a source could not be opened.
@ Unreachable
The server could not be reached, or the connection broke during the handshake.
@ NoUiService
The peer answered, but serves no UI (unknown envelope kind: ui-hello).
@ Unsupported
The bundle needs a vocabulary this client does not speak; the message names it.
@ Unverified
A file or the manifest did not match its digest.
@ Refused
The application refused this client or this user: a sign-in, a licence, an authoriser.
A file a download brought back.
std::string name
Its name.
std::string bytes
Its bytes.
std::string type
Its media type.
What a file is, as its uploader described it.
The body of a ui-file request: one step of an upload or a download, named by op.
std::string op
begin, chunk, end or read.
The body of a ui-hello reply.
The body of a ui-hello request: what the client speaks and carries.
A bundle the client verified: what the shell mounts, and what it was verified against.
CatalogReply catalog
The catalog for the principal; empty when the bundle was opened offline.
Manifest manifest
The manifest.
Bundle bundle
The application's id, the manifest digest, and the documents (the screens it may open and the app doc...
std::map< std::string, std::string, std::less<> > files
Every verified file the bundle holds, by content hash: documents, themes, message catalogues and,...
HelloReply hello
The ui-hello reply; empty when the bundle was opened offline.
std::string hash
The file's content hash.
std::string kind
screen, app, theme, messages, component or icon; a client fetches no kind it does not know.
A bundle's manifest: every file the application serves, by its hash (spec 5 §12, "Content addressing"...
std::vector< ManifestFile > files
The files, sorted by kind then id.
JSON wire envelope used between any client and RemoteServer.
std::string kind
Discriminator — see class docstring for valid values.
::morph::session::Context session
Session context for authorization and routing. Populated on execute; ignored on every other kind.
std::string body
Serialized JSON of the action payload (execute) or of the deserializable result (ok).
The frontend seam: main picks a frontend at runtime, and the frontend runs whatever an AppSource open...
Theme tokens (ui-theme/1, spec 6 §6): DTCG 2025.10 token files read and checked, layered into a varia...
bool isThemeIndex(std::string_view text)
Whether a theme file is a theme index rather than a token file: its top level has a string vocab.
std::expected< ThemeIndex, std::string > parseThemeIndex(std::string_view text)
Parses a theme index: {"vocab": "ui-theme/1", "base": id, "dark": id, …, "density": {....
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Node text(Text spec)
Static or bound text.
Node progress(Progress spec)
A progress bar.