10#include <glaze/glaze.hpp>
19#include "../session/session.hpp"
23namespace morph::wire {
36inline constexpr std::size_t kMaxEnvelopeBytes = 8u * 1024u * 1024u;
45inline constexpr std::uint32_t kProtocolVersion = 1;
177inline constexpr std::string_view kCapabilityCancel =
"cancel";
186 std::uint32_t
min = kProtocolVersion;
189 std::uint32_t
max = kProtocolVersion;
202inline Envelope makeRegister(std::string typeId, std::string contextKey = {}) {
204 env.kind =
"register";
205 env.typeId = std::move(typeId);
206 env.contextKey = std::move(contextKey);
222inline Envelope makeRegisterShared(std::string typeId, std::string primary, std::string contextKey = {}) {
224 env.kind =
"register";
225 env.typeId = std::move(typeId);
226 env.primary = std::move(primary);
227 env.contextKey = std::move(contextKey);
245inline Envelope makeAttach(std::string typeId, std::string primary, uint64_t modelId = 0,
246 std::string contextKey = {}) {
249 env.typeId = std::move(typeId);
250 env.primary = std::move(primary);
251 env.modelId = modelId;
252 env.contextKey = std::move(contextKey);
266inline Envelope makeAssign(std::string typeId, std::string primary, uint64_t modelId) {
269 env.typeId = std::move(typeId);
270 env.primary = std::move(primary);
271 env.modelId = modelId;
282inline Envelope makeInstances(std::string typeId) {
284 env.kind =
"instances";
285 env.typeId = std::move(typeId);
299inline Envelope makeSchemas(std::string typeId) {
301 env.kind =
"schemas";
302 env.typeId = std::move(typeId);
314inline Envelope makeCancel(uint64_t targetCallId) {
317 env.cancelCallId = targetCallId;
322inline Envelope makeDeregister(uint64_t modelId) {
324 env.kind =
"deregister";
325 env.modelId = modelId;
331inline Envelope makeOk(uint64_t callId = 0, std::string body = {}, uint64_t modelId = 0) {
335 env.body = std::move(body);
336 env.modelId = modelId;
361[[nodiscard]]
inline std::string sanitizeControlChars(std::string_view text) {
363 out.reserve(
text.size());
364 for (
char c : text) {
365 auto byte =
static_cast<unsigned char>(c);
366 if (
byte < 0x20 ||
byte == 0x7F) {
367 static constexpr char kHex[] =
"0123456789abcdef";
369 out += kHex[(
byte >> 4) & 0xF];
370 out += kHex[
byte & 0xF];
454inline constexpr std::array<std::string_view, 12> kOmittableEnvelopeKeys{
455 "typeId",
"contextKey",
"primary",
"shared",
"modelId",
"modelType",
456 "actionType",
"body",
"message",
"session",
"protocolVersion",
"cancelCallId"};
463[[nodiscard]]
inline bool isDefaultSession(const ::morph::session::Context& ctx)
noexcept {
469 static_assert(glz::reflect<::morph::session::Context>::size == 5,
470 "session::Context gained or lost a member: update isDefaultSession, or the new "
471 "member is silently dropped from every envelope with an otherwise-empty session.");
472 return ctx.principal.empty() && ctx.token.empty() && ctx.requestId.empty() && ctx.locale.empty() &&
473 ctx.metadata.empty();
481[[nodiscard]]
inline std::span<const std::string_view> defaultValuedKeys(
482 const Envelope& env, std::array<std::string_view, kOmittableEnvelopeKeys.size()>& out)
noexcept {
487 static_assert(glz::reflect<Envelope>::size == 14,
488 "Envelope gained or lost a member: decide whether it is omittable when default "
489 "and update kOmittableEnvelopeKeys. A new member left off that list is always "
490 "written, which is merely the old behaviour; one added with a wrong spelling is "
491 "caught by the assertion below.");
492 static_assert(std::ranges::all_of(kOmittableEnvelopeKeys,
493 [](std::string_view key) {
494 return std::ranges::find(glz::reflect<Envelope>::keys, key) !=
495 std::ranges::end(glz::reflect<Envelope>::keys);
497 "kOmittableEnvelopeKeys names a key Envelope does not have — a rename, or a typo.");
499 std::size_t count = 0;
500 auto omit = [&out, &count](std::string_view key)
noexcept { out.at(count++) = key; };
505 if (
env.typeId.empty()) {
508 if (
env.contextKey.empty()) {
511 if (
env.primary.empty()) {
517 if (
env.modelId == 0) {
520 if (
env.modelType.empty()) {
523 if (
env.actionType.empty()) {
526 if (
env.body.empty()) {
529 if (
env.message.empty()) {
532 if (isDefaultSession(
env.session)) {
535 if (
env.protocolVersion == 0) {
536 omit(
"protocolVersion");
538 if (
env.cancelCallId == 0) {
539 omit(
"cancelCallId");
541 return std::span<const std::string_view>{out.data(),
count};
568[[nodiscard]]
inline std::uint64_t peekCallId(std::string_view json, std::size_t maxScanBytes = 1024) noexcept {
569 static constexpr std::string_view kKey =
"\"callId\":";
570 const std::string_view window = json.substr(0, std::min(json.size(), maxScanBytes));
571 const auto keyPos = window.find(kKey);
572 if (keyPos == std::string_view::npos) {
575 std::size_t idx = keyPos + kKey.size();
578 while (idx < window.size() && (window[idx] ==
' ' || window[idx] ==
'\t')) {
582 std::uint64_t value = 0;
583 for (; idx < window.size(); ++idx) {
586 const char chr = window[idx];
587 if (chr <
'0' || chr >
'9') {
590 const auto digit =
static_cast<std::uint64_t
>(chr -
'0');
591 if (value > (std::numeric_limits<std::uint64_t>::max() - digit) / 10U) {
594 value = (value * 10U) + digit;
609inline Envelope makeErr(std::string message, uint64_t callId = 0) {
613 env.message = detail::sanitizeControlChars(message);
618inline constexpr std::string_view kEventKind =
"event";
634[[nodiscard]]
inline Envelope makeEvent(std::string_view topic, std::string payloadJson) {
636 env.kind = std::string{kEventKind};
638 if (payloadJson.empty()) {
639 payloadJson =
"null";
641 EventBody body{.topic = std::string{topic}, .payload = glz::raw_json{std::move(payloadJson)}};
642 env.body = glz::write_json(body).value_or(std::string{});
647inline constexpr std::string_view kChangesTopic =
"changes";
650inline constexpr std::string_view kResyncTopic =
"resync";
653inline constexpr std::string_view kWatchKind =
"watch";
656inline constexpr std::string_view kUnwatchKind =
"unwatch";
663inline constexpr std::size_t kMaxWatchTopics = 4096;
668inline constexpr std::size_t kMaxTopicNameBytes = 1024;
709[[nodiscard]]
inline std::expected<T, std::string> readBody(std::string_view json, std::string_view what) {
710 if (json.size() > kMaxEnvelopeBytes) {
711 return std::unexpected(std::string{what} +
" exceeds " + std::to_string(kMaxEnvelopeBytes) +
" bytes");
714 static constexpr glz::opts kLenient{.null_terminated =
false, .error_on_unknown_keys =
false};
715 if (
auto const error = glz::read<kLenient>(out, json)) {
716 return std::unexpected(std::string{what} +
": " + glz::format_error(error, json));
722[[nodiscard]]
inline std::optional<std::string> checkTopicName(std::string_view name, std::string_view what) {
723 if (name.size() > kMaxTopicNameBytes) {
724 return std::string{what} +
" is longer than " + std::to_string(kMaxTopicNameBytes) +
" bytes";
729[[nodiscard]]
inline std::optional<std::string> checkNames(
const std::vector<std::string>& names,
730 std::string_view what) {
731 if (
names.size() > kMaxWatchTopics) {
732 return std::string{what} +
" list more than " + std::to_string(kMaxWatchTopics) +
" topics";
734 for (
const auto& name : names) {
735 if (
auto reason = checkTopicName(name, what)) {
742[[nodiscard]]
inline std::optional<std::string> checkInstances(
const std::vector<InstanceTopic>& topics,
743 std::string_view what) {
744 if (topics.size() > kMaxWatchTopics) {
745 return std::string{what} +
" list more than " + std::to_string(kMaxWatchTopics) +
" topics";
747 for (
const auto& topic : topics) {
748 if (
auto reason = checkTopicName(topic.modelType,
"a model type")) {
751 if (
auto reason = checkTopicName(topic.primary,
"a primary key")) {
767[[nodiscard]]
inline std::expected<WatchBody, std::string> readWatchBody(std::string_view json) {
768 auto body = detail::readBody<WatchBody>(json,
"the watch body");
772 if (
auto reason = detail::checkNames(body->records,
"a record name")) {
773 return std::unexpected(*reason);
775 if (
auto reason = detail::checkInstances(body->instances,
"an instance topic")) {
776 return std::unexpected(*reason);
786[[nodiscard]]
inline std::expected<WatchReply, std::string> readWatchReply(std::string_view json) {
787 auto body = detail::readBody<WatchReply>(json,
"the watch reply");
791 if (
auto reason = detail::checkNames(body->refusedRecords,
"a record name")) {
792 return std::unexpected(*reason);
794 if (
auto reason = detail::checkInstances(body->refusedInstances,
"an instance topic")) {
795 return std::unexpected(*reason);
807[[nodiscard]]
inline std::expected<ChangesBody, std::string> readChangesBody(std::string_view json) {
808 auto body = detail::readBody<ChangesBody>(json,
"the changes payload");
812 if (body->records.size() > kMaxWatchTopics) {
813 return std::unexpected(
"a record list more than " + std::to_string(kMaxWatchTopics) +
" topics");
815 for (
const auto& change : body->records) {
816 if (
auto reason = detail::checkTopicName(change.record,
"a record name")) {
817 return std::unexpected(*reason);
819 if (change.keys && change.keys->size() > effects::kMaxChangeKeys) {
820 return std::unexpected(
"a record's keys list more than " + std::to_string(effects::kMaxChangeKeys) +
824 if (
auto reason = detail::checkInstances(body->instances,
"an instance topic")) {
825 return std::unexpected(*reason);
847inline constexpr std::string_view kExecuteTimeoutMessage =
"timeout";
854inline Envelope makeHello(std::uint32_t protocolVersion = kProtocolVersion) {
857 env.protocolVersion = protocolVersion;
887 std::array<std::string_view, detail::kOmittableEnvelopeKeys.size()> omitted{};
899[[nodiscard]]
inline const WireCodecOps& defaultWireCodecOps() {
900 static const WireCodecOps ops;
921inline std::string encode(
const Envelope& env,
const WireCodecOps& ops = defaultWireCodecOps()) {
925 if (
auto errCode = ops.writeEnvelope(env, out)) {
928 throw std::runtime_error(
"envelope encode failed: " + glz::format_error(errCode, out));
952inline Envelope decode(std::string_view json) {
954 if (json.size() > kMaxEnvelopeBytes) {
955 throw std::runtime_error(
"envelope decode failed: input exceeds maximum size (" + std::to_string(json.size()) +
956 " > " + std::to_string(kMaxEnvelopeBytes) +
" bytes)");
970 static constexpr glz::opts kLenient{.null_terminated =
false, .error_on_unknown_keys =
false};
971 if (
auto errCode = glz::read<kLenient>(env, json)) {
972 throw std::runtime_error(
"envelope decode failed: " + glz::format_error(errCode, json));
979enum class ProtocolNegotiationResult : std::uint8_t {
998inline ProtocolNegotiationResult interpretHelloReply(
const Envelope& reply) {
999 if (reply.kind ==
"ok") {
1000 return ProtocolNegotiationResult::Negotiated;
1006 if (reply.message ==
"unknown envelope kind: hello") {
1007 return ProtocolNegotiationResult::LegacyPeer;
1009 throw std::runtime_error(
"protocol negotiation failed: " +
1010 (reply.message.empty() ? std::string{
"malformed reply"} : reply.message));
1024[[nodiscard]]
inline bool helloAdvertises(
const Envelope& reply, std::string_view capability) {
1025 if (reply.kind !=
"ok") {
1029 static constexpr glz::opts kLenient{.null_terminated =
false, .error_on_unknown_keys =
false};
1030 if (glz::read<kLenient>(range, reply.body)) {
1033 return std::ranges::find(range.capabilities, capability) != range.capabilities.end();
Node text(TextOptions const &options)
A text node.
AnyHandle env(std::string const &name)
A client environment value, {"env": name}: widthClass, locale, platform, online, backend.
bool names(DocNode const &node, std::string_view key)
Whether a node has a property that names it: present, and not the constant "" or null.
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_ZONE_TEXT(text)
Attaches text to the enclosing scope's MORPH_ZONE; an empty text attaches nothing.
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Per-call session context carried through the bridge to the model.
The payload of a changes event.
std::vector< effects::RecordChange > records
Record changes; a record with no keys changed as a whole.
std::vector< InstanceTopic > instances
Instance changes.
JSON wire envelope used between any client and RemoteServer.
std::uint64_t cancelCallId
The callId of the execute a "cancel" asks the server to stop.
std::string modelType
Model type id for execute (routing key in ActionDispatcher).
std::string message
Free-text error message on err. Empty on success.
std::string actionType
Action type id for execute (the second routing key).
uint64_t modelId
Existing model instance id for deregister, execute, ok(register).
bool shared
Whether a register should join the shared instance directory.
std::uint32_t protocolVersion
Protocol version the sender speaks.
std::string kind
Discriminator — see class docstring for valid values.
uint64_t callId
Correlation id for matching async replies to their requests.
::morph::session::Context session
Session context for authorization and routing. Populated on execute; ignored on every other kind.
std::string contextKey
Stable identity of the model instance being registered or attached to (e.g. an account id)....
std::string body
Serialized JSON of the action payload (execute) or of the deserializable result (ok).
std::string primary
Primary key of the instance being registered or attached to.
std::string typeId
Model type id for register.
The decoded body of an event envelope.
std::string topic
What the event is about (changes, resync).
glz::raw_json payload
The topic's payload: a JSON value, carried verbatim.
One instance topic: a model type, its directory key text and its instance id.
std::string primary
The directory key's text for a keyed shared instance; empty for a private one.
std::uint64_t instance
The instance id; 0 where the topic is named by type and primary alone (a request).
std::string modelType
The model's registered type name.
Inclusive protocol-version range a server advertises in reply to "hello".
std::uint32_t max
Newest protocol version the server accepts.
std::vector< std::string > capabilities
Optional envelope kinds the server honours, beyond those every server of this protocol version does (...
std::uint32_t min
Oldest protocol version the server accepts.
The body of a watch or unwatch request.
std::vector< InstanceTopic > instances
Instance topics to watch (or stop watching).
bool replace
For watch: replace the connection's whole watch set instead of adding to it.
std::vector< std::string > records
Record wire names to watch (or stop watching).
The body of the ok reply to a watch: the topics the server refused.
std::vector< InstanceTopic > refusedInstances
Instance topics the server refused.
std::vector< std::string > refusedRecords
Record names the server does not publish or the caller may not read.
The serialisation primitive encode calls, as an injectable strategy.
std::function< glz::error_ctx(const Envelope &env, std::string &out)> writeEnvelope
Serialises env into out. Mirrors the glz::write call encode would otherwise make directly.
Write options that escape ASCII control bytes as \\uXXXX sequences.
bool escape_control_characters
Emit control bytes as \\uXXXX rather than raw.