Motion Master 6.0.0-alpha.86
Next-generation motion control software
Loading...
Searching...
No Matches
router.h File Reference

Routes whose handlers cannot block the event loop, because they do not run on it. More...

#include <uwebsockets/App.h>
#include <BS_thread_pool.hpp>
#include <atomic>
#include <charconv>
#include <chrono>
#include <functional>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
Include dependency graph for router.h:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Classes

class  mm::api::Request
 A request, snapshotted on the loop thread so a handler can outlive it. More...
 
struct  mm::api::Response
 A complete response, produced off the loop and written by the framework. More...
 
class  mm::api::Router
 Registers routes whose handlers run off the event loop. More...
 

Namespaces

namespace  mm
 
namespace  mm::api
 HTTP-transport glue shared by the built-in server and route plug-in libs.
 

Typedefs

using mm::api::Handler = std::function< Response(const Request &)>
 What a route does: a pure function from a snapshotted request to a response.
 

Functions

std::string mm::api::percentDecode (std::string_view text)
 Percent-decodes a URL path component — %20 to a space, %2F to a slash.
 
std::vector< std::string > mm::api::parameterNames (std::string_view pattern)
 The :name tokens of a route pattern, in the order uWS will index them.
 
Response mm::api::json (const nlohmann::json &body)
 A 200 response carrying body as JSON.
 
Response mm::api::bytes (std::string contentType, std::string body)
 A 200 response carrying body verbatim under contentType.
 
Response mm::api::error (std::string status, std::string_view message)
 A status response carrying a {"error": message} body.
 
Response mm::api::badRequest (std::string_view message)
 A 400 with message — the most common failure, so it gets a name.
 
Response mm::api::notFound (std::string_view message)
 A 404 with message.
 
Response mm::api::statusOnly (std::string status)
 A bare status response with no body — a 202 that means "under way", a 204 delete.
 
Response mm::api::withWireTime (Response response, std::chrono::microseconds wireUs)
 Attaches the server-measured device time (X-Wire-Us) to response and returns it.
 
template<typename Op >
Response mm::api::timed (Op &&op, std::string errorStatus="500 Internal Server Error")
 Runs a device operation, times it, and turns its std::expected into a timed response.
 

Detailed Description

Routes whose handlers cannot block the event loop, because they do not run on it.

The problem this exists to remove. uWebSockets runs every handler on the app's single loop thread. A handler that blocks therefore blocks the whole HTTP API, not merely its own endpoint — and Motion Master is full of handlers that block for seconds by nature: an FoE transfer, an object-dictionary enumeration, any SDO waiting behind a busy control-plane lock. Measured during a firmware installation: a GET /api/devices/state waiting on the control-plane lock for a 12-second file transfer stalled every other request behind it, including /api/version, which touches no hardware at all, and the procedure-progress poll — the one thing the user was actually watching.

It could be fixed by wrapping each blocking handler by hand, and that was tried first. It is the wrong shape: it leaves the dangerous thing (touch the bus directly in a handler) as the default and the safe thing as something to remember, across seventy routes and every future one. So the framework does it instead — **a Handler cannot block the loop, because it never runs on it.**

What a handler is. A plain function from a Request to a Response. No uWS types, no response pointer, no lifetime rules, nothing to remember. It runs on a worker thread and returns a value; the framework writes it. That also makes handlers unit-testable without a server, which the old shape made impossible.

router.get("/api/devices/:slavePosition/state", [&dm](const Request& req) -> Response {
auto pos = req.parameterAs<uint16_t>("slavePosition");
if (!pos) return badRequest("slavePosition must be a number");
return timed([&] { return dm.deviceStates({*pos}); });
});

The three rules the framework keeps so a handler does not have to. A uWS response may only be touched from the loop thread — so every write happens inside uWS::Loop::defer. A response dies with its connection — so an abort flag, set by onAborted on the loop thread, is checked inside that same deferred callback, where the two cannot interleave. And a request object is valid only for the synchronous call — so it is snapshotted into a Request before anything is dispatched.