Files
Renderive/web_server/app/Gallery_Protocol.cpp
T
2026-08-12 06:16:00 +08:00

560 lines
28 KiB
C++

#include "Gallery_Protocol.h"
#include "Gallery_Renderables.h"
#include "Gallery_Session_Control_Adminive.h"
#include "adminive/adminive.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
namespace renderive::web::gallery_detail {
using Json = nlohmann::json;
struct Case_Model {
std::string id;
std::string title;
std::string component;
std::string category;
std::string description;
int order{};
int preferred_width = 560;
int preferred_height = 320;
};
using Action_Model = Gallery_Action_Model;
const std::vector<Case_Model>& cases() {
static const std::vector<Case_Model> value{
{"axis_lab", "坐标轴实验室", "Axis / Frequency_Axis / Time_Axis", "基础控件",
"普通轴、频率轴和时间轴的布局、格式化、滚轮与拖拽 API。", 10},
{"spectrum", "实时频谱", "Spectrum", "三种频率图",
"频率控制模式 1:曲线、保持线、扫频区、峰值和自定义 Marker。", 20},
{"afterglow", "余辉频谱", "Afterglow", "三种频率图",
"频率控制模式 2:二维功率密度、衰减和功率 bin 插值。", 30},
{"sweep_spectrum", "扫频频谱", "Sweep_Spectrum", "三种频率图",
"频率控制模式 3:分块扫频、当前频率游标和六种线插值。", 40},
{"waterfall", "瀑布图", "Waterfall", "热力图控件",
"逐行时频热力图;Nearest、Bilinear、Bicubic 三种图像模式均可切换。", 50},
{"frequency_trace", "时间轨迹", "Frequency_Trace", "曲线控件",
"Time_Axis 驱动的连续轨迹,覆盖时间格式和属性化笔刷 API。", 60},
{"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件",
"鼠标框选、多区域保留、标注样式和清空。", 70},
{"constellation", "星座图", "Constellation_Diagram", "点图控件",
"PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80}
};
return value;
}
const Case_Model* find_case(std::string_view id) {
const auto& all = cases();
const auto found = std::find_if(all.begin(), all.end(),
[id](const Case_Model& item) { return item.id == id; });
return found == all.end() ? nullptr : &*found;
}
std::vector<Action_Model> registered_actions(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
std::vector<Action_Model> result;
append_gallery_actions<Gallery_Session_Control>(result);
for (auto& action : gallery_frame_actions(frame_mode)) {
result.push_back(std::move(action));
}
append_gallery_renderable_actions(case_id, result);
return result;
}
Json parse_request(std::string_view message) {
return Json::parse(message.begin(), message.end());
}
adminive::Table_View action_view();
} // namespace renderive::web::gallery_detail
namespace adminive {
template <>
struct Type_Descriptor<renderive::web::gallery_detail::Case_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Case_Model;
return object<T>("renderive_gallery_case",
ADMINIVE_FIELD_LABEL(T, id, "标识"),
ADMINIVE_FIELD_LABEL(T, title, "标题"),
ADMINIVE_FIELD_LABEL(T, component, "Core2 控件"),
ADMINIVE_FIELD_LABEL(T, category, "分类"),
ADMINIVE_FIELD_LABEL(T, description, "说明"),
ADMINIVE_FIELD_LABEL(T, order, "顺序"),
ADMINIVE_FIELD_LABEL(T, preferred_width, "建议宽度"),
ADMINIVE_FIELD_LABEL(T, preferred_height, "建议高度"))
.label("Renderive 控件用例");
}
};
template <>
struct Type_Descriptor<renderive::web::gallery_detail::Action_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Action_Model;
return object<T>("renderive_gallery_action",
ADMINIVE_FIELD_LABEL(T, id, "动作"),
ADMINIVE_FIELD_LABEL(T, label, "名称"),
ADMINIVE_FIELD_LABEL(T, api, "Core2 API"),
ADMINIVE_FIELD_LABEL(T, description, "说明"),
ADMINIVE_FIELD_LABEL(T, group, "分组"),
ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"),
ADMINIVE_FIELD_LABEL(T, argument_label, "参数名称"),
ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值"),
ADMINIVE_FIELD_LABEL(T, request_frame, "执行后请求像素帧"))
.label("后端动作菜单");
}
};
} // namespace adminive
namespace renderive::web::gallery_detail {
adminive::Table_View action_view() {
using T = Action_Model;
return adminive::table_view<T>(
adminive::column<&T::label>("动作"),
adminive::column<&T::group>("分组"),
adminive::column<&T::api>("Core2 API"))
.titled("保留 API 动作");
}
} // namespace renderive::web::gallery_detail
namespace renderive::web {
namespace {
using gallery_detail::Json;
Json protocol_base() {
return {{"category", "gallery"},
{"protocol", "renderive.control-gallery"},
{"protocol_version", 4}};
}
Json case_contract(std::string_view case_id) {
const auto* item = gallery_detail::find_case(case_id);
return item ? adminive::to_frontend_json<Json>(*item) : Json::object();
}
std::string_view frame_mode_id(Gallery_Frame_Mode mode) {
switch (mode) {
case Gallery_Frame_Mode::Manual:
return "manual";
case Gallery_Frame_Mode::Low_Latency:
return "low_latency";
case Gallery_Frame_Mode::Playback:
return "playback";
}
return "low_latency";
}
std::optional<Gallery_Frame_Mode> parse_frame_mode(std::string_view value) {
if (value == "manual")
return Gallery_Frame_Mode::Manual;
if (value == "low_latency")
return Gallery_Frame_Mode::Low_Latency;
if (value == "playback")
return Gallery_Frame_Mode::Playback;
return std::nullopt;
}
Json dashboard_field(std::string_view label, std::string_view source,
std::string_view format = "integer", int digits = -1) {
Json result{{"label", label}, {"source", source}, {"format", format}};
if (digits >= 0)
result["digits"] = digits;
return result;
}
Json mapped_dashboard_field(std::string_view label, std::string_view source,
std::string_view format, std::string_view value_map) {
Json result = dashboard_field(label, source, format);
result["value_map"] = value_map;
return result;
}
Json dashboard_contract() {
Json point_pair{{"label", "输入→绘制"}, {"format", "pair"},
{"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})},
{"separator", ""}};
Json bottleneck = mapped_dashboard_field("当前瓶颈", "low_latency_limit.current",
"duration_enum", "limit_state");
bottleneck["duration_sources"] = {
{"frequency_limited", "kernel_observer.target_interval_ns"},
{"paint_limited", "kernel_observer.paint_duration_ns"},
{"render_limited", "kernel_observer.render_duration_ns"},
{"consumer_limited", "kernel_observer.consumer_interval_ns"}
};
Json configured_frequency = dashboard_field("配置频率", "kernel_observer.configured_frequency_hz",
"frequency");
configured_frequency["enabled_source"] = "kernel_observer.frequency_limit_enabled";
configured_frequency["disabled_label"] = "已关闭";
Json dashboard{
{"value_maps", {
{"enabled", {{"true", "启用"}, {"false", "关闭"}}},
{"consumer_feedback_source", {
{"disabled", "总开关关闭"}, {"none", ""}, {"pixel", "像素响应"},
{"presentation", "浏览器呈现"}, {"manual", "手动"}
}},
{"limit_state", {
{"frequency_limited", "Kernel 频率受限"}, {"paint_limited", "PaintEvent 受限"},
{"render_limited", "后台渲染受限"}, {"consumer_limited", "消费者反馈受限"},
{"unlimited", "无限制"}, {"not_applicable", "N/A"}
}}
}},
{"performance", {
{"fields", Json::array({
dashboard_field("后端渲染 FPS", "performance.measured_fps", "fixed", 1),
dashboard_field("像素响应 FPS", "performance.pixel_response_fps", "fixed", 1),
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fixed", 1),
dashboard_field("WS 往返 ms", "client_performance.frame_round_trip_ms", "fixed", 2),
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
dashboard_field("Core 渲染 ms", "performance.last_render_ms", "fixed", 2),
dashboard_field("像素编码 ms", "performance.last_pixel_encode_ms", "fixed", 2),
dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1),
dashboard_field("Kernel 待处理", "kernel_observer.pending_frame_count"),
dashboard_field("Kernel 丢弃", "kernel_observer.dropped_frame_count"),
std::move(point_pair), std::move(bottleneck),
dashboard_field("观察事件", "kernel_observer.last_event", "text"),
dashboard_field("像素超时", "client_performance.frame_request_timeout_count"),
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0)
})}
}},
{"menu_views", {
{"observer", {{"source", "kernel_observer"}}},
{"performance", Json::object()}
}},
{"limits", {
{"aria_label", "低延迟限速来源"}, {"title", "限速来源"},
{"current_source", "low_latency_limit.current"},
{"active_label", "当前瓶颈"}, {"inactive_label", "未受限"},
{"disabled_label", "已关闭"},
{"fields", Json::array({
{{"label", "Kernel 用户频率"}, {"active_value", "frequency_limited"},
{"duration_source", "kernel_observer.target_interval_ns"},
{"enabled_source", "kernel_observer.frequency_limit_enabled"}},
{{"label", "Kernel PaintEvent"}, {"active_value", "paint_limited"},
{"duration_source", "kernel_observer.paint_duration_ns"}},
{{"label", "Kernel 后台渲染"}, {"active_value", "render_limited"},
{"duration_source", "kernel_observer.render_duration_ns"}},
{{"label", "消费者反馈"}, {"active_value", "consumer_limited"},
{"duration_source", "kernel_observer.consumer_interval_ns"},
{"enabled_source", "kernel_observer.consumer_feedback_enabled"}}
})}
}},
{"observer", {
{"aria_label", "Kernel 低延迟全量统计"},
{"header", {
{"prefix", "KERNEL"}, {"suffix", "OBSERVER"}, {"event_label", "事件"},
{"mode", dashboard_field("", "kernel_observer.mode", "text")},
{"limit", mapped_dashboard_field("", "kernel_observer.limit_state", "enum", "limit_state")},
{"event", dashboard_field("", "kernel_observer.last_event", "text")}
}},
{"sections", Json::array({
{{"class_name", "observer-counters"}, {"fields", Json::array({
std::move(configured_frequency),
dashboard_field("观察次数", "kernel_observer.observation_count"),
dashboard_field("最新序号", "kernel_observer.latest_sequence"),
dashboard_field("发布", "kernel_observer.produced_frame_count"),
dashboard_field("完成", "kernel_observer.consumed_frame_count"),
dashboard_field("待处理", "kernel_observer.pending_frame_count"),
dashboard_field("丢弃", "kernel_observer.dropped_frame_count"),
dashboard_field("失败", "kernel_observer.failed_operation_count")
})}},
{{"class_name", "latency-summary"}, {"fields", Json::array({
dashboard_field("目标间隔", "kernel_observer.target_interval_ns", "nanoseconds"),
dashboard_field("PaintEvent", "kernel_observer.paint_duration_ns", "nanoseconds"),
dashboard_field("后台渲染", "kernel_observer.render_duration_ns", "nanoseconds"),
dashboard_field("内部瓶颈", "kernel_observer.bottleneck_duration_ns", "nanoseconds"),
mapped_dashboard_field("消费者总开关", "kernel_observer.consumer_feedback_master_enabled", "enum", "enabled"),
mapped_dashboard_field("Kernel 反馈有效", "kernel_observer.consumer_feedback_enabled", "enum", "enabled"),
mapped_dashboard_field("像素响应反馈", "kernel_observer.consumer_pixel_feedback_enabled", "enum", "enabled"),
mapped_dashboard_field("浏览器呈现反馈", "kernel_observer.consumer_presentation_feedback_enabled", "enum", "enabled"),
mapped_dashboard_field("手动消费者反馈", "kernel_observer.consumer_manual_feedback_enabled", "enum", "enabled"),
dashboard_field("手动消费者 FPS", "kernel_observer.consumer_manual_fps", "fps", 2),
mapped_dashboard_field("生效反馈来源", "kernel_observer.consumer_feedback_source", "flags", "consumer_feedback_source"),
dashboard_field("像素响应周期", "kernel_observer.consumer_pixel_interval_ns", "nanoseconds"),
dashboard_field("浏览器呈现周期", "kernel_observer.consumer_presentation_interval_ns", "nanoseconds"),
dashboard_field("手动消费者周期", "kernel_observer.consumer_manual_interval_ns", "nanoseconds"),
dashboard_field("消费者原始采样", "kernel_observer.consumer_sample_interval_ns", "nanoseconds"),
dashboard_field("消费者平滑周期", "kernel_observer.consumer_smoothed_interval_ns", "nanoseconds"),
dashboard_field("消费者抖动", "kernel_observer.consumer_variation_ns", "nanoseconds"),
dashboard_field("消费者安全期限", "kernel_observer.consumer_safety_interval_ns", "nanoseconds"),
dashboard_field("消费者限速周期", "kernel_observer.consumer_interval_ns", "nanoseconds"),
dashboard_field("消费者限速 FPS", "kernel_observer.consumer_interval_ns", "inverse_fps", 2),
dashboard_field("下次刷新", "kernel_observer.next_refresh_interval_ns", "nanoseconds"),
Json{{"label", "端到端延迟"}, {"source", "kernel_observer.end_to_end_ns"},
{"format", "nanoseconds"}, {"cell_class", "critical"}}
})}},
{{"class_name", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"},
{"fields", Json::array({
dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3),
dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3),
dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3),
dashboard_field("RAF P95-P50 抖动", "client_performance.display_jitter_ms", "milliseconds", 3),
dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3),
dashboard_field("像素响应 FPS", "client_performance.transport_fps", "fps", 2),
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fps", 2),
dashboard_field("WS 缓冲", "client_performance.websocket_buffered_bytes", "bytes"),
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
dashboard_field("变化像素帧", "client_performance.changed_pixel_frames"),
dashboard_field("重复像素帧", "client_performance.duplicate_pixel_frames"),
dashboard_field("像素请求超时", "client_performance.frame_request_timeout_count"),
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 3),
dashboard_field("最近变化龄", "client_performance.last_pixel_change_age_ms", "milliseconds", 3)
})}},
{{"class_name", "latency-details"}, {"aria_label", "Kernel 各阶段等待耗时"},
{"fields", Json::array({
dashboard_field("Painter Lease 等待", "kernel_observer.paint_lease_wait_ns", "nanoseconds"),
dashboard_field("Paint State 等待", "kernel_observer.paint_state_wait_ns", "nanoseconds"),
dashboard_field("Publish State 等待", "kernel_observer.publish_state_wait_ns", "nanoseconds"),
dashboard_field("Ready 等待", "kernel_observer.ready_wait_ns", "nanoseconds"),
dashboard_field("开始渲染时帧龄", "kernel_observer.frame_age_at_render_ns", "nanoseconds"),
dashboard_field("Render Lease 等待", "kernel_observer.render_lease_wait_ns", "nanoseconds"),
dashboard_field("Render State 等待", "kernel_observer.render_state_wait_ns", "nanoseconds"),
dashboard_field("Render Finish 等待", "kernel_observer.render_finish_state_wait_ns", "nanoseconds"),
dashboard_field("回放队列等待", "kernel_observer.queue_wait_ns", "nanoseconds")
})}}
})}
}}
};
return dashboard;
}
Json frame_mode_contract(Gallery_Frame_Mode mode) {
switch (mode) {
case Gallery_Frame_Mode::Manual:
return {{"id", "manual"}, {"title", "手动刷新"},
{"strategy", "Manual_Refresh_Strategy"},
{"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"},
{"automatic", false}, {"request_after_response", false},
{"request_on_animation_frame", false}, {"observer_visible", false},
{"frame_button_label", "手动刷新一帧"}, {"accent", "#ffd166aa"}, {"order", 10}};
case Gallery_Frame_Mode::Low_Latency:
return {{"id", "low_latency"}, {"title", "低延迟"},
{"strategy", "Low_Latency_Strategy"},
{"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"},
{"automatic", true}, {"request_after_response", true},
{"request_on_animation_frame", false}, {"observer_visible", true},
{"frame_button_label", "立即刷新"}, {"accent", "#45ddbeaa"}, {"order", 20}};
case Gallery_Frame_Mode::Playback:
return {{"id", "playback"}, {"title", "回放队列"},
{"strategy", "Flow_Refresh_Strategy"},
{"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"},
{"automatic", true}, {"request_after_response", true},
{"request_on_animation_frame", true}, {"observer_visible", false},
{"frame_button_label", "消费下一帧"}, {"accent", "#6aa9ffaa"}, {"order", 30}};
}
return Json::object();
}
} // namespace
std::string Gallery_Protocol::catalog_json() {
Json result = protocol_base();
result["type"] = "catalog";
result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp1"},
{"http_api", false}, {"socket_per_canvas", true}};
result["navigation"] = {
{"default_mode", "low_latency"}, {"all_categories_label", "全部"},
{"catalog_loaded_text", "Kernel 策略目录已加载"},
{"hero_eyebrow", "REAL KERNEL STRATEGIES"},
{"hero_title", "对称策略页面,同一批控件,直接比较"}
};
result["dashboard"] = dashboard_contract();
result["case_descriptor"] =
adminive::to_descriptor_json<Json, gallery_detail::Case_Model>();
result["frame_modes"] = Json::array();
constexpr Gallery_Frame_Mode frame_modes[]{Gallery_Frame_Mode::Manual,
Gallery_Frame_Mode::Low_Latency,
Gallery_Frame_Mode::Playback};
for (const auto mode : frame_modes)
result["frame_modes"].push_back(frame_mode_contract(mode));
result["cases"] = Json::array();
std::size_t controls_total{};
std::size_t actions_total{};
for (const auto& item : gallery_detail::cases()) {
Json entry = adminive::to_frontend_json<Json>(item);
entry["control_count_by_mode"] = Json::object();
entry["action_count_by_mode"] = Json::object();
const std::size_t controls = gallery_control_count<Gallery_Session_Control>() +
gallery_renderable_control_count(item.id);
for (const auto mode : frame_modes) {
const std::string key(frame_mode_id(mode));
const std::size_t actions =
gallery_detail::registered_actions(item.id, mode).size();
entry["control_count_by_mode"][key] = controls;
entry["action_count_by_mode"][key] = actions;
controls_total += controls;
actions_total += actions;
}
entry["control_count"] = controls;
entry["action_count"] =
gallery_detail::registered_actions(
item.id, Gallery_Frame_Mode::Low_Latency).size();
result["cases"].push_back(std::move(entry));
}
result["coverage"] = {{"case_count", gallery_detail::cases().size()},
{"page_count", std::size(frame_modes)},
{"canvas_count", gallery_detail::cases().size() * std::size(frame_modes)},
{"manual_control_count", controls_total},
{"manual_action_count", actions_total},
{"frequency_modes", Json::array({"spectrum", "afterglow", "sweep_spectrum"})},
{"image_interpolation_modes", Json::array({"Nearest", "Bilinear", "Bicubic"})}};
return result.dump();
}
bool Gallery_Protocol::is_case(std::string_view case_id) {
return gallery_detail::find_case(case_id) != nullptr;
}
std::string Gallery_Protocol::case_json_from_controls(
std::string_view case_id,
std::string_view controls_json,
std::string_view telemetry_json,
std::string_view notice,
Gallery_Frame_Mode frame_mode) {
Json result = protocol_base();
result["type"] = "case_state";
result["case"] = case_contract(case_id);
result["frame_mode"] = frame_mode_contract(frame_mode);
try {
result["controls"] = {
{"resources", Json::parse(controls_json.begin(), controls_json.end())}
};
} catch (const std::exception&) {
result["controls"] = {{"resources", Json::array()}};
}
result["actions"] = {
{"descriptor", adminive::to_descriptor_json<Json, gallery_detail::Action_Model>()},
{"view", adminive::to_view_json<Json, gallery_detail::Action_Model>(
gallery_detail::action_view())},
{"data", Json::array()}
};
for (const auto& action : gallery_detail::registered_actions(case_id, frame_mode))
result["actions"]["data"].push_back(
adminive::to_frontend_json<Json>(action));
try {
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
} catch (const std::exception&) {
result["telemetry"] = Json::object();
}
if (!notice.empty())
result["notice"] = notice;
return result.dump();
}
std::string Gallery_Protocol::error_json(std::string_view message,
std::string_view field) {
Json result = protocol_base();
result["type"] = "error";
result["message"] = message;
result["field_errors"] = Json::object();
if (!field.empty())
result["field_errors"][std::string(field)] = message;
return result.dump();
}
std::string Gallery_Protocol::observer_json(std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view telemetry_json) {
Json result = protocol_base();
result["type"] = "observer_state";
result["case_id"] = case_id;
result["frame_mode"] = frame_mode_contract(frame_mode);
try {
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
} catch (const std::exception&) {
result["telemetry"] = Json::object();
}
return result.dump();
}
std::optional<Gallery_Open_Request> Gallery_Protocol::open_request(std::string_view message) {
try {
const Json request = gallery_detail::parse_request(message);
if (!request.is_object() || request.value("category", "") != "event" ||
request.value("type", "") != "gallery_open" ||
!request.contains("case") || !request.at("case").is_string())
return std::nullopt;
const std::string value = request.at("case").get<std::string>();
if (!is_case(value) || !request.contains("frame_mode") ||
!request.at("frame_mode").is_string())
return std::nullopt;
const auto mode = parse_frame_mode(request.at("frame_mode").get<std::string>());
return mode ? std::optional<Gallery_Open_Request>(Gallery_Open_Request{value, *mode})
: std::nullopt;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<Gallery_Action_Request> Gallery_Protocol::action_request(
std::string_view message) {
try {
const Json request = gallery_detail::parse_request(message);
if (!request.is_object() || request.value("category", "") != "event" ||
request.value("type", "") != "gallery_action" ||
!request.contains("action") || !request.at("action").is_string())
return std::nullopt;
Gallery_Action_Request result{request.at("action").get<std::string>()};
if (request.contains("argument")) {
const Json& argument = request.at("argument");
if (argument.is_boolean())
result.argument = argument.get<bool>();
else if (argument.is_number()) {
const double value = argument.get<double>();
if (!std::isfinite(value))
return std::nullopt;
result.argument = value;
} else if (argument.is_string())
result.argument = argument.get<std::string>();
else
return std::nullopt;
}
return result;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<Gallery_Control_Patch_Request> Gallery_Protocol::control_patch_request(
std::string_view message) {
try {
const Json request = gallery_detail::parse_request(message);
if (!request.is_object() || request.value("category", "") != "event" ||
request.value("type", "") != "gallery_patch" ||
!request.contains("target") || !request.at("target").is_string() ||
!request.contains("patch") || !request.at("patch").is_object())
return std::nullopt;
return Gallery_Control_Patch_Request{
request.at("target").get<std::string>(), request.at("patch").dump()};
} catch (const std::exception&) {
return std::nullopt;
}
}
bool Gallery_Protocol::action_available(std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view action_id) {
const auto available = gallery_detail::registered_actions(case_id, frame_mode);
return std::any_of(available.begin(), available.end(),
[action_id](const gallery_detail::Action_Model& item) {
return item.id == action_id;
});
}
} // namespace renderive::web